Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2892b78
feat(exporter): add Prisma ORM exporter
L33gn21 Jun 20, 2026
88bde9e
test(exporter): add Prisma ORM snapshots
L33gn21 Jun 20, 2026
0c6f27b
refactor(exporter): split Prisma exporter into mod/render/types/enums
L33gn21 Jun 22, 2026
dcbff76
fix(exporter): correct Prisma constraint names, integer-enum defaults…
L33gn21 Jun 23, 2026
c93b6aa
fix(exporter): drop Prisma provider/clientOutput config, make relatio…
yyuneu Jul 6, 2026
be8b305
fix(exporter): build Prisma test config via deserialization instead o…
yyuneu Jul 6, 2026
4ba12f2
test(exporter): accept stale Prisma snapshots predating dcbff76's fixes
yyuneu Jul 6, 2026
cd560ef
fix(exporter): Prisma에 존재하지 않는 @db.Interval/@db.Cidr/@db.Macaddr 속성 제거
yyuneu Jul 9, 2026
891f8dc
style: 이전 커밋에서 누락된 cargo fmt 적용
yyuneu Jul 9, 2026
3990cb2
feat(exporter): Prisma 스키마 생성에 백엔드별 provider/@db.* 매핑 추가
yyuneu Jul 9, 2026
2f4095b
feat(cli): export --orm prisma에 --backend(-b) 플래그 추가
yyuneu Jul 9, 2026
ce6d60d
fix(exporter): 최신 Prisma 형식에 맞춰 datasource url 줄 제거
yyuneu Jul 9, 2026
803da81
chore: 릴리즈 버전 산정용 changepack 추가 (exporter/config/cli Minor)
yyuneu Jul 9, 2026
a69138e
test: 뮤테이션 테스트 대응
yyuneu Jul 9, 2026
f68a8bd
test: 커버리지 보정
yyuneu Jul 9, 2026
9e187a4
Merge branch 'main' into feat/prisma-exporter
yyuneu Jul 11, 2026
911c22f
chore: RustSec 권고 대응
yyuneu Jul 11, 2026
d4ea60b
test: ERD 커버리지 보정
yyuneu Jul 11, 2026
12d239c
style(cli): export 주석의 깨진 화살표 문자 복원
yyuneu Jul 22, 2026
925dd80
refactor(config): Prisma relationMode 옵션 및 PrismaConfig 제거
yyuneu Jul 22, 2026
7f25710
refactor(exporter): PrismaProvider를 DatabaseBackend로 통합
yyuneu Jul 22, 2026
de11f22
test(cli): export Prisma 테스트를 tests 모듈로 분리
yyuneu Jul 22, 2026
158dd43
refactor(exporter): Prisma 스키마 생성 백엔드 중립화
yyuneu Jul 22, 2026
115b585
feat(exporter): Prisma uuid 필드에 MySQL binary(16) 주석 추가
yyuneu Jul 22, 2026
aadc00e
chore: clippy 1.97 신규 린트 대응
yyuneu Jul 22, 2026
3173c60
test: 뮤테이션 테스트 대응
yyuneu Jul 22, 2026
5048a6f
refactor(exporter): Prisma PascalCase 변환을 vespertide-naming으로 통합
yyuneu Jul 26, 2026
8cdb962
fix(exporter): Prisma 미래 타입 분기를 unreachable로 정렬
yyuneu Jul 26, 2026
081f593
refactor(exporter): Prisma uuid 주석 제거
yyuneu Jul 26, 2026
8751b39
refactor(exporter): Prisma 출력에서 datasource/generator 블록 제거
yyuneu Jul 26, 2026
eaff5ac
refactor(naming): SCREAMING_SNAKE 변환 헬퍼 추가 및 ORM 백엔드 통합
yyuneu Jul 26, 2026
8240d8d
test(exporter): numeric default 픽스처를 숫자형으로 교정
yyuneu Jul 26, 2026
332938a
test: 다중 입력 테스트를 rstest 케이스로 전환
yyuneu Aug 2, 2026
3134c37
test(exporter): Prisma default/enum 렌더링 계약 테스트 보강
yyuneu Aug 2, 2026
ea6319f
fix(exporter): Prisma 필드명 충돌·동명 enum 병합·default 따옴표 절단 수정
yyuneu Aug 2, 2026
373196f
fix(exporter): 식별자로 쓸 수 없는 테이블·컬럼·enum·관계 이름을 백엔드별 규칙으로 정규화
yyuneu Aug 2, 2026
0a01b89
docs(exporter): ORM 개수 표기를 5개로 맞추고 충돌 픽스처 주석 정정
yyuneu Aug 2, 2026
49b0169
fix(exporter): Prisma 모델 수준 제약이 참조하는 컬럼 이름도 정규화
yyuneu Aug 2, 2026
3dd63f1
refactor(exporter): 리뷰 반영 — ERD 래퍼 제거·Prisma 스키마 테스트 스냅샷 전환
yyuneu Aug 4, 2026
c587be6
docs(exporter): 백엔드 전용 진입점의 스냅샷 테스트 예외를 정책에 명시
yyuneu Aug 4, 2026
59dc670
fix(exporter): Prisma 복합 FK 관계 방출
yyuneu Aug 4, 2026
c7db9ae
fix(exporter): Prisma 출력을 SQL 계층 산출물과 일치 — 참조 액션 명시, 인덱스 이름 map
yyuneu Aug 4, 2026
f52edb4
docs: ORM Export 목록에 JPA·Prisma 반영
yyuneu Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_vKVGmtRxkfW3nE7rIhMcz.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"crates/vespertide-naming/Cargo.toml":"Minor","crates/vespertide-exporter/Cargo.toml":"Minor","crates/vespertide-cli/Cargo.toml":"Minor"},"note":"Prisma ORM exporter 추가","date":"2026-07-09T14:25:30.6412773Z"}
14 changes: 8 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ vespertide/
│ ├── vespertide-planner/ # Schema diffing, baseline reconstruction, validation
│ ├── vespertide-query/ # SQL generation (Postgres/MySQL/SQLite)
│ ├── vespertide-cli/ # CLI commands: init, diff, sql, revision, export
│ ├── vespertide-exporter/ # ORM codegen: SeaORM, SQLAlchemy, SQLModel
│ ├── vespertide-exporter/ # ORM codegen: SeaORM, SQLAlchemy, SQLModel, JPA, Prisma
│ ├── vespertide-loader/ # Filesystem loading of models/migrations
│ ├── vespertide-config/ # vespertide.json configuration
│ ├── vespertide-lsp/ # Language server: 13 LSP capabilities + HS-7~11 caching
Expand Down Expand Up @@ -47,7 +47,7 @@ vespertide/
| Schema diffing | `vespertide-planner/src/diff/` | topological sort for FK deps |
| SQL generation | `vespertide-query/src/sql/` | One file per action type |
| CLI commands | `vespertide-cli/src/commands/` | `cmd_*` functions |
| ORM export | `vespertide-exporter/src/{seaorm,sqlalchemy,sqlmodel,jpa}/` | Backend-specific generators |
| ORM export | `vespertide-exporter/src/{seaorm,sqlalchemy,sqlmodel,jpa,prisma}/` | Backend-specific generators |
| Compile-time macro | `vespertide-macro/src/lib.rs` | `vespertide_migration!` proc macro |
| **LSP RingCache (HS-7~11)** | `vespertide-lsp/src/cache.rs` | Generic ring-buffer LRU shared across symbols/diagnostics/drift/semantic-token caches |
| **LSP drift cache** | `vespertide-lsp/src/drift/cache.rs` | HS-10 drift cache implementation |
Expand Down Expand Up @@ -169,7 +169,7 @@ See `docs/clippy-allow-audit.md` for the full audit history.
| `QueryError::Other(...)` in new code | Emits deprecation warning. Use `SchemaError` / `InvalidColumnType` / `BackendError` / `UnsupportedAction` |
| Exhaustive struct literal for `MigrationOptions` / `VespertideConfig` | `#[non_exhaustive]` — use `..Default::default()` |
| Comparing newtype with `String::eq(&name.to_string(), "user")` | `TableName: PartialEq<&str>` — use `name == "user"` directly |
| Per-ORM exporter snapshot test (single ORM) | Use the 4-ORM `orm_cases!` macro; snapshots must cross-compare all ORMs |
| Per-ORM exporter snapshot test (single ORM) | Use the 5-ORM `orm_cases!` macro; snapshots must cross-compare all ORMs |

## COMMANDS

Expand Down Expand Up @@ -368,12 +368,14 @@ fn create_table_snapshot(#[case] backend: DatabaseBackend) {
}
```

This is the same pattern used by `vespertide-query` (3 backends, 357 snapshots) and `vespertide-exporter` (4 ORMs via `Orm` enum, 232 cross-ORM snapshots). When adding a new backend / ORM / format, the change is **one `#[case::name(Value)]` line**.
This is the same pattern used by `vespertide-query` (3 backends, 357 snapshots) and `vespertide-exporter` (5 ORMs via `Orm` enum, 335 cross-ORM snapshots). When adding a new backend / ORM / format, the change is **one `#[case::name(Value)]` line**.

### Exporter snapshots MUST cover ALL ORMs (no per-ORM snapshots)
Every `vespertide-exporter` snapshot test MUST be written through the shared `orm_cases!` rstest macro in `crates/vespertide-exporter/src/tests/mod.rs`, which renders each fixture for **all four ORMs** (`Orm::SeaOrm`, `Orm::SqlAlchemy`, `Orm::SqlModel`, `Orm::Jpa`). A new export scenario = ONE fixture + ONE `orm_cases!(...)` line, producing exactly four snapshots (one per ORM) in the single shared `crates/vespertide-exporter/src/tests/snapshots/` directory.
Every `vespertide-exporter` snapshot test MUST be written through the shared `orm_cases!` rstest macro in `crates/vespertide-exporter/src/tests/mod.rs`, which renders each fixture for **all five ORMs** (`Orm::SeaOrm`, `Orm::SqlAlchemy`, `Orm::SqlModel`, `Orm::Jpa`, `Orm::Prisma`). A new export scenario = ONE fixture + ONE `orm_cases!(...)` line, producing exactly five snapshots (one per ORM) in the single shared `crates/vespertide-exporter/src/tests/snapshots/` directory.

FORBIDDEN: per-ORM `#[test]` snapshot functions inside `src/seaorm/`, `src/sqlalchemy/`, `src/sqlmodel/`, `src/jpa/`, or any `snapshots/` directory other than `src/tests/snapshots/`. A scenario snapshotted for only one ORM is a defect — ORM output must always be cross-compared across all four. When adding a new ORM the change is a single `#[case::<orm>(Orm::<Variant>)]` line in the macro, never a new per-ORM test.
FORBIDDEN: per-ORM `#[test]` snapshot functions inside `src/seaorm/`, `src/sqlalchemy/`, `src/sqlmodel/`, `src/jpa/`, `src/prisma/`, or any `snapshots/` directory other than `src/tests/snapshots/`. A scenario snapshotted for only one ORM is a defect — ORM output must always be cross-compared across all five. When adding a new ORM the change is a single `#[case::<orm>(Orm::<Variant>)]` line in the macro, never a new per-ORM test.

Exception: an entry point that exists in only one backend (e.g. Prisma's single-file `render_schema`, which deduplicates enums globally) is not a cross-ORM scenario, so its snapshot tests live as inline tests of that module — with the snapshot files still written to the shared `src/tests/snapshots/` via `with_settings!(snapshot_path => ...)`.

### `#[cfg(test)]` test-oracle pattern
When a function exists solely as an oracle for a regression test (e.g. comparing
Expand Down
32 changes: 17 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Declarative database schema management. Define your schemas in JSON, and Vespert
- **Enum Types**: Native string enums and integer enums (no migration needed for new values)
- **Zero-Runtime Migrations**: Compile-time macro generates database-specific SQL
- **JSON Schema Validation**: Ships with JSON Schemas for IDE autocompletion and validation
- **ORM Export**: Export schemas to SeaORM, SQLAlchemy, SQLModel
- **ORM Export**: Export schemas to SeaORM, SQLAlchemy, SQLModel, JPA, Prisma
- **Language Server**: First-class editor support via the bundled `vespertide-lsp` — see [LSP Features](#lsp-features) below

## What's new in 0.2.0
Expand Down
1 change: 1 addition & 0 deletions crates/vespertide-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dot-writer = "0.1.4"
vespertide-config = { workspace = true, features = ["cli", "schema"] }
vespertide-core = { workspace = true, features = ["schema"] }
vespertide-loader = { workspace = true }
vespertide-naming = { workspace = true }
vespertide-planner = { workspace = true }
vespertide-query = { workspace = true }
vespertide-exporter = { workspace = true }
Expand Down
14 changes: 8 additions & 6 deletions crates/vespertide-cli/src/commands/erd/dot.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use dot_writer::{Attributes, DotWriter, RankDirection, Shape};
use vespertide_core::{ColumnDef, TableDef};
use vespertide_naming::{IdentifierStart, sanitize_identifier};

use super::{
ForeignKeyRelation, collect_foreign_key_relations, column_markers, sanitize_identifier,
};
use super::{ForeignKeyRelation, collect_foreign_key_relations, column_markers};

pub fn render_dot(tables: &[TableDef]) -> String {
DotWriter::write_string(|writer| {
Expand All @@ -25,16 +24,19 @@ pub fn render_dot(tables: &[TableDef]) -> String {
}

for table in tables {
let mut node = digraph.node_named(sanitize_identifier(&table.name));
let mut node = digraph.node_named(sanitize_identifier(
&table.name,
IdentifierStart::Underscore,
));
node.set_shape(Shape::Record);
node.set_label(&record_label(table));
}

for relation in collect_foreign_key_relations(tables) {
let mut edge_attributes = digraph
.edge(
sanitize_identifier(&relation.child_table),
sanitize_identifier(&relation.parent_table),
sanitize_identifier(&relation.child_table, IdentifierStart::Underscore),
sanitize_identifier(&relation.parent_table, IdentifierStart::Underscore),
)
.attributes();
edge_attributes.set_label(&relationship_label(&relation));
Expand Down
17 changes: 11 additions & 6 deletions crates/vespertide-cli/src/commands/erd/mermaid.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
use std::fmt::Write as _;

use vespertide_core::{ColumnType, ComplexColumnType, EnumValues, SimpleColumnType, TableDef};
use vespertide_naming::{IdentifierStart, sanitize_identifier};

use super::{
Cardinality, ForeignKeyRelation, collect_foreign_key_relations, is_foreign_key_column,
is_primary_key_column, sanitize_identifier,
is_primary_key_column,
};

pub fn render_mermaid(tables: &[TableDef]) -> String {
let mut output = String::from("erDiagram\n");

for table in tables {
writeln!(output, " {} {{", sanitize_identifier(&table.name))
.expect("write Mermaid table header");
writeln!(
output,
" {} {{",
sanitize_identifier(&table.name, IdentifierStart::Underscore)
)
.expect("write Mermaid table header");

for column in &table.columns {
let primary_key = if is_primary_key_column(table, &column.name) {
Expand All @@ -30,7 +35,7 @@ pub fn render_mermaid(tables: &[TableDef]) -> String {
output,
" {} {}{}{}",
column_type_to_mermaid(&column.r#type),
sanitize_identifier(&column.name),
sanitize_identifier(&column.name, IdentifierStart::Underscore),
primary_key,
foreign_key
)
Expand All @@ -45,9 +50,9 @@ pub fn render_mermaid(tables: &[TableDef]) -> String {
writeln!(
output,
" {} {} {} : \"{}\"",
sanitize_identifier(left_table),
sanitize_identifier(left_table, IdentifierStart::Underscore),
connector,
sanitize_identifier(right_table),
sanitize_identifier(right_table, IdentifierStart::Underscore),
escape_mermaid_label(&relation.child_columns.join(", "))
)
.expect("write Mermaid relationship");
Expand Down
21 changes: 0 additions & 21 deletions crates/vespertide-cli/src/commands/erd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,27 +262,6 @@ pub(super) fn column_markers(table: &TableDef, column: &ColumnDef) -> String {
}
}

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
}
}

fn inline_foreign_key_relation(
table: &TableDef,
column: &ColumnDef,
Expand Down
40 changes: 18 additions & 22 deletions crates/vespertide-cli/src/commands/erd/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,28 +539,6 @@ fn svg_cardinality_labels(schema: &[TableDef]) -> String {
.join("\n")
}

// === sanitize_identifier edge cases ===

#[test]
fn sanitize_identifier_digit_first_prefixes_underscore() {
assert_eq!(sanitize_identifier("9lives"), "_9lives");
}

#[test]
fn sanitize_identifier_empty_returns_underscore() {
assert_eq!(sanitize_identifier(""), "_");
}

#[test]
fn sanitize_identifier_non_ascii_becomes_underscore() {
assert_eq!(sanitize_identifier("a b-c.d"), "a_b_c_d");
}

#[test]
fn sanitize_identifier_preserves_underscores_and_alphanumerics() {
assert_eq!(sanitize_identifier("table_99_ok"), "table_99_ok");
}

// === parallel FK edges between same (child, parent) pair ===

#[test]
Expand Down Expand Up @@ -664,6 +642,24 @@ fn malformed_inline_fk_reference_is_ignored() {
assert!(collect_foreign_key_relations(&[users, bad]).is_empty());
}

#[test]
fn table_level_fk_to_missing_table_is_ignored() {
let mut orphan = table(
"orphan",
vec![primary_key("id", integer()), column("ghost_id", integer())],
);
orphan.constraints.push(TableConstraint::ForeignKey {
name: Some("fk_orphan_ghost".into()),
columns: vec!["ghost_id".into()],
ref_table: "ghost".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
orphan_strategy: Default::default(),
});
assert!(collect_foreign_key_relations(&[orphan]).is_empty());
}

#[test]
fn inline_unique_group_variants_drive_one_to_one_detection() {
let users = normalize(&table("users", vec![primary_key("id", integer())]));
Expand Down
Loading