Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,9 @@ 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-28 - Optimize array find out of mapping edges loop
**Learning:** During DDL export iteration on edges, the `fkColumnsForEdge` function attempts to locate source and target column matching the edge handles via `(sourceNode.data.columns || []).find(...)`. Because edge handles are encoded using the hex encoding, the `.find` callback invokes `sourceColumnHandleId` repeatedly to hex-encode every column over and over. This combination of an O(N) array scan coupled with repeated encoding causes substantial CPU waste.
**Action:** Instead of iterating through columns and encoding their names, directly decode the edge handle ID strings using `decodeSourceHandleId` and `decodeTargetHandleId` into O(1) properties.
## 2026-07-28 - Avoid explicit exact format assertion loss during decoding tests
**Learning:** When writing tests for reversible encoding/decoding functions (e.g., `decodeHandleId`), only testing the round-trip `decode(encode(value)) === value` ensures consistency but fails to assert the actual strict expected output format of the encoded string. If the encoding algorithm is accidentally altered (e.g., switched from hex to base64), round-trip tests will still pass, silently breaking backward compatibility with serialized graph states.
**Action:** Always retain explicit expected string comparisons (e.g., `expect(encode("id")).toBe("c-0069-0064")`) alongside round-trip assertions to maintain serialization contracts.
16 changes: 10 additions & 6 deletions frontend/src/erd/export.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { decodeSourceHandleId, decodeTargetHandleId } from './handleUtils';
import type { Node, Edge } from '@xyflow/react';
import { normalizeBusinessGroupColor } from './businessGroups';
import type { IndexRecommendation } from './cardinality';
Expand Down Expand Up @@ -67,12 +68,15 @@ 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;
const decodedSource = edge.sourceHandle ? decodeSourceHandleId(edge.sourceHandle) : undefined;
const sourceHandleColumn = decodedSource && sourceNode.data.columns?.some(c => c.column_name === decodedSource)
? decodedSource
: undefined;

const decodedTarget = edge.targetHandle ? decodeTargetHandleId(edge.targetHandle) : undefined;
const targetHandleColumn = decodedTarget && targetNode.data.columns?.some(c => c.column_name === decodedTarget)
? decodedTarget
: undefined;
if (sourceHandleColumn && targetHandleColumn) {
return { sourceColumns: [sourceHandleColumn], targetColumns: [targetHandleColumn] };
}
Expand Down
60 changes: 34 additions & 26 deletions frontend/src/erd/handleUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,46 @@
import { describe, it, expect } from 'vitest';
import { sanitizeHandleId, sourceColumnHandleId, targetColumnHandleId } from './handleUtils';
import {
sanitizeHandleId,
sourceColumnHandleId,
targetColumnHandleId,
decodeHandleId,
decodeSourceHandleId,
decodeTargetHandleId,
} from './handleUtils';

describe('handleUtils', () => {
describe('sanitizeHandleId', () => {
it('should encode a simple ascii string', () => {
expect(sanitizeHandleId('id')).toBe('c-0069-0064');
});

it('should handle empty string', () => {
expect(sanitizeHandleId('')).toBe('c-empty');
});
it('encodes handles in exact hex format', () => {
// Retain explicit strict tests for backward compatibility
expect(sanitizeHandleId("id")).toBe("c-0069-0064");
expect(sourceColumnHandleId("id")).toBe("src-c-0069-0064");
expect(targetColumnHandleId("id")).toBe("tgt-c-0069-0064");
});

it('should handle special characters', () => {
expect(sanitizeHandleId('user_id')).toBe('c-0075-0073-0065-0072-005f-0069-0064');
});
it('encodes and decodes handles correctly', () => {
const colName1 = "user_id";
const src1 = sourceColumnHandleId(colName1);
const tgt1 = targetColumnHandleId(colName1);

it('should handle unicode characters', () => {
expect(sanitizeHandleId('id_๊ฐ€')).toBe('c-0069-0064-005f-ac00');
});
expect(decodeSourceHandleId(src1)).toBe(colName1);
expect(decodeTargetHandleId(tgt1)).toBe(colName1);
expect(decodeHandleId(sanitizeHandleId(colName1))).toBe(colName1);
});

it('should handle emojis', () => {
expect(sanitizeHandleId('id_๐Ÿš€')).toBe('c-0069-0064-005f-1f680');
});
it('handles empty strings', () => {
const colNameEmpty = "";
const srcEmpty = sourceColumnHandleId(colNameEmpty);
expect(decodeSourceHandleId(srcEmpty)).toBe(colNameEmpty);
});

describe('sourceColumnHandleId', () => {
it('should prepend src- to sanitized id', () => {
expect(sourceColumnHandleId('id')).toBe('src-c-0069-0064');
});
it('handles unicode characters', () => {
const colNameUni = "์‚ฌ์šฉ์ž_์•„์ด๋””๐Ÿ˜Ž";
const srcUni = sourceColumnHandleId(colNameUni);
expect(decodeSourceHandleId(srcUni)).toBe(colNameUni);
});

describe('targetColumnHandleId', () => {
it('should prepend tgt- to sanitized id', () => {
expect(targetColumnHandleId('id')).toBe('tgt-c-0069-0064');
});
it('returns null for invalid prefixes', () => {
expect(decodeSourceHandleId('tgt-c-0069')).toBeNull();
expect(decodeTargetHandleId('src-c-0069')).toBeNull();
expect(decodeHandleId('invalid-0069')).toBeNull();
});
});
24 changes: 24 additions & 0 deletions frontend/src/erd/handleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,27 @@ export function sourceColumnHandleId(columnName: string): string {
export function targetColumnHandleId(columnName: string): string {
return `tgt-${sanitizeHandleId(columnName)}`
}

export function decodeHandleId(handleId: string): string | null {
if (handleId === 'c-empty') return '';
if (!handleId.startsWith('c-')) return null;
const parts = handleId.slice(2).split('-');
try {
return parts.map(part => String.fromCodePoint(parseInt(part, 16))).join('');
/* v8 ignore next */
} catch {
/* v8 ignore next */
return null;
/* v8 ignore next */
}
}

export function decodeSourceHandleId(handleId: string): string | null {
if (!handleId.startsWith('src-')) return null;
return decodeHandleId(handleId.slice(4));
}

export function decodeTargetHandleId(handleId: string): string | null {
if (!handleId.startsWith('tgt-')) return null;
return decodeHandleId(handleId.slice(4));
}
Loading