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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ 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-13 - Optimize O(N * C * E) Complexity in Prisma Export Dictionary
**Learning:** Nested array iterations (`for (const [_, edgeInfo] of edgesProcessed)`) inside loops parsing graph models and fields (`node.data.columns`) result in O(N * C * E) complexity, creating significant bottlenecks during Prisma schema export for large graphs.
**Action:** Replace repeated linear edge evaluations by precomputing an O(1) Lookup Map (`edgesProcessedBySource.set(sourceKey, edgeInfo)`) before looping over fields, bringing complexity down to O(E + N * C).
21 changes: 14 additions & 7 deletions frontend/src/erd/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export function exportPrisma(
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 }>();
// ⚑ Bolt: Precompute edge lookups by sourceModel+sourceField to avoid O(N*C*E) loops
const edgesProcessedBySource = new Map<string, Array<{ sourceModel: string, targetModel: string, sourceFields: string[], targetFields: string[], relationName: string }>>();

for (const edge of edges) {
const sourceNode = nodesById.get(edge.source);
Expand Down Expand Up @@ -93,13 +95,19 @@ export function exportPrisma(
});
incomingRelationsByNode.set(edge.target, relList);

edgesProcessed.set(edge.id, {
const edgeInfo = {
sourceModel: sanitizeName(sourceNode.data.title),
targetModel: sanitizeName(targetNode.data.title),
sourceFields: [sanitizeName(sourceField)],
targetFields: [sanitizeName(targetField)],
relationName: relName
});
};
edgesProcessed.set(edge.id, edgeInfo);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ” Redundant edge index remains allocated

edgesProcessed has no readers after this change, but each edge still enters it. Large exports retain two edge indexes instead of one.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.


const sourceKey = `${edgeInfo.sourceModel}:${edgeInfo.sourceFields[0]}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ”΄ Snapshot foreign keys lack forward relations

For normal snapshot edges, edgesProcessedBySource indexes encoded handles instead of column names. The field lookup uses sanitizeName(col.column_name), so exported Prisma models omit their forward relations.

Learn more

Graph edges created by snapshotToGraph use src-c-... handles from sourceColumnHandleId. The exporter removes src-, then indexes c_... after sanitization; a column named user_id is looked up as user_id. The original edge scan had the same mismatch, but the new index preserves it in the changed relation path. Back-relations are still emitted, leaving a broken one-sided Prisma relation.

Example: A posts.user_id foreign key gets handle src-c-0075-0073-0065-0072-005f-0069-0064. Its index key ends with c_0075_..., while the posts.user_id field lookup ends with user_id; no users_user_id relation field appears.

Recommended fix: Resolve sourceHandle and targetHandle against each node's columns using sourceColumnHandleId and targetColumnHandleId before building relation keys and references. Also support legacy unencoded handles where needed. Add a test with an edge returned by snapshotToGraph.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

const fieldEdges = edgesProcessedBySource.get(sourceKey) || [];
fieldEdges.push(edgeInfo);
edgesProcessedBySource.set(sourceKey, fieldEdges);
}
}

Expand Down Expand Up @@ -136,12 +144,11 @@ export function exportPrisma(

// Determine if there is a relation defined on this field
let relationDef = "";
for (const [_, edgeInfo] of edgesProcessed) {
if (edgeInfo.sourceModel === modelName && edgeInfo.sourceFields.includes(fieldName)) {
// This field is a foreign key, but in Prisma, we typically define the relation object field
// alongside the scalar field. We will add the relation object field here.
const fieldEdges = edgesProcessedBySource.get(`${modelName}:${fieldName}`);
if (fieldEdges) {
for (const edgeInfo of fieldEdges) {
const relField = sanitizeName(edgeInfo.targetModel) + "_" + fieldName;
relationDef = `\n ${relField} ${edgeInfo.targetModel}${optional} @relation("${edgeInfo.relationName}", fields: [${fieldName}], references: [${edgeInfo.targetFields[0]}])`;
relationDef += `\n ${relField} ${edgeInfo.targetModel}${optional} @relation("${edgeInfo.relationName}", fields: [${fieldName}], references: [${edgeInfo.targetFields[0]}])`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟑 Repeated foreign keys invalidate Prisma models

When two edges connect one source field to the same target model, relationDef emits both with the same name. Prisma rejects the duplicate field, whereas the previous exporter emitted only the last relation.

Learn more

Each matching edge contributes a Prisma relation field named from its target model and source field. Appending every matching edge creates the same field name twice when edges share both values. The previous exporter overwrote relationDef and produced just one forward field, even though it retained both back-relations.

Example: Two edges with different IDs use sourceHandle: 'src-user_id' from posts to users. Both generate users_user_id inside posts, and Prisma rejects the exported model as having duplicate fields.

Recommended fix: Decide how to represent multiple relations on the same scalar field, and ensure generated relation field names are unique and correspond to matching back-relations. Cover two edges with the same source field and target model in the exporter tests.

Devin Review


Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

}
}

Expand Down
Loading