feat(exporter): Prisma ORM exporter 추가 (PR #151 이어받음) - #166
Conversation
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>
…f a struct literal
|
정말 애매하네요 이게... 모든 provider를 동시에 지원할 수 없다는게 정말 마음이 아픕니다 |
|
@yyuneu 다만 현재 mysql, postgres, sqlite 만 지원하니 PlanetScale등 fk를 지원하지 않는 DB는 신경쓰지 않아도 되므로 관련 config은 폐기함이 맞다고 생각합니다 그리고 각 provider 별 타입이 일치하지 않는게 무엇이 있는지 확인이 필요합니다, 현재 vespertide가 지원하는 타입의 경우 각 DB에서 딱히 지원하지 않는 타입이 없어모여 여쭙습니다 |
|
@owjs3901 피드백 감사합니다. 다시 보니 문제는 타입이 아니라 exporter가 붙이는 실제로 지원 타입 전체를 담은 모델을 export해서 provider만 바꿔가며
속성이 유효해도 어긋나는 경우가 있습니다. vespertide 자체가 MySQL에서는 그래서 이런 방향으로 다시 하겠습니다. config에 DB 정보를 넣지 않는 원칙은 유지하고, 그리고 이번에 |
|
지난 코멘트에서 확인했던 provider별 타입 문제를 반영했고, CI 실패도 같이 정리했습니다. 존재하지 않는 네이티브 타입 제거
백엔드별 스키마 생성 (
|
| 검증 | 방법 | 결과 |
|---|---|---|
| 생성 스키마 유효성 (수정 전) | 지원 타입 전체 모델 export 후 provider별 prisma validate |
postgresql 에러 3, mysql 에러 8, sqlite 에러 13 |
| 생성 스키마 유효성 (수정 후) | export --orm prisma -b postgres/mysql/sqlite × prisma validate |
3개 모두 통과 |
| url 제거 후 최신 Prisma 호환 | 최신 CLI로 3개 provider validate | 3개 모두 통과 |
| 전체 테스트 | cargo test --workspace --all-features |
0 failed (신규 테스트 +30개) |
| 기존 스냅샷 | Prisma 스냅샷 60개 (기본 provider postgres 유지) | diff 없음 |
| 뮤테이션 | cargo mutants --in-diff (CI와 동일 스코프) |
12개 중 missed 0 |
| 커버리지 | CI와 동일 절차로 cargo tarpaulin --engine llvm |
이 PR이 만든/수정한 파일 전부 100% |
| 린트/포맷/라인 정책 | clippy -D warnings / fmt / check-line-budget |
통과 |
61790c8 to
d4ea60b
Compare
|
포크에서 동일 워크플로우로 CI를 사전 검증했고, 실패 2건을 조치했습니다.
로컬에서 cargo deny / clippy / 전체 테스트(4076 passed) 통과를 확인했습니다. |
| /// Derive `crate::` prefix from the export directory path. | ||
| /// | ||
| /// For example: `src/models` → `crate::models`, `src/db/entities` → `crate::db::entities`. | ||
| /// For example: `src/models` ??`crate::models`, `src/db/entities` ??`crate::db::entities`. |
| /// For example: `admin/admin.json` ??`["admin", "admin"]` | ||
| /// `estimate/estimate_checker.vespertide.json` ??`["estimate", "estimate_checker"]` |
| fn prisma_provider_for_backend(backend: DatabaseBackend) -> PrismaProvider { | ||
| match backend { | ||
| DatabaseBackend::Postgres => PrismaProvider::Postgres, | ||
| DatabaseBackend::MySql => PrismaProvider::MySql, | ||
| DatabaseBackend::Sqlite => PrismaProvider::Sqlite, | ||
| } | ||
| } |
There was a problem hiding this comment.
PrismaProvider 구조체를 이용하지 않아도 될 것 같습니다
There was a problem hiding this comment.
enum을 제거하는 방향으로 갔기 때문에 From trait도 함께 사라졌습니다.
| #[rstest] | ||
| #[case::postgres( | ||
| DatabaseBackend::Postgres, | ||
| "provider = \"postgresql\"", | ||
| "@db.Timestamptz", | ||
| None | ||
| )] | ||
| #[case::mysql( | ||
| DatabaseBackend::MySql, | ||
| "provider = \"mysql\"", | ||
| "@db.Timestamp", | ||
| Some("@db.Timestamptz") | ||
| )] | ||
| #[case::sqlite( | ||
| DatabaseBackend::Sqlite, | ||
| "provider = \"sqlite\"", | ||
| "DateTime", | ||
| Some("@db.") | ||
| )] | ||
| #[tokio::test] | ||
| #[serial] | ||
| async fn export_prisma_writes_provider_specific_schema( | ||
| #[case] backend: DatabaseBackend, | ||
| #[case] provider_line: &str, | ||
| #[case] expected: &str, | ||
| #[case] absent: Option<&str>, | ||
| ) { | ||
| let tmp = tempdir().unwrap(); | ||
| let _guard = CwdGuard::new(&tmp.path().to_path_buf()); | ||
| write_config(); | ||
|
|
||
| let mut model = sample_table("events"); | ||
| model.columns.push(ColumnDef { | ||
| name: "occurred_at".into(), | ||
| r#type: ColumnType::Simple(SimpleColumnType::Timestamptz), | ||
| nullable: false, | ||
| default: None, | ||
| comment: None, | ||
| primary_key: None, | ||
| unique: None, | ||
| index: None, | ||
| foreign_key: None, | ||
| }); | ||
| write_model(Path::new("models/events.json"), &model); | ||
|
|
||
| cmd_export(OrmArg::Prisma, None, backend).await.unwrap(); | ||
|
|
||
| let out = PathBuf::from("src/models/schema.prisma"); | ||
| assert!(out.exists()); | ||
| let content = std_fs::read_to_string(out).unwrap(); | ||
| assert!(content.contains(provider_line)); | ||
| assert!(content.contains(expected)); | ||
| if let Some(absent) = absent { | ||
| assert!(!content.contains(absent)); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
prisma를 위한 테스트이므로 해당 파일에 존재하는 것은 부적절해보입니다
There was a problem hiding this comment.
Prisma 전용 테스트를 export/tests/prisma.rs로 분리했습니다. export.rs는 export/{mod.rs, tests/mod.rs, tests/prisma.rs}로 나뉘었습니다.
| #[case(OrmArg::Sqlmodel, Orm::SqlModel)] | ||
| #[case(OrmArg::Jpa, Orm::Jpa)] | ||
| #[case(OrmArg::Prisma, Orm::Prisma)] | ||
| fn orm_arg_maps_to_enum(#[case] arg: OrmArg, #[case] expected: Orm) { |
| /// Prisma-specific export configuration. | ||
| #[serde(default)] | ||
| pub prisma: PrismaConfig, |
There was a problem hiding this comment.
확인 감사합니다. 이후 PrismaConfig 자체를 제거해서 이 부분도 함께 사라졌고, vespertide-config 크레이트는 main과 diff가 0입니다.
| /// Emulate referential integrity in Prisma Client instead of relying on | ||
| /// database-level foreign keys. Needed for datastores that don't support | ||
| /// FK constraints (e.g. PlanetScale, MongoDB). | ||
| Prisma, |
There was a problem hiding this comment.
prisma를 위한 옵션을 제거하는 것이 옳을 것 같습니다
mongoDB를 지원하고 있지 않으므로 관련 옵션 제거가 필요해보입니다
There was a problem hiding this comment.
RelationMode와 PrismaConfig를 통째로 제거했습니다. relationMode가 유일한 필드였어서 빈 구조체를 남길 이유가 없었고, vespertide-config 크레이트와 config.schema.json은 main과 동일해졌습니다.
| pub enum PrismaProvider { | ||
| #[default] | ||
| Postgres, | ||
| MySql, | ||
| Sqlite, | ||
| } |
There was a problem hiding this comment.
DatabaseBackend로 교체했다가, 최종적으로 백엔드 분기 자체를 없애면서 enum이 통째로 제거됐습니다.
| impl PrismaProvider { | ||
| /// The `provider = "..."` string used in the `datasource` block. | ||
| pub fn as_datasource_str(self) -> &'static str { | ||
| match self { | ||
| PrismaProvider::Postgres => "postgresql", | ||
| PrismaProvider::MySql => "mysql", | ||
| PrismaProvider::Sqlite => "sqlite", | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
From trait을 사용하는 것이 옳음 다만 enum 자체를 제거할 것이라서..
There was a problem hiding this comment.
DatabaseBackend로 교체했다가, 최종적으로 백엔드 분기 자체를 없애면서 enum이 통째로 제거됐습니다.
| #[rstest] | ||
| #[case::postgres(PrismaProvider::Postgres, "provider = \"postgresql\"")] | ||
| #[case::mysql(PrismaProvider::MySql, "provider = \"mysql\"")] | ||
| #[case::sqlite(PrismaProvider::Sqlite, "provider = \"sqlite\"")] | ||
| fn render_schema_emits_configured_provider( | ||
| #[case] provider: PrismaProvider, | ||
| #[case] expected_line: &str, | ||
| ) { | ||
| let config = PrismaConfig::default(); | ||
| let tables = vec![basic_single_pk()]; | ||
| let schema = | ||
| PrismaExporterWithConfig::with_provider(&config, provider).render_schema(&tables); | ||
|
|
||
| assert!(schema.contains(expected_line)); | ||
| assert!(!schema.contains("output")); | ||
| assert!(!schema.contains("relationMode")); | ||
| } |
There was a problem hiding this comment.
provider에 따른 분리를 전부 제거했습니다. 현재는 @db.* 네이티브 속성을 하나도 내보내지 않고 datasource 블록도 없어서, 출력이 백엔드와 무관하게 동일합니다.
b9c961a to
8751b39
Compare
|
리뷰 감사합니다. 9개 코멘트를 모두 확인했습니다. 4건은 코드에 반영했고, 나머지는 확인 결과와 근거를 아래에 정리했습니다. 두 건은 이 PR에 넣을지 여쭙고 싶습니다. PascalCase 유틸 중복지적대로 부수적으로, Prisma가 자체 구현을 갖지 않게 되면서 테스트 전용 접근자( SCREAMING_SNAKE 유틸 중복동명 유틸이
이는 저장소의 기존 스냅샷에 이미 나와 있는 동작입니다. class EventSeverity(str, enum.Enum):
E_R_R_O_R__L_E_V_E_L = "ERROR_LEVEL"
1CRITICAL = "1critical"
그래서 이번 PR에서는 Prisma 자체 구현을 그대로 두었습니다. 한 번 통합해봤는데, Prisma 구현을 정본으로 커버리지 제외
_ => unreachable!("SimpleColumnType is #[non_exhaustive]; all variants are matched above"),
테스트 유틸리티 위치위에서 적은 대로 해당 함수 자체가 없어졌습니다. 파일명 정규화이 줄은 이 PR에서 작성한 코드가 아니라 main에 이미 있던 테스트입니다. diff에 새 코드로 표시된 이유는 이렇습니다. 앞선 리뷰에서 Prisma 테스트를 별도 파일로 분리해달라고 하셔서 git이 1→3 분할을 rename으로 인식하지 못해 그래서 이번 PR에서는 수정하지 않았습니다. datasource provider
사용자 쪽 datasource와 결합하면 출력 파일명은 Decimal 기본값모델에 선언된 default 문자열을 그대로 통과시킨 결과이고, 5개 ORM이 모두 같습니다. Prisma만 uuid 주석제거했습니다. 검증은 로컬에서 CI와 같은 항목을 모두 돌렸습니다. 또한 레포 ci를 이용하여 100% 통과를 확인 했습니다. |
owjs3901
left a comment
There was a problem hiding this comment.
코멘트 단 것에 대한 답변 회신이 없습니다, 모든 코멘트에 답신이 와야 합니다
@owjs3901 전부 회신 했습니다. 감사합니다. |
| .collect() | ||
| } | ||
|
|
||
| pub(super) fn to_screaming_snake(s: &str) -> String { |
There was a problem hiding this comment.
기존의 함수명과 역할이 같은 만큼 기존 것을 개선하는 방향으로 가는 것이 옳습니다, 다만 실험하신 방향도 맞다고 생각됩니다
최종적으로는 숫자로 table 등을 시작할 수 없으니 이 경우 선재적으로 blocking을 하고 해당 함수는 유틸리티로 공통함수로 분리하는 것이 옳다고 생각됩니다
같은 역할의 동명의 함수를 추가할 수는 없습니다
| --- | ||
| model Products { | ||
| id Int | ||
| price Decimal @default(0.00) |
There was a problem hiding this comment.
동의합니다, 의도에 맞춘 default 값, 좋은 인사이트입니다
| --- | ||
| model Products { | ||
| id Int | ||
| price Decimal @default(0.00) |
There was a problem hiding this comment.
다만 scale = 2로 설정을 하는 옵션을 별도로 만드는 것이 어떨까합니다
추가적으로 큰 문제가 없고 scale 2 옵션을 넣을 수가 없다면 모든 export가 0으로 default value가 정의되어야 한다고 생각합니다
| #[case::sqlalchemy(Orm::SqlAlchemy)] | ||
| #[case::sqlmodel(Orm::SqlModel)] | ||
| #[case::jpa(Orm::Jpa)] | ||
| #[case::prisma(Orm::Prisma)] |
| #[test] | ||
| fn test_to_screaming_snake_case() { | ||
| assert_eq!(to_screaming_snake_case("pending"), "PENDING"); | ||
| assert_eq!(to_screaming_snake_case("not_started"), "NOT_STARTED"); | ||
| assert_eq!(to_screaming_snake_case("inProgress"), "IN_PROGRESS"); | ||
| assert_eq!(to_screaming_snake_case("order-status"), "ORDER_STATUS"); | ||
| assert_eq!(to_screaming_snake_case(""), ""); | ||
| // Already-uppercase input survives: a position-based word-boundary rule | ||
| // would explode these into `E_R_R_O_R__L_E_V_E_L` / `H_T_T_P__500`. | ||
| assert_eq!(to_screaming_snake_case("ERROR_LEVEL"), "ERROR_LEVEL"); | ||
| assert_eq!(to_screaming_snake_case("HTTP_500"), "HTTP_500"); | ||
| // Trailing separators are trimmed and a leading digit is prefixed, so | ||
| // the result stays a valid identifier. | ||
| assert_eq!(to_screaming_snake_case("status-"), "STATUS"); | ||
| assert_eq!(to_screaming_snake_case("1critical"), "_1CRITICAL"); | ||
| } |
There was a problem hiding this comment.
반영했습니다.
지적하신 테스트를 rstest 케이스로 바꿨고, 같은 기준을 이 브랜치에서 작성한 테스트 전체에 적용했습니다. 입력이 2개 이상인 테스트는 #[case::name(...)]으로 쓰고, 단일 입력만 plain #[test]로 남겼습니다.
익스포터 스냅샷은 개별 #[test]를 만들지 않고 orm_cases! 매크로로만 추가했습니다. 시나리오 하나를 넣으면 5개 ORM 스냅샷이 한 번에 생성되므로 백엔드별 출력이 항상 교차 비교됩니다. 이번에 추가한 시나리오 2개도 같은 방식이라 스냅샷 10장이 함께 생성됩니다.
|
리뷰에서 지적된 항목과 그 과정에서 드러난 식별자 문제를 정리했습니다. ##risma 외의 백엔드까지 바뀐 이유 이번 커밋이 pr범위에 조금 벗어난다 생각해 추가로 이유를 적습니다. SeaORM·JPA·SQLAlchemy·SQLModel·CLI까지 바뀌어 있어서, 그 경위를 먼저 적습니다.
그래서 실제로 바뀐 범위는 이렇습니다.
바뀐 6장은 이게 전부입니다. 앞의 것은 Prisma 익스포터
식별자 정규화SQL은 따옴표로 감싸면 어떤 이름이든 받지만 대상 언어는 그렇지 않습니다. 언어별 시작 문자 규칙을
이름이 바뀌지 않으면 매핑을 추가하지 않으므로 기존 출력은 영향받지 않습니다. 관계에서 파생되는 이름컬럼 이름만 고쳐서는 부족했습니다. 관계 이름은 별도 경로에서 생성되고 있었고 그쪽에는 escape가 없었습니다.
그 외 수정
검증스냅샷은 출력이 "바뀌었는지"는 잡지만 "유효한지"는 잡지 않습니다. 그래서 생성물을 실제 도구에 넣어 확인했습니다. 입력: 식별자 케이스 8종 +
리포지토리 게이트: fmt / clippy 0, 테스트 통과, 스냅샷 드리프트 0, line budget 통과. 테스트
|
|
확인 과정에서 이번 PR 범위 밖의 문제도 나와 여기에 정리 합니다. 1. SQLAlchemy 출력은 import 자체가 되지 않습니다.
2. JPA는 enum을 파일마다 top-level로 내보내서 같은 패키지 안에서 충돌합니다.
3. JPA enum 컬럼의 기본값이 문자열로 대입됩니다.
4. Python 출력 파일 이름은 정규화되지 않습니다. 모델 파일이 5. FK 컬럼 이름이 Rust 키워드면 SeaORM 출력이 컴파일되지 않습니다. sea-orm이 main은 전자, 현재 브랜치는 후자입니다. 해결하려면 SeaORM 필드의 키워드 escape를 6. 복합 FK가 Prisma와 JPA에서 조용히 사라집니다. 기존
두 백엔드 모두 표현 수단이 있습니다. Prisma는 Prisma 쪽 필터는 이어받은 PR #151의 이번 커밋 범위가 넓고, 작성된 코드가 나와 prisma부분은 아직 고치지 않았습니다. 7. 익스포터 출력의 유효성을 검사하는 CI가 없습니다. ← 위 6건의 공통 원인
그래서 위 1~7번이 CI 초록인 상태로 남아 있었습니다.
같은 검사가 필요하다 생각합니다. |
|
또한 escape 문자 하나만 판단을 여쭙니다. Prisma와 SQLModel은 선두 여기서 SeaORM만 다르게 동작합니다. 지금은 Rust만 대문자 강제인데, 접두 문자를 다른 것으로 바꾸거나 모든 백엔드에서 대문자로 통일하는 편이 낫다면 맞추겠습니다. |
| pub(super) fn sanitize_identifier(input: &str) -> String { | ||
| let mut identifier = String::new(); | ||
|
|
||
| for (index, ch) in input.chars().enumerate() { | ||
| if ch == '_' || ch.is_ascii_alphanumeric() { | ||
| if index == 0 && ch.is_ascii_digit() { | ||
| identifier.push('_'); | ||
| } | ||
| identifier.push(ch); | ||
| } else { | ||
| identifier.push('_'); | ||
| } | ||
| } | ||
|
|
||
| if identifier.is_empty() { | ||
| "_".to_string() | ||
| } else { | ||
| identifier | ||
| } | ||
| vespertide_naming::sanitize_identifier(input, IdentifierStart::Underscore) |
There was a problem hiding this comment.
이런함수는 좋지 않아보입니다, 차라리 vespertide_naming에 있어야할 것 같아요
| #[rstest] | ||
| #[case::already_screaming( | ||
| vec!["DRAFT".into(), "PUBLISHED".into()], | ||
| "enum DocStatus {\n DRAFT\n PUBLISHED\n}" | ||
| )] | ||
| #[case::normalized( | ||
| vec!["draft".into(), "in progress".into()], | ||
| "enum DocStatus {\n DRAFT @map(\"draft\")\n IN_PROGRESS @map(\"in progress\")\n}" | ||
| )] | ||
| // `_1CRITICAL` would be rejected by Prisma's parser, so the escape is a | ||
| // letter; `@map` still carries the value the database stores. | ||
| #[case::leading_digit( | ||
| vec!["1critical".into()], | ||
| "enum DocStatus {\n X1CRITICAL @map(\"1critical\")\n}" | ||
| )] | ||
| fn string_variants_carry_map_only_when_normalization_changes_them( | ||
| #[case] values: Vec<String>, | ||
| #[case] expected: &str, |
| pub fn render_entity(table: &TableDef) -> String { | ||
| render_entity_with_schema(table, std::slice::from_ref(table)) | ||
| } |
| assert!(schema.starts_with("model ")); | ||
| assert!(!schema.contains("datasource")); | ||
| assert!(!schema.contains("generator")); | ||
| assert!(!schema.contains("provider")); | ||
| assert!(!schema.contains("@db.")); |
There was a problem hiding this comment.
스냠 snapshot을 찍는게 차라리 나을 것 같아요
| assert!(schema.contains("enum OrdersStatus {")); | ||
| assert!(schema.contains("enum TicketsStatus {")); | ||
| assert!(!schema.contains("enum Status {")); | ||
| assert!(schema.contains(" st OrdersStatus")); | ||
| assert!(schema.contains(" st TicketsStatus")); |
개요
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 기본값을 가집니다.
2. 왜
export.rs에if matches!(orm, OrmArg::Prisma) { return cmd_export_prisma(...) }분기가 있는가기존 export 파이프라인은 모델 N개 → 파일 N개 전제입니다:
walk_models→ 모델별render_entity_with_schema→ 파일별 write → mod chain.Prisma는 모델 N개 → 파일 1개이며 위 파이프라인이 만족시킬 수 없는 두 가지
요구가 있습니다:
블록은 파일에 한 번만 등장해야 함)
그래서
cmd_export가 진입 즉시cmd_export_prisma로 분기하고,PrismaExporterWithConfig::render_schema(&all_tables)에서 스키마 전체를 한 번에조립합니다.
Orm::Prisma는 인터페이스 일관성 + 분기 안 자체 clean 호출을 위해clean_export_dir/build_output_path에prisma확장자로 함께 등록만 해두었습니다.
테스트 설계
명세는
crates/vespertide-exporter/src/prisma/TESTING.md에 있습니다. 3-layer 구조:render_entity(table)로 검증.render_entity_with_schema(table, schema)로 검증.TableConstraint::ForeignKey가하나라도 있는
TableDef는 무조건 Layer 2.설계 규칙:
insta::assert_snapshot!사용.assert!(result.contains(...))방식은 금지 — 회귀 감지 정확도 확보 및 출력 전체를 고정.
singularize 등)는
#[rstest]파라미터화로 작성.ColumnDef생성은 로컬 헬퍼(col,col_null,col_with_default,col_with_comment,pk,uniq,idx,fk,table,render_schema_all)로분리하여 케이스 본문을 짧게 유지.
테스트 65개 / 스냅샷 54개가 커버하는 범위:
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:
prisma generate가 Client 라이브러리를 어디에 생성할지 정하는 값이라modelsDir(모델 소스 위치)이랑 정확히 같은 개념은 아니지만, vespertide의 책임 범위(스키마/모델 정의) 밖이라는 말씀이 맞다고 판단해 필드를 제거했습니다.provider: 지금 exporter는 PostgreSQL 네이티브 타입(
@db.Uuid등)만 생성하는데, config로 mysql/sqlite를 선택할 수 있게 열어두면 실제로는 깨진 스키마가 나오면서도 마치 지원하는 것처럼 보이는 문제가 있었습니다. 그래서 옵션은 제거하고provider = "postgresql"을 고정값으로 넣었습니다. (자세한 이유는 아래 참고 부탁드립니다.)relationMode:
RelationModeenum(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-clicargo test -p vespertide-config -p vespertide-exporter -p vespertide-cli— 전부 통과cargo test -p vespertide-exporter— 567 passed, 0 failedcargo run -p vespertide-schema-gen -- --out schemas재생성 후 diff 확인 —PrismaConfig관련 의도된 변경만 존재vespertide export --orm prisma실행해서 실제schema.prisma확인 (provider 고정, clientOutput/relationMode 줄 없음,@@Map 유지)
괜찮으실지 확인 부탁드립니다.