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/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, 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]