diff --git a/.jules/bolt.md b/.jules/bolt.md index f1a8c1466..5a4b0e20b 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. +## 2024-10-25 - ERD 내보내기 시 불필요한 O(N*C) 핸들 인코딩 제거 +**Learning:** Mermaid, Prisma 등 ERD 내보내기 과정에서 매 컬럼마다 호출되는 `sanitizeHandleId` 함수(O(N*C)) 내의 문자열 hex 인코딩이 심각한 성능 저하와 병목을 유발합니다. 또한 Prisma 내보내기에서는 핸들을 원래 컬럼명으로 취급하여 `@relation` 정의 시 잘못된 이름이 생성되는 버그가 있었습니다. +**Action:** 엣지의 `edge.data.sourceColumns`를 우선 활용하여 O(1) 조회 Set을 미리 구성하고, 레거시 핸들이 없는 경우 `fkNodeHandlePairs.size > 0` 조건을 확인하여 O(N*C) 인코딩 연산을 완전히 우회합니다. diff --git a/frontend/src/erd/exportDataDictionary.ts b/frontend/src/erd/exportDataDictionary.ts index 0111660d9..6062000aa 100644 --- a/frontend/src/erd/exportDataDictionary.ts +++ b/frontend/src/erd/exportDataDictionary.ts @@ -78,8 +78,11 @@ function isForeignKeyColumn( return true; } - const handleId = sourceColumnHandleId(columnName); - return info.handles.has(handleId); + if (info.handles.size > 0) { + const handleId = sourceColumnHandleId(columnName); + return info.handles.has(handleId); + } + return false; } function exampleValue(value: TableNodeData['columns'][number]['example_value']): string { diff --git a/frontend/src/erd/mermaid.ts b/frontend/src/erd/mermaid.ts index 6e77a9bca..55e422eff 100644 --- a/frontend/src/erd/mermaid.ts +++ b/frontend/src/erd/mermaid.ts @@ -26,17 +26,27 @@ export function exportMermaid( } const fkNodeColumnPairs = new Set(); + const fkNodeHandlePairs = new Set(); const fkNodesWithoutHandles = new Set(); for (const edge of edges) { - if (edge.sourceHandle?.startsWith("src-")) { - fkNodeColumnPairs.add(`${edge.source}:${edge.sourceHandle.slice(4)}`); + const edgeData = edge.data as any; + if (edgeData?.sourceColumns) { + for (const col of edgeData.sourceColumns) { + fkNodeColumnPairs.add(`${edge.source}:${col}`); + } + } else if (edge.sourceHandle?.startsWith("src-")) { + fkNodeHandlePairs.add(`${edge.source}:${edge.sourceHandle.slice(4)}`); } else if (!edge.sourceHandle) { fkNodesWithoutHandles.add(edge.source); } - if (edge.targetHandle?.startsWith("tgt-")) { - fkNodeColumnPairs.add(`${edge.target}:${edge.targetHandle.slice(4)}`); + if (edgeData?.targetColumns) { + for (const col of edgeData.targetColumns) { + fkNodeColumnPairs.add(`${edge.target}:${col}`); + } + } else if (edge.targetHandle?.startsWith("tgt-")) { + fkNodeHandlePairs.add(`${edge.target}:${edge.targetHandle.slice(4)}`); } } @@ -48,11 +58,15 @@ export function exportMermaid( let modifiers = ""; if (col.is_pk) modifiers += " PK"; - const safeId = sanitizeHandleId(col.column_name); - // ⚡ Bolt: O(1) lookups instead of O(E) array search for every column - const isFk = - fkNodeColumnPairs.has(`${node.id}:${safeId}`) || - (fkNodesWithoutHandles.has(node.id) && node.data.badges?.fk); + // ⚡ Bolt: Use direct O(1) string matching against edge data to bypass expensive O(N*C) sanitizeHandleId encodings. + let isFk = fkNodeColumnPairs.has(`${node.id}:${col.column_name}`) || + (fkNodesWithoutHandles.has(node.id) && node.data.badges?.fk); + + // Fallback only if there are legacy edges that require handle matching + if (!isFk && fkNodeHandlePairs.size > 0) { + const safeId = sanitizeHandleId(col.column_name); + isFk = fkNodeHandlePairs.has(`${node.id}:${safeId}`); + } if (isFk && !col.is_pk) modifiers += " FK"; diff --git a/frontend/src/erd/prisma.ts b/frontend/src/erd/prisma.ts index 211dfdd8d..66ba51aec 100644 --- a/frontend/src/erd/prisma.ts +++ b/frontend/src/erd/prisma.ts @@ -57,6 +57,7 @@ export function exportPrisma( // Prisma relations require a field on both sides if we want back-relations, // but let's just generate the minimal required relations. const fkNodeColumnPairs = new Set(); + const fkNodeHandlePairs = new Set(); const fkNodesWithoutHandles = new Set(); const incomingRelationsByNode = new Map>(); const edgesProcessed = new Map(); @@ -67,17 +68,23 @@ export function exportPrisma( if (!sourceNode || !targetNode) continue; const relName = sanitizeName(String(edge.label || `${sourceNode.data.title}_${targetNode.data.title}`)); + const edgeData = edge.data as { sourceColumns?: string[], targetColumns?: string[] } | undefined; let sourceField = ""; - if (edge.sourceHandle?.startsWith("src-")) { - sourceField = edge.sourceHandle.slice(4); + if (edgeData?.sourceColumns?.[0]) { + sourceField = edgeData.sourceColumns[0]; fkNodeColumnPairs.add(`${edge.source}:${sourceField}`); + } else if (edge.sourceHandle?.startsWith("src-")) { + sourceField = edge.sourceHandle.slice(4); // Legacy encoded handle + fkNodeHandlePairs.add(`${edge.source}:${sourceField}`); } else if (!edge.sourceHandle) { fkNodesWithoutHandles.add(edge.source); } let targetField = "id"; // fallback - if (edge.targetHandle?.startsWith("tgt-")) { + if (edgeData?.targetColumns?.[0]) { + targetField = edgeData.targetColumns[0]; + } else if (edge.targetHandle?.startsWith("tgt-")) { targetField = edge.targetHandle.slice(4); } @@ -112,9 +119,12 @@ export function exportPrisma( for (const col of node.data.columns) { const fieldName = sanitizeName(col.column_name); - const isFk = - fkNodeColumnPairs.has(`${node.id}:${sanitizeHandleId(col.column_name)}`) || - (fkNodesWithoutHandles.has(node.id) && node.data.badges?.fk); + let isFk = fkNodeColumnPairs.has(`${node.id}:${col.column_name}`) || + (fkNodesWithoutHandles.has(node.id) && node.data.badges?.fk); + + if (!isFk && fkNodeHandlePairs.size > 0) { + isFk = fkNodeHandlePairs.has(`${node.id}:${sanitizeHandleId(col.column_name)}`); + } const prismaType = mapToPrismaType(col.data_type, isFk);