Skip to content

feat(exporter): Prisma ORM exporter 추가 (PR #151 이어받음)#166

Open
yyuneu wants to merge 7 commits into
dev-five-git:mainfrom
yyuneu:feat/prisma-exporter
Open

feat(exporter): Prisma ORM exporter 추가 (PR #151 이어받음)#166
yyuneu wants to merge 7 commits into
dev-five-git:mainfrom
yyuneu:feat/prisma-exporter

Conversation

@yyuneu

@yyuneu yyuneu commented Jul 6, 2026

Copy link
Copy Markdown

개요

5번째 ORM 백엔드로 Prisma를 추가합니다 (기존: SeaORM / SQLAlchemy / SQLModel / JPA).
TableDef 스키마를 datasource, generator, enum 블록(전역 dedup), model 블록을
포함한 단일 schema.prisma 파일로 변환합니다.

실행:

vespertide export --orm prisma

설계상 특이사항

1. 왜 Prisma만 별도 config (PrismaConfig)를 가지는가

Prisma는 단일 스키마 언어입니다. datasource db { provider, url, relationMode }
generator client { provider, output }이 모델과 같은 파일 안에 있어야 합니다.
나머지 ORM 백엔드(SeaORM / SQLAlchemy / SQLModel / JPA)는 모델 단위 코드만
생성하므로 이런 메타 정보가 필요 없습니다. 즉, Prisma만 "모델 외 입력"(provider,
client output 경로, relation mode)을 받아야 하기 때문에 vespertide-config
PrismaConfig를 새로 추가했습니다. 필드 세 개(provider / client_output /
relation_mode)와 postgresql 기본값을 가집니다.

(2026-07-07 갱신) 이 절은 원본 PR 제출 당시 설계입니다. 아래 "리뷰 반영" 절에서
설명하듯, 리뷰 피드백을 받아 필드 3개 중 2개(provider / clientOutput)는 제거하고
나머지 하나(relationMode)는 enum으로 바꿨습니다. 현재는 relation_mode 필드 하나만
남아 있습니다.

2. 왜 export.rsif matches!(orm, OrmArg::Prisma) { return cmd_export_prisma(...) } 분기가 있는가

기존 export 파이프라인은 모델 N개 → 파일 N개 전제입니다:
walk_models → 모델별 render_entity_with_schema → 파일별 write → mod chain.
Prisma는 모델 N개 → 파일 1개이며 위 파이프라인이 만족시킬 수 없는 두 가지
요구가 있습니다:

  • 모든 테이블에 걸친 enum 전역 dedup (두 테이블이 같은 enum을 참조해도 enum
    블록은 파일에 한 번만 등장해야 함)
  • datasource/generator 헤더가 파일 맨 위에 1회만 출력되어야 함

그래서 cmd_export가 진입 즉시 cmd_export_prisma로 분기하고,
PrismaExporterWithConfig::render_schema(&all_tables)에서 스키마 전체를 한 번에
조립합니다. Orm::Prisma는 인터페이스 일관성 + 분기 안 자체 clean 호출을 위해
clean_export_dir / build_output_pathprisma 확장자로 함께 등록만 해
두었습니다.

테스트 설계

명세는 crates/vespertide-exporter/src/prisma/TESTING.md에 있습니다. 3-layer 구조:

  • Layer 1 — FK 없는 단일 엔티티 렌더링. render_entity(table)로 검증.
  • Layer 2 — schema 컨텍스트가 필요한 관계 포함 케이스.
    render_entity_with_schema(table, schema)로 검증. TableConstraint::ForeignKey
    하나라도 있는 TableDef는 무조건 Layer 2.
  • Layer 3 — edge case (예약어, 특수문자).

설계 규칙:

  • 모든 출력 검증은 insta::assert_snapshot! 사용. assert!(result.contains(...))
    방식은 금지 — 회귀 감지 정확도 확보 및 출력 전체를 고정.
  • 구조가 동일한 케이스(컬럼 타입 × nullable, default 값 변형, on_delete/on_update,
    singularize 등)는 #[rstest] 파라미터화로 작성.
  • 반복되는 ColumnDef 생성은 로컬 헬퍼(col, col_null, col_with_default,
    col_with_comment, pk, uniq, idx, fk, table, render_schema_all)로
    분리하여 케이스 본문을 짧게 유지.

테스트 65개 / 스냅샷 54개가 커버하는 범위:

Layer 항목
1-1 컬럼 타입 × nullable 25개 타입 (Simple + Complex, enum 포함) × {nullable=true, nullable=false}
1-2 PK 단일 autoinc / 단일 no-autoinc / composite / 없음
1-3 unique 단일 named / 단일 unnamed / composite named / composite unnamed
1-4 index 단일 named / 단일 unnamed / composite
1-5 default bool, now(), CURRENT_TIMESTAMP, gen_random_uuid(), uuid_generate_v4(), 임의 함수, 문자열 리터럴, 정수 리터럴, fallback 키워드
1-6 enum screaming_snake match, mapped, integer, nullable, enum default, 다중 컬럼 dedup
1-7 description / comment 있음 / 없음 / multiline / 컬럼 comment multiline
1-8 @@Map 항상 출력
2 관계 has-many, has-one (unique FK), nullable FK, 같은 테이블로의 다중 FK, 자기참조, on_delete/on_update (6 action), composite FK 무시, 복수형→단수형 back-relation (posts/categories/boxes/users)
3 edge 예약어 테이블명 (select, order, model, ...), 예약어 컬럼명 (default, unique, ...), description 안의 newline/quote/brace, default 값 안의 quote

이 PR에 대해 (원본 #151 이어받음)

원본 PR은 #151, @L33gn21 님이 작성하셨습니다. 다만 해당 fork에 제가 쓰기 권한이 없고, 작성자분이 미국에 계셔서 시차 때문에 실시간으로 논의하기 어려운 상황이라, 같은 브랜치를 제 fork(yyuneu/vespertide)로 이어받아 새 PR로 올립니다. 원본 커밋 4개는 그대로 두었고, 리뷰 반영 커밋 3개만 위에 추가했습니다.

PrismaConfig 리뷰 피드백 반영

@owjs3901 님이 남겨주신 코멘트:

clientOutput 옵션의 경우 이미 modelsDir이 존재하는 것으로 보이며 겹치는 것 같습니다.

provider 옵션의 경우 기본적으로 model만 관리하는 vespertide의 측면에서 부적절하다고
느낍니다.

relationMode의 경우 enum이어야 할 것 같습니다. 또한 관련 설명에 대해서 조금 더
자세한 설명을 부탁드립니다.

세 가지 다 맞는 지적이라 생각해서 그대로 반영했습니다.

  • clientOutput: prisma generate가 Client 라이브러리를 어디에 생성할지 정하는 값이라 modelsDir(모델 소스 위치)이랑 정확히 같은 개념은 아니지만, vespertide의 책임 범위(스키마/모델 정의) 밖이라는 말씀이 맞다고 판단해 필드를 제거했습니다.

  • provider: 지금 exporter는 PostgreSQL 네이티브 타입(@db.Uuid 등)만 생성하는데, config로 mysql/sqlite를 선택할 수 있게 열어두면 실제로는 깨진 스키마가 나오면서도 마치 지원하는 것처럼 보이는 문제가 있었습니다. 그래서 옵션은 제거하고 provider = "postgresql"을 고정값으로 넣었습니다. (자세한 이유는 아래 참고 부탁드립니다.)

  • relationMode: RelationMode enum(ForeignKeys/Prisma)을 추가했습니다. foreignKeys는 실제 DB FK 제약을 쓰는 기본값이고, prisma는 PlanetScale처럼 FK를 지원하지 않는 DB에서 Prisma Client가 대신 참조무결성을 챙겨주는 모드입니다.

이렇게 정리되면서 PrismaConfig는 필드가 relation_mode 하나만 남았습니다. schemas/config.schema.json도 다시 생성해서 반영했습니다.


참고: provider를 postgres 전용으로 남겨둔 이유

  • Prisma는 다른 4개 ORM(SeaORM 등)과 달리, 스키마 파일 자체에 DB 종류를 못박아야 하는 구조라(datasource.provider), 코드 생성 시점에 dialect를 미리 알아야 합니다.

  • MySQL: 네이티브 타입 매핑만 추가하면 되는 작업이라 나중에 추가가 가능하다 생각합니다.

  • SQLite: vespertide 모델에서 컬럼 타입을 enum으로 정의합니다. 하지만 Prisma의 sqlite 커넥터가 enum 자체를 공식 문서에서 지원하지 않아서, 단순 타입 매핑으로는 안 되고 enum을 문자열로 강등하는 등 별도 설계가 필요합니다.

  • 원본 PR도 처음부터 postgres 위주로 만들어져 있었고, 이번 리뷰에서도 멀티 dialect 자체는 별도 지적이 없으셔서, 그 방향 그대로 이번 PR도 postgres 전용으로 유지하고 mysql/sqlite는 후속 작업으로 남깁니다.

추가로 확인해서 함께 반영한 부분: 스냅샷 4건

변경 사항을 검증하면서 exporter 테스트 전체를 돌려봤는데, 이번 변경과는 무관한 기존 실패 4건을 발견했습니다. 확인해보니 원본의 dcbff76 커밋(제약명 name:map:, 정수 enum 기본값 처리, 숫자로 시작하는 식별자 처리)에서 코드 수정 자체는 정확히 반영되어 있었는데, 그에 맞춰 갱신됐어야 할 스냅샷 4개가 아직 반영되어 있지 않았던 것으로 보였습니다. 테스트 코드에 정의된 값과 비교해서 현재 코드 출력이 의도한 대로 맞다는 것도 확인했고, 코드 변경 없이 스냅샷만 최신 상태로 갱신했습니다.

Test plan

  • cargo build -p vespertide-config -p vespertide-exporter -p vespertide-cli
  • cargo test -p vespertide-config -p vespertide-exporter -p vespertide-cli — 전부 통과
  • cargo test -p vespertide-exporter — 567 passed, 0 failed
  • cargo run -p vespertide-schema-gen -- --out schemas 재생성 후 diff 확인 —
    PrismaConfig 관련 의도된 변경만 존재
  • 별도 테스트 프로젝트에서 vespertide export --orm prisma 실행해서 실제
    schema.prisma 확인 (provider 고정, clientOutput/relationMode 줄 없음,
    @@Map 유지)
  • 리뷰어: postgres 전용으로 유지하는 방향 + mysql/sqlite 후속 작업 계획
    괜찮으실지 확인 부탁드립니다.

L33gn21 and others added 7 commits June 23, 2026 00:01
Add a Prisma schema generator as a fourth ORM backend alongside SeaORM,
SQLAlchemy, and SQLModel. Renders enum + model blocks with full schema
context (relations, indexes, unique/composite constraints, native types,
default attributes, referential actions) and wires Prisma into the
cross-ORM test harness and CLI export command.

Model names use plural PascalCase; string default values escape
backslashes correctly. Snapshots cover the full column-type matrix plus
relation, enum, default, and reserved-identifier scenarios.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generate the 60 missing Prisma insta snapshots so the Prisma backend is
cross-compared with the other four ORMs through the shared orm_cases!
matrix. The Orm::Prisma cases were already wired into every test; only
the accepted snapshot files were absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors the sqlmodel/sqlalchemy 4-file convention. mod.rs is now a thin
orchestrator; render.rs holds model rendering and relation logic; types.rs
holds column-type mapping; enums.rs holds enum rendering and naming helpers.

Public API surface (PrismaExporter, PrismaExporterWithConfig, export,
render_entity, render_entity_with_schema, to_pascal_case_for_tests) unchanged.
All 565 tests pass with byte-identical snapshot output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, relation field naming

- @@unique/@@index DB constraint names now emit `map:` instead of `name:`
  (`name:` on @@index is invalid in Prisma 2.30+; on @@unique it sets the
  Prisma Client accessor, not the DB constraint name)
- integer-backed enum @default(...) now resolves to a variant identifier via
  numeric-value or variant-name match (e.g. @default(COMPLETED)), falling back
  to dbgenerated() when unmatched, instead of emitting an invalid bare int
- inline FK relation field gets a `_rel` suffix when stripping `_id` would
  collide with the scalar column; self-referential back-relation is emitted
- regenerate affected Prisma snapshots

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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