Skip to content

Commit dcbff76

Browse files
L33gn21claude
andcommitted
fix(exporter): correct Prisma constraint names, integer-enum defaults, 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>
1 parent 0c6f27b commit dcbff76

10 files changed

Lines changed: 62 additions & 14 deletions

crates/vespertide-exporter/src/prisma/enums.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,11 @@ pub(super) fn to_screaming_snake(s: &str) -> String {
5555
prev_lower = false;
5656
}
5757
}
58-
result.trim_end_matches('_').to_string()
58+
let result = result.trim_end_matches('_').to_string();
59+
// Prisma identifiers cannot start with a digit; prefix with '_' to keep it valid.
60+
if result.starts_with(|c: char| c.is_ascii_digit()) {
61+
format!("_{result}")
62+
} else {
63+
result
64+
}
5965
}

crates/vespertide-exporter/src/prisma/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,13 @@ fn collect_table_enums(table: &TableDef) -> Vec<(&str, &EnumValues)> {
9898
result
9999
}
100100

101-
/// Render enum blocks + model block without schema context (no back-relations).
101+
/// Render enum blocks + model block without schema context.
102+
///
103+
/// Passes the table itself as a one-element schema so that self-referential FK
104+
/// back-relations are always emitted (Prisma requires both sides of a relation to
105+
/// be present in the model, including self-referential ones).
102106
pub fn render_entity(table: &TableDef) -> String {
103-
render_entity_with_schema(table, &[])
107+
render_entity_with_schema(table, std::slice::from_ref(table))
104108
}
105109

106110
/// Render enum blocks + model block with full schema context (includes back-relations).

crates/vespertide-exporter/src/prisma/render.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,16 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String {
249249

250250
// Emit inline relation field for FK columns
251251
if let Some(fk) = fk_by_col.get(col_name) {
252-
let rel_field_name = infer_relation_field_name(col_name);
252+
// `rel_name_segment` drives @relation("…") naming — must stay consistent
253+
// with the segment computed by collect_back_relations on the other side.
254+
let rel_name_segment = infer_relation_field_name(col_name);
255+
// When no `_id` was stripped the inferred name equals the FK column name,
256+
// which would collide with the scalar field. Append `_rel` to deduplicate.
257+
let rel_field_name = if rel_name_segment == col_name {
258+
format!("{col_name}_rel")
259+
} else {
260+
rel_name_segment.clone()
261+
};
253262
let rel_model = to_pascal_case(fk.ref_table);
254263
let rel_type = if col.nullable {
255264
format!("{rel_model}?")
@@ -263,8 +272,9 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String {
263272

264273
let mut rel_args: Vec<String> = Vec::new();
265274
if needs_name {
275+
// Use rel_name_segment (pre-dedup) so the name matches back-relations.
266276
let table_pascal = to_pascal_case(&table.name);
267-
let field_pascal = to_pascal_case(&rel_field_name);
277+
let field_pascal = to_pascal_case(&rel_name_segment);
268278
rel_args.push(format!("\"{table_pascal}{field_pascal}\""));
269279
}
270280
rel_args.push(format!("fields: [{col_name}]"));
@@ -321,7 +331,7 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String {
321331
{
322332
let cols = columns.join(", ");
323333
if let Some(n) = name {
324-
lines.push(format!(" @@unique([{cols}], name: \"{n}\")"));
334+
lines.push(format!(" @@unique([{cols}], map: \"{n}\")"));
325335
} else {
326336
lines.push(format!(" @@unique([{cols}])"));
327337
}
@@ -333,7 +343,7 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String {
333343
if let TableConstraint::Index { name, columns } = c {
334344
let cols = columns.join(", ");
335345
if let Some(n) = name {
336-
lines.push(format!(" @@index([{cols}], name: \"{n}\")"));
346+
lines.push(format!(" @@index([{cols}], map: \"{n}\")"));
337347
} else {
338348
lines.push(format!(" @@index([{cols}])"));
339349
}
@@ -349,6 +359,28 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String {
349359
}
350360

351361
fn prisma_default_attr(default_sql: &str, col_type: &ColumnType) -> String {
362+
// Integer-backed enum: resolve to a variant identifier (SCREAMING_SNAKE), never a bare int.
363+
if let ColumnType::Complex(ComplexColumnType::Enum {
364+
values: EnumValues::Integer(int_values),
365+
..
366+
}) = col_type
367+
{
368+
let key = default_sql.trim_matches(|c: char| c == '\'' || c == '"');
369+
// 1) numeric value match → variant name
370+
if let Ok(n) = key.parse::<i64>()
371+
&& let Some(v) = int_values.iter().find(|v| v.value == n)
372+
{
373+
return format!("@default({})", to_screaming_snake(&v.name));
374+
}
375+
// 2) exact variant-name match → variant name
376+
if let Some(v) = int_values.iter().find(|v| v.name == key) {
377+
return format!("@default({})", to_screaming_snake(&v.name));
378+
}
379+
// 3) no match → dbgenerated fallback (valid PSL; avoids bare-int type error)
380+
let escaped = key.replace('"', "\\\"");
381+
return format!("@default(dbgenerated(\"{escaped}\"))");
382+
}
383+
352384
if default_sql == "true" {
353385
return "@default(true)".into();
354386
}

crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: crates/vespertide-exporter/src/tests/mod.rs
3+
assertion_line: 172
34
expression: rendered
45
---
56
model OrderItems {
@@ -10,7 +11,7 @@ model OrderItems {
1011
quantity Int
1112

1213
@@id([order_id, product_id])
13-
@@unique([order_id, product_id], name: "uq_order_items__order_product")
14-
@@index([order_id], name: "ix_order_items__order_id")
14+
@@unique([order_id, product_id], map: "uq_order_items__order_product")
15+
@@index([order_id], map: "ix_order_items__order_id")
1516
@@map("order_items")
1617
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---
22
source: crates/vespertide-exporter/src/tests/mod.rs
3+
assertion_line: 182
34
expression: rendered
45
---
56
model CompositeIndex {
67
id Int @id
78
tenant_id Int
89
name String @db.Text
910

10-
@@index([tenant_id, name], name: "idx_tenant_name")
11+
@@index([tenant_id, name], map: "idx_tenant_name")
1112
@@map("composite_index")
1213
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---
22
source: crates/vespertide-exporter/src/tests/mod.rs
3+
assertion_line: 252
34
expression: rendered
45
---
56
model AccountAliases {
67
id Int @id
78
tenant_id Int
89
slug String @db.Text
910

10-
@@unique([tenant_id, slug], name: "uq_account_aliases__tenant_slug")
11+
@@unique([tenant_id, slug], map: "uq_account_aliases__tenant_slug")
1112
@@map("account_aliases")
1213
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---
22
source: crates/vespertide-exporter/src/tests/mod.rs
3+
assertion_line: 177
34
expression: rendered
45
---
56
model CompositeUnique {
67
id Int @id
78
tenant_id Int
89
name String @db.Text
910

10-
@@unique([tenant_id, name], name: "uq_tenant_name")
11+
@@unique([tenant_id, name], map: "uq_tenant_name")
1112
@@map("composite_unique")
1213
}

crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: rendered
55
model Session {
66
id String @id @db.Uuid
77
username String @db.Text
8-
username User @relation(fields: [username], references: [username])
8+
username_rel User @relation(fields: [username], references: [username])
99

1010
@@map("session")
1111
}

crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ model Employees {
66
id Int @id
77
manager_id Int?
88
manager Employees? @relation("EmployeesManager", fields: [manager_id], references: [id], onDelete: SetNull)
9+
manager_employees Employees[] @relation("EmployeesManager")
910

1011
@@map("employees")
1112
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
---
22
source: crates/vespertide-exporter/src/tests/mod.rs
3+
assertion_line: 131
34
expression: rendered
45
---
56
model Articles {
67
id Int @id
78
title String @db.Text
89
created_at DateTime @db.Timestamptz
910

10-
@@index([created_at], name: "idx_articles_created_at")
11+
@@index([created_at], map: "idx_articles_created_at")
1112
@@index([title])
1213
@@map("articles")
1314
}

0 commit comments

Comments
 (0)