Skip to content

feat(exporter): Drizzle ORM exporter 추가 - #186

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

feat(exporter): Drizzle ORM exporter 추가#186
yyuneu wants to merge 6 commits into
dev-five-git:mainfrom
yyuneu:feat/drizzle-exporter

Conversation

@yyuneu

@yyuneu yyuneu commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

개요

6번째 ORM 백엔드로 Drizzle(TypeScript)을 추가합니다.
Drizzle은 백엔드 중립 표현이 없어서, 한 번의 export가 방언별 파일 3개를 씁니다.

vespertide export --orm drizzle
# → src/models/models.pg.ts, models.mysql.ts, models.sqlite.ts

완성 기준은 vespertide로 만든 DB에 drizzle-kit push를 돌려도 변경이 없어야 한다로 잡았습니다.
도커(postgres:17 / mysql:8)에 vespertide가 생성한 DDL을 적용하고 push를 반복해 확인했습니다.

설계상 특이사항

1. 출력이 방언별 3파일인 이유

Drizzle에는 백엔드 중립 표현이 없습니다. pgTable / mysqlTable / sqliteTable이 서로 다른 패키지(drizzle-orm/pg-core 등)에 있어 import 라인에서 갈라지고, 타입 생성자와 기본값 표기도 방언마다 다릅니다. Prisma처럼 중립 단일 파일을 만들 수 없어 한 번의 export가 방언당 한 파일을 씁니다. OrmExporter 트레이트 경로(크로스-ORM 비교용 단일 String)는 PostgreSQL을 정본으로 렌더합니다.

2. 이름·타입·기본값 철자를 전부 SQL 계층에 맞춘 이유

drizzle-kit push는 모델과 DB를 사실상 문자열 수준으로 비교하므로, 모델은 vespertide가 실제로 만든 것을 그대로 말해야 합니다. 제약 이름은 SQL 계층과 같은 vespertide-naming 빌더(build_unique_constraint_name / build_index_name / build_foreign_key_name / build_enum_type_name)를 사용하고, 나머지 규칙은 왕복에서 드리프트 문장이 나온 항목을 하나씩 제거하며 도출했습니다:

항목 규칙 근거(실측)
unique uniqueIndex(): 컬럼 체인 .unique() 금지 SQL 계층은 전부 CREATE UNIQUE INDEX. unique()는 제약으로 introspect되어 kit가 드랍 후 재생성
pg auto-increment .generatedByDefaultAsIdentity() SQL 계층은 IDENTITY. serial이면 kit가 DROP IDENTITY를 시도
복합 PK 이름 pg {table}_pkey / mysql {table}_{cols} 이름이 없으면 kit가 PK를 드랍·재추가하려다 FK 의존으로 실패까지 함
pg enum 타입명 항상 {table}_{enum}, 테이블당 pgEnum 1개 SQL 계층의 CREATE TYPE이 항상 테이블-프리픽스. Prisma처럼 전역 dedup하면 타입명 자체가 드리프트
bytea / xml / custom 타입 customType 헬퍼 text로 넓히면 실타입 드리프트. bytea는 Uint8Array@types/node 없이 컴파일
타임스탬프 기본값 pg sql`CURRENT_TIMESTAMP` / mysql .defaultNow() / sqlite sql`(CURRENT_TIMESTAMP)` 방언마다 무드리프트 철자가 정확히 하나씩이고, pg와 mysql이 반대 방향
mysql uuid 기본값 sql`(uuid())` 소문자 information_schema가 소문자로 저장하고 kit 비교는 대소문자 민감
sqlite FK 무명 sqlite는 FK 이름을 저장하지 않음. 이름을 넣으면 영구 드리프트

3. FK가 .references() 체인이 아니라 foreignKey() 연산자인 이유

체인이 못 하는 것 세 가지 때문입니다: 제약 이름 운반(위 표), 복합키, 자기참조(foreignColumns를 콜백의 t에서 가져와 테이블 const가 자기 초기화식 타입 추론에 들어가는 것을 회피).

4. 파일-스코프 바인딩 충돌 처리 (drizzle/bindings.rs)

한 파일에 top-level const가 4종(customType / pgEnum / 테이블 / {table}Relations) 공존하고, import 심볼·콜백 파라미터와도 네임스페이스를 공유합니다. to_camel_case_-를 접기 때문에 서로 다른 DB 이름이 한 바인딩으로 겹칠 수 있고(user_relations 테이블 vs user의 relations 블록), 테이블명이 import(sql, integer)나 콜백 파라미터(t, one, many)와 같을 수도 있습니다. 특히 t가 겹치면 FK의 foreignColumns가 콜백 파라미터로 해석되어 컴파일은 되는데도 틀린 출력이 됩니다. 그래서 선언 순서대로 바인딩을 claim하고 충돌 시 숫자 접미사를 붙이며, 모든 참조가 확정된 이름을 따라갑니다. 충돌이 없는 스키마의 출력은 바이트 단위로 동일합니다.

5. drizzle export가 확장자 글롭 클린을 쓰지 않는 이유

기존 export 경로는 재생성 전에 export 디렉터리에서 해당 확장자 파일을 재귀 삭제합니다. drizzle의 확장자는 .ts인데, export 루트(기본 src/models)가 사용자 소스 디렉터리와 겹치는 프로젝트에서는 사용자가 직접 쓴 파일까지 지워집니다. 출력 파일명이 3개로 고정이라 덮어쓰기로 충분하므로 drizzle 경로는 클린을 생략하게 했습니다.

테스트 설계

  • 새 export 시나리오는 공용 orm_cases! 픽스처 1개 + 매크로 1줄로 6-ORM 스냅샷을 만드는 기존 규칙을 따랐습니다. Drizzle 추가 자체는 각 매크로에 #[case::drizzle] 1줄
  • 방언 축은 단일 백엔드 전용 진입점(render_schema(tables, dialect))이라 Prisma-exception 패턴대로 모듈 인라인 스냅샷(스냅샷 파일은 공유 src/tests/snapshots/)
  • types.rs는 전 타입 × 3방언 rstest 매트릭스(기대 문자열 직접 명시), render.rs는 default_chain·FK entry 등 분기 단위 테스트
  • 바인딩 어휘 가드: 렌더된 import 라인을 파싱해 어휘 누락을 기계적으로 검출
  • 수치: exporter 테스트 960 / 스냅샷 421 (Drizzle 크로스-ORM 69 + 방언 풀파일 3)

렌더링 밖 검증:

  • 도커 왕복: vespertide DDL 적용 → drizzle-kit push 반복 → 수렴 확인 (개요의 결과)
  • 110테이블 스키마: export 0.6s, 3방언 tsc --strict 무오류
  • 픽스처 전수(53개 × 3방언) strict tsc 실패는 전부 "스키마에 없는 테이블로의 FK" 픽스처로, export가 참조를 검증하지 않는 기존 동작 그대로 입니다.

함께 들어간 변경

  • Drizzle과 Prisma가 공유하게 된 로직을 공용 모듈로 추출했습니다: enum_scan(테이블 enum 수집), constraint_scan(FK 관계 네이밍·역관계 수집), utils/common(필드명 claim). Prisma는 위임으로 전환했고 기존 5개 백엔드의 스냅샷은 전부 무변경입니다
  • vespertide-namingto_camel_case / infer_relation_field_name을 추가했습니다 (JPA의 _id 스트립도 후자로 위임)
  • CLI export의 "클린 + 디렉터리 생성" 전처리를 prepare_export_dir로 공용화했습니다
  • vespertide-exporter 0.3.0 → 0.4.0: Orm이 exhaustive pub enum이라 변형 추가가 breaking입니다(cargo-semver-checks 기준). Orm#[non_exhaustive]를 붙여 이후 백엔드 추가를 minor로 만들지는 API 계약 판단이라 이번에는 손대지 않았습니다
  • Cargo.lock 갱신

한계

  • sqlite 무드리프트는 구조적으로 불가: drizzle-kit sqlite는 타입명 문자열을 비교하는데, vespertide sqlite 타입(timestamp_text, enum_text 등)은 sqlite-core 생성자로 표현할 수 없습니다. 런타임 동작(타입 affinity)은 문제가 없어 문서화로 종결했습니다
  • 왕복 검증 중 drizzle-kit 자체 버그 2건을 발견해 kit가 스스로 생성한 스키마만으로 재현해 두었습니다(비-PK unique 컬럼을 참조하는 FK가 있으면 kit 자신의 스키마도 첫 push에 실패하는 문장 순서 문제 / introspect가 FK가 의존하는 피참조 테이블의 unique index를 constraint-generated로 오분류해 영구 드리프트).

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.

1 participant