diff --git a/.jules/bolt.md b/.jules/bolt.md index f1a8c146..8195f276 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -77,3 +77,6 @@ Optimized metric route processing to O(N) by creating a mapping of routes direct ## 2024-07-13 - [Optimize Export Dictionary FK lookups] **Learning:** Found O(N * C * E) performance bottleneck in ERD export dictionaries due to repeated array searching with `edges.some()` inside a nested loop over nodes and columns. **Action:** Replace repeated linear array scans for edges by precomputing O(1) Set lookups of foreign key column handles per node before looping. +## 2026-07-22 - Avoid redundant Map instantiation in unrelated export paths +**Learning:** When optimizing ERD export functions with pre-computed lookup Maps (like `columnByHandle`), instantiating them indiscriminately in unrelated functions (like `exportDiagramSvg`) that don't utilize the Map creates unnecessary CPU and memory allocation regressions (O(N*C)), even if the Map was beneficial for functions that actually parse edges like `exportDDL`. Test mocks also frequently contain undefined column objects, causing type errors during map generation. +**Action:** When adding pre-computed Maps for O(1) lookups, only instantiate and populate them within the specific functions that actually require them. Additionally, explicitly guard against incomplete test mock data (`if (c && c.column_name)`) before passing values to handle generation functions. diff --git a/frontend/src/erd/export.ts b/frontend/src/erd/export.ts index 62ce7219..44598843 100644 --- a/frontend/src/erd/export.ts +++ b/frontend/src/erd/export.ts @@ -59,6 +59,7 @@ function fkColumnsForEdge( edge: Edge, sourceNode: Node, targetNode: Node, + columnByHandle?: Map, ): { sourceColumns: string[]; targetColumns: string[] } | null { const data = edge.data as ForeignKeyEdgeData | undefined; const sourceColumns = data?.sourceColumns?.filter(Boolean) || []; @@ -67,22 +68,47 @@ function fkColumnsForEdge( return { sourceColumns, targetColumns }; } - const sourceHandleColumn = (sourceNode.data.columns || []) - .find((column) => sourceColumnHandleId(column.column_name) === edge.sourceHandle) - ?.column_name; - const targetHandleColumn = (targetNode.data.columns || []) - .find((column) => targetColumnHandleId(column.column_name) === edge.targetHandle) - ?.column_name; + let sourceHandleColumn: string | undefined = undefined; + let targetHandleColumn: string | undefined = undefined; + + if (columnByHandle) { + sourceHandleColumn = edge.sourceHandle ? columnByHandle.get(`${edge.source}:${edge.sourceHandle}`) : undefined; + targetHandleColumn = edge.targetHandle ? columnByHandle.get(`${edge.target}:${edge.targetHandle}`) : undefined; + } else { + if (sourceNode.data.columns) { + for (const column of sourceNode.data.columns) { + if (sourceColumnHandleId(column.column_name) === edge.sourceHandle) { + sourceHandleColumn = column.column_name; + break; + } + } + } + if (targetNode.data.columns) { + for (const column of targetNode.data.columns) { + if (targetColumnHandleId(column.column_name) === edge.targetHandle) { + targetHandleColumn = column.column_name; + break; + } + } + } + } + if (sourceHandleColumn && targetHandleColumn) { return { sourceColumns: [sourceHandleColumn], targetColumns: [targetHandleColumn] }; } - const fallbackSource = (sourceNode.data.columns || []) - .filter((column) => !column.is_pk) - .map((column) => column.column_name); - const fallbackTarget = (targetNode.data.columns || []) - .filter((column) => column.is_pk) - .map((column) => column.column_name); + const fallbackSource: string[] = []; + if (sourceNode.data.columns) { + for (const column of sourceNode.data.columns) { + if (!column.is_pk) fallbackSource.push(column.column_name); + } + } + const fallbackTarget: string[] = []; + if (targetNode.data.columns) { + for (const column of targetNode.data.columns) { + if (column.is_pk) fallbackTarget.push(column.column_name); + } + } if (fallbackSource.length > 0 && fallbackSource.length === fallbackTarget.length) { return { sourceColumns: fallbackSource, targetColumns: fallbackTarget }; } @@ -96,8 +122,15 @@ export function exportDDL(nodes: Node[], edges: Edge[]): string { // Bolt: Use map for O(1) node lookup instead of O(N) array find // Avoid Map(array.map) to prevent O(N) intermediate tuple array allocation overhead const nodesById = new Map>(); + const columnByHandle = new Map(); for (const n of nodes) { nodesById.set(n.id, n); + for (const c of n.data.columns || []) { + if (c && c.column_name) { + columnByHandle.set(`${n.id}:${sourceColumnHandleId(c.column_name)}`, c.column_name); + columnByHandle.set(`${n.id}:${targetColumnHandleId(c.column_name)}`, c.column_name); + } + } } // Export tables @@ -133,7 +166,7 @@ export function exportDDL(nodes: Node[], edges: Edge[]): string { const targetNode = nodesById.get(edge.target); if (sourceNode && targetNode) { - const fkColumns = fkColumnsForEdge(edge, sourceNode, targetNode); + const fkColumns = fkColumnsForEdge(edge, sourceNode, targetNode, columnByHandle); const constraintName = edge.label ? edge.label : `fk_${edge.source}_${edge.target}`; const sourceTable = quoteSqlIdentifier(sourceNode.data.title || sourceNode.id); const targetTable = quoteSqlIdentifier(targetNode.data.title || targetNode.id);