diff --git a/backend/src/controllers/schemaController.ts b/backend/src/controllers/schemaController.ts index 96d1a77..67714bb 100644 --- a/backend/src/controllers/schemaController.ts +++ b/backend/src/controllers/schemaController.ts @@ -31,7 +31,7 @@ interface SchemaData { updated_at: string; } -// Parse PostgreSQL schema +// Parse PostgreSQL schema - optimized with bulk queries async function parsePostgresSchema(databaseId: string): Promise { const startTime = Date.now(); console.log(`[Schema] Starting schema parse for database ${databaseId}`); @@ -41,10 +41,8 @@ async function parsePostgresSchema(databaseId: string): Promise { throw new Error('Database not found or not active'); } - console.log(`[Schema] Fetching tables list...`); - const tablesStartTime = Date.now(); - - // Get all tables with comments via pg_catalog + // Get all tables with comments + console.log(`[Schema] Fetching tables...`); const tablesResult = await pool.query(` SELECT t.table_schema, @@ -57,68 +55,95 @@ async function parsePostgresSchema(databaseId: string): Promise { AND t.table_type = 'BASE TABLE' 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`); - - const tables: TableInfo[] = []; - let processedCount = 0; - - 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 - c.column_name, - c.data_type, - c.is_nullable, - c.column_default, - 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 - FROM information_schema.columns c - LEFT JOIN pg_catalog.pg_class pc ON pc.relname = c.table_name - LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = pc.relnamespace AND pn.nspname = c.table_schema - LEFT JOIN ( - SELECT ku.column_name, ku.table_schema, ku.table_name - FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage ku - ON tc.constraint_name = ku.constraint_name - AND tc.table_schema = ku.table_schema - WHERE tc.constraint_type = 'PRIMARY KEY' - ) pk ON c.column_name = pk.column_name - AND c.table_schema = pk.table_schema - AND c.table_name = pk.table_name - WHERE c.table_schema = $1 AND c.table_name = $2 - ORDER BY c.ordinal_position - `, [table.table_schema, table.table_name]); - - // Get foreign keys - const fkResult = await pool.query(` - SELECT - kcu.column_name, - ccu.table_name AS references_table, - ccu.column_name AS references_column, - tc.constraint_name + // Get ALL columns in one query + console.log(`[Schema] Fetching all columns...`); + const columnsStartTime = Date.now(); + const allColumnsResult = await pool.query(` + SELECT + c.table_schema, + c.table_name, + c.column_name, + c.data_type, + c.is_nullable, + c.column_default, + c.ordinal_position, + 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 + FROM information_schema.columns c + LEFT JOIN pg_catalog.pg_class pc ON pc.relname = c.table_name + LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = pc.relnamespace AND pn.nspname = c.table_schema + LEFT JOIN ( + SELECT ku.column_name, ku.table_schema, ku.table_name FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - JOIN information_schema.constraint_column_usage ccu - ON ccu.constraint_name = tc.constraint_name - AND ccu.table_schema = tc.table_schema - WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = $1 - AND tc.table_name = $2 - `, [table.table_schema, table.table_name]); + JOIN information_schema.key_column_usage ku + ON tc.constraint_name = ku.constraint_name + AND tc.table_schema = ku.table_schema + WHERE tc.constraint_type = 'PRIMARY KEY' + ) pk ON c.column_name = pk.column_name + AND c.table_schema = pk.table_schema + AND c.table_name = pk.table_name + WHERE c.table_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY c.table_schema, c.table_name, c.ordinal_position + `); + console.log(`[Schema] Found ${allColumnsResult.rows.length} columns in ${Date.now() - columnsStartTime}ms`); - tables.push({ + // Get ALL foreign keys in one query + console.log(`[Schema] Fetching all foreign keys...`); + const fkStartTime = Date.now(); + const allFkResult = await pool.query(` + SELECT + tc.table_schema, + tc.table_name, + kcu.column_name, + ccu.table_name AS references_table, + ccu.column_name AS references_column, + tc.constraint_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema NOT IN ('pg_catalog', 'information_schema') + `); + console.log(`[Schema] Found ${allFkResult.rows.length} foreign keys in ${Date.now() - fkStartTime}ms`); + + // Group columns by table + const columnsByTable = new Map(); + 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(); + 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, schema: table.table_schema, comment: table.table_comment, - columns: columnsResult.rows.map(col => ({ + columns: columns.map(col => ({ name: col.column_name, type: col.data_type, nullable: col.is_nullable === 'YES', @@ -126,17 +151,17 @@ async function parsePostgresSchema(databaseId: string): Promise { is_primary: col.is_primary, comment: col.column_comment, })), - foreign_keys: fkResult.rows.map(fk => ({ + foreign_keys: fks.map(fk => ({ column: fk.column_name, references_table: fk.references_table, references_column: fk.references_column, constraint_name: fk.constraint_name, })), - }); - } + }; + }); 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 { tables, diff --git a/frontend/src/pages/DatabaseSchema.tsx b/frontend/src/pages/DatabaseSchema.tsx index aa962df..2dc0ab9 100644 --- a/frontend/src/pages/DatabaseSchema.tsx +++ b/frontend/src/pages/DatabaseSchema.tsx @@ -36,7 +36,7 @@ function TableNode({ data }: { data: TableInfo & Record }) { {/* Columns */} -
+
{data.columns.map((col) => (
0 ? 28 : 0; - const maxVisibleColumns = 10; - const visibleColumns = Math.min(table.columns.length, maxVisibleColumns); - return headerHeight + (visibleColumns * columnHeight) + fkBarHeight + 8; + // No max limit - show all columns + return headerHeight + (table.columns.length * columnHeight) + fkBarHeight + 8; } function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[] } { @@ -139,11 +138,24 @@ function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[] // Create edges const edges: Edge[] = []; + let totalFks = 0; + let matchedFks = 0; + schema.tables.forEach((table) => { const sourceId = `${table.schema}.${table.name}`; 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) { + matchedFks++; const targetId = `${targetTable.schema}.${targetTable.name}`; edges.push({ id: `${fk.constraint_name}`, @@ -161,10 +173,14 @@ function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[] labelBgStyle: { fill: 'white', fillOpacity: 0.9 }, 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 }; }