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;
|
||||
}
|
||||
|
||||
// Parse PostgreSQL schema
|
||||
// Parse PostgreSQL schema - optimized with bulk queries
|
||||
async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
||||
const startTime = Date.now();
|
||||
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');
|
||||
}
|
||||
|
||||
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,25 +55,20 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
||||
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(`
|
||||
// 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
|
||||
@@ -91,13 +84,18 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
||||
) 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]);
|
||||
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`);
|
||||
|
||||
// Get foreign keys
|
||||
const fkResult = await pool.query(`
|
||||
// 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,
|
||||
@@ -110,15 +108,42 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
||||
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]);
|
||||
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
`);
|
||||
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,
|
||||
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<SchemaData> {
|
||||
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,
|
||||
|
||||
@@ -36,7 +36,7 @@ function TableNode({ data }: { data: TableInfo & Record<string, unknown> }) {
|
||||
</div>
|
||||
|
||||
{/* 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) => (
|
||||
<div
|
||||
key={col.name}
|
||||
@@ -78,11 +78,10 @@ const nodeTypes = {
|
||||
// Calculate node height based on columns count
|
||||
function getNodeHeight(table: TableInfo): number {
|
||||
const headerHeight = 44;
|
||||
const columnHeight = 28;
|
||||
const columnHeight = 26;
|
||||
const fkBarHeight = table.foreign_keys.length > 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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user