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.
## 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) 인코딩 연산을 완전히 우회합니다.
7 changes: 5 additions & 2 deletions frontend/src/erd/exportDataDictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 23 additions & 9 deletions frontend/src/erd/mermaid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,27 @@ export function exportMermaid(
}

const fkNodeColumnPairs = new Set<string>();
const fkNodeHandlePairs = new Set<string>();
const fkNodesWithoutHandles = new Set<string>();

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)}`);
}
}

Expand All @@ -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";

Expand Down
22 changes: 16 additions & 6 deletions frontend/src/erd/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
const fkNodeHandlePairs = new Set<string>();
const fkNodesWithoutHandles = new Set<string>();
const incomingRelationsByNode = new Map<string, Array<{ relationName: string, sourceModel: string, sourceField: string, isUnique: boolean }>>();
const edgesProcessed = new Map<string, { sourceModel: string, targetModel: string, sourceFields: string[], targetFields: string[], relationName: string }>();
Expand All @@ -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);
}
Comment on lines +74 to 89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 User-visible export change missing CHANGELOG entry

This PR changes Prisma export output — foreign-key reference field names now use real column names instead of hex-encoded handle ids — a user-visible behavior change. Neither CHANGELOG.md nor frontend/CHANGELOG.md is updated, which the repo conventions require.

Prompt for agents
CLAUDE.md and CONTRIBUTING.md require user-visible frontend changes to be recorded in CHANGELOG.md (Korean) and frontend/CHANGELOG.md. This PR changes the Prisma export so foreign-key @relation fields/references use real column names rather than hex-encoded handle ids, and adjusts Mermaid/Data Dictionary FK detection. Add appropriate entries to both CHANGELOG.md and frontend/CHANGELOG.md describing the Prisma FK reference fix and the export performance improvement.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +74 to 89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Prisma composite FKs reference only the first column

For a composite foreign key, exportPrisma uses only sourceColumns[0]/targetColumns[0], so the generated @relation references a single field for a multi-column FK. Not a regression (composite edges previously produced no relation), but the emitted schema for composite FKs stays incomplete.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


Expand Down Expand Up @@ -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)}`);
}
Comment on lines +122 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Recomputed isFk has no effect in Prisma

The recomputed isFk is passed to mapToPrismaType, but that function ignores its isFk parameter (prisma.ts:14-39). Relations are driven entirely by edgesProcessed, so this computation is dead. Pre-existing, and unchanged in effect by the diff.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +125 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Prisma tests use raw handles, not real hex-encoded ones

prisma.test.ts exercises the legacy fallback with raw handles like src-user_id, but convert.ts emits hex-encoded handles (src-c-<hex>) via sourceColumnHandleId. The encoded-handle fallback in Prisma is not covered by a test reflecting real convert output; only the Mermaid FK test at mermaid.test.ts:72 uses an encoded handle.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


const prismaType = mapToPrismaType(col.data_type, isFk);

Expand Down
Loading