feat(exporter): Drizzle ORM exporter 추가 - #186
Conversation
|
제 발상을 뒤집네요 너무 좋습니다 |
| assert!(pg.contains("pgTable(\"events\"")); | ||
| assert!(pg.contains("from \"drizzle-orm/pg-core\"")); | ||
| assert!(mysql.contains("mysqlTable(\"events\"")); | ||
| assert!(mysql.contains("from \"drizzle-orm/mysql-core\"")); | ||
| assert!(sqlite.contains("sqliteTable(\"events\"")); | ||
| assert!(sqlite.contains("from \"drizzle-orm/sqlite-core\"")); |
There was a problem hiding this comment.
이런건 차라리 snapshot 테스트가 맞다고 생각해요
There was a problem hiding this comment.
말씀대로 방언별 rstest 케이스 + 케이스마다 스냅샷으로 바꿨습니다.
contains로 볼 때는 안 보이던 차이가 스냅샷에는 그대로 남습니다. 같은 integer 컬럼이 pg/sqlite에서는 integer, mysql에서는 int로 나가는 것 같은 부분입니다.
owjs3901
left a comment
There was a problem hiding this comment.
snapshot에 각각 postgresql과 sqlite 등 따로따로 있어야 하는게 아닌가 싶습니다
| "export {};" | ||
| ); | ||
| let pg = std_fs::read_to_string(root.join("models.pg.ts")).unwrap(); | ||
| assert!(pg.contains("pgTable(\"events\"")); |
There was a problem hiding this comment.
snapshot 테스트가 맞다고 생각합니다
rstest로 각 db에 대해서 따로따로 돌면 예쁠것 같네요
There was a problem hiding this comment.
이 테스트도 방언별 rstest로 나눴습니다. 케이스마다 그 방언의 출력 파일을 stale 상태로 심어두고, export 후 덮어써졌는지와 사용자가 직접 쓴 .ts가 살아있는지를 봅니다.
여기서는 스냅샷을 쓰지 않았습니다. 이 테스트의 주제가 사용자 파일 생존과 덮어쓰기라는 동작이고, 파일 내용은 바로 위 스냅샷 테스트가 이미 방언별로 고정하고 있어서 같은 내용을 두 벌로 들고 있을 이유가 없다고 봤습니다.
|
방언별로 따로 두도록 전부 바꿨습니다.
바인딩 충돌은 나눠놓고 보니 mysql만 기준이 문서에 없어서 이런 차이가 생긴 것 같아 AGENTS.md에 정리해뒀습니다.
스냅샷에 남아 있던 마지막 커밋(
|
| pub(super) fn enum_db_name(table: &str, enum_name: &str) -> String { | ||
| build_enum_type_name(table, enum_name) | ||
| } | ||
|
|
||
| /// The natural `const` binding for an enum declaration, derived from the | ||
| /// database type name so the two stay recognisably paired. The final binding | ||
| /// comes from `FileBindings`, which suffixes this name on a file-scope | ||
| /// collision. | ||
| pub(super) fn enum_const_name(table: &str, enum_name: &str) -> String { | ||
| super::js_name(&enum_db_name(table, enum_name)) | ||
| } |
There was a problem hiding this comment.
enum_db_name은 지웠습니다. 인자도 반환도 그대로 넘기기만 하던 별칭이라 호출하는 두 곳에서 build_enum_type_name을 직접 부르게 했고, 거기 달려 있던 SQL 계층이 모든 enum 타입을 테이블-프리픽스로 만들기 때문에 모델도 그래야 한다는 근거는 모듈 doc으로 올렸습니다.
바로 아래 enum_const_name은 남겼습니다. 이쪽은 별칭이 아니라 enum 바인딩 이름은 DB 타입명을 camelCase로 바꾼 것"이라는 규칙 자체이고, bindings.rs가 claim할 때와 폴백할 때 양쪽에서 씁니다. 인라인하면 규칙이 두 곳으로 갈려서 한쪽만 고쳐지는 상황이 생길 수 있어서요.
| vespertide-planner = { path = "crates/vespertide-planner", version = "=0.3.0" } | ||
| vespertide-query = { path = "crates/vespertide-query", version = "=0.3.0" } | ||
| vespertide-exporter = { path = "crates/vespertide-exporter", version = "=0.3.0" } | ||
| vespertide-exporter = { path = "crates/vespertide-exporter", version = "=0.4.0" } |
There was a problem hiding this comment.
혼자 0.4.0인데 개선 혹은 확인이 필요합니다.
There was a problem hiding this comment.
제가 손으로 올린 건데, CI의 semver 주석대로 feature PR에서 버전을 올리지 않고 changepack이 머지 시 올리는 구조더라고요.
핀을 =0.3.0으로 되돌리고 .changepacks/에 descriptor를 넣었습니다.
| [package] | ||
| name = "vespertide-exporter" | ||
| version = "0.3.0" | ||
| version = "0.4.0" |
There was a problem hiding this comment.
0.3.0으로 되돌렸습니다. 위 핀과 같은 원인이었고, 버전은 changepack descriptor로만 올라가게 했습니다.
| source: crates/vespertide-exporter/src/tests/mod.rs | ||
| expression: rendered | ||
| --- | ||
| export const events = pgTable("events", { |
There was a problem hiding this comment.
mysql snapshot은 어디에 있나요? 정확히는 파일이름에 psql이라는 것이 안붙어 있다보니까 불안합니다
There was a problem hiding this comment.
파일명에 방언을 넣었습니다. 크로스-ORM 스냅샷 69개를 ..._Drizzle.snap → ..._Drizzle_pg.snap으로 바꿨습니다.
이쪽이 pg 하나뿐인 이유는, 크로스-ORM 하니스가 부르는 OrmExporter 트레이트가 String 하나를 반환해서 방언을 고를 자리가 없기 때문입니다. 그래서 트레이트 경로는 pg를 정본으로 렌더하고, 세 방언 비교는 drizzle 모듈 자체 스냅샷(render_schema_full_file_per_dialect@{pg,mysql,sqlite})이 맡고 있습니다.
라벨은 DrizzleDialect::Pg.file_suffix()에서 파생시켜서 문자열을 따로 관리하지 않아도 표기가 어긋나지 않게 했고, 나머지 두 방언이 어디 있는지는 헬퍼 주석과 exporter AGENTS.md에도 적어뒀습니다.
개요
6번째 ORM 백엔드로 Drizzle(TypeScript)을 추가합니다.
Drizzle은 백엔드 중립 표현이 없어서, 한 번의 export가 방언별 파일 3개를 씁니다.
완성 기준은 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)를 사용하고, 나머지 규칙은 왕복에서 드리프트 문장이 나온 항목을 하나씩 제거하며 도출했습니다:uniqueIndex(): 컬럼 체인.unique()금지CREATE UNIQUE INDEX.unique()는 제약으로 introspect되어 kit가 드랍 후 재생성.generatedByDefaultAsIdentity()DROP IDENTITY를 시도{table}_pkey/ mysql{table}_{cols}{table}_{enum}, 테이블당pgEnum1개CREATE TYPE이 항상 테이블-프리픽스. Prisma처럼 전역 dedup하면 타입명 자체가 드리프트customType헬퍼Uint8Array라@types/node없이 컴파일sql`CURRENT_TIMESTAMP`/ mysql.defaultNow()/ sqlitesql`(CURRENT_TIMESTAMP)`sql`(uuid())`소문자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테이블 vsuser의 relations 블록), 테이블명이 import(sql,integer)나 콜백 파라미터(t,one,many)와 같을 수도 있습니다. 특히t가 겹치면 FK의 foreignColumns가 콜백 파라미터로 해석되어 컴파일은 되는데도 틀린 출력이 됩니다. 그래서 선언 순서대로 바인딩을 claim하고 충돌 시 숫자 접미사를 붙이며, 모든 참조가 확정된 이름을 따라갑니다. 충돌이 없는 스키마의 출력은 바이트 단위로 동일합니다.5. drizzle export가 확장자 글롭 클린을 쓰지 않는 이유
기존 export 경로는 재생성 전에 export 디렉터리에서 해당 확장자 파일을 재귀 삭제합니다. drizzle의 확장자는
.ts인데, export 루트(기본src/models)가 사용자 소스 디렉터리와 겹치는 프로젝트에서는 사용자가 직접 쓴 파일까지 지워집니다. 출력 파일명이 3개로 고정이라 덮어쓰기로 충분하므로 drizzle 경로는 클린을 생략하게 했습니다.테스트 설계
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 등 분기 단위 테스트렌더링 밖 검증:
drizzle-kit push반복 → 수렴 확인 (개요의 결과)tsc --strict무오류함께 들어간 변경
enum_scan(테이블 enum 수집),constraint_scan(FK 관계 네이밍·역관계 수집),utils/common(필드명 claim). Prisma는 위임으로 전환했고 기존 5개 백엔드의 스냅샷은 전부 무변경입니다vespertide-naming에to_camel_case/infer_relation_field_name을 추가했습니다 (JPA의_id스트립도 후자로 위임)prepare_export_dir로 공용화했습니다vespertide-exporter0.3.0 → 0.4.0:Orm이 exhaustive pub enum이라 변형 추가가 breaking입니다(cargo-semver-checks 기준).Orm에#[non_exhaustive]를 붙여 이후 백엔드 추가를 minor로 만들지는 API 계약 판단이라 이번에는 손대지 않았습니다한계
timestamp_text,enum_text등)은 sqlite-core 생성자로 표현할 수 없습니다. 런타임 동작(타입 affinity)은 문제가 없어 문서화로 종결했습니다