Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 반영).
Expand Down
6 changes: 5 additions & 1 deletion backend/app/spec/dbml_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions backend/tests/test_dbml_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading