modified: backend/src/controllers/schemaController.ts
modified: frontend/src/pages/DatabaseSchema.tsx
This commit is contained in:
@@ -31,7 +31,7 @@ interface SchemaData {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse PostgreSQL schema
|
// Parse PostgreSQL schema - optimized with bulk queries
|
||||||
async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
console.log(`[Schema] Starting schema parse for database ${databaseId}`);
|
console.log(`[Schema] Starting schema parse for database ${databaseId}`);
|
||||||
@@ -41,10 +41,8 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
|||||||
throw new Error('Database not found or not active');
|
throw new Error('Database not found or not active');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Schema] Fetching tables list...`);
|
// Get all tables with comments
|
||||||
const tablesStartTime = Date.now();
|
console.log(`[Schema] Fetching tables...`);
|
||||||
|
|
||||||
// Get all tables with comments via pg_catalog
|
|
||||||
const tablesResult = await pool.query(`
|
const tablesResult = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
t.table_schema,
|
t.table_schema,
|
||||||
@@ -57,25 +55,20 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
|||||||
AND t.table_type = 'BASE TABLE'
|
AND t.table_type = 'BASE TABLE'
|
||||||
ORDER BY t.table_schema, t.table_name
|
ORDER BY t.table_schema, t.table_name
|
||||||
`);
|
`);
|
||||||
|
console.log(`[Schema] Found ${tablesResult.rows.length} tables in ${Date.now() - startTime}ms`);
|
||||||
|
|
||||||
console.log(`[Schema] Found ${tablesResult.rows.length} tables in ${Date.now() - tablesStartTime}ms`);
|
// Get ALL columns in one query
|
||||||
|
console.log(`[Schema] Fetching all columns...`);
|
||||||
const tables: TableInfo[] = [];
|
const columnsStartTime = Date.now();
|
||||||
let processedCount = 0;
|
const allColumnsResult = await pool.query(`
|
||||||
|
|
||||||
for (const table of tablesResult.rows) {
|
|
||||||
processedCount++;
|
|
||||||
if (processedCount % 50 === 0 || processedCount === tablesResult.rows.length) {
|
|
||||||
console.log(`[Schema] Processing table ${processedCount}/${tablesResult.rows.length}: ${table.table_schema}.${table.table_name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get columns for each table
|
|
||||||
const columnsResult = await pool.query(`
|
|
||||||
SELECT
|
SELECT
|
||||||
|
c.table_schema,
|
||||||
|
c.table_name,
|
||||||
c.column_name,
|
c.column_name,
|
||||||
c.data_type,
|
c.data_type,
|
||||||
c.is_nullable,
|
c.is_nullable,
|
||||||
c.column_default,
|
c.column_default,
|
||||||
|
c.ordinal_position,
|
||||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary,
|
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary,
|
||||||
pg_catalog.col_description(pc.oid, c.ordinal_position) as column_comment
|
pg_catalog.col_description(pc.oid, c.ordinal_position) as column_comment
|
||||||
FROM information_schema.columns c
|
FROM information_schema.columns c
|
||||||
@@ -91,13 +84,18 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
|||||||
) pk ON c.column_name = pk.column_name
|
) pk ON c.column_name = pk.column_name
|
||||||
AND c.table_schema = pk.table_schema
|
AND c.table_schema = pk.table_schema
|
||||||
AND c.table_name = pk.table_name
|
AND c.table_name = pk.table_name
|
||||||
WHERE c.table_schema = $1 AND c.table_name = $2
|
WHERE c.table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||||
ORDER BY c.ordinal_position
|
ORDER BY c.table_schema, c.table_name, c.ordinal_position
|
||||||
`, [table.table_schema, table.table_name]);
|
`);
|
||||||
|
console.log(`[Schema] Found ${allColumnsResult.rows.length} columns in ${Date.now() - columnsStartTime}ms`);
|
||||||
|
|
||||||
// Get foreign keys
|
// Get ALL foreign keys in one query
|
||||||
const fkResult = await pool.query(`
|
console.log(`[Schema] Fetching all foreign keys...`);
|
||||||
|
const fkStartTime = Date.now();
|
||||||
|
const allFkResult = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
|
tc.table_schema,
|
||||||
|
tc.table_name,
|
||||||
kcu.column_name,
|
kcu.column_name,
|
||||||
ccu.table_name AS references_table,
|
ccu.table_name AS references_table,
|
||||||
ccu.column_name AS references_column,
|
ccu.column_name AS references_column,
|
||||||
@@ -110,15 +108,42 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
|||||||
ON ccu.constraint_name = tc.constraint_name
|
ON ccu.constraint_name = tc.constraint_name
|
||||||
AND ccu.table_schema = tc.table_schema
|
AND ccu.table_schema = tc.table_schema
|
||||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||||
AND tc.table_schema = $1
|
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||||
AND tc.table_name = $2
|
`);
|
||||||
`, [table.table_schema, table.table_name]);
|
console.log(`[Schema] Found ${allFkResult.rows.length} foreign keys in ${Date.now() - fkStartTime}ms`);
|
||||||
|
|
||||||
tables.push({
|
// Group columns by table
|
||||||
|
const columnsByTable = new Map<string, any[]>();
|
||||||
|
for (const col of allColumnsResult.rows) {
|
||||||
|
const key = `${col.table_schema}.${col.table_name}`;
|
||||||
|
if (!columnsByTable.has(key)) {
|
||||||
|
columnsByTable.set(key, []);
|
||||||
|
}
|
||||||
|
columnsByTable.get(key)!.push(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group foreign keys by table
|
||||||
|
const fkByTable = new Map<string, any[]>();
|
||||||
|
for (const fk of allFkResult.rows) {
|
||||||
|
const key = `${fk.table_schema}.${fk.table_name}`;
|
||||||
|
if (!fkByTable.has(key)) {
|
||||||
|
fkByTable.set(key, []);
|
||||||
|
}
|
||||||
|
fkByTable.get(key)!.push(fk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tables array
|
||||||
|
console.log(`[Schema] Building schema structure...`);
|
||||||
|
const tables: TableInfo[] = tablesResult.rows.map(table => {
|
||||||
|
const key = `${table.table_schema}.${table.table_name}`;
|
||||||
|
const columns = columnsByTable.get(key) || [];
|
||||||
|
const fks = fkByTable.get(key) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
name: table.table_name,
|
name: table.table_name,
|
||||||
schema: table.table_schema,
|
schema: table.table_schema,
|
||||||
comment: table.table_comment,
|
comment: table.table_comment,
|
||||||
columns: columnsResult.rows.map(col => ({
|
columns: columns.map(col => ({
|
||||||
name: col.column_name,
|
name: col.column_name,
|
||||||
type: col.data_type,
|
type: col.data_type,
|
||||||
nullable: col.is_nullable === 'YES',
|
nullable: col.is_nullable === 'YES',
|
||||||
@@ -126,17 +151,17 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
|||||||
is_primary: col.is_primary,
|
is_primary: col.is_primary,
|
||||||
comment: col.column_comment,
|
comment: col.column_comment,
|
||||||
})),
|
})),
|
||||||
foreign_keys: fkResult.rows.map(fk => ({
|
foreign_keys: fks.map(fk => ({
|
||||||
column: fk.column_name,
|
column: fk.column_name,
|
||||||
references_table: fk.references_table,
|
references_table: fk.references_table,
|
||||||
references_column: fk.references_column,
|
references_column: fk.references_column,
|
||||||
constraint_name: fk.constraint_name,
|
constraint_name: fk.constraint_name,
|
||||||
})),
|
})),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const totalTime = Date.now() - startTime;
|
const totalTime = Date.now() - startTime;
|
||||||
console.log(`[Schema] Completed! ${tables.length} tables, ${tables.reduce((acc, t) => acc + t.columns.length, 0)} columns, ${tables.reduce((acc, t) => acc + t.foreign_keys.length, 0)} FKs in ${totalTime}ms`);
|
console.log(`[Schema] Completed! ${tables.length} tables, ${allColumnsResult.rows.length} columns, ${allFkResult.rows.length} FKs in ${totalTime}ms`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tables,
|
tables,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function TableNode({ data }: { data: TableInfo & Record<string, unknown> }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Columns */}
|
{/* Columns */}
|
||||||
<div className="divide-y divide-gray-100 max-h-64 overflow-y-auto">
|
<div className="divide-y divide-gray-100">
|
||||||
{data.columns.map((col) => (
|
{data.columns.map((col) => (
|
||||||
<div
|
<div
|
||||||
key={col.name}
|
key={col.name}
|
||||||
@@ -78,11 +78,10 @@ const nodeTypes = {
|
|||||||
// Calculate node height based on columns count
|
// Calculate node height based on columns count
|
||||||
function getNodeHeight(table: TableInfo): number {
|
function getNodeHeight(table: TableInfo): number {
|
||||||
const headerHeight = 44;
|
const headerHeight = 44;
|
||||||
const columnHeight = 28;
|
const columnHeight = 26;
|
||||||
const fkBarHeight = table.foreign_keys.length > 0 ? 28 : 0;
|
const fkBarHeight = table.foreign_keys.length > 0 ? 28 : 0;
|
||||||
const maxVisibleColumns = 10;
|
// No max limit - show all columns
|
||||||
const visibleColumns = Math.min(table.columns.length, maxVisibleColumns);
|
return headerHeight + (table.columns.length * columnHeight) + fkBarHeight + 8;
|
||||||
return headerHeight + (visibleColumns * columnHeight) + fkBarHeight + 8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[] } {
|
function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[] } {
|
||||||
@@ -139,11 +138,24 @@ function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[]
|
|||||||
|
|
||||||
// Create edges
|
// Create edges
|
||||||
const edges: Edge[] = [];
|
const edges: Edge[] = [];
|
||||||
|
let totalFks = 0;
|
||||||
|
let matchedFks = 0;
|
||||||
|
|
||||||
schema.tables.forEach((table) => {
|
schema.tables.forEach((table) => {
|
||||||
const sourceId = `${table.schema}.${table.name}`;
|
const sourceId = `${table.schema}.${table.name}`;
|
||||||
table.foreign_keys.forEach((fk) => {
|
table.foreign_keys.forEach((fk) => {
|
||||||
const targetTable = schema.tables.find(t => t.name === fk.references_table);
|
totalFks++;
|
||||||
|
// Try to find target table - check both same schema and other schemas
|
||||||
|
let targetTable = schema.tables.find(t =>
|
||||||
|
t.name === fk.references_table && t.schema === table.schema
|
||||||
|
);
|
||||||
|
// If not found in same schema, try any schema
|
||||||
|
if (!targetTable) {
|
||||||
|
targetTable = schema.tables.find(t => t.name === fk.references_table);
|
||||||
|
}
|
||||||
|
|
||||||
if (targetTable) {
|
if (targetTable) {
|
||||||
|
matchedFks++;
|
||||||
const targetId = `${targetTable.schema}.${targetTable.name}`;
|
const targetId = `${targetTable.schema}.${targetTable.name}`;
|
||||||
edges.push({
|
edges.push({
|
||||||
id: `${fk.constraint_name}`,
|
id: `${fk.constraint_name}`,
|
||||||
@@ -161,10 +173,14 @@ function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[]
|
|||||||
labelBgStyle: { fill: 'white', fillOpacity: 0.9 },
|
labelBgStyle: { fill: 'white', fillOpacity: 0.9 },
|
||||||
labelBgPadding: [4, 2] as [number, number],
|
labelBgPadding: [4, 2] as [number, number],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`[Schema] FK not matched: ${sourceId}.${fk.column} -> ${fk.references_table}.${fk.references_column}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[Schema] Created ${edges.length} edges from ${matchedFks}/${totalFks} FKs`);
|
||||||
|
|
||||||
return { nodes, edges };
|
return { nodes, edges };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user