From 01a756335bc0aaabf9d31aacaa76d423609cfc0d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:54:27 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0]=20FK=20=ED=95=B8=EB=93=A4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20O(1)=20Map=20?= =?UTF-8?q?=EC=82=AC=EC=A0=84=20=EA=B3=84=EC=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’‘ What: `exportDDL`μ—μ„œ ν…Œμ΄λΈ” 컬럼 정보λ₯Ό μ‘°νšŒν•˜μ—¬ O(1) ν•΄μ‹œ λ§΅(`columnByHandle`)을 μƒμ„±ν•˜κ³  이λ₯Ό `fkColumnsForEdge`에 μ „λ‹¬ν•˜λ„λ‘ λ³€κ²½ν–ˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, `fkColumnsForEdge` λ‚΄λΆ€μ˜ λ°°μ—΄ 체이닝(`.filter().map()`, `.find()`)을 단일 반볡문(`for`) 및 O(1) Map 쑰회둜 λŒ€μ²΄ν–ˆμŠ΅λ‹ˆλ‹€. 🎯 Why: κΈ°μ‘΄ 방식은 κ·Έλž˜ν”„μ˜ κ°„μ„ (Edges, $E$)λ§ˆλ‹€ 각 λ…Έλ“œμ˜ 컬럼($C$)을 λ°°μ—΄ μ²΄μ΄λ‹μ΄λ‚˜ `.find()`둜 κ²€μƒ‰ν•˜λ―€λ‘œ $O(E \times C)$의 μ‹œκ°„ λ³΅μž‘λ„μ™€ λΆˆν•„μš”ν•œ κ°€λΉ„μ§€ μ»¬λ ‰μ…˜ 뢀담이 λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. O(1) μ‘°νšŒμ™€ 단일 반볡문으둜 λ³€κ²½ν•˜λ©΄ 큰 λ‹€μ΄μ–΄κ·Έλž¨ λ‚΄λ³΄λ‚΄κΈ°μ—μ„œ μ„±λŠ₯이 크게 κ°œμ„ λ©λ‹ˆλ‹€. πŸ“Š Impact: κ°„μ„  처리 루프 λ‚΄ λ°°μ—΄ 탐색이 O(C)μ—μ„œ O(1)둜 λ‹¨μΆ•λ©λ‹ˆλ‹€. 쀑간 λ°°μ—΄ 할당이 μ œκ±°λ˜μ–΄ λ©”λͺ¨λ¦¬ 압박이 κ°μ†Œν•˜κ³  큰 μŠ€ν‚€λ§ˆ DDL 내보내기 μ„±λŠ₯이 ν–₯μƒλ©λ‹ˆλ‹€. πŸ”¬ Measurement: ν”„λ‘ νŠΈμ—”λ“œ μœ λ‹› ν…ŒμŠ€νŠΈ(Vitest) 및 TypeScript λΉŒλ“œκ°€ 정상 μž‘λ™ν•¨μ„ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. μ—£μ§€ ν…ŒμŠ€νŠΈ(`coverageEdges.test.ts`)κ°€ μ„±κ³΅μ μœΌλ‘œ ν†΅κ³Όν•©λ‹ˆλ‹€. --- .jules/bolt.md | 3 ++ frontend/src/erd/export.ts | 59 +++++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 13 deletions(-) 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);