Skip to content

Django / GORM exporter 완결성 보완 - #169

Open
2heunxun wants to merge 114 commits into
dev-five-git:mainfrom
2heunxun:main
Open

2heunxun wants to merge 114 commits into
dev-five-git:mainfrom
2heunxun:main

Conversation

@2heunxun

Copy link
Copy Markdown

요약

Django, GORM exporter를 검토해서 발견한 부족한 점 7가지를 전부 구현했습니다.
관계(FK) 코드생성이 두 백엔드에서 부분적으로만 지원되고 있었고, 공유 테스트
스위트에도 편입돼 있지 않았습니다. 이번 작업으로 두 백엔드를 나머지 4개
ORM(SeaORM/SQLAlchemy/SQLModel/JPA)과 동등한 수준으로 끌어올렸습니다.

변경 사항

  1. Django M2M 정션 테이블 인식 — 복합 PK + FK 2개 이상인 정션 테이블을
    감지해서 ManyToManyField(..., through=...)를 양쪽에 생성. 기존에는
    스키마 컨텍스트를 아예 무시하고 있었음.
  2. Django/GORM 복합(다중 컬럼) FK 지원 — GORM은 실제 관계 필드로
    (foreignKey:...;references:...), Django는 네이티브 지원이 없어서 주석으로
    관계 정보를 남기도록 처리 (기존엔 컬럼만 남고 관계 정보가 조용히 사라짐).
  3. GORM 자기참조 FK 버그 수정 — 계층구조 스키마(parent_id 같은)에서
    역방향(has-many) 관계가 아예 생성되지 않던 버그 발견 및 수정.
  4. Django/GORM을 공유 cross-ORM 스냅샷 스위트에 편입 — 기존엔 4개 ORM만
    공유 테스트로 교차검증되고 있었음. 편입 과정에서 Django의 실제 버그 2개를
    추가로 발견:
    • 인식 못 하는 SQL 상수 default가 정의되지 않은 Python 이름으로 그대로
      출력되던 문제 (import 시점에 크래시)
    • auto-increment PK 컬럼에 primary_key=True가 누락되던 문제 (Django
      자체 시스템 체크(fields.E100)에 걸림 — auto PK를 쓰는 거의 모든
      스키마에 영향)
  5. DjangoConfig / GormConfig 설정 섹션 추가vespertide.json에서
    Django app_label, GORM package_name을 커스터마이징할 수 있도록 지원
    (기존엔 SeaORM만 이런 설정 진입점이 있었음).
  6. Django clean_export_dir 회귀 테스트 추가 — 다른 ORM들은 다 있었는데
    Django만 빠져 있었음.
  7. 문서 업데이트 — README/AGENTS.md에 JPA/GORM/Django 반영 (GORM은
    머지된 지 오래됐는데도 문서에 전혀 언급이 안 되고 있었음).
  8. (추가 발견) Django 복합 PK 표현 버그 — 위 작업을 다 마친 뒤 Django를
    다른 5개 백엔드와 다시 비교 검토하다 발견. 복합 PK 테이블에서 어떤
    필드에도 primary_key=True가 안 붙고 Meta에도 아무 표시가 없어서,
    Django가 자체적으로 엉뚱한 auto id PK를 암묵적으로 추가해버리는
    문제였음 (실제 DB의 PK와 전혀 안 맞음). GORM/SeaORM은 둘 다 이미 제대로
    처리하고 있어서 비교하다 바로 드러남. Django 5.2+ 의 네이티브
    pk = models.CompositePrimaryKey(...)로 수정.

커버리지

위 작업 도중 실제 coverage 회귀(99.92%)가 발생한 걸 CI 실패로 확인하고,
Docker로 CI와 동일한 환경(특수 rustfmt 설정 + RUST_TEST_THREADS=1 +
PROPTEST_CASES=1024)을 재현해서 정밀 진단 후 전부 수정했습니다.
최종적으로 로컬 재현 환경에서 100.00% (12234/12234 lines) 확인.

검증

  • cargo test --workspace 전체 통과
  • cargo clippy --workspace -- -D warnings 클린
  • cargo fmt --all --check 클린
  • scripts/check-line-budget.sh 통과
  • cargo tarpaulin --engine llvm --fail-under 100 — 100% (Docker로 CI 환경
    동일 재현하여 확인)
  • CI 실제 실행 결과: coverage 포함 전 항목 통과 (codecov 업로드만 별도
    CODECOV_TOKEN 미설정 이슈로 실패 — 코드와 무관)

2heunxun and others added 30 commits May 16, 2026 19:04
Conflict resolution: kept GORM exporter support added in fork
while integrating upstream's 0.2.0 API changes, LSP features,
newtype identifiers, and refactored test structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds tests for all branches identified as uncovered (59 lines):
- Django: SmallAutoField, BigAutoField, Macaddr, Numeric, Custom type,
  UUID functional default, export() multi-table, nullable FK with db_column
- GORM: conflicting enum qualified names, Char type tag, FK relation field
  name collision, reverse relation disambiguation (two FKs same target)
- CLI: OrmArg::Django mapping, build_output_path Gorm .go extension,
  clean_export_dir Gorm .go cleanup

Also removes the erroneous targets line from rust-toolchain.toml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `cache-key: no-musl` to every `setup-rust-toolchain@v1` step that
does not already specify an explicit cross-compilation target.  This
changes the Rust toolchain cache key so that the previous cache (written
when rust-toolchain.toml briefly had `targets = ["x86_64-unknown-linux-musl"]`)
is not restored, eliminating the recurring
"override toolchain 'stable-x86_64-unknown-linux-musl' is not installed" error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sea-orm v2.0.0-rc.42 drops its dependency on ouroboros v0.18.5 (and
aliasable, id-arena). ouroboros has a RUSTSEC advisory for unsound
self-referential structs; without an explicit ignore entry in deny.toml
the cargo-deny CI gate was flagging it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h 100%

Django: build_default Bool(false) and functional-default-on-non-special-type
branches; reference_action_str Restrict/SetDefault/NoAction arms; column
comment rendering; unnamed Index and unnamed composite UniqueConstraint in
Meta; to_pascal_case None arm via double-underscore input.

GORM: Numeric column (add_column_type, build_gorm_tag, decimal import);
unnamed and named Index (collect_index_info body + build_gorm_tag loop);
auto-named composite unique (collect_composite_unique_info None closure);
singular source-table plural (find_reverse_relations format!("{pascal}s"));
FK on_update body + nullable FK pointer type (render_fk_relation_field);
reference_action_str SetNull/SetDefault/NoAction; to_pascal_case None arm
via double-underscore table name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enerator

arb_default_string() could produce reserved words like "in" as bare SQL
DEFAULT expressions, causing pg_query to reject the emitted CREATE TABLE
with "syntax error at or near 'in'". Added is_pg_reserved_keyword() (full
PG 17 §C.1 Type-A list) and a prop_filter on the bare-ident branch so the
strategy only generates non-reserved identifiers as unquoted defaults.

Also fixes fmt issues in the coverage tests added in the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- erd: add test for inline FK referencing absent parent table, covering
  the None early-return branch of inline_foreign_key_relation
- gorm/django: convert single-expression #[cfg(not(tarpaulin_include))]
  arms to block form so tarpaulin's source-level exclusion correctly
  identifies and skips the non_exhaustive future-variant guards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bution

erd: normalize_tables' map(closure).collect() becomes an explicit for loop
(the map closure was the same LLVM source-coverage attribution blind spot
documented in this crate's AGENTS.md; the prior fix only made the inner
with_context closure eager but left the outer map closure in place).

django: replace the enum max_length iterator chain (map(String::len).max())
with an explicit loop, and fold the single-statement primary_key/unique
kwargs into a for-loop over conditions instead of standalone trivial ifs.

gorm: render_enum's match was the last statement of a unit-returning
function; convert to sequential if let with an early return so the function
body ends on a plain statement instead of a match tail-expression.

All three spots are proven to execute today (existing snapshots already show
unique=True, max_length=9, and rendered enum consts), so this is a pure
coverage-attribution fix with no behavior change — snapshots are byte-identical
and all local build/test/clippy/fmt checks pass. Local tarpaulin isn't
runnable on Windows, so the 100% gate result is confirmed by CI on push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diagnosed via a local tarpaulin run (Docker, same xd009642/tarpaulin
container CI uses) inspecting raw LLVM coverage regions instead of
guessing from reformatted line numbers. Each of the 5 previously-uncovered
lines had a distinct, verified cause:

- erd: normalize_tables' `?` error-propagation branch was never exercised
  by any test (all existing tests only feed valid tables). Added a test
  with a malformed inline FK reference to trigger normalize()'s Err path.
- erd: collect_foreign_key_relations' table-level FK `let-else { continue }`
  branch (absent referenced table) was untested — only the *inline* FK
  equivalent had a test (from 584b04b). Added the table-level counterpart.
- django: build_default's Bool(true) path was never tested (only
  Bool(false) was). Added test_bool_true_default.
- django: build_default's `return match { guarded-arm => {...} }` construct
  had a proven LLVM gap-region artifact on the match/guard header lines
  (arm bodies demonstrably execute via existing tests, e.g.
  test_server_default_timezone). Restructured into plain if-chains.
- gorm: render_enum's trailing if-let block's closing brace showed 0 hits
  despite its body executing (same gap-region artifact); restructured to
  collect into a Vec and lines.extend() it as a genuine trailing statement.
- gorm: go_base_type's ComplexColumnType::Enum arm was genuinely dead code
  (its only caller, go_type_for_column_mapped, already intercepts Enum
  before ever calling go_base_type) — removed.

Verified locally end-to-end: cargo tarpaulin --engine llvm against the
exact CI container reports erd/mod.rs 212/212, django/types.rs 94/94,
gorm/mod.rs 289/289 (100% each), with no other regressions across either
crate. cargo build/test/clippy/fmt and the line-budget check all pass on
the real (normally-formatted) source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI fmt job flagged these two spots as unformatted (from 510e868); rustfmt
--check now passes locally with no other diffs.
…hema

DjangoExporter previously fell back to the OrmExporter trait's default
render_entity_with_schema (schema-context ignored), so composite-PK
junction tables never produced a ManyToManyField on either side. Detect
2-FK junction tables (mirroring the SeaORM junction-detection pattern)
and emit ManyToManyField(..., through=..., related_name="+") for both
sides, with _via_<junction> disambiguation when multiple junctions link
the same pair of tables. Purely self-referential junctions are skipped
rather than guessed at.
Both exporters only recognized single-column FKs (columns.len() == 1),
so composite FKs silently dropped to plain scalar columns with no
relation info at all.

- GORM: composite FKs are a genuine native feature (comma-separated
  foreignKey/references tags), so emit a real belongs-to relation field,
  with numeric-suffix disambiguation on field-name collisions.
- Django: there is no native multi-column FK field, so emit a comment
  documenting the relationship instead of silently dropping it; the
  underlying columns still render normally and referential integrity is
  enforced by the generated database schema. Reuses the existing
  crate::utils::python::collect_composite_fks helper already shared with
  SQLAlchemy.
find_reverse_relations() skipped any "other" table equal to the current
table name, which meant a self-referencing FK (e.g. categories.parent_id
-> categories.id) only ever produced the forward belongs-to relation
("Parent"), never the reverse has-many ("Children"). The forward FK and
reverse-scan loop are independent, so the skip was unconditionally
dropping half of every self-referential relationship.

Removed the skip and special-cased self-ref naming to "Children" instead
of a pluralized table name (which would otherwise collide with the
struct's own name). Added regression tests for both directions, split
into gorm/tests/relations.rs (composite-FK + self-ref tests) to keep
gorm/tests/mod.rs under the 1200-line test-file budget.
… suite

Extends orm_cases! (60 fixtures) and render_entity_with_schema_snapshots
(14 relation-heavy scenarios) to render through Gorm/Django alongside the
existing 4 ORMs, matching the project's "every scenario cross-compared
across all ORMs" convention. Adds 120 new baseline snapshots; all render
successfully with no panics.

Reviewing the new baselines surfaced two real, pre-existing Django
correctness bugs (never caught because Django had zero cross-ORM fixture
coverage before this):

1. build_default()'s final fallback emitted unrecognized SQL constants
   verbatim as a bare Python identifier (e.g. `default=SOME_CONSTANT`),
   which is an undefined name and would crash at Django import time. Now
   omits the default unless it parses as a numeric literal.
2. Any auto-increment primary key (AutoField/SmallAutoField/BigAutoField)
   was rendered WITHOUT `primary_key=True` on the assumption that the
   Auto*Field type alone implies it — it does not. Django's own system
   checks (fields.E100) reject an explicit AutoField without
   primary_key=True, so every auto-PK schema this exporter has ever
   produced was invalid at `manage.py check`. Fixed by always emitting
   primary_key=True when the column is the (non-composite) PK; removed
   the now-dead is_auto_field() helper and the auto_increment parameter
   it existed solely to feed.
Django and GORM had no vespertide.json config surface at all, unlike
SeaOrmConfig. Adds two minimal, well-scoped knobs mirroring
SeaOrmExporterWithConfig's existing pattern:

- DjangoConfig.app_label: optional explicit `app_label` written into
  every generated model's Meta class, for projects where models don't
  live inside a standard Django app package (Django can't infer the
  label there). Omitted from Meta and from JSON when None.
- GormConfig.package_name: Go package name emitted at the top of every
  file (`package <name>`), default "models".

Both structs are #[non_exhaustive] and threaded through new
DjangoExporterWithConfig / GormExporterWithConfig wrappers, wired into
the CLI's cmd_export alongside the existing SeaOrmExporterWithConfig
special-case. Regenerated schemas/config.schema.json (schema-drift CI
gate) to include the two new sections.
SeaOrm/SqlAlchemy/SqlModel/Jpa/Gorm each had a dedicated
clean_export_dir_removes_*_for_* test; Django was the only export
target without one, even though it shares the .py cleanup path with
SqlAlchemy/SqlModel.
@yyuneu

yyuneu commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

@2heunxun 님의 작업을 중간부터 제가 이어받아 마무리했습니다. 앞으로의 리뷰 대응도 제가 하겠습니다.

브랜치는 그대로 두고 커밋만 추가했습니다.

PR 본문의 GormConfig 설명 등 일부 내용은 현재 코드와 맞지 않습니다.

이어받은 뒤 생성물을 실제 Django와 Go 툴체인에 넣어 확인해 보니, 체크나 컴파일 단계에서 걸리는 출력이 적지 않았습니다. 그 때문에 리뷰에서 지적해 주신 부분 외에도 수정 범위가 많이 늘어났습니다. 아래에는 리뷰 지적 반영과 그 밖의 변경을 나누어 적었습니다.

non_exhaustive 제거

말씀하신 대로 SimpleColumnTypeReferenceAction에서 #[non_exhaustive]를 제거하고, 미러 enum SimpleColumnKind·ReferenceActionKind를 삭제했습니다. exporter는 원래 타입을 그대로 exhaustive하게 매치합니다.

이 변경으로 필요 없어진 와일드카드 arm 12곳(query 3, exporter 8, planner 1)도 함께 제거했고, schemas/의 JSON Schema는 description만 재생성되었습니다. 0.x 기준 breaking change이므로 changepack에 기재했습니다.

ComplexColumnType은 범위를 넓히지 않기 위해 #[non_exhaustive]를 그대로 두었습니다. 따라서 새 두 백엔드에도 다른 백엔드와 동일한 _ => unreachable!(…) arm이 하나씩 있습니다. 같은 방향으로 정리하는 편이 낫다면 이 PR에서 함께 처리하겠습니다.

그 밖의 리뷰 지적 반영

  • gorm 설정 섹션을 제거했습니다. 패키지명은 export 디렉터리 이름에서만 정해지며, 해당 추론도 config가 아니라 exporter 안에 있습니다. Django의 app_label은 유지했고, 이유는 해당 스레드에 적었습니다.
  • 테스트 전용 re-export를 제거하고 테스트는 #[cfg(test)] mod tests 안으로 옮겼습니다.
  • Django enum의 라벨 인자를 제거했습니다.
  • CLAUDE.md, CHANGELOG.md, .github, .idea, .gitignore, JPA 스냅샷, erd/mod.rs는 upstream과 동일해져 현재 diff에 없습니다. upstream main을 병합해 충돌도 해소했습니다.
  • 랜딩 페이지 관련 지적은 2heunxun 님이 스레드별로 반영하고 답변해 두셨습니다. 저는 ORM 목록 문구를 8개 백엔드에 맞추는 것만 수정했습니다.

CLI 출력 방식을 바꿨습니다

GORM과 Django는 Prisma·Drizzle처럼 스키마 전체를 한 파일(models.go, models.py)에 씁니다.

  • Go는 디렉터리가 곧 패키지입니다. 관계가 항상 양쪽에서 렌더되므로 모델 디렉터리를 나누면 두 패키지가 서로를 import하게 되어 컴파일되지 않습니다.
  • Django는 앱의 모델을 models 모듈 하나에서 읽습니다. 테이블별로 파일을 나누면 __init__.py에서 전부 import해 주지 않는 한 모델이 등록되지 않습니다.
  • 고정된 파일 하나만 쓰기 때문에 확장자 기준 sweep은 하지 않습니다(Drizzle과 동일). export 디렉터리에 있는 사용자의 .go·.py 파일은 건드리지 않습니다.
  • 파일 첫 줄에는 Code generated by vespertide. DO NOT EDIT.를 씁니다. 이 줄로 시작하지 않는 기존 models.go·models.py가 있으면 덮어쓰지 않고 에러를 반환합니다. startapp이 만든 models.py를 지우지 않기 위함입니다.

생성 코드에서 고친 것

대상 고친 것
Django 시스템 체크 _로 시작하는 모델명(models.E023), 필드가 수행할 수 없는 on_delete(fields.E320·E321), 복합 PK 모델로의 관계(fields.E347·E336), unique가 아닌 컬럼을 참조하는 FK(fields.E311), FK의 attname과 같은 이름의 컬럼(models.E006), 30자를 넘는 인덱스 이름(models.E034), 모델 자체 속성과 같은 필드명(check·save·objects·Meta 등), callable이 아닌 JSONField 기본값(fields.E010)
Django 조인 PK가 아닌 컬럼을 참조하는 FK에 to_field가 빠져 있어 Django가 조용히 PK로 조인하고 있었습니다.
Django 그 밖 managed = False, unique·PK인 FK는 OneToOneField, JSONB는 JSONField, 파이썬 키워드 컬럼명, M2M 필드명과 컬럼의 충돌, 문자열 기본값의 SQL 이스케이프('it''s')
GORM 컴파일 Go로 export할 수 없는 식별자(1usersX1users), 구조체의 TableName() 메서드와 같은 이름의 필드, 값 타입 belongs-to가 만드는 재귀 타입(항상 포인터로), enum 타입·상수의 중복 선언
GORM 관계와 태그 PK가 아닌 컬럼을 참조하는 FK의 references: 누락, 복합 FK의 역방향 관계 누락, one-to-one이 has-many로 나오던 문제(unique FK, 그리고 FK가 곧 그 테이블의 PK인 경우), 네이밍 빌더와 어긋나던 index·unique 이름, char(N)과 네트워크 타입의 type:, 기본값 태그(GORM은 문자열 필드의 기본값을 값 그대로 읽습니다)
GORM 레이아웃 출력 전부가 gofmt 레이아웃과 달랐습니다. 현재는 gofmt -l을 그대로 통과합니다.
두 백엔드 공통 한 파일에 스키마 전체를 쓰면서 테이블·enum 타입·enum 상수의 이름을 파일 단위로 한 번만 점유합니다(scope_names.rs). 설명·컬럼명·enum 값·기본값의 "·\·개행은 공용 string_literal로 이스케이프합니다.

다른 백엔드에 닿는 변경

기존 스냅샷은 하나도 바뀌지 않았습니다.

출력이 달라지는 것은 지금까지 fixture가 없던 입력입니다.

  • Prisma·Drizzle·GORM: FK가 곧 그 테이블의 PK인 one-to-one은 역방향이 has-many가 아니라 has-one으로 나옵니다(Profile[]Profile?). 세 백엔드가 함께 쓰는 역방향 스캔이 unique만 보고 있었습니다. SeaORM은 원래 has-one으로 내고 있었습니다.
  • SQLAlchemy·SQLModel: 파이썬 키워드인 컬럼명은 속성명에 _를 붙입니다(fromfrom_). DB 컬럼명은 그대로 넘깁니다. 이전 출력은 파이썬 문법 오류였습니다.
  • SQLAlchemy·SQLModel·Django 공용 enum 멤버 이름: __로 시작하면 파이썬 name mangling 대상이 되므로 _ 하나로 줄입니다.
  • CLI가 쓰는 SQLAlchemy·SQLModel 파일명을 모듈로 import할 수 있는 식별자로 바꿉니다.

출력이 그대로인 정리는 다음과 같습니다.

  • Drizzle의 ts_string을 공용 string_literal로 옮겼습니다. 정수 enum 기본값을 값으로 바꾸는 integer_enum_variant_value도 공용이며 Drizzle이 함께 사용합니다.
  • SeaORM의 정션 판정을 constraint_scan::junction_targets로 분리해 Django와 같이 사용하도록 했습니다(SeaORM 2곳 교체).
  • single_column_fk_targets를 없애고 single_column_fk_details 하나로 합쳤습니다. SQLAlchemy·SQLModel의 호출부만 바뀌었습니다.
  • collect_back_relationsBackRelation에 FK·참조 컬럼과 액션을 추가했습니다. Prisma·Drizzle이 쓰는 필드는 그대로입니다.
  • codegen 벤치마크가 8개 ORM을 모두 돕니다.
  • 문서: README, AGENTS 3곳, bridge README, facade lib.rs의 백엔드 목록, schemas/config.schema.jsondjango 섹션을 수정했습니다.

테스트

렌더 결과를 contains()로 확인하던 모듈별 테스트와 모듈별 스냅샷을 공용 8-ORM 스냅샷으로 옮겼습니다(전체 646개). 각 모듈에는 타입·기본값·이름 규칙 같은 함수 단위 매핑 테스트만 남겼습니다.

CLI에는 GORM·Django 출력 스냅샷 4개가 있습니다. orm_cases!가 닿지 않는 CLI 배선(단일 파일 출력, 생성 표식, 패키지명)을 고정하기 위한 것입니다.

검증

확인한 것 결과
fmt, clippy(-D warnings), 전체 테스트, line-budget, schema-gen 무변경, cargo-deny 통과
같은 커밋을 제 포크의 PR로 실행 CI 전 잡(coverage 100%, changepacks, semver-checks 포함), mutation 16개 샤드, benchmarks 통과
Django 6.1 시스템 체크 examples/app(모델 11개)과 깨지기 쉬운 입력만 모은 스키마에서 메시지 0건. 단일 테이블 스냅샷에는 파일 밖 모델을 참조해서 나오는 메시지만 남습니다.
go vet(실제 gorm.io/gorm 의존), gofmt -l 위 두 스키마 통과, GORM 스냅샷 79개 모두 gofmt 레이아웃과 일치
런타임 vespertide가 만든 SQLite DDL로 DB를 만들고 Django ORM과 GORM으로 삽입·조회했습니다. 자연키 FK 조인, one-to-one의 양방향, self-reference, 복합 PK 정션과 M2M, CHECK, ON DELETE를 확인했습니다.

미리 말씀드릴 것

  • scope_names.rs는 Drizzle의 drizzle::bindings와 역할이 겹칩니다. 다만 Drizzle 쪽은 import 심볼과 콜백 파라미터를 먼저 점유하고, customType·relations const까지 다루며, enum을 항상 테이블로 한정하는 등 규칙이 달라 이 PR에서는 합치지 않았습니다. 합치는 편이 낫다고 보시면 후속 PR로 하겠습니다.
  • Django Meta.indexesname=은 SQL 계층이 만든 이름(ix_{table}__{key})이 30자 이내일 때만 씁니다. managed = False라 Django가 해당 인덱스를 만들 일이 없기 때문에, 30자를 넘으면 이름을 쓰지 않습니다.
  • 기본값을 쓰지 못하는 경우가 두 가지 있습니다. Django JSONField는 callable만 받고, GORM은 문자열 기본값의 양끝 따옴표를 전부 잘라내기 때문에 따옴표로 시작하거나 끝나는 값을 표현할 수 없습니다. 두 경우 모두 기본값을 생략하며, DB 쪽 기본값은 그대로입니다.

이 PR에서 다루지 않은 것

작업 중 확인한 upstream 기존 동작의 결함입니다. 다른 백엔드나 다른 크레이트의 동작을 바꿔야 하는 내용이라 이 PR에서는 손대지 않았습니다.

결함 증상
export의 확장자 sweep이 사용자 파일을 지움 쓰기 전에 export 디렉터리에서 해당 확장자 파일을 재귀적으로 전부 지웁니다. 생성한 파일인지 구분하지 않아 SeaORM·SQLAlchemy·SQLModel·JPA는 함께 둔 사용자 소스가, Prisma는 schema.prisma가 지워집니다. Drizzle과 이 PR의 GORM·Django는 sweep하지 않습니다.
SQLAlchemy 출력이 import에서 실패 모델이 DeclarativeBase를 직접 상속해 SQLAlchemy 2.0에서 InvalidRequestError가 납니다. 특정 입력이 아니라 모든 모델이 해당합니다. 파일 간 relationship 참조를 위한 공용 Base__init__.py도 없습니다.
JSON 기본값의 DDL 실패 json 컬럼의 default{"a": 1}로 쓰면 SQL 계층이 DEFAULT {"a": 1}을 그대로 내보내 DDL이 실패합니다(SQLite에서 확인). '{"a": 1}'처럼 SQL 리터럴로 써야 합니다. exporter fixture json_default도 앞의 형태입니다.
출력 경로 충돌 무검사 서로 다른 모델 파일이 같은 출력 경로로 매핑되면 경고 없이 한쪽이 덮어써집니다. 쓰기가 동시에 진행되어 어느 쪽이 남는지도 정해져 있지 않습니다.
JPA package 선언 없음 모델 디렉터리가 중첩되면 하위 디렉터리에 쓰이지만 package 선언이 없어 컴파일되지 않습니다.
JPA enum 상수 값에 하이픈이나 선두 숫자가 있으면 Java 식별자가 될 수 없는 상수가 그대로 출력됩니다.
case 변환 뒤 같아지는 컬럼명 user_iduserId가 한 테이블에 있으면 SeaORM·JPA·Drizzle에서 같은 필드가 두 번 선언됩니다. GORM·Django는 공용 claim_binding으로 접미사를 붙입니다.
SQLAlchemy의 custom 타입 컬럼 mapped_column("JSONB", …)로 렌더되어, 타입이 아니라 이름이 "JSONB"인 컬럼이 선언됩니다.
SQLAlchemy·SQLModel enum 값을 이스케이프하지 않고 클래스 이름도 식별자로 바꾸지 않습니다. 값에 "·\가 있거나 이름이 1st·info-level이면 문법 오류가 됩니다.
cmd_exportvalidate_schema를 거치지 않음 FK 대상 테이블이 없거나 PK가 없는 스키마가 그대로 들어와, 한 파일 백엔드 넷이 정의되지 않은 이름을 출력합니다. Prisma는 같은 이름으로 접히는 enum·테이블을 하나로 합칩니다.
모델 로더의 미정렬 collect_model_pathsread_dir 순서를 그대로 사용해, 같은 식별자로 접히는 테이블의 번호가 환경에 따라 달라질 수 있습니다.
같은 컬럼 목록의 이름 없는 인덱스 둘이면 SQL 계층과 모든 exporter에서 이름이 겹칩니다.
SeaORM의 공용 헬퍼 사본 pluralize 사본, 별도의 역방향 관계 스캔, 동작이 다른 primary_key_columns가 남아 있습니다.
--export-dir 도움말 기본값을 modelsDir라고 안내하지만 실제 값은 modelExportDir이며, 파일이 지워질 수 있다는 안내도 없습니다.
CLI export의 죽은 경로 build_output_path의 마지막 arm은 도달할 수 없는데 테스트 2개가 이를 고정하고 있고, tests/prisma.rs는 생성 파일을 contains()로 확인합니다.

@2heunxun
2heunxun requested a review from owjs3901 September 17, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants