modified: backend/src/controllers/schemaController.ts

modified:   frontend/src/pages/DatabaseSchema.tsx
This commit is contained in:
2026-01-28 00:04:01 +03:00
parent 4fb92470ce
commit c5b4799dcb
2 changed files with 112 additions and 71 deletions

View File

@@ -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,68 +55,95 @@ 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(`
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<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,

View File

@@ -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 };
}