From e73c976b84a14d7cc039e375fee006cdac663662 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:09:05 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20DBML=20impor?= =?UTF-8?q?t=20column=20positioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 3 +++ backend/app/spec/dbml_import.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index f1a8c146..150c5cb4 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-05-18 - Optimize DBML Import Column Positions +**Learning:** In Python, calculating incremental positions or ordinal rankings inside a loop using inline generator expressions like `sum(1 for i in list if condition)` creates hidden $O(N^2)$ complexity. For operations over schemas with many columns, this causes exponential degradation and severe GC pressure. +**Action:** Always maintain an auxiliary state counter (like a `dict[int, int]` mapping parent OID to current child count) to perform these tallies in $O(1)$ amortized time during sequential processing loops. diff --git a/backend/app/spec/dbml_import.py b/backend/app/spec/dbml_import.py index b93454a9..f4016cb1 100644 --- a/backend/app/spec/dbml_import.py +++ b/backend/app/spec/dbml_import.py @@ -136,6 +136,8 @@ def parse_dbml(text: str) -> dict[str, Any]: in_ignored_block = 0 in_indexes = False + col_counts_by_oid: dict[int, int] = {} + for raw_line in text.splitlines(): # ReDoS guard: no legitimate DBML line approaches this length; capping # input size per regex call bounds worst-case backtracking to O(1). @@ -207,11 +209,13 @@ def parse_dbml(text: str) -> dict[str, Any]: settings = (cm.group("settings") or "").lower() oid = oid_by_table[current] is_pk = bool(re.search(r"\bpk\b|primary\s+key", settings)) + pos = col_counts_by_oid.get(oid, 0) + 1 + col_counts_by_oid[oid] = pos columns.append( { "relation_oid": oid, "column_name": col_name, - "column_position": sum(1 for c in columns if c["relation_oid"] == oid) + 1, + "column_position": pos, "data_type": cm.group("type"), "is_not_null": is_pk or "not null" in settings, "has_default": "default:" in settings, From be079bdd935619896d672c18bcba35c3722608e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 12:47:37 +0900 Subject: [PATCH 2/9] test(dbml): cover large contiguous column positions --- backend/tests/test_dbml_import.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/backend/tests/test_dbml_import.py b/backend/tests/test_dbml_import.py index 2c034bef..cfb64a55 100644 --- a/backend/tests/test_dbml_import.py +++ b/backend/tests/test_dbml_import.py @@ -110,3 +110,32 @@ def test_pathological_table_header_dots_are_rejected_fast(): assert {(r["schema_name"], r["relation_name"]) for r in snap["relations"]} == { ("public", "users") } + + +def test_column_positions_remain_contiguous_per_relation_for_large_import(): + """Keep O(1) position accounting correct across large relation boundaries.""" + + first_count = 1_000 + second_count = 7 + first_columns = "\n".join( + f" first_{column_index} integer" for column_index in range(first_count) + ) + second_columns = "\n".join( + f" second_{column_index} text" for column_index in range(second_count) + ) + dbml_text = ( + f"Table first_table {{\n{first_columns}\n}}\n" + f"Table second_table {{\n{second_columns}\n}}\n" + ) + + snapshot = parse_dbml(dbml_text) + positions_by_relation: dict[int, list[int]] = {} + for column_record in snapshot["columns"]: + positions_by_relation.setdefault(column_record["relation_oid"], []).append( + column_record["column_position"] + ) + + assert positions_by_relation == { + 1: list(range(1, first_count + 1)), + 2: list(range(1, second_count + 1)), + } From 167376594b0609602b162d69f9aba5d9303a003b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 12:47:58 +0900 Subject: [PATCH 3/9] docs(changelog): record linear DBML position accounting --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 679a6202..0deca53f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- [BE] ⚡ **DBML 대규모 컬럼 위치 계산 O(N)화**: relation별 증분 카운터로 `column_position`을 계산해 growing-list 재순회를 제거하고, 1,000개 컬럼과 다중 relation의 연속 위치를 회귀 테스트로 검증합니다. - [BE] 🔒 **공유 export 전 경로 redaction**: 공개 share의 SQL / index-design / reversing-spec export에서 코멘트·`example_value`를 제거합니다. 단위 테스트로 누출을 차단합니다. - [BE] 🛠️ **함수 인덱스 중복 오탐 수정**: `lower(email)` 등 expression index를 평문 컬럼 인덱스의 중복으로 잘못 판단하지 않도록 괄호 파서를 강화했습니다. - [Docs] README를 상용 기준 기능 설명으로 갱신 (MVP skeleton 표현 제거, share redaction·diff/export 반영). From 760f12cc0231ca39b79498705b41ddf65e371c9c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 5 Aug 2026 03:52:06 +0000 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20DBML=20impor?= =?UTF-8?q?t=20column=20positioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 - backend/tests/test_dbml_import.py | 29 ----------------------------- 2 files changed, 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0deca53f..679a6202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## Unreleased -- [BE] ⚡ **DBML 대규모 컬럼 위치 계산 O(N)화**: relation별 증분 카운터로 `column_position`을 계산해 growing-list 재순회를 제거하고, 1,000개 컬럼과 다중 relation의 연속 위치를 회귀 테스트로 검증합니다. - [BE] 🔒 **공유 export 전 경로 redaction**: 공개 share의 SQL / index-design / reversing-spec export에서 코멘트·`example_value`를 제거합니다. 단위 테스트로 누출을 차단합니다. - [BE] 🛠️ **함수 인덱스 중복 오탐 수정**: `lower(email)` 등 expression index를 평문 컬럼 인덱스의 중복으로 잘못 판단하지 않도록 괄호 파서를 강화했습니다. - [Docs] README를 상용 기준 기능 설명으로 갱신 (MVP skeleton 표현 제거, share redaction·diff/export 반영). diff --git a/backend/tests/test_dbml_import.py b/backend/tests/test_dbml_import.py index cfb64a55..2c034bef 100644 --- a/backend/tests/test_dbml_import.py +++ b/backend/tests/test_dbml_import.py @@ -110,32 +110,3 @@ def test_pathological_table_header_dots_are_rejected_fast(): assert {(r["schema_name"], r["relation_name"]) for r in snap["relations"]} == { ("public", "users") } - - -def test_column_positions_remain_contiguous_per_relation_for_large_import(): - """Keep O(1) position accounting correct across large relation boundaries.""" - - first_count = 1_000 - second_count = 7 - first_columns = "\n".join( - f" first_{column_index} integer" for column_index in range(first_count) - ) - second_columns = "\n".join( - f" second_{column_index} text" for column_index in range(second_count) - ) - dbml_text = ( - f"Table first_table {{\n{first_columns}\n}}\n" - f"Table second_table {{\n{second_columns}\n}}\n" - ) - - snapshot = parse_dbml(dbml_text) - positions_by_relation: dict[int, list[int]] = {} - for column_record in snapshot["columns"]: - positions_by_relation.setdefault(column_record["relation_oid"], []).append( - column_record["column_position"] - ) - - assert positions_by_relation == { - 1: list(range(1, first_count + 1)), - 2: list(range(1, second_count + 1)), - } From acbe7ce1158ca282ad54b7d9212d4392ad9ab7c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 16:23:59 +0900 Subject: [PATCH 5/9] test(dbml): verify scaled column ordinals --- backend/tests/test_dbml_import.py | 42 +++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_dbml_import.py b/backend/tests/test_dbml_import.py index 2c034bef..e172e188 100644 --- a/backend/tests/test_dbml_import.py +++ b/backend/tests/test_dbml_import.py @@ -56,10 +56,16 @@ def test_reverse_arrow_and_schema_qualified_and_quoted(): Ref: auth.accounts.account_id < "Order Items".account_id ''' snap = parse_dbml(text) - assert ("auth", "accounts") in {(r["schema_name"], r["relation_name"]) for r in snap["relations"]} + assert ("auth", "accounts") in { + (r["schema_name"], r["relation_name"]) for r in snap["relations"] + } edge = snap["fk_edges"][0] # '<' means the right side references the left - child = next(r for r in snap["relations"] if r["relation_oid"] == edge["child_relation_oid"]) + child = next( + r + for r in snap["relations"] + if r["relation_oid"] == edge["child_relation_oid"] + ) assert child["relation_name"] == "Order Items" @@ -90,6 +96,38 @@ def test_dbml_snapshot_feeds_existing_ddl_export(): assert "PRIMARY KEY" in ddl +def test_column_positions_scale_and_reset_per_relation() -> None: + first_columns = "\n".join(f" column_{index} integer" for index in range(1_000)) + text = f""" +Table wide_relation {{ +{first_columns} +}} +Table second_relation {{ + first_column integer + second_column integer +}} +""" + + snapshot = parse_dbml(text) + relation_oids = { + relation["relation_name"]: relation["relation_oid"] + for relation in snapshot["relations"] + } + wide_positions = [ + column["column_position"] + for column in snapshot["columns"] + if column["relation_oid"] == relation_oids["wide_relation"] + ] + second_positions = [ + column["column_position"] + for column in snapshot["columns"] + if column["relation_oid"] == relation_oids["second_relation"] + ] + + assert wide_positions == list(range(1, 1_001)) + assert second_positions == [1, 2] + + def test_pathological_long_line_is_skipped_fast(): import time From 45527d7fab87c5e3dcd26397eca8771b28f2fb08 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 16:25:09 +0900 Subject: [PATCH 6/9] chore: remove superseded performance journal entry --- .jules/bolt.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 150c5cb4..f1a8c146 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -77,6 +77,3 @@ 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-05-18 - Optimize DBML Import Column Positions -**Learning:** In Python, calculating incremental positions or ordinal rankings inside a loop using inline generator expressions like `sum(1 for i in list if condition)` creates hidden $O(N^2)$ complexity. For operations over schemas with many columns, this causes exponential degradation and severe GC pressure. -**Action:** Always maintain an auxiliary state counter (like a `dict[int, int]` mapping parent OID to current child count) to perform these tallies in $O(1)$ amortized time during sequential processing loops. From 26d324d55d0857c2c773c5e5b7cef9da89daa305 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 16:25:44 +0900 Subject: [PATCH 7/9] docs(changelog): record linear DBML ordinals --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 679a6202..6e32feb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- [BE] ⚡ **DBML 컬럼 순번 계산 선형화**: relation별 O(1) 카운터로 컬럼 위치를 계산하여 대규모 스키마 import의 전체 순번 계산을 O(N²)에서 O(N)으로 줄이고, 1,000개 컬럼과 복수 relation 회귀 테스트로 순서와 relation별 초기화를 검증합니다. - [BE] 🔒 **공유 export 전 경로 redaction**: 공개 share의 SQL / index-design / reversing-spec export에서 코멘트·`example_value`를 제거합니다. 단위 테스트로 누출을 차단합니다. - [BE] 🛠️ **함수 인덱스 중복 오탐 수정**: `lower(email)` 등 expression index를 평문 컬럼 인덱스의 중복으로 잘못 판단하지 않도록 괄호 파서를 강화했습니다. - [Docs] README를 상용 기준 기능 설명으로 갱신 (MVP skeleton 표현 제거, share redaction·diff/export 반영). From 0026071ade73d06b509e799eb0356f58ea3ef036 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:48:28 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20DBML=20impor?= =?UTF-8?q?t=20column=20positioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 - backend/tests/test_dbml_import.py | 42 ++----------------------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e32feb7..679a6202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## Unreleased -- [BE] ⚡ **DBML 컬럼 순번 계산 선형화**: relation별 O(1) 카운터로 컬럼 위치를 계산하여 대규모 스키마 import의 전체 순번 계산을 O(N²)에서 O(N)으로 줄이고, 1,000개 컬럼과 복수 relation 회귀 테스트로 순서와 relation별 초기화를 검증합니다. - [BE] 🔒 **공유 export 전 경로 redaction**: 공개 share의 SQL / index-design / reversing-spec export에서 코멘트·`example_value`를 제거합니다. 단위 테스트로 누출을 차단합니다. - [BE] 🛠️ **함수 인덱스 중복 오탐 수정**: `lower(email)` 등 expression index를 평문 컬럼 인덱스의 중복으로 잘못 판단하지 않도록 괄호 파서를 강화했습니다. - [Docs] README를 상용 기준 기능 설명으로 갱신 (MVP skeleton 표현 제거, share redaction·diff/export 반영). diff --git a/backend/tests/test_dbml_import.py b/backend/tests/test_dbml_import.py index e172e188..2c034bef 100644 --- a/backend/tests/test_dbml_import.py +++ b/backend/tests/test_dbml_import.py @@ -56,16 +56,10 @@ def test_reverse_arrow_and_schema_qualified_and_quoted(): Ref: auth.accounts.account_id < "Order Items".account_id ''' snap = parse_dbml(text) - assert ("auth", "accounts") in { - (r["schema_name"], r["relation_name"]) for r in snap["relations"] - } + assert ("auth", "accounts") in {(r["schema_name"], r["relation_name"]) for r in snap["relations"]} edge = snap["fk_edges"][0] # '<' means the right side references the left - child = next( - r - for r in snap["relations"] - if r["relation_oid"] == edge["child_relation_oid"] - ) + child = next(r for r in snap["relations"] if r["relation_oid"] == edge["child_relation_oid"]) assert child["relation_name"] == "Order Items" @@ -96,38 +90,6 @@ def test_dbml_snapshot_feeds_existing_ddl_export(): assert "PRIMARY KEY" in ddl -def test_column_positions_scale_and_reset_per_relation() -> None: - first_columns = "\n".join(f" column_{index} integer" for index in range(1_000)) - text = f""" -Table wide_relation {{ -{first_columns} -}} -Table second_relation {{ - first_column integer - second_column integer -}} -""" - - snapshot = parse_dbml(text) - relation_oids = { - relation["relation_name"]: relation["relation_oid"] - for relation in snapshot["relations"] - } - wide_positions = [ - column["column_position"] - for column in snapshot["columns"] - if column["relation_oid"] == relation_oids["wide_relation"] - ] - second_positions = [ - column["column_position"] - for column in snapshot["columns"] - if column["relation_oid"] == relation_oids["second_relation"] - ] - - assert wide_positions == list(range(1, 1_001)) - assert second_positions == [1, 2] - - def test_pathological_long_line_is_skipped_fast(): import time From 38aeaf2f0901e465a014940d75c028af29a87770 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:14:06 +0000 Subject: [PATCH 9/9] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Restore=20missing=20tes?= =?UTF-8?q?ts=20and=20changelog=20for=20DBML=20positioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + backend/tests/test_dbml_import.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 679a6202..0deca53f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- [BE] ⚡ **DBML 대규모 컬럼 위치 계산 O(N)화**: relation별 증분 카운터로 `column_position`을 계산해 growing-list 재순회를 제거하고, 1,000개 컬럼과 다중 relation의 연속 위치를 회귀 테스트로 검증합니다. - [BE] 🔒 **공유 export 전 경로 redaction**: 공개 share의 SQL / index-design / reversing-spec export에서 코멘트·`example_value`를 제거합니다. 단위 테스트로 누출을 차단합니다. - [BE] 🛠️ **함수 인덱스 중복 오탐 수정**: `lower(email)` 등 expression index를 평문 컬럼 인덱스의 중복으로 잘못 판단하지 않도록 괄호 파서를 강화했습니다. - [Docs] README를 상용 기준 기능 설명으로 갱신 (MVP skeleton 표현 제거, share redaction·diff/export 반영). diff --git a/backend/tests/test_dbml_import.py b/backend/tests/test_dbml_import.py index 2c034bef..e76a43bb 100644 --- a/backend/tests/test_dbml_import.py +++ b/backend/tests/test_dbml_import.py @@ -110,3 +110,33 @@ def test_pathological_table_header_dots_are_rejected_fast(): assert {(r["schema_name"], r["relation_name"]) for r in snap["relations"]} == { ("public", "users") } +def test_column_positions_scale_and_reset_per_relation() -> None: + first_columns = "\n".join(f" column_{index} integer" for index in range(1_000)) + text = f""" +Table wide_relation {{ +{first_columns} +}} +Table second_relation {{ + first_column integer + second_column integer +}} +""" + + snapshot = parse_dbml(text) + relation_oids = { + relation["relation_name"]: relation["relation_oid"] + for relation in snapshot["relations"] + } + wide_positions = [ + column["column_position"] + for column in snapshot["columns"] + if column["relation_oid"] == relation_oids["wide_relation"] + ] + second_positions = [ + column["column_position"] + for column in snapshot["columns"] + if column["relation_oid"] == relation_oids["second_relation"] + ] + + assert wide_positions == list(range(1, 1_001)) + assert second_positions == [1, 2]