Django / GORM exporter 완결성 보완 - #169
Open
2heunxun wants to merge 34 commits into
Open
Conversation
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.
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
marked this pull request as draft
July 26, 2026 05:06
2heunxun
marked this pull request as ready for review
July 26, 2026 05:06
Author
|
@owjs3901 리뷰부탁드립니다! |
owjs3901
self-requested a review
July 26, 2026 13:11
owjs3901
requested changes
Jul 26, 2026
owjs3901
left a comment
Contributor
There was a problem hiding this comment.
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 | ||
|
|
Contributor
There was a problem hiding this comment.
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> |
Contributor
There was a problem hiding this comment.
typography를 등록 후 사용하는 것이 더 이롭습니다
| {title} | ||
| {emphasis && ( | ||
| <> | ||
| {' '} |
Contributor
There was a problem hiding this comment.
차라리 중간에 gap을 넣는 것이 더 적절합니다, text를 통한 공백 구현은 부적절합니다
| <VStack alignItems="flex-start" gap="16px" maxW="720px"> | ||
| <Text | ||
| color="$vespertidePrimary" | ||
| fontFamily="D2Coding" |
| alignItems="center" | ||
| bg="$vespertideBg" | ||
| border="1px solid $border" | ||
| borderRadius="999px" |
Comment on lines
+459
to
+460
| px="28px" | ||
| py="32px" |
| <Box maxW="1240px" mx="auto"> | ||
| <Flex | ||
| alignItems="flex-start" | ||
| flexDir={['column', null, null, 'row']} |
| <Text color="$textSub" typography="body"> | ||
| <Text as="span" color="$title" fontWeight="600"> | ||
| {it.k} | ||
| </Text>{' '} |
| <Box mt="60px"> | ||
| <Text | ||
| color="$vespertidePrimary" | ||
| fontFamily="D2Coding" |
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; |
Contributor
There was a problem hiding this comment.
django와는 무관한 변경사항이 PR에 올라온 것 같습니다
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
요약
Django, GORM exporter를 검토해서 발견한 부족한 점 7가지를 전부 구현했습니다.
관계(FK) 코드생성이 두 백엔드에서 부분적으로만 지원되고 있었고, 공유 테스트
스위트에도 편입돼 있지 않았습니다. 이번 작업으로 두 백엔드를 나머지 4개
ORM(SeaORM/SQLAlchemy/SQLModel/JPA)과 동등한 수준으로 끌어올렸습니다.
변경 사항
감지해서
ManyToManyField(..., through=...)를 양쪽에 생성. 기존에는스키마 컨텍스트를 아예 무시하고 있었음.
(
foreignKey:...;references:...), Django는 네이티브 지원이 없어서 주석으로관계 정보를 남기도록 처리 (기존엔 컬럼만 남고 관계 정보가 조용히 사라짐).
역방향(has-many) 관계가 아예 생성되지 않던 버그 발견 및 수정.
공유 테스트로 교차검증되고 있었음. 편입 과정에서 Django의 실제 버그 2개를
추가로 발견:
출력되던 문제 (import 시점에 크래시)
primary_key=True가 누락되던 문제 (Django자체 시스템 체크(
fields.E100)에 걸림 — auto PK를 쓰는 거의 모든스키마에 영향)
vespertide.json에서Django
app_label, GORMpackage_name을 커스터마이징할 수 있도록 지원(기존엔 SeaORM만 이런 설정 진입점이 있었음).
clean_export_dir회귀 테스트 추가 — 다른 ORM들은 다 있었는데Django만 빠져 있었음.
머지된 지 오래됐는데도 문서에 전혀 언급이 안 되고 있었음).
다른 5개 백엔드와 다시 비교 검토하다 발견. 복합 PK 테이블에서 어떤
필드에도
primary_key=True가 안 붙고Meta에도 아무 표시가 없어서,Django가 자체적으로 엉뚱한 auto
idPK를 암묵적으로 추가해버리는문제였음 (실제 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 환경동일 재현하여 확인)
CODECOV_TOKEN미설정 이슈로 실패 — 코드와 무관)