Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
59 changes: 46 additions & 13 deletions frontend/src/erd/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function fkColumnsForEdge(
edge: Edge,
sourceNode: Node<TableNodeData>,
targetNode: Node<TableNodeData>,
columnByHandle?: Map<string, string>,
): { sourceColumns: string[]; targetColumns: string[] } | null {
const data = edge.data as ForeignKeyEdgeData | undefined;
const sourceColumns = data?.sourceColumns?.filter(Boolean) || [];
Expand All @@ -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 };
}
Expand All @@ -96,8 +122,15 @@ export function exportDDL(nodes: Node<TableNodeData>[], 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<string, Node<TableNodeData>>();
const columnByHandle = new Map<string, string>();
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);
}
}
}
Comment on lines 122 to 134

// Export tables
Expand Down Expand Up @@ -133,7 +166,7 @@ export function exportDDL(nodes: Node<TableNodeData>[], 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);
Expand Down
Loading