Skip to content

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

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

Django / GORM exporter 완결성 보완#169
2heunxun wants to merge 34 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.
2heunxun added 4 commits July 26, 2026 11:44
Both had zero mentions of gorm/django (GORM had already merged before
AGENTS.md's own generation date), and README's Features/CLI-usage lines
listed GORM but not Django or JPA.

- README.md: Features bullet and --orm CLI examples now list all six
  supported ORMs.
- AGENTS.md: crate-tree comment and the ORM-export WHERE-TO-LOOK path
  now include gorm/django; corrected the now-stale "4 ORMs / 232
  snapshots" figures in the orm_cases! section to the current 6 ORMs /
  362 snapshots after wiring Django and GORM into the shared macro.
- crates/vespertide-exporter/AGENTS.md: added GORM and Django to the
  crate summary/STRUCTURE tree, and two new BACKEND NOTES subsections
  documenting their relation-inference behavior, config knobs
  (GormExporterWithConfig/DjangoExporterWithConfig), and known gaps
  (GORM still has no M2M/junction detection). Updated the stale
  "66 snapshot files" figure and noted the shared-vs-module-local
  snapshot suite split.
…n main)

CI coverage (--fail-under 100) has been red on main since the composite-FK
and M2M work landed, because I only verified test/clippy/fmt/line-budget
locally and deferred coverage confirmation to CI without circling back
when later pushes showed "coverage" job failures (which I'd wrongly
assumed were the already-known codecov-token issue from earlier in the
session, without checking each one).

Reproduced the exact CI coverage run locally via the xd009642/tarpaulin
Docker image and confirmed 4 genuine (non gap-region-artifact) gaps, all
now covered:

- django/render.rs: a composite-PK junction table whose FKs point at two
  *other* tables (unrelated to the table being rendered) was never
  exercised — added a schema with such a table.
- django/render.rs: unique_name()'s incrementing-suffix branch (second+
  collision) was never exercised — added a direct unit test.
- gorm/mod.rs: the equivalent double-collision branch for composite-FK
  relation field naming was never exercised — added a table with two
  pre-colliding column names.
- gorm/mod.rs: on_update was never set on a composite FK test fixture, so
  the OnUpdate constraint-tag branch was never hit.

Verified via `cargo tarpaulin --engine llvm -p vespertide-exporter` in
the same container CI uses: django/render.rs 279/279, gorm/mod.rs
443/443.
The write_futures closure's "create parent dir" if-let block showed as
an LLVM gap-region artifact (closing brace + next statement 0-hit
despite executing). More importantly, the actual reason it surfaced:
render_export_entity's new Orm::Django/Orm::Gorm match arms had no
end-to-end test at all — every existing export test only exercised
--orm seaorm, so the arms added for the DjangoExporterWithConfig/
GormExporterWithConfig wiring were never hit.

- Extracted the parent-dir-creation logic to an `ensure_parent_dir`
  helper, which incidentally also resolves the gap-region artifact by
  changing the closure's control-flow shape.
- Added end-to-end integration tests that run `export --orm django` and
  `export --orm gorm` against a real temp project and assert the
  generated file exists with the expected content.

Verified 100.00% (12234/12234 lines) by reproducing the exact CI
coverage recipe locally in the xd009642/tarpaulin container: the
CI-only wide-width `.rustfmt.toml` + `cargo fmt` reformat pass (which
measurably changes LLVM's coverage region boundaries — skipping it
produces dozens of false gaps in unrelated files) plus
RUST_TEST_THREADS=1 and PROPTEST_CASES=1024 to match the coverage job's
determinism settings exactly.
…ryKey

Re-auditing Django against the other 5 backends (specifically asked to
re-check for gaps) surfaced a real correctness bug: for a composite-PK
table, no field was ever marked primary_key=True (only handled for the
single-column case), and Meta declared nothing either. Django's actual
behavior when no field declares primary_key=True is to silently add its
own implicit auto `id` AutoField as the PK — which doesn't correspond to
any real uniqueness constraint on the underlying table, so every
composite-PK schema this exporter has ever produced generated a Django
model whose primary key semantics didn't match the database at all.

GORM and SeaORM both already representing composite PKs correctly
(multiple primaryKey tags / multiple #[sea_orm(primary_key)] columns)
made the Django gap obvious by comparison.

Fixed using Django 5.2+'s native `pk = models.CompositePrimaryKey(...)`,
referencing each column by its Django attname. For FK columns this is
`{field_name}_id` (Django always uses this pattern regardless of any
db_column override) rather than the stripped field name used for the
ForeignKey attribute itself — covered by a dedicated test since it's the
one non-obvious part of the mapping.

Verified 100% coverage of the new code path via the same Docker
tarpaulin reproduction used for the earlier coverage-gap fixes.
@2heunxun
2heunxun marked this pull request as draft July 26, 2026 05:06
@2heunxun
2heunxun marked this pull request as ready for review July 26, 2026 05:06
@2heunxun

Copy link
Copy Markdown
Author

@owjs3901 리뷰부탁드립니다!

@owjs3901
owjs3901 self-requested a review July 26, 2026 13:11

@owjs3901 owjs3901 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CICD파일을 변경할 이유가 없습니다
.idea 폴더를 추가할 이유가 없습니다

Comment on lines +16 to +20
const [active, setActive] = useState(examples[0]?.key ?? '')
const current = examples.find((e) => e.key === active) ?? examples[0]

if (!current) return null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use client를 사용하지 않을 방법을 더 찾아봅시다
fallback value를 너무 많이 사용하는 것 같습니다

특히 index 0 만 사용하고 검증하려고 하는 형태는 좋지 않습니다

Comment on lines +34 to +39
<Text color="$caption" fontFamily="D2Coding" fontSize="13px">
$
</Text>
<Text color="$title" fontFamily="D2Coding" fontSize="13px">
{command}
</Text>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typography를 등록 후 사용하는 것이 더 이롭습니다

{title}
{emphasis && (
<>
{' '}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

차라리 중간에 gap을 넣는 것이 더 적절합니다, text를 통한 공백 구현은 부적절합니다

<VStack alignItems="flex-start" gap="16px" maxW="720px">
<Text
color="$vespertidePrimary"
fontFamily="D2Coding"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typography 사용

alignItems="center"
bg="$vespertideBg"
border="1px solid $border"
borderRadius="999px"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

50%가 더 적절해보입니다

Comment on lines +459 to +460
px="28px"
py="32px"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모바일에서도 적절한 값일까요?

<Box maxW="1240px" mx="auto">
<Flex
alignItems="flex-start"
flexDir={['column', null, null, 'row']}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VStack사용

<Text color="$textSub" typography="body">
<Text as="span" color="$title" fontWeight="600">
{it.k}
</Text>{' '}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

띄어쓰기를 통한 공백 구현은 부적절합니다

<Box mt="60px">
<Text
color="$vespertidePrimary"
fontFamily="D2Coding"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typography를 사용함이 옳습니다

Comment on lines +8 to +17
@Entity
@Table(name = "users")
public class Users {

@Id
@Column(name = "id")
private Integer id;

@Column(name = "display_name", columnDefinition = "TEXT")
private String displayName;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

django와는 무관한 변경사항이 PR에 올라온 것 같습니다

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.

2 participants