From 2892b7829cd59991a80f69db02a0299c78738e8a Mon Sep 17 00:00:00 2001 From: L33gn21 Date: Sun, 21 Jun 2026 04:02:07 +0900 Subject: [PATCH 1/4] feat(exporter): add Prisma ORM exporter 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 --- crates/vespertide-cli/src/commands/export.rs | 44 +- crates/vespertide-config/src/config.rs | 54 ++ crates/vespertide-config/src/lib.rs | 2 +- crates/vespertide-exporter/src/lib.rs | 4 +- crates/vespertide-exporter/src/orm.rs | 9 +- crates/vespertide-exporter/src/prisma/mod.rs | 607 ++++++++++++++++++ crates/vespertide-exporter/src/tests/mod.rs | 6 + .../tests/parallel_consolidated.rs | 1 + schemas/config.schema.json | 32 + 9 files changed, 754 insertions(+), 5 deletions(-) create mode 100644 crates/vespertide-exporter/src/prisma/mod.rs diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs index 7eec2f5..76b0db7 100644 --- a/crates/vespertide-cli/src/commands/export.rs +++ b/crates/vespertide-cli/src/commands/export.rs @@ -8,7 +8,10 @@ use rayon::prelude::*; use tokio::fs; use vespertide_config::VespertideConfig; use vespertide_core::TableDef; -use vespertide_exporter::{Orm, render_entity_with_schema, seaorm::SeaOrmExporterWithConfig}; +use vespertide_exporter::{ + Orm, render_entity_with_schema, prisma::PrismaExporterWithConfig, + seaorm::SeaOrmExporterWithConfig, +}; use crate::parallel_config::{EXPORT_RENDER_PAR_MIN_LEN, EXPORT_RENDER_PAR_THRESHOLD}; use crate::utils::load_config; @@ -19,6 +22,7 @@ pub enum OrmArg { Sqlalchemy, Sqlmodel, Jpa, + Prisma, } impl From for Orm { @@ -28,6 +32,7 @@ impl From for Orm { OrmArg::Sqlalchemy => Orm::SqlAlchemy, OrmArg::Sqlmodel => Orm::SqlModel, OrmArg::Jpa => Orm::Jpa, + OrmArg::Prisma => Orm::Prisma, } } } @@ -51,6 +56,11 @@ pub async fn cmd_export(orm: OrmArg, export_dir: Option) -> Result<()> let target_root = resolve_export_dir(export_dir, &config); + // Prisma uses a single-file output strategy + if matches!(orm, OrmArg::Prisma) { + return cmd_export_prisma(config, normalized_models, target_root).await; + } + // Clean the export directory before regenerating let orm_kind: Orm = orm.into(); clean_export_dir(&target_root, orm_kind).await?; @@ -238,6 +248,7 @@ async fn clean_export_dir(root: &Path, orm: Orm) -> Result<()> { Orm::SeaOrm => "rs", Orm::SqlAlchemy | Orm::SqlModel => "py", Orm::Jpa => "java", + Orm::Prisma => "prisma", }; clean_dir_recursive(root, ext).await?; @@ -330,6 +341,7 @@ fn build_output_path(root: &Path, rel_path: &Path, orm: Orm) -> PathBuf { Orm::SeaOrm => "rs", Orm::SqlAlchemy | Orm::SqlModel => "py", Orm::Jpa => "java", + Orm::Prisma => "prisma", }; // Java requires filename to match PascalCase class name let file_stem = if matches!(orm, Orm::Jpa) { @@ -426,6 +438,36 @@ async fn ensure_mod_chain(root: &Path, rel_path: &Path) -> Result<()> { Ok(()) } +async fn cmd_export_prisma( + config: VespertideConfig, + normalized_models: Vec<(TableDef, PathBuf)>, + target_root: PathBuf, +) -> Result<()> { + let all_tables: Vec = normalized_models.iter().map(|(t, _)| t.clone()).collect(); + let content = PrismaExporterWithConfig::new(config.prisma()).render_schema(&all_tables); + + clean_export_dir(&target_root, Orm::Prisma).await?; + + if !target_root.exists() { + fs::create_dir_all(&target_root) + .await + .with_context(|| format!("create export dir {}", target_root.display()))?; + } + + let out_path = target_root.join("schema.prisma"); + fs::write(&out_path, &content) + .await + .with_context(|| format!("write {}", out_path.display()))?; + + println!( + "Exported {} model(s) -> {}", + normalized_models.len(), + out_path.display() + ); + + Ok(()) +} + #[async_recursion::async_recursion] async fn walk_models( root: &Path, diff --git a/crates/vespertide-config/src/config.rs b/crates/vespertide-config/src/config.rs index 126c28c..c4a7690 100644 --- a/crates/vespertide-config/src/config.rs +++ b/crates/vespertide-config/src/config.rs @@ -78,6 +78,51 @@ impl SeaOrmConfig { } } +/// Prisma-specific export configuration. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct PrismaConfig { + /// Database provider: postgresql, mysql, sqlite, sqlserver, mongodb, cockroachdb. + #[serde(default = "default_prisma_provider")] + pub provider: String, + /// Optional output path for the generated Prisma client. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_output: Option, + /// Optional relationMode override ("foreignKeys" or "prisma"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub relation_mode: Option, +} + +fn default_prisma_provider() -> String { + "postgresql".to_string() +} + +impl Default for PrismaConfig { + fn default() -> Self { + Self { + provider: default_prisma_provider(), + client_output: None, + relation_mode: None, + } + } +} + +impl PrismaConfig { + pub fn provider(&self) -> &str { + &self.provider + } + + pub fn client_output(&self) -> Option<&str> { + self.client_output.as_deref() + } + + pub fn relation_mode(&self) -> Option<&str> { + self.relation_mode.as_deref() + } +} + /// Top-level vespertide configuration. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -100,6 +145,9 @@ pub struct VespertideConfig { /// SeaORM-specific export configuration. #[serde(default)] pub seaorm: SeaOrmConfig, + /// Prisma-specific export configuration. + #[serde(default)] + pub prisma: PrismaConfig, /// Prefix to add to all table names (including migration version table). /// Default: "" (no prefix) #[serde(default)] @@ -138,6 +186,7 @@ impl Default for VespertideConfig { migration_filename_pattern: default_migration_filename_pattern(), model_export_dir: default_model_export_dir(), seaorm: SeaOrmConfig::default(), + prisma: PrismaConfig::default(), prefix: String::new(), lock_timeout_ms: None, statement_timeout_ms: None, @@ -191,6 +240,11 @@ impl VespertideConfig { &self.seaorm } + /// Prisma-specific export configuration. + pub fn prisma(&self) -> &PrismaConfig { + &self.prisma + } + /// Prefix to add to all table names. pub fn prefix(&self) -> &str { &self.prefix diff --git a/crates/vespertide-config/src/lib.rs b/crates/vespertide-config/src/lib.rs index e8f6640..0e72765 100644 --- a/crates/vespertide-config/src/lib.rs +++ b/crates/vespertide-config/src/lib.rs @@ -7,7 +7,7 @@ pub mod config; pub mod file_format; pub mod name_case; -pub use config::{SeaOrmConfig, VespertideConfig, default_migration_filename_pattern}; +pub use config::{PrismaConfig, SeaOrmConfig, VespertideConfig, default_migration_filename_pattern}; pub use file_format::FileFormat; pub use name_case::NameCase; diff --git a/crates/vespertide-exporter/src/lib.rs b/crates/vespertide-exporter/src/lib.rs index 0d4732c..c1d5069 100644 --- a/crates/vespertide-exporter/src/lib.rs +++ b/crates/vespertide-exporter/src/lib.rs @@ -1,9 +1,10 @@ //! Helpers to convert `TableDef` models into ORM-specific representations -//! such as `SeaORM`, `SQLAlchemy`, `SQLModel`, and JPA. +//! such as `SeaORM`, `SQLAlchemy`, `SQLModel`, JPA, and Prisma. pub mod jpa; pub mod orm; mod parallel_config; +pub mod prisma; pub mod seaorm; pub mod sqlalchemy; pub mod sqlmodel; @@ -13,6 +14,7 @@ mod utils; pub use jpa::JpaExporter; pub use orm::{Orm, OrmExporter, render_entity, render_entity_with_schema}; +pub use prisma::PrismaExporter; pub use seaorm::{SeaOrmExporter, render_entity as render_seaorm_entity}; pub use sqlalchemy::SqlAlchemyExporter; pub use sqlmodel::SqlModelExporter; diff --git a/crates/vespertide-exporter/src/orm.rs b/crates/vespertide-exporter/src/orm.rs index ea18829..d1d68d2 100644 --- a/crates/vespertide-exporter/src/orm.rs +++ b/crates/vespertide-exporter/src/orm.rs @@ -1,8 +1,8 @@ use vespertide_core::TableDef; use crate::{ - jpa::JpaExporter, seaorm::SeaOrmExporter, sqlalchemy::SqlAlchemyExporter, - sqlmodel::SqlModelExporter, + jpa::JpaExporter, prisma::PrismaExporter, seaorm::SeaOrmExporter, + sqlalchemy::SqlAlchemyExporter, sqlmodel::SqlModelExporter, }; /// Supported ORM targets. @@ -12,6 +12,7 @@ pub enum Orm { SqlAlchemy, SqlModel, Jpa, + Prisma, } /// Standardized exporter interface for all supported ORMs. @@ -36,6 +37,7 @@ pub fn render_entity(orm: Orm, table: &TableDef) -> Result { Orm::SqlAlchemy => SqlAlchemyExporter.render_entity(table), Orm::SqlModel => SqlModelExporter.render_entity(table), Orm::Jpa => JpaExporter.render_entity(table), + Orm::Prisma => PrismaExporter.render_entity(table), } } @@ -50,6 +52,7 @@ pub fn render_entity_with_schema( Orm::SqlAlchemy => SqlAlchemyExporter.render_entity_with_schema(table, schema), Orm::SqlModel => SqlModelExporter.render_entity_with_schema(table, schema), Orm::Jpa => JpaExporter.render_entity_with_schema(table, schema), + Orm::Prisma => PrismaExporter.render_entity_with_schema(table, schema), } } @@ -64,6 +67,7 @@ mod tests { #[case::sqlalchemy(Orm::SqlAlchemy)] #[case::sqlmodel(Orm::SqlModel)] #[case::jpa(Orm::Jpa)] + #[case::prisma(Orm::Prisma)] fn dispatch_render_entity_succeeds(#[case] orm: Orm) { let table = basic_single_pk(); assert!(render_entity(orm, &table).is_ok()); @@ -74,6 +78,7 @@ mod tests { #[case::sqlalchemy(Orm::SqlAlchemy)] #[case::sqlmodel(Orm::SqlModel)] #[case::jpa(Orm::Jpa)] + #[case::prisma(Orm::Prisma)] fn dispatch_render_entity_with_schema_succeeds(#[case] orm: Orm) { let table = basic_single_pk(); let schema = vec![table.clone()]; diff --git a/crates/vespertide-exporter/src/prisma/mod.rs b/crates/vespertide-exporter/src/prisma/mod.rs new file mode 100644 index 0000000..c28629a --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/mod.rs @@ -0,0 +1,607 @@ +use std::collections::{HashMap, HashSet}; + +use crate::orm::OrmExporter; +use vespertide_config::PrismaConfig; +use vespertide_core::schema::column::{ColumnType, ComplexColumnType, EnumValues, SimpleColumnType}; +use vespertide_core::schema::constraint::TableConstraint; +use vespertide_core::schema::names::ColumnName; +use vespertide_core::schema::reference::ReferenceAction; +use vespertide_core::TableDef; + +pub struct PrismaExporter; + +impl OrmExporter for PrismaExporter { + fn render_entity(&self, table: &TableDef) -> Result { + Ok(render_entity(table)) + } + + fn render_entity_with_schema( + &self, + table: &TableDef, + schema: &[TableDef], + ) -> Result { + Ok(render_entity_with_schema(table, schema)) + } +} + +/// Prisma exporter with configuration support. +/// +/// Assembles a complete `schema.prisma` file from a full table list. +pub struct PrismaExporterWithConfig<'a> { + pub config: &'a PrismaConfig, +} + +impl<'a> PrismaExporterWithConfig<'a> { + pub fn new(config: &'a PrismaConfig) -> Self { + Self { config } + } + + /// Render a complete `schema.prisma` file for all tables. + /// + /// Output order: datasource → generator → (globally deduped) enum blocks → model blocks. + pub fn render_schema(&self, tables: &[TableDef]) -> String { + let mut seen_enums: HashSet = HashSet::new(); + let mut enum_blocks: Vec = Vec::new(); + for table in tables { + for (name, values) in collect_table_enums(table) { + if seen_enums.insert(name.to_string()) { + enum_blocks.push(render_enum(name, values)); + } + } + } + + let mut parts: Vec = Vec::new(); + + let mut datasource = vec![ + "datasource db {".to_string(), + format!(" provider = \"{}\"", self.config.provider()), + " url = env(\"DATABASE_URL\")".to_string(), + ]; + if let Some(rm) = self.config.relation_mode() { + datasource.push(format!(" relationMode = \"{}\"", rm)); + } + datasource.push("}".to_string()); + parts.push(datasource.join("\n")); + + let mut generator = vec![ + "generator client {".to_string(), + " provider = \"prisma-client-js\"".to_string(), + ]; + if let Some(output) = self.config.client_output() { + generator.push(format!(" output = \"{}\"", output)); + } + generator.push("}".to_string()); + parts.push(generator.join("\n")); + + parts.extend(enum_blocks); + + for table in tables { + parts.push(render_model(table, tables)); + } + + parts.join("\n\n") + "\n" + } +} + +fn collect_table_enums<'a>(table: &'a TableDef) -> Vec<(&'a str, &'a EnumValues)> { + let mut seen = HashSet::new(); + let mut result = Vec::new(); + for col in &table.columns { + if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) = &col.r#type { + if seen.insert(name.as_str()) { + result.push((name.as_str(), values)); + } + } + } + result +} + +/// Render enum blocks + model block without schema context (no back-relations). +pub fn render_entity(table: &TableDef) -> String { + render_entity_with_schema(table, &[]) +} + +/// Render enum blocks + model block with full schema context (includes back-relations). +pub fn render_entity_with_schema(table: &TableDef, schema: &[TableDef]) -> String { + let mut parts: Vec = Vec::new(); + for (name, values) in collect_table_enums(table) { + parts.push(render_enum(name, values)); + } + parts.push(render_model(table, schema)); + parts.join("\n\n") +} + +/// Multi-table entry point: render every table (enum + model blocks) with full +/// schema context and join them. Mirrors the other ORMs' `export` so the +/// cross-ORM test harness can dispatch Prisma through a single call. The +/// `datasource`/`generator` wrapper lives in [`PrismaExporterWithConfig`]. +pub fn export(schema: &[TableDef]) -> Result { + Ok(schema + .iter() + .map(|table| render_entity_with_schema(table, schema)) + .collect::>() + .join("\n\n")) +} + +/// Test-only accessor for the internal `to_pascal_case` helper, mirroring the +/// other ORM backends so the cross-ORM consolidation test can exercise it +/// without making the helper generally public. +#[cfg(test)] +pub fn to_pascal_case_for_tests(s: &str) -> String { + to_pascal_case(s) +} + +fn render_enum(name: &str, values: &EnumValues) -> String { + let enum_name = to_pascal_case(name); + let mut lines = Vec::new(); + lines.push(format!("enum {} {{", enum_name)); + match values { + EnumValues::String(vals) => { + for val in vals { + let variant = to_screaming_snake(val); + if variant == *val { + lines.push(format!(" {}", variant)); + } else { + lines.push(format!(" {} @map(\"{}\")", variant, val)); + } + } + } + EnumValues::Integer(vals) => { + // Prisma doesn't support integer enums natively; emit as string variants with comment + for val in vals { + let variant = to_screaming_snake(&val.name); + lines.push(format!(" {} // = {}", variant, val.value)); + } + } + } + lines.push("}".into()); + lines.join("\n") +} + +struct PkInfo { + columns: Vec, + auto_increment: bool, +} + +fn extract_pk_info(constraints: &[TableConstraint]) -> PkInfo { + for c in constraints { + if let TableConstraint::PrimaryKey { auto_increment, columns, .. } = c { + return PkInfo { + columns: columns.iter().map(|c| c.to_string()).collect(), + auto_increment: *auto_increment, + }; + } + } + PkInfo { columns: Vec::new(), auto_increment: false } +} + +struct FkInfo<'a> { + ref_table: &'a str, + ref_cols: &'a [ColumnName], + on_delete: Option<&'a ReferenceAction>, + on_update: Option<&'a ReferenceAction>, +} + +fn render_model(table: &TableDef, schema: &[TableDef]) -> String { + let mut lines: Vec = Vec::new(); + + if let Some(desc) = &table.description { + for line in desc.lines() { + lines.push(format!("/// {}", line)); + } + } + + let model_name = to_pascal_case(&table.name); + lines.push(format!("model {} {{", model_name)); + + let pk_info = extract_pk_info(&table.constraints); + let pk_columns: HashSet<&str> = pk_info.columns.iter().map(|s| s.as_str()).collect(); + let is_composite_pk = pk_info.columns.len() > 1; + + let unique_single: HashMap<&str, Option<&str>> = table.constraints.iter() + .filter_map(|c| { + if let TableConstraint::Unique { name, columns, .. } = c { + if columns.len() == 1 { Some((columns[0].as_str(), name.as_deref())) } else { None } + } else { None } + }) + .collect(); + + // FK lookup by column + let fk_by_col: HashMap<&str, FkInfo<'_>> = table.constraints.iter() + .filter_map(|c| { + if let TableConstraint::ForeignKey { columns, ref_table, ref_columns, on_delete, on_update, .. } = c { + if columns.len() == 1 { + Some(( + columns[0].as_str(), + FkInfo { + ref_table: ref_table.as_str(), + ref_cols: ref_columns.as_slice(), + on_delete: on_delete.as_ref(), + on_update: on_update.as_ref(), + }, + )) + } else { None } + } else { None } + }) + .collect(); + + // Count FKs per ref_table for disambiguation detection + let mut ref_table_fk_count: HashMap<&str, usize> = HashMap::new(); + for fk in fk_by_col.values() { + *ref_table_fk_count.entry(fk.ref_table).or_default() += 1; + } + + // Render scalar fields + inline relation fields + for col in &table.columns { + let col_name = col.name.as_str(); + let in_pk = pk_columns.contains(col_name); + let is_single_pk = in_pk && !is_composite_pk; + let auto_inc = is_single_pk && pk_info.auto_increment; + let is_unique = unique_single.get(col_name).copied(); + + if let Some(ref comment) = col.comment { + lines.push(format!(" /// {}", comment.replace('\n', " "))); + } + + let (type_str, native_attr) = column_type_to_prisma(&col.r#type, col.nullable); + let mut attrs: Vec = Vec::new(); + + if is_single_pk { + attrs.push("@id".into()); + if auto_inc { + attrs.push("@default(autoincrement())".into()); + } + } + + if !auto_inc { + if let Some(ref default) = col.default { + attrs.push(prisma_default_attr(default.to_sql(), &col.r#type)); + } + } + + if let Some(unique_name) = is_unique { + if !is_single_pk { + match unique_name { + Some(n) => attrs.push(format!("@unique(map: \"{}\")", n)), + None => attrs.push("@unique".into()), + } + } + } + + if let Some(ref native) = native_attr { + attrs.push(native.clone()); + } + + let attrs_str = if attrs.is_empty() { + String::new() + } else { + format!(" {}", attrs.join(" ")) + }; + + lines.push(format!(" {} {}{}", col_name, type_str, attrs_str)); + + // Emit inline relation field for FK columns + if let Some(fk) = fk_by_col.get(col_name) { + let rel_field_name = infer_relation_field_name(col_name); + let rel_model = to_pascal_case(fk.ref_table); + let rel_type = if col.nullable { + format!("{}?", rel_model) + } else { + rel_model.clone() + }; + + let multi_fk = ref_table_fk_count.get(fk.ref_table).copied().unwrap_or(0) > 1; + let is_self_ref = fk.ref_table == table.name.as_str(); + let needs_name = multi_fk || is_self_ref; + + let mut rel_args: Vec = Vec::new(); + if needs_name { + let rel_name = format!( + "{}{}", + to_pascal_case(&table.name), + to_pascal_case(&rel_field_name) + ); + rel_args.push(format!("\"{}\"", rel_name)); + } + rel_args.push(format!("fields: [{}]", col_name)); + rel_args.push(format!( + "references: [{}]", + fk.ref_cols.iter().map(|s| s.as_str()).collect::>().join(", ") + )); + if let Some(od) = fk.on_delete { + rel_args.push(format!("onDelete: {}", reference_action_to_prisma(od))); + } + if let Some(ou) = fk.on_update { + rel_args.push(format!("onUpdate: {}", reference_action_to_prisma(ou))); + } + + lines.push(format!( + " {} {} @relation({})", + rel_field_name, + rel_type, + rel_args.join(", ") + )); + } + } + + // Back-relations from schema context + if !schema.is_empty() { + let back_rels = collect_back_relations(&table.name, schema); + for br in &back_rels { + let (field_name, rel_type) = back_rel_field(br); + let rel_attr = match &br.relation_name { + Some(name) => format!(" @relation(\"{}\")", name), + None => String::new(), + }; + lines.push(format!(" {} {}{}", field_name, rel_type, rel_attr)); + } + } + + // Blank line before model-level attributes + lines.push(String::new()); + + // Composite PK + if is_composite_pk { + lines.push(format!(" @@id([{}])", pk_info.columns.join(", "))); + } + + // Composite unique constraints + for c in &table.constraints { + if let TableConstraint::Unique { name, columns, .. } = c { + if columns.len() > 1 { + let cols = columns.join(", "); + if let Some(n) = name { + lines.push(format!(" @@unique([{}], name: \"{}\")", cols, n)); + } else { + lines.push(format!(" @@unique([{}])", cols)); + } + } + } + } + + // All index constraints + for c in &table.constraints { + if let TableConstraint::Index { name, columns } = c { + let cols = columns.join(", "); + if let Some(n) = name { + lines.push(format!(" @@index([{}], name: \"{}\")", cols, n)); + } else { + lines.push(format!(" @@index([{}])", cols)); + } + } + } + + // @@map (always present since model is PascalCase but table is snake_case) + lines.push(format!(" @@map(\"{}\")", table.name)); + lines.push("}".into()); + + lines.join("\n") +} + +struct BackRelation { + source_table: String, + fk_col: String, + is_one_to_one: bool, + relation_name: Option, +} + +fn back_rel_field(br: &BackRelation) -> (String, String) { + let source_pascal = to_pascal_case(&br.source_table); + let rel_type = if br.is_one_to_one { + format!("{}?", source_pascal) + } else { + format!("{}[]", source_pascal) + }; + + // source_table is already the plural table name — use it directly + let field_name = if br.relation_name.is_some() { + let rel_field = infer_relation_field_name(&br.fk_col); + if br.is_one_to_one { + format!("{}_{}", rel_field, br.source_table) + } else { + format!("{}_{}", rel_field, &br.source_table) + } + } else if br.is_one_to_one { + br.source_table.clone() + } else { + br.source_table.clone() + }; + + (field_name, rel_type) +} + +fn collect_back_relations(target_table: &str, schema: &[TableDef]) -> Vec { + let mut result = Vec::new(); + + for source in schema { + let fks_to_target: Vec<(&str, &[ColumnName])> = source.constraints.iter() + .filter_map(|c| { + if let TableConstraint::ForeignKey { columns, ref_table, ref_columns, .. } = c { + if ref_table.as_str() == target_table && columns.len() == 1 { + Some((columns[0].as_str(), ref_columns.as_slice())) + } else { None } + } else { None } + }) + .collect(); + + if fks_to_target.is_empty() { continue; } + + let multi_fk = fks_to_target.len() > 1; + let is_self_ref = source.name.as_str() == target_table; + + for (fk_col, _) in &fks_to_target { + let is_unique = source.constraints.iter().any(|c| { + matches!(c, TableConstraint::Unique { columns, .. } + if columns.len() == 1 && columns[0].as_str() == *fk_col) + }); + + let needs_name = multi_fk || is_self_ref; + let relation_name = if needs_name { + let rel_field = infer_relation_field_name(fk_col); + Some(format!( + "{}{}", + to_pascal_case(&source.name), + to_pascal_case(&rel_field) + )) + } else { + None + }; + + result.push(BackRelation { + source_table: source.name.as_str().to_string(), + fk_col: fk_col.to_string(), + is_one_to_one: is_unique, + relation_name, + }); + } + } + + result +} + +fn column_type_to_prisma(ty: &ColumnType, nullable: bool) -> (String, Option) { + let q = if nullable { "?" } else { "" }; + + match ty { + ColumnType::Simple(simple) => { + let (base, native) = match simple { + SimpleColumnType::SmallInt => ("Int", Some("@db.SmallInt")), + SimpleColumnType::Integer => ("Int", None), + SimpleColumnType::BigInt => ("BigInt", None), + SimpleColumnType::Real => ("Float", Some("@db.Real")), + SimpleColumnType::DoublePrecision => ("Float", None), + SimpleColumnType::Text => ("String", Some("@db.Text")), + SimpleColumnType::Boolean => ("Boolean", None), + SimpleColumnType::Date => ("DateTime", Some("@db.Date")), + SimpleColumnType::Time => ("DateTime", Some("@db.Time")), + SimpleColumnType::Timestamp => ("DateTime", Some("@db.Timestamp")), + SimpleColumnType::Timestamptz => ("DateTime", Some("@db.Timestamptz")), + SimpleColumnType::Interval => ("String", Some("@db.Interval")), + SimpleColumnType::Bytea => ("Bytes", None), + SimpleColumnType::Uuid => ("String", Some("@db.Uuid")), + SimpleColumnType::Json => ("Json", None), + SimpleColumnType::Inet => ("String", Some("@db.Inet")), + SimpleColumnType::Cidr => ("String", Some("@db.Cidr")), + SimpleColumnType::Macaddr => ("String", Some("@db.Macaddr")), + SimpleColumnType::Xml => ("String", Some("@db.Xml")), + // Unknown/future simple types fall back to a plain String column. + _ => ("String", None), + }; + (format!("{}{}", base, q), native.map(str::to_string)) + } + ColumnType::Complex(complex) => match complex { + ComplexColumnType::Varchar { length } => { + (format!("String{}", q), Some(format!("@db.VarChar({})", length))) + } + ComplexColumnType::Char { length } => { + (format!("String{}", q), Some(format!("@db.Char({})", length))) + } + ComplexColumnType::Numeric { precision, scale } => { + (format!("Decimal{}", q), Some(format!("@db.Decimal({}, {})", precision, scale))) + } + ComplexColumnType::Custom { custom_type } => { + (format!("Unsupported(\"{}\"){}", custom_type, q), None) + } + ComplexColumnType::Enum { name, .. } => { + (format!("{}{}", to_pascal_case(name), q), None) + } + // Unknown/future complex types fall back to a plain String column. + _ => (format!("String{}", q), None), + }, + } +} + +fn prisma_default_attr(default_sql: String, col_type: &ColumnType) -> String { + if default_sql == "true" { + return "@default(true)".into(); + } + if default_sql == "false" { + return "@default(false)".into(); + } + + let lower = default_sql.to_lowercase(); + if lower.contains("now()") || lower.starts_with("current_timestamp") { + return "@default(now())".into(); + } + if lower.contains("gen_random_uuid()") + || lower.contains("uuid_generate_v4()") + || lower.contains("newid()") + { + return "@default(uuid())".into(); + } + + // Any remaining function call → dbgenerated + if default_sql.contains('(') { + let escaped = default_sql.replace('"', "\\\""); + return format!("@default(dbgenerated(\"{}\"))", escaped); + } + + // String literal with quotes — may be an enum value + if default_sql.starts_with('\'') || default_sql.starts_with('"') { + let stripped = default_sql.trim_matches(|c| c == '\'' || c == '"'); + if let ColumnType::Complex(ComplexColumnType::Enum { values, .. }) = col_type { + if let EnumValues::String(variants) = values { + if variants.iter().any(|v| v.as_str() == stripped) { + let variant = to_screaming_snake(stripped); + return format!("@default({})", variant); + } + } + } + return format!("@default(\"{}\")", stripped.replace('\\', "\\\\").replace('"', "\\\"")); + } + + // Numeric + if default_sql.parse::().is_ok() { + return format!("@default({})", default_sql); + } + + // Fallback + let escaped = default_sql.replace('"', "\\\""); + format!("@default(dbgenerated(\"{}\"))", escaped) +} + +fn reference_action_to_prisma(action: &ReferenceAction) -> &'static str { + match action { + ReferenceAction::Cascade => "Cascade", + ReferenceAction::Restrict => "Restrict", + ReferenceAction::SetNull => "SetNull", + ReferenceAction::SetDefault => "SetDefault", + ReferenceAction::NoAction => "NoAction", + // Unknown/future referential actions fall back to Prisma's default. + _ => "NoAction", + } +} + +fn infer_relation_field_name(fk_col: &str) -> String { + fk_col.strip_suffix("_id").unwrap_or(fk_col).to_string() +} + +fn to_pascal_case(s: &str) -> String { + s.split('_') + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } + }) + .collect() +} + +fn to_screaming_snake(s: &str) -> String { + let mut result = String::new(); + let mut prev_lower = false; + for ch in s.chars() { + if ch.is_uppercase() && prev_lower { + result.push('_'); + } + if ch.is_alphanumeric() { + result.push(ch.to_ascii_uppercase()); + prev_lower = ch.is_lowercase(); + } else { + result.push('_'); + prev_lower = false; + } + } + result.trim_end_matches('_').to_string() +} diff --git a/crates/vespertide-exporter/src/tests/mod.rs b/crates/vespertide-exporter/src/tests/mod.rs index 5d99b90..fc6cc63 100644 --- a/crates/vespertide-exporter/src/tests/mod.rs +++ b/crates/vespertide-exporter/src/tests/mod.rs @@ -17,6 +17,7 @@ fn render_schema(orm: Orm, schema: &[TableDef]) -> Result { Orm::SqlAlchemy => crate::sqlalchemy::export(schema), Orm::SqlModel => crate::sqlmodel::render_entities(schema), Orm::Jpa => crate::jpa::render_entities(schema).map(|entities| entities.join("\n")), + Orm::Prisma => crate::prisma::export(schema), } } @@ -29,6 +30,7 @@ macro_rules! orm_cases { #[case::sqlalchemy(Orm::SqlAlchemy)] #[case::sqlmodel(Orm::SqlModel)] #[case::jpa(Orm::Jpa)] + #[case::prisma(Orm::Prisma)] fn $test_name(#[case] orm: Orm) { let table = $fixture(); let rendered = render_entity(orm, &table).unwrap(); @@ -45,6 +47,7 @@ macro_rules! orm_cases { #[case::sqlalchemy(Orm::SqlAlchemy)] #[case::sqlmodel(Orm::SqlModel)] #[case::jpa(Orm::Jpa)] + #[case::prisma(Orm::Prisma)] fn $test_name(#[case] orm: Orm) { let schema: Vec = $fixture(); let rendered = render_schema(orm, &schema).unwrap(); @@ -299,6 +302,7 @@ fn to_pascal_case_for(orm: Orm, s: &str) -> String { Orm::SqlAlchemy => crate::sqlalchemy::to_pascal_case_for_tests(s), Orm::SqlModel => crate::sqlmodel::to_pascal_case_for_tests(s), Orm::Jpa => crate::jpa::to_pascal_case_for_tests(s), + Orm::Prisma => crate::prisma::to_pascal_case_for_tests(s), } } @@ -318,6 +322,7 @@ fn to_pascal_case_for(orm: Orm, s: &str) -> String { #[case::sqlalchemy(Orm::SqlAlchemy)] #[case::sqlmodel(Orm::SqlModel)] #[case::jpa(Orm::Jpa)] +#[case::prisma(Orm::Prisma)] fn to_pascal_case_shared_semantics( #[values( ("", ""), @@ -344,6 +349,7 @@ fn to_pascal_case_shared_semantics( #[case::sqlalchemy(Orm::SqlAlchemy)] #[case::sqlmodel(Orm::SqlModel)] #[case::jpa(Orm::Jpa)] +#[case::prisma(Orm::Prisma)] fn render_entity_with_schema_snapshots( #[values( "many_to_many_article", diff --git a/crates/vespertide-exporter/tests/parallel_consolidated.rs b/crates/vespertide-exporter/tests/parallel_consolidated.rs index f93674b..423b548 100644 --- a/crates/vespertide-exporter/tests/parallel_consolidated.rs +++ b/crates/vespertide-exporter/tests/parallel_consolidated.rs @@ -37,6 +37,7 @@ fn render_schema(orm: Orm, schema: &[TableDef]) -> Result { Orm::Jpa => { vespertide_exporter::jpa::render_entities(schema).map(|entities| entities.join("\n")) } + Orm::Prisma => vespertide_exporter::prisma::export(schema), } } diff --git a/schemas/config.schema.json b/schemas/config.schema.json index fa5f285..f310b77 100644 --- a/schemas/config.schema.json +++ b/schemas/config.schema.json @@ -44,6 +44,13 @@ "type": "string", "default": "" }, + "prisma": { + "description": "Prisma-specific export configuration.", + "$ref": "#/$defs/PrismaConfig", + "default": { + "provider": "postgresql" + } + }, "seaorm": { "description": "SeaORM-specific export configuration.", "$ref": "#/$defs/SeaOrmConfig", @@ -94,6 +101,31 @@ "pascal" ] }, + "PrismaConfig": { + "description": "Prisma-specific export configuration.", + "type": "object", + "properties": { + "clientOutput": { + "description": "Optional output path for the generated Prisma client.", + "type": [ + "string", + "null" + ] + }, + "provider": { + "description": "Database provider: postgresql, mysql, sqlite, sqlserver, mongodb, cockroachdb.", + "type": "string", + "default": "postgresql" + }, + "relationMode": { + "description": "Optional relationMode override (\"foreignKeys\" or \"prisma\").", + "type": [ + "string", + "null" + ] + } + } + }, "SeaOrmConfig": { "description": "SeaORM-specific export configuration.", "type": "object", From 88bde9e9dc4a9a525ba21c38e1b177e185b8aac2 Mon Sep 17 00:00:00 2001 From: L33gn21 Date: Sun, 21 Jun 2026 04:27:42 +0900 Subject: [PATCH 2/4] test(exporter): add Prisma ORM snapshots 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 --- ...ypes_snapshot@all_simple_types_Prisma.snap | 27 +++++++++++++++++++ ...le_pk_snapshot@basic_single_pk_Prisma.snap | 10 +++++++ ...t@basic_table_with_description_Prisma.snap | 14 ++++++++++ ...x_types_snapshot@complex_types_Prisma.snap | 13 +++++++++ ...snapshot@composite_constraints_Prisma.snap | 16 +++++++++++ ...index_snapshot@composite_index_Prisma.snap | 12 +++++++++ ...osite_pk_snapshot@composite_pk_Prisma.snap | 11 ++++++++ ...snapshot@composite_primary_key_Prisma.snap | 12 +++++++++ ...ot@composite_unique_constraint_Prisma.snap | 12 +++++++++ ...ique_snapshot@composite_unique_Prisma.snap | 12 +++++++++ ...ts__defaults_snapshot@defaults_Prisma.snap | 12 +++++++++ ...snapshot@enum_multiple_columns_Prisma.snap | 23 ++++++++++++++++ ...um_shared_snapshot@enum_shared_Prisma.snap | 17 ++++++++++++ ...s_snapshot@enum_special_values_Prisma.snap | 17 ++++++++++++ ...ult_snapshot@enum_with_default_Prisma.snap | 18 +++++++++++++ ...snapshot@false_boolean_default_Prisma.snap | 10 +++++++ ...ith_comment_and_auto_increment_Prisma.snap | 12 +++++++++ ...__inline_pk_snapshot@inline_pk_Prisma.snap | 10 +++++++ ...integer_enum_all_variant_types_Prisma.snap | 17 ++++++++++++ ...shot@integer_enum_with_default_Prisma.snap | 15 +++++++++++ ...eger_enum_with_variant_default_Prisma.snap | 15 +++++++++++ ..._default_snapshot@json_default_Prisma.snap | 10 +++++++ ...ype_snapshot@jsonb_custom_type_Prisma.snap | 12 +++++++++ ...iption_snapshot@no_description_Prisma.snap | 9 +++++++ ...umns_snapshot@nullable_columns_Prisma.snap | 11 ++++++++ ...le_enum_snapshot@nullable_enum_Prisma.snap | 15 +++++++++++ ...snapshot@numeric_default_value_Prisma.snap | 10 +++++++ ...er_snapshot@pk_and_fk_together_Prisma.snap | 19 +++++++++++++ ..._snapshots@composite_fk_parent_Prisma.snap | 11 ++++++++ ...apshots@dual_reverse_relations_Prisma.snap | 11 ++++++++ ...snapshots@many_to_many_article_Prisma.snap | 10 +++++++ ...ts@many_to_many_missing_target_Prisma.snap | 10 +++++++ ...any_to_many_multiple_junctions_Prisma.snap | 11 ++++++++ ...ma_snapshots@many_to_many_user_Prisma.snap | 10 +++++++ ...apshots@multiple_fk_same_table_Prisma.snap | 13 +++++++++ ...ots@multiple_has_one_relations_Prisma.snap | 11 ++++++++ ...ots@multiple_reverse_relations_Prisma.snap | 11 ++++++++ ..._junction_fk_not_in_pk_another_Prisma.snap | 10 +++++++ ...ot_junction_fk_not_in_pk_other_Prisma.snap | 10 +++++++ ...apshots@not_junction_single_pk_Prisma.snap | 10 +++++++ ...shots@triple_reverse_relations_Prisma.snap | 12 +++++++++ ...h_schema_snapshots@username_fk_Prisma.snap | 11 ++++++++ ...shot@reserved_word_identifiers_Prisma.snap | 11 ++++++++ ...k_snapshot@self_referencing_fk_Prisma.snap | 11 ++++++++ ...erver_default_and_true_boolean_Prisma.snap | 13 +++++++++ ...aults_snapshot@server_defaults_Prisma.snap | 12 +++++++++ ...@small_multi_schema_sequential_Prisma.snap | 20 ++++++++++++++ ...efault_snapshot@string_default_Prisma.snap | 10 +++++++ ...vel_pk_snapshot@table_level_pk_Prisma.snap | 11 ++++++++ ...apshot@table_with_composite_fk_Prisma.snap | 12 +++++++++ ..._enum_snapshot@table_with_enum_Prisma.snap | 16 +++++++++++ ...with_fk_snapshot@table_with_fk_Prisma.snap | 12 +++++++++ ...es_snapshot@table_with_indexes_Prisma.snap | 13 +++++++++ ...apshot@table_with_integer_enum_Prisma.snap | 16 +++++++++++ ...ed_snapshot@unique_and_indexed_Prisma.snap | 14 ++++++++++ ...pshot@unknown_constant_default_Prisma.snap | 10 +++++++ ...pshot@unknown_function_default_Prisma.snap | 10 +++++++ ...apshot@unnamed_composite_index_Prisma.snap | 12 +++++++++ ...pshot@unnamed_composite_unique_Prisma.snap | 12 +++++++++ ...pshot@unnamed_index_and_unique_Prisma.snap | 13 +++++++++ 60 files changed, 770 insertions(+) create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__all_simple_types_snapshot@all_simple_types_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__basic_single_pk_snapshot@basic_single_pk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__basic_table_with_description_snapshot@basic_table_with_description_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__complex_types_snapshot@complex_types_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_index_snapshot@composite_index_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_pk_snapshot@composite_pk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_primary_key_snapshot@composite_primary_key_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_constraint_snapshot@composite_unique_constraint_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_snapshot@composite_unique_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__defaults_snapshot@defaults_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_multiple_columns_snapshot@enum_multiple_columns_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_shared_snapshot@enum_shared_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_special_values_snapshot@enum_special_values_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_with_default_snapshot@enum_with_default_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__false_boolean_default_snapshot@false_boolean_default_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__fk_with_comment_and_auto_increment_snapshot@fk_with_comment_and_auto_increment_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__inline_pk_snapshot@inline_pk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_all_variant_types_snapshot@integer_enum_all_variant_types_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_with_default_snapshot@integer_enum_with_default_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_with_variant_default_snapshot@integer_enum_with_variant_default_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__json_default_snapshot@json_default_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__jsonb_custom_type_snapshot@jsonb_custom_type_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__no_description_snapshot@no_description_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__nullable_columns_snapshot@nullable_columns_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__nullable_enum_snapshot@nullable_enum_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__numeric_default_value_snapshot@numeric_default_value_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__pk_and_fk_together_snapshot@pk_and_fk_together_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@composite_fk_parent_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@dual_reverse_relations_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_article_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_missing_target_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_multiple_junctions_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_user_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_fk_same_table_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_has_one_relations_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_reverse_relations_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_fk_not_in_pk_another_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_fk_not_in_pk_other_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_single_pk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@triple_reverse_relations_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__reserved_word_identifiers_snapshot@reserved_word_identifiers_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__server_default_and_true_boolean_snapshot@server_default_and_true_boolean_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__server_defaults_snapshot@server_defaults_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__small_multi_schema_sequential_snapshot@small_multi_schema_sequential_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__string_default_snapshot@string_default_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_level_pk_snapshot@table_level_pk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_composite_fk_snapshot@table_with_composite_fk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_enum_snapshot@table_with_enum_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_fk_snapshot@table_with_fk_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_indexes_snapshot@table_with_indexes_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_integer_enum_snapshot@table_with_integer_enum_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unique_and_indexed_snapshot@unique_and_indexed_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unknown_constant_default_snapshot@unknown_constant_default_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unknown_function_default_snapshot@unknown_function_default_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_composite_index_snapshot@unnamed_composite_index_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_composite_unique_snapshot@unnamed_composite_unique_Prisma.snap create mode 100644 crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_index_and_unique_snapshot@unnamed_index_and_unique_Prisma.snap diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__all_simple_types_snapshot@all_simple_types_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__all_simple_types_snapshot@all_simple_types_Prisma.snap new file mode 100644 index 0000000..d9449b4 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__all_simple_types_snapshot@all_simple_types_Prisma.snap @@ -0,0 +1,27 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model AllTypes { + id Int @id + small Int @db.SmallInt + big BigInt + real_num Float @db.Real + double_num Float + text_col String @db.Text + bool_col Boolean + date_col DateTime @db.Date + time_col DateTime @db.Time + ts_col DateTime @db.Timestamp + tstz_col DateTime @db.Timestamptz + interval_col String @db.Interval + bytea_col Bytes + uuid_col String @db.Uuid + json_col Json + inet_col String @db.Inet + cidr_col String @db.Cidr + macaddr_col String @db.Macaddr + xml_col String @db.Xml + + @@map("all_types") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__basic_single_pk_snapshot@basic_single_pk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__basic_single_pk_snapshot@basic_single_pk_Prisma.snap new file mode 100644 index 0000000..8e48a73 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__basic_single_pk_snapshot@basic_single_pk_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Users { + id Int @id + display_name String? @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__basic_table_with_description_snapshot@basic_table_with_description_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__basic_table_with_description_snapshot@basic_table_with_description_Prisma.snap new file mode 100644 index 0000000..8e8c5a5 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__basic_table_with_description_snapshot@basic_table_with_description_Prisma.snap @@ -0,0 +1,14 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +/// User accounts table +model Users { + /// Primary key + id Int @id @default(autoincrement()) + /// User email address + email String @unique @db.Text + name String? @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__complex_types_snapshot@complex_types_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__complex_types_snapshot@complex_types_Prisma.snap new file mode 100644 index 0000000..0db0f66 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__complex_types_snapshot@complex_types_Prisma.snap @@ -0,0 +1,13 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model ComplexTypes { + id Int @id + varchar_col String @db.VarChar(100) + char_col String @db.Char(10) + numeric_col Decimal @db.Decimal(10, 2) + custom_col Unsupported("CUSTOM_TYPE") + + @@map("complex_types") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap new file mode 100644 index 0000000..c439a59 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap @@ -0,0 +1,16 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model OrderItems { + order_id Int + order Orders @relation(fields: [order_id], references: [id]) + product_id Int + product Products @relation(fields: [product_id], references: [id]) + quantity Int + + @@id([order_id, product_id]) + @@unique([order_id, product_id], name: "uq_order_items__order_product") + @@index([order_id], name: "ix_order_items__order_id") + @@map("order_items") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_index_snapshot@composite_index_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_index_snapshot@composite_index_Prisma.snap new file mode 100644 index 0000000..f4c1907 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_index_snapshot@composite_index_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model CompositeIndex { + id Int @id + tenant_id Int + name String @db.Text + + @@index([tenant_id, name], name: "idx_tenant_name") + @@map("composite_index") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_pk_snapshot@composite_pk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_pk_snapshot@composite_pk_Prisma.snap new file mode 100644 index 0000000..94e0e48 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_pk_snapshot@composite_pk_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Accounts { + id Int + tenant_id BigInt + + @@id([id, tenant_id]) + @@map("accounts") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_primary_key_snapshot@composite_primary_key_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_primary_key_snapshot@composite_primary_key_Prisma.snap new file mode 100644 index 0000000..698e9d3 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_primary_key_snapshot@composite_primary_key_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Membership { + tenant_id Int + user_id Int + role String @db.Text + + @@id([tenant_id, user_id]) + @@map("membership") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_constraint_snapshot@composite_unique_constraint_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_constraint_snapshot@composite_unique_constraint_Prisma.snap new file mode 100644 index 0000000..2ef045c --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_constraint_snapshot@composite_unique_constraint_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model AccountAliases { + id Int @id + tenant_id Int + slug String @db.Text + + @@unique([tenant_id, slug], name: "uq_account_aliases__tenant_slug") + @@map("account_aliases") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_snapshot@composite_unique_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_snapshot@composite_unique_Prisma.snap new file mode 100644 index 0000000..c62e246 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_snapshot@composite_unique_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model CompositeUnique { + id Int @id + tenant_id Int + name String @db.Text + + @@unique([tenant_id, name], name: "uq_tenant_name") + @@map("composite_unique") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__defaults_snapshot@defaults_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__defaults_snapshot@defaults_Prisma.snap new file mode 100644 index 0000000..a3cb11e --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__defaults_snapshot@defaults_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Articles { + id Int @id @default(autoincrement()) + published Boolean @default(false) + view_count Int @default(0) + status String @default("draft") @db.Text + + @@map("articles") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_multiple_columns_snapshot@enum_multiple_columns_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_multiple_columns_snapshot@enum_multiple_columns_Prisma.snap new file mode 100644 index 0000000..85a1806 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_multiple_columns_snapshot@enum_multiple_columns_Prisma.snap @@ -0,0 +1,23 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum ProductCategory { + ELECTRONICS @map("electronics") + CLOTHING @map("clothing") + FOOD @map("food") +} + +enum AvailabilityStatus { + IN_STOCK @map("in_stock") + OUT_OF_STOCK @map("out_of_stock") + PRE_ORDER @map("pre_order") +} + +model Products { + id Int + category ProductCategory + availability AvailabilityStatus + + @@map("products") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_shared_snapshot@enum_shared_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_shared_snapshot@enum_shared_Prisma.snap new file mode 100644 index 0000000..86f0234 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_shared_snapshot@enum_shared_Prisma.snap @@ -0,0 +1,17 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum DocStatus { + DRAFT @map("draft") + PUBLISHED @map("published") + ARCHIVED @map("archived") +} + +model Documents { + id Int + status DocStatus + review_status DocStatus? + + @@map("documents") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_special_values_snapshot@enum_special_values_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_special_values_snapshot@enum_special_values_Prisma.snap new file mode 100644 index 0000000..cb2b4c2 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_special_values_snapshot@enum_special_values_Prisma.snap @@ -0,0 +1,17 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum EventSeverity { + INFO_LEVEL @map("info-level") + WARNING_LEVEL @map("warning_level") + ERROR_LEVEL + 1CRITICAL @map("1critical") +} + +model Events { + id Int + severity EventSeverity + + @@map("events") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_with_default_snapshot@enum_with_default_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_with_default_snapshot@enum_with_default_Prisma.snap new file mode 100644 index 0000000..29cd4ce --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__enum_with_default_snapshot@enum_with_default_Prisma.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum TaskStatus { + PENDING @map("pending") + IN_PROGRESS @map("in_progress") + COMPLETED @map("completed") +} + +model Tasks { + id Int + status TaskStatus @default(PENDING) + priority Int @default(0) + is_archived Boolean @default(false) + + @@map("tasks") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__false_boolean_default_snapshot@false_boolean_default_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__false_boolean_default_snapshot@false_boolean_default_Prisma.snap new file mode 100644 index 0000000..a738b4e --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__false_boolean_default_snapshot@false_boolean_default_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model BoolDefaults { + id Int @id + is_deleted Boolean @default(false) + + @@map("bool_defaults") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__fk_with_comment_and_auto_increment_snapshot@fk_with_comment_and_auto_increment_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__fk_with_comment_and_auto_increment_snapshot@fk_with_comment_and_auto_increment_Prisma.snap new file mode 100644 index 0000000..36c1060 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__fk_with_comment_and_auto_increment_snapshot@fk_with_comment_and_auto_increment_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Child { + /// References parent table + parent_id Int @id @default(autoincrement()) + parent Parent @relation(fields: [parent_id], references: [id]) + value String @db.Text + + @@map("child") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__inline_pk_snapshot@inline_pk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__inline_pk_snapshot@inline_pk_Prisma.snap new file mode 100644 index 0000000..a1a8735 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__inline_pk_snapshot@inline_pk_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Users { + id String @default(uuid()) @db.Uuid + email String @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_all_variant_types_snapshot@integer_enum_all_variant_types_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_all_variant_types_snapshot@integer_enum_all_variant_types_Prisma.snap new file mode 100644 index 0000000..ff867cf --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_all_variant_types_snapshot@integer_enum_all_variant_types_Prisma.snap @@ -0,0 +1,17 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum EdgeState { + UNKNOWN // = -1 + NOT_STARTED // = 0 + IN_PROGRESS // = 10 + HTTP_500 // = 500 +} + +model WorkflowRuns { + id Int @id + state EdgeState + + @@map("workflow_runs") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_with_default_snapshot@integer_enum_with_default_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_with_default_snapshot@integer_enum_with_default_Prisma.snap new file mode 100644 index 0000000..dec7034 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_with_default_snapshot@integer_enum_with_default_Prisma.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum TaskStatus { + PENDING // = 0 + COMPLETED // = 100 +} + +model Tasks { + id Int + status TaskStatus @default(1) + + @@map("tasks") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_with_variant_default_snapshot@integer_enum_with_variant_default_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_with_variant_default_snapshot@integer_enum_with_variant_default_Prisma.snap new file mode 100644 index 0000000..f34d556 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__integer_enum_with_variant_default_snapshot@integer_enum_with_variant_default_Prisma.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum TaskRunStatus { + PENDING // = 0 + COMPLETED // = 100 +} + +model TaskRuns { + id Int + status TaskRunStatus @default(dbgenerated("Completed")) + + @@map("task_runs") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__json_default_snapshot@json_default_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__json_default_snapshot@json_default_Prisma.snap new file mode 100644 index 0000000..bb69f71 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__json_default_snapshot@json_default_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Configs { + id Int @id + data Json @default(dbgenerated("{\"hello\": \"world\"}")) + + @@map("configs") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__jsonb_custom_type_snapshot@jsonb_custom_type_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__jsonb_custom_type_snapshot@jsonb_custom_type_Prisma.snap new file mode 100644 index 0000000..2b61c89 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__jsonb_custom_type_snapshot@jsonb_custom_type_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model JsonStruct { + id Int + json_data Json + jsonb_data Unsupported("JSONB") + jsonb_nullable Unsupported("jsonb")? + + @@map("json_struct") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__no_description_snapshot@no_description_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__no_description_snapshot@no_description_Prisma.snap new file mode 100644 index 0000000..63eb785 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__no_description_snapshot@no_description_Prisma.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model NoDesc { + id Int @id + + @@map("no_desc") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__nullable_columns_snapshot@nullable_columns_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__nullable_columns_snapshot@nullable_columns_Prisma.snap new file mode 100644 index 0000000..e820cb0 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__nullable_columns_snapshot@nullable_columns_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Profiles { + id Int @id @default(autoincrement()) + bio String? @db.Text + avatar_url String? @db.VarChar(500) + + @@map("profiles") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__nullable_enum_snapshot@nullable_enum_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__nullable_enum_snapshot@nullable_enum_Prisma.snap new file mode 100644 index 0000000..6600f6a --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__nullable_enum_snapshot@nullable_enum_Prisma.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum StatusType { + ACTIVE @map("active") + INACTIVE @map("inactive") +} + +model NullableEnum { + id Int @id + status StatusType? + + @@map("nullable_enum") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__numeric_default_value_snapshot@numeric_default_value_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__numeric_default_value_snapshot@numeric_default_value_Prisma.snap new file mode 100644 index 0000000..a6393f2 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__numeric_default_value_snapshot@numeric_default_value_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Products { + id Int + price Decimal @default(0.00) @db.Decimal(10, 2) + + @@map("products") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__pk_and_fk_together_snapshot@pk_and_fk_together_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__pk_and_fk_together_snapshot@pk_and_fk_together_Prisma.snap new file mode 100644 index 0000000..4751d68 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__pk_and_fk_together_snapshot@pk_and_fk_together_Prisma.snap @@ -0,0 +1,19 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model ArticleUser { + article_id String @db.Uuid + article Article @relation(fields: [article_id], references: [id], onDelete: Cascade) + user_id String @db.Uuid + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) + author_order Int @default(1) + role String @default("contributor") @db.VarChar(20) + is_lead Boolean @default(false) + created_at DateTime @default(now()) @db.Timestamptz + + @@id([article_id, user_id]) + @@index([article_id]) + @@index([user_id]) + @@map("article_user") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@composite_fk_parent_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@composite_fk_parent_Prisma.snap new file mode 100644 index 0000000..d6e65fa --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@composite_fk_parent_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Parent { + id1 Int + id2 Int + + @@id([id1, id2]) + @@map("parent") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@dual_reverse_relations_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@dual_reverse_relations_Prisma.snap new file mode 100644 index 0000000..0a6af1d --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@dual_reverse_relations_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Dual { + username String @id @db.Text + username_dual_rel DualRel[] @relation("DualRelUsername") + checker_username_dual_rel DualRel[] @relation("DualRelCheckerUsername") + + @@map("dual") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_article_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_article_Prisma.snap new file mode 100644 index 0000000..5fc9573 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_article_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Article { + id BigInt @id + article_user ArticleUser[] + + @@map("article") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_missing_target_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_missing_target_Prisma.snap new file mode 100644 index 0000000..5fc9573 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_missing_target_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Article { + id BigInt @id + article_user ArticleUser[] + + @@map("article") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_multiple_junctions_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_multiple_junctions_Prisma.snap new file mode 100644 index 0000000..ba3f732 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_multiple_junctions_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model User { + id String @id @db.Uuid + user_media_role UserMediaRole[] + user_media_favorite UserMediaFavorite[] + + @@map("user") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_user_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_user_Prisma.snap new file mode 100644 index 0000000..2ee63a6 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@many_to_many_user_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model User { + id String @id @db.Uuid + article_user ArticleUser[] + + @@map("user") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_fk_same_table_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_fk_same_table_Prisma.snap new file mode 100644 index 0000000..e90b4fd --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_fk_same_table_Prisma.snap @@ -0,0 +1,13 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Post { + id String @id @db.Uuid + creator_user_id String @db.Uuid + creator_user User @relation("PostCreatorUser", fields: [creator_user_id], references: [id]) + used_by_user_id String @db.Uuid + used_by_user User @relation("PostUsedByUser", fields: [used_by_user_id], references: [id]) + + @@map("post") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_has_one_relations_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_has_one_relations_Prisma.snap new file mode 100644 index 0000000..824aa18 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_has_one_relations_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model User { + id String @id @db.Uuid + created_by_user_settings Settings? @relation("SettingsCreatedByUser") + updated_by_user_settings Settings? @relation("SettingsUpdatedByUser") + + @@map("user") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_reverse_relations_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_reverse_relations_Prisma.snap new file mode 100644 index 0000000..2f8b8d4 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@multiple_reverse_relations_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model User { + id String @id @db.Uuid + preferred_user_profile Profile[] @relation("ProfilePreferredUser") + backup_user_profile Profile[] @relation("ProfileBackupUser") + + @@map("user") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_fk_not_in_pk_another_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_fk_not_in_pk_another_Prisma.snap new file mode 100644 index 0000000..849af19 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_fk_not_in_pk_another_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Another { + id Int @id + not_junction NotJunction[] + + @@map("another") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_fk_not_in_pk_other_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_fk_not_in_pk_other_Prisma.snap new file mode 100644 index 0000000..ae8747a --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_fk_not_in_pk_other_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Other { + id Int @id + not_junction NotJunction[] + + @@map("other") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_single_pk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_single_pk_Prisma.snap new file mode 100644 index 0000000..68f8850 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@not_junction_single_pk_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Other { + id Int @id + regular Regular[] + + @@map("other") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@triple_reverse_relations_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@triple_reverse_relations_Prisma.snap new file mode 100644 index 0000000..1dbab1e --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@triple_reverse_relations_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Dual { + username String @id @db.Text + username_triple_rel TripleRel[] @relation("TripleRelUsername") + checker_username_triple_rel TripleRel[] @relation("TripleRelCheckerUsername") + other_username_triple_rel TripleRel[] @relation("TripleRelOtherUsername") + + @@map("dual") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap new file mode 100644 index 0000000..f20329d --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Session { + id String @id @db.Uuid + username String @db.Text + username User @relation(fields: [username], references: [username]) + + @@map("session") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__reserved_word_identifiers_snapshot@reserved_word_identifiers_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__reserved_word_identifiers_snapshot@reserved_word_identifiers_Prisma.snap new file mode 100644 index 0000000..dbd9361 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__reserved_word_identifiers_snapshot@reserved_word_identifiers_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Order { + id Int @id + user String @db.Text + select Int + + @@map("order") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap new file mode 100644 index 0000000..36a999c --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Employees { + id Int @id + manager_id Int? + manager Employees? @relation("EmployeesManager", fields: [manager_id], references: [id], onDelete: SetNull) + + @@map("employees") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__server_default_and_true_boolean_snapshot@server_default_and_true_boolean_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__server_default_and_true_boolean_snapshot@server_default_and_true_boolean_Prisma.snap new file mode 100644 index 0000000..2838a1f --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__server_default_and_true_boolean_snapshot@server_default_and_true_boolean_Prisma.snap @@ -0,0 +1,13 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Logs { + id Int @id @default(autoincrement()) + active Boolean @default(true) + created_at DateTime @default(now()) @db.Timestamptz + score Float @default(1.5) @db.Real + tag String @default(dbgenerated("UNKNOWN_EXPR")) @db.Text + + @@map("logs") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__server_defaults_snapshot@server_defaults_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__server_defaults_snapshot@server_defaults_Prisma.snap new file mode 100644 index 0000000..8953edc --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__server_defaults_snapshot@server_defaults_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model WithDefaults { + id Int @id + created_at DateTime @default(now()) @db.Timestamptz + status String @default("active") @db.Text + count Int @default(0) + + @@map("with_defaults") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__small_multi_schema_sequential_snapshot@small_multi_schema_sequential_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__small_multi_schema_sequential_snapshot@small_multi_schema_sequential_Prisma.snap new file mode 100644 index 0000000..1477260 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__small_multi_schema_sequential_snapshot@small_multi_schema_sequential_Prisma.snap @@ -0,0 +1,20 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Users { + id Int @id + display_name String? @db.Text + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id + user_id Int + user Users @relation(fields: [user_id], references: [id]) + title String @db.Text + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__string_default_snapshot@string_default_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__string_default_snapshot@string_default_Prisma.snap new file mode 100644 index 0000000..bd34221 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__string_default_snapshot@string_default_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model StringDefaults { + id Int @id + status String @default("active") @db.Text + + @@map("string_defaults") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_level_pk_snapshot@table_level_pk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_level_pk_snapshot@table_level_pk_Prisma.snap new file mode 100644 index 0000000..5d7872c --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_level_pk_snapshot@table_level_pk_Prisma.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Orders { + id String @id @db.Uuid + customer_id String @db.Uuid + total Float @db.Real + + @@map("orders") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_composite_fk_snapshot@table_with_composite_fk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_composite_fk_snapshot@table_with_composite_fk_Prisma.snap new file mode 100644 index 0000000..74988b5 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_composite_fk_snapshot@table_with_composite_fk_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model LineItems { + id Int @id + order_id Int + order_version Int + sku String @db.Text + + @@map("line_items") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_enum_snapshot@table_with_enum_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_enum_snapshot@table_with_enum_Prisma.snap new file mode 100644 index 0000000..c069ed4 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_enum_snapshot@table_with_enum_Prisma.snap @@ -0,0 +1,16 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum OrderStatus { + PENDING @map("pending") + SHIPPED @map("shipped") + DELIVERED @map("delivered") +} + +model Orders { + id Int + status OrderStatus + + @@map("orders") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_fk_snapshot@table_with_fk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_fk_snapshot@table_with_fk_Prisma.snap new file mode 100644 index 0000000..2535a68 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_fk_snapshot@table_with_fk_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Posts { + id Int @id + user_id Int + user Users @relation(fields: [user_id], references: [id]) + title String @db.Text + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_indexes_snapshot@table_with_indexes_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_indexes_snapshot@table_with_indexes_Prisma.snap new file mode 100644 index 0000000..a69cbe6 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_indexes_snapshot@table_with_indexes_Prisma.snap @@ -0,0 +1,13 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Articles { + id Int @id + title String @db.Text + created_at DateTime @db.Timestamptz + + @@index([created_at], name: "idx_articles_created_at") + @@index([title]) + @@map("articles") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_integer_enum_snapshot@table_with_integer_enum_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_integer_enum_snapshot@table_with_integer_enum_Prisma.snap new file mode 100644 index 0000000..367e1ee --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_integer_enum_snapshot@table_with_integer_enum_Prisma.snap @@ -0,0 +1,16 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +enum PriorityLevel { + LOW // = 0 + MEDIUM // = 10 + HIGH // = 20 +} + +model Tasks { + id Int @id + priority PriorityLevel + + @@map("tasks") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unique_and_indexed_snapshot@unique_and_indexed_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unique_and_indexed_snapshot@unique_and_indexed_Prisma.snap new file mode 100644 index 0000000..326c4d6 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unique_and_indexed_snapshot@unique_and_indexed_Prisma.snap @@ -0,0 +1,14 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Users { + id Int + email String @unique @db.Text + username String @unique(map: "uq_username") @db.Text + department String? @db.Text + status String @default("active") @db.Text + + @@index([department], name: "idx_department") + @@map("users") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unknown_constant_default_snapshot@unknown_constant_default_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unknown_constant_default_snapshot@unknown_constant_default_Prisma.snap new file mode 100644 index 0000000..601d431 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unknown_constant_default_snapshot@unknown_constant_default_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model UnknownDefault { + id Int @id + value String @default(dbgenerated("SOME_CONSTANT")) @db.Text + + @@map("unknown_default") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unknown_function_default_snapshot@unknown_function_default_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unknown_function_default_snapshot@unknown_function_default_Prisma.snap new file mode 100644 index 0000000..8eeb4d7 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unknown_function_default_snapshot@unknown_function_default_Prisma.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model UnknownDefaults { + id Int @id + code String @default(dbgenerated("gen_code()")) @db.Text + + @@map("unknown_defaults") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_composite_index_snapshot@unnamed_composite_index_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_composite_index_snapshot@unnamed_composite_index_Prisma.snap new file mode 100644 index 0000000..3112061 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_composite_index_snapshot@unnamed_composite_index_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model UnnamedIndex { + id Int @id + col_a Int + col_b Int + + @@index([col_a, col_b]) + @@map("unnamed_index") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_composite_unique_snapshot@unnamed_composite_unique_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_composite_unique_snapshot@unnamed_composite_unique_Prisma.snap new file mode 100644 index 0000000..895a961 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_composite_unique_snapshot@unnamed_composite_unique_Prisma.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model UnnamedUnique { + id Int @id + col_a Int + col_b Int + + @@unique([col_a, col_b]) + @@map("unnamed_unique") +} diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_index_and_unique_snapshot@unnamed_index_and_unique_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_index_and_unique_snapshot@unnamed_index_and_unique_Prisma.snap new file mode 100644 index 0000000..fd0d466 --- /dev/null +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__unnamed_index_and_unique_snapshot@unnamed_index_and_unique_Prisma.snap @@ -0,0 +1,13 @@ +--- +source: crates/vespertide-exporter/src/tests/mod.rs +expression: rendered +--- +model Events { + id Int @id + venue_id Int + date DateTime @db.Date + + @@unique([venue_id, date]) + @@index([venue_id, date]) + @@map("events") +} From 0c6f27bb4c5c3f434a925c8590efedb574e97724 Mon Sep 17 00:00:00 2001 From: L33gn21 Date: Tue, 23 Jun 2026 00:15:30 +0900 Subject: [PATCH 3/4] refactor(exporter): split Prisma exporter into mod/render/types/enums 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 --- .../vespertide-exporter/src/prisma/enums.rs | 59 ++ crates/vespertide-exporter/src/prisma/mod.rs | 513 +----------------- .../vespertide-exporter/src/prisma/render.rs | 415 ++++++++++++++ .../vespertide-exporter/src/prisma/types.rs | 57 ++ 4 files changed, 551 insertions(+), 493 deletions(-) create mode 100644 crates/vespertide-exporter/src/prisma/enums.rs create mode 100644 crates/vespertide-exporter/src/prisma/render.rs create mode 100644 crates/vespertide-exporter/src/prisma/types.rs diff --git a/crates/vespertide-exporter/src/prisma/enums.rs b/crates/vespertide-exporter/src/prisma/enums.rs new file mode 100644 index 0000000..d7c025b --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/enums.rs @@ -0,0 +1,59 @@ +use vespertide_core::schema::column::EnumValues; + +pub(super) fn render_enum(name: &str, values: &EnumValues) -> String { + let enum_name = to_pascal_case(name); + let mut lines = Vec::new(); + lines.push(format!("enum {enum_name} {{")); + match values { + EnumValues::String(vals) => { + for val in vals { + let variant = to_screaming_snake(val); + if variant == *val { + lines.push(format!(" {variant}")); + } else { + lines.push(format!(" {variant} @map(\"{val}\")")); + } + } + } + EnumValues::Integer(vals) => { + // Prisma doesn't support integer enums natively; emit as string variants with comment + for val in vals { + let variant = to_screaming_snake(&val.name); + let value = val.value; + lines.push(format!(" {variant} // = {value}")); + } + } + } + lines.push("}".into()); + lines.join("\n") +} + +pub(super) fn to_pascal_case(s: &str) -> String { + s.split('_') + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } + }) + .collect() +} + +pub(super) fn to_screaming_snake(s: &str) -> String { + let mut result = String::new(); + let mut prev_lower = false; + for ch in s.chars() { + if ch.is_uppercase() && prev_lower { + result.push('_'); + } + if ch.is_alphanumeric() { + result.push(ch.to_ascii_uppercase()); + prev_lower = ch.is_lowercase(); + } else { + result.push('_'); + prev_lower = false; + } + } + result.trim_end_matches('_').to_string() +} diff --git a/crates/vespertide-exporter/src/prisma/mod.rs b/crates/vespertide-exporter/src/prisma/mod.rs index c28629a..b24d01a 100644 --- a/crates/vespertide-exporter/src/prisma/mod.rs +++ b/crates/vespertide-exporter/src/prisma/mod.rs @@ -1,12 +1,13 @@ -use std::collections::{HashMap, HashSet}; +mod enums; +mod render; +mod types; + +use std::collections::HashSet; use crate::orm::OrmExporter; use vespertide_config::PrismaConfig; -use vespertide_core::schema::column::{ColumnType, ComplexColumnType, EnumValues, SimpleColumnType}; -use vespertide_core::schema::constraint::TableConstraint; -use vespertide_core::schema::names::ColumnName; -use vespertide_core::schema::reference::ReferenceAction; use vespertide_core::TableDef; +use vespertide_core::schema::column::{ColumnType, ComplexColumnType, EnumValues}; pub struct PrismaExporter; @@ -45,20 +46,21 @@ impl<'a> PrismaExporterWithConfig<'a> { for table in tables { for (name, values) in collect_table_enums(table) { if seen_enums.insert(name.to_string()) { - enum_blocks.push(render_enum(name, values)); + enum_blocks.push(enums::render_enum(name, values)); } } } let mut parts: Vec = Vec::new(); + let provider = self.config.provider(); let mut datasource = vec![ "datasource db {".to_string(), - format!(" provider = \"{}\"", self.config.provider()), + format!(" provider = \"{provider}\""), " url = env(\"DATABASE_URL\")".to_string(), ]; if let Some(rm) = self.config.relation_mode() { - datasource.push(format!(" relationMode = \"{}\"", rm)); + datasource.push(format!(" relationMode = \"{rm}\"")); } datasource.push("}".to_string()); parts.push(datasource.join("\n")); @@ -68,7 +70,7 @@ impl<'a> PrismaExporterWithConfig<'a> { " provider = \"prisma-client-js\"".to_string(), ]; if let Some(output) = self.config.client_output() { - generator.push(format!(" output = \"{}\"", output)); + generator.push(format!(" output = \"{output}\"")); } generator.push("}".to_string()); parts.push(generator.join("\n")); @@ -76,21 +78,21 @@ impl<'a> PrismaExporterWithConfig<'a> { parts.extend(enum_blocks); for table in tables { - parts.push(render_model(table, tables)); + parts.push(render::render_model(table, tables)); } parts.join("\n\n") + "\n" } } -fn collect_table_enums<'a>(table: &'a TableDef) -> Vec<(&'a str, &'a EnumValues)> { +fn collect_table_enums(table: &TableDef) -> Vec<(&str, &EnumValues)> { let mut seen = HashSet::new(); let mut result = Vec::new(); for col in &table.columns { - if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) = &col.r#type { - if seen.insert(name.as_str()) { - result.push((name.as_str(), values)); - } + if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) = &col.r#type + && seen.insert(name.as_str()) + { + result.push((name.as_str(), values)); } } result @@ -105,9 +107,9 @@ pub fn render_entity(table: &TableDef) -> String { pub fn render_entity_with_schema(table: &TableDef, schema: &[TableDef]) -> String { let mut parts: Vec = Vec::new(); for (name, values) in collect_table_enums(table) { - parts.push(render_enum(name, values)); + parts.push(enums::render_enum(name, values)); } - parts.push(render_model(table, schema)); + parts.push(render::render_model(table, schema)); parts.join("\n\n") } @@ -128,480 +130,5 @@ pub fn export(schema: &[TableDef]) -> Result { /// without making the helper generally public. #[cfg(test)] pub fn to_pascal_case_for_tests(s: &str) -> String { - to_pascal_case(s) -} - -fn render_enum(name: &str, values: &EnumValues) -> String { - let enum_name = to_pascal_case(name); - let mut lines = Vec::new(); - lines.push(format!("enum {} {{", enum_name)); - match values { - EnumValues::String(vals) => { - for val in vals { - let variant = to_screaming_snake(val); - if variant == *val { - lines.push(format!(" {}", variant)); - } else { - lines.push(format!(" {} @map(\"{}\")", variant, val)); - } - } - } - EnumValues::Integer(vals) => { - // Prisma doesn't support integer enums natively; emit as string variants with comment - for val in vals { - let variant = to_screaming_snake(&val.name); - lines.push(format!(" {} // = {}", variant, val.value)); - } - } - } - lines.push("}".into()); - lines.join("\n") -} - -struct PkInfo { - columns: Vec, - auto_increment: bool, -} - -fn extract_pk_info(constraints: &[TableConstraint]) -> PkInfo { - for c in constraints { - if let TableConstraint::PrimaryKey { auto_increment, columns, .. } = c { - return PkInfo { - columns: columns.iter().map(|c| c.to_string()).collect(), - auto_increment: *auto_increment, - }; - } - } - PkInfo { columns: Vec::new(), auto_increment: false } -} - -struct FkInfo<'a> { - ref_table: &'a str, - ref_cols: &'a [ColumnName], - on_delete: Option<&'a ReferenceAction>, - on_update: Option<&'a ReferenceAction>, -} - -fn render_model(table: &TableDef, schema: &[TableDef]) -> String { - let mut lines: Vec = Vec::new(); - - if let Some(desc) = &table.description { - for line in desc.lines() { - lines.push(format!("/// {}", line)); - } - } - - let model_name = to_pascal_case(&table.name); - lines.push(format!("model {} {{", model_name)); - - let pk_info = extract_pk_info(&table.constraints); - let pk_columns: HashSet<&str> = pk_info.columns.iter().map(|s| s.as_str()).collect(); - let is_composite_pk = pk_info.columns.len() > 1; - - let unique_single: HashMap<&str, Option<&str>> = table.constraints.iter() - .filter_map(|c| { - if let TableConstraint::Unique { name, columns, .. } = c { - if columns.len() == 1 { Some((columns[0].as_str(), name.as_deref())) } else { None } - } else { None } - }) - .collect(); - - // FK lookup by column - let fk_by_col: HashMap<&str, FkInfo<'_>> = table.constraints.iter() - .filter_map(|c| { - if let TableConstraint::ForeignKey { columns, ref_table, ref_columns, on_delete, on_update, .. } = c { - if columns.len() == 1 { - Some(( - columns[0].as_str(), - FkInfo { - ref_table: ref_table.as_str(), - ref_cols: ref_columns.as_slice(), - on_delete: on_delete.as_ref(), - on_update: on_update.as_ref(), - }, - )) - } else { None } - } else { None } - }) - .collect(); - - // Count FKs per ref_table for disambiguation detection - let mut ref_table_fk_count: HashMap<&str, usize> = HashMap::new(); - for fk in fk_by_col.values() { - *ref_table_fk_count.entry(fk.ref_table).or_default() += 1; - } - - // Render scalar fields + inline relation fields - for col in &table.columns { - let col_name = col.name.as_str(); - let in_pk = pk_columns.contains(col_name); - let is_single_pk = in_pk && !is_composite_pk; - let auto_inc = is_single_pk && pk_info.auto_increment; - let is_unique = unique_single.get(col_name).copied(); - - if let Some(ref comment) = col.comment { - lines.push(format!(" /// {}", comment.replace('\n', " "))); - } - - let (type_str, native_attr) = column_type_to_prisma(&col.r#type, col.nullable); - let mut attrs: Vec = Vec::new(); - - if is_single_pk { - attrs.push("@id".into()); - if auto_inc { - attrs.push("@default(autoincrement())".into()); - } - } - - if !auto_inc { - if let Some(ref default) = col.default { - attrs.push(prisma_default_attr(default.to_sql(), &col.r#type)); - } - } - - if let Some(unique_name) = is_unique { - if !is_single_pk { - match unique_name { - Some(n) => attrs.push(format!("@unique(map: \"{}\")", n)), - None => attrs.push("@unique".into()), - } - } - } - - if let Some(ref native) = native_attr { - attrs.push(native.clone()); - } - - let attrs_str = if attrs.is_empty() { - String::new() - } else { - format!(" {}", attrs.join(" ")) - }; - - lines.push(format!(" {} {}{}", col_name, type_str, attrs_str)); - - // Emit inline relation field for FK columns - if let Some(fk) = fk_by_col.get(col_name) { - let rel_field_name = infer_relation_field_name(col_name); - let rel_model = to_pascal_case(fk.ref_table); - let rel_type = if col.nullable { - format!("{}?", rel_model) - } else { - rel_model.clone() - }; - - let multi_fk = ref_table_fk_count.get(fk.ref_table).copied().unwrap_or(0) > 1; - let is_self_ref = fk.ref_table == table.name.as_str(); - let needs_name = multi_fk || is_self_ref; - - let mut rel_args: Vec = Vec::new(); - if needs_name { - let rel_name = format!( - "{}{}", - to_pascal_case(&table.name), - to_pascal_case(&rel_field_name) - ); - rel_args.push(format!("\"{}\"", rel_name)); - } - rel_args.push(format!("fields: [{}]", col_name)); - rel_args.push(format!( - "references: [{}]", - fk.ref_cols.iter().map(|s| s.as_str()).collect::>().join(", ") - )); - if let Some(od) = fk.on_delete { - rel_args.push(format!("onDelete: {}", reference_action_to_prisma(od))); - } - if let Some(ou) = fk.on_update { - rel_args.push(format!("onUpdate: {}", reference_action_to_prisma(ou))); - } - - lines.push(format!( - " {} {} @relation({})", - rel_field_name, - rel_type, - rel_args.join(", ") - )); - } - } - - // Back-relations from schema context - if !schema.is_empty() { - let back_rels = collect_back_relations(&table.name, schema); - for br in &back_rels { - let (field_name, rel_type) = back_rel_field(br); - let rel_attr = match &br.relation_name { - Some(name) => format!(" @relation(\"{}\")", name), - None => String::new(), - }; - lines.push(format!(" {} {}{}", field_name, rel_type, rel_attr)); - } - } - - // Blank line before model-level attributes - lines.push(String::new()); - - // Composite PK - if is_composite_pk { - lines.push(format!(" @@id([{}])", pk_info.columns.join(", "))); - } - - // Composite unique constraints - for c in &table.constraints { - if let TableConstraint::Unique { name, columns, .. } = c { - if columns.len() > 1 { - let cols = columns.join(", "); - if let Some(n) = name { - lines.push(format!(" @@unique([{}], name: \"{}\")", cols, n)); - } else { - lines.push(format!(" @@unique([{}])", cols)); - } - } - } - } - - // All index constraints - for c in &table.constraints { - if let TableConstraint::Index { name, columns } = c { - let cols = columns.join(", "); - if let Some(n) = name { - lines.push(format!(" @@index([{}], name: \"{}\")", cols, n)); - } else { - lines.push(format!(" @@index([{}])", cols)); - } - } - } - - // @@map (always present since model is PascalCase but table is snake_case) - lines.push(format!(" @@map(\"{}\")", table.name)); - lines.push("}".into()); - - lines.join("\n") -} - -struct BackRelation { - source_table: String, - fk_col: String, - is_one_to_one: bool, - relation_name: Option, -} - -fn back_rel_field(br: &BackRelation) -> (String, String) { - let source_pascal = to_pascal_case(&br.source_table); - let rel_type = if br.is_one_to_one { - format!("{}?", source_pascal) - } else { - format!("{}[]", source_pascal) - }; - - // source_table is already the plural table name — use it directly - let field_name = if br.relation_name.is_some() { - let rel_field = infer_relation_field_name(&br.fk_col); - if br.is_one_to_one { - format!("{}_{}", rel_field, br.source_table) - } else { - format!("{}_{}", rel_field, &br.source_table) - } - } else if br.is_one_to_one { - br.source_table.clone() - } else { - br.source_table.clone() - }; - - (field_name, rel_type) -} - -fn collect_back_relations(target_table: &str, schema: &[TableDef]) -> Vec { - let mut result = Vec::new(); - - for source in schema { - let fks_to_target: Vec<(&str, &[ColumnName])> = source.constraints.iter() - .filter_map(|c| { - if let TableConstraint::ForeignKey { columns, ref_table, ref_columns, .. } = c { - if ref_table.as_str() == target_table && columns.len() == 1 { - Some((columns[0].as_str(), ref_columns.as_slice())) - } else { None } - } else { None } - }) - .collect(); - - if fks_to_target.is_empty() { continue; } - - let multi_fk = fks_to_target.len() > 1; - let is_self_ref = source.name.as_str() == target_table; - - for (fk_col, _) in &fks_to_target { - let is_unique = source.constraints.iter().any(|c| { - matches!(c, TableConstraint::Unique { columns, .. } - if columns.len() == 1 && columns[0].as_str() == *fk_col) - }); - - let needs_name = multi_fk || is_self_ref; - let relation_name = if needs_name { - let rel_field = infer_relation_field_name(fk_col); - Some(format!( - "{}{}", - to_pascal_case(&source.name), - to_pascal_case(&rel_field) - )) - } else { - None - }; - - result.push(BackRelation { - source_table: source.name.as_str().to_string(), - fk_col: fk_col.to_string(), - is_one_to_one: is_unique, - relation_name, - }); - } - } - - result -} - -fn column_type_to_prisma(ty: &ColumnType, nullable: bool) -> (String, Option) { - let q = if nullable { "?" } else { "" }; - - match ty { - ColumnType::Simple(simple) => { - let (base, native) = match simple { - SimpleColumnType::SmallInt => ("Int", Some("@db.SmallInt")), - SimpleColumnType::Integer => ("Int", None), - SimpleColumnType::BigInt => ("BigInt", None), - SimpleColumnType::Real => ("Float", Some("@db.Real")), - SimpleColumnType::DoublePrecision => ("Float", None), - SimpleColumnType::Text => ("String", Some("@db.Text")), - SimpleColumnType::Boolean => ("Boolean", None), - SimpleColumnType::Date => ("DateTime", Some("@db.Date")), - SimpleColumnType::Time => ("DateTime", Some("@db.Time")), - SimpleColumnType::Timestamp => ("DateTime", Some("@db.Timestamp")), - SimpleColumnType::Timestamptz => ("DateTime", Some("@db.Timestamptz")), - SimpleColumnType::Interval => ("String", Some("@db.Interval")), - SimpleColumnType::Bytea => ("Bytes", None), - SimpleColumnType::Uuid => ("String", Some("@db.Uuid")), - SimpleColumnType::Json => ("Json", None), - SimpleColumnType::Inet => ("String", Some("@db.Inet")), - SimpleColumnType::Cidr => ("String", Some("@db.Cidr")), - SimpleColumnType::Macaddr => ("String", Some("@db.Macaddr")), - SimpleColumnType::Xml => ("String", Some("@db.Xml")), - // Unknown/future simple types fall back to a plain String column. - _ => ("String", None), - }; - (format!("{}{}", base, q), native.map(str::to_string)) - } - ColumnType::Complex(complex) => match complex { - ComplexColumnType::Varchar { length } => { - (format!("String{}", q), Some(format!("@db.VarChar({})", length))) - } - ComplexColumnType::Char { length } => { - (format!("String{}", q), Some(format!("@db.Char({})", length))) - } - ComplexColumnType::Numeric { precision, scale } => { - (format!("Decimal{}", q), Some(format!("@db.Decimal({}, {})", precision, scale))) - } - ComplexColumnType::Custom { custom_type } => { - (format!("Unsupported(\"{}\"){}", custom_type, q), None) - } - ComplexColumnType::Enum { name, .. } => { - (format!("{}{}", to_pascal_case(name), q), None) - } - // Unknown/future complex types fall back to a plain String column. - _ => (format!("String{}", q), None), - }, - } -} - -fn prisma_default_attr(default_sql: String, col_type: &ColumnType) -> String { - if default_sql == "true" { - return "@default(true)".into(); - } - if default_sql == "false" { - return "@default(false)".into(); - } - - let lower = default_sql.to_lowercase(); - if lower.contains("now()") || lower.starts_with("current_timestamp") { - return "@default(now())".into(); - } - if lower.contains("gen_random_uuid()") - || lower.contains("uuid_generate_v4()") - || lower.contains("newid()") - { - return "@default(uuid())".into(); - } - - // Any remaining function call → dbgenerated - if default_sql.contains('(') { - let escaped = default_sql.replace('"', "\\\""); - return format!("@default(dbgenerated(\"{}\"))", escaped); - } - - // String literal with quotes — may be an enum value - if default_sql.starts_with('\'') || default_sql.starts_with('"') { - let stripped = default_sql.trim_matches(|c| c == '\'' || c == '"'); - if let ColumnType::Complex(ComplexColumnType::Enum { values, .. }) = col_type { - if let EnumValues::String(variants) = values { - if variants.iter().any(|v| v.as_str() == stripped) { - let variant = to_screaming_snake(stripped); - return format!("@default({})", variant); - } - } - } - return format!("@default(\"{}\")", stripped.replace('\\', "\\\\").replace('"', "\\\"")); - } - - // Numeric - if default_sql.parse::().is_ok() { - return format!("@default({})", default_sql); - } - - // Fallback - let escaped = default_sql.replace('"', "\\\""); - format!("@default(dbgenerated(\"{}\"))", escaped) -} - -fn reference_action_to_prisma(action: &ReferenceAction) -> &'static str { - match action { - ReferenceAction::Cascade => "Cascade", - ReferenceAction::Restrict => "Restrict", - ReferenceAction::SetNull => "SetNull", - ReferenceAction::SetDefault => "SetDefault", - ReferenceAction::NoAction => "NoAction", - // Unknown/future referential actions fall back to Prisma's default. - _ => "NoAction", - } -} - -fn infer_relation_field_name(fk_col: &str) -> String { - fk_col.strip_suffix("_id").unwrap_or(fk_col).to_string() -} - -fn to_pascal_case(s: &str) -> String { - s.split('_') - .map(|word| { - let mut chars = word.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().chain(chars).collect(), - } - }) - .collect() -} - -fn to_screaming_snake(s: &str) -> String { - let mut result = String::new(); - let mut prev_lower = false; - for ch in s.chars() { - if ch.is_uppercase() && prev_lower { - result.push('_'); - } - if ch.is_alphanumeric() { - result.push(ch.to_ascii_uppercase()); - prev_lower = ch.is_lowercase(); - } else { - result.push('_'); - prev_lower = false; - } - } - result.trim_end_matches('_').to_string() + enums::to_pascal_case(s) } diff --git a/crates/vespertide-exporter/src/prisma/render.rs b/crates/vespertide-exporter/src/prisma/render.rs new file mode 100644 index 0000000..5773d25 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/render.rs @@ -0,0 +1,415 @@ +use std::collections::HashMap; + +use vespertide_core::TableDef; +use vespertide_core::schema::column::{ColumnType, ComplexColumnType, EnumValues}; +use vespertide_core::schema::constraint::TableConstraint; +use vespertide_core::schema::names::ColumnName; +use vespertide_core::schema::reference::ReferenceAction; + +use super::enums::{to_pascal_case, to_screaming_snake}; +use super::types::column_type_to_prisma; + +struct PkInfo { + columns: Vec, + auto_increment: bool, +} + +fn extract_pk_info(constraints: &[TableConstraint]) -> PkInfo { + for c in constraints { + if let TableConstraint::PrimaryKey { + auto_increment, + columns, + .. + } = c + { + return PkInfo { + columns: columns.iter().map(ToString::to_string).collect(), + auto_increment: *auto_increment, + }; + } + } + PkInfo { + columns: Vec::new(), + auto_increment: false, + } +} + +struct FkInfo<'a> { + ref_table: &'a str, + ref_cols: &'a [ColumnName], + on_delete: Option<&'a ReferenceAction>, + on_update: Option<&'a ReferenceAction>, +} + +pub(super) struct BackRelation { + pub(super) source_table: String, + pub(super) fk_col: String, + pub(super) is_one_to_one: bool, + pub(super) relation_name: Option, +} + +pub(super) fn back_rel_field(br: &BackRelation) -> (String, String) { + let source_pascal = to_pascal_case(&br.source_table); + let rel_type = if br.is_one_to_one { + format!("{source_pascal}?") + } else { + format!("{source_pascal}[]") + }; + + let field_name = if br.relation_name.is_some() { + let rel_field = infer_relation_field_name(&br.fk_col); + format!("{rel_field}_{}", br.source_table) + } else { + br.source_table.clone() + }; + + (field_name, rel_type) +} + +pub(super) fn collect_back_relations(target_table: &str, schema: &[TableDef]) -> Vec { + let mut result = Vec::new(); + + for source in schema { + let fks_to_target: Vec<(&str, &[ColumnName])> = source + .constraints + .iter() + .filter_map(|c| { + if let TableConstraint::ForeignKey { + columns, + ref_table, + ref_columns, + .. + } = c + { + if ref_table.as_str() == target_table && columns.len() == 1 { + Some((columns[0].as_str(), ref_columns.as_slice())) + } else { + None + } + } else { + None + } + }) + .collect(); + + if fks_to_target.is_empty() { + continue; + } + + let multi_fk = fks_to_target.len() > 1; + let is_self_ref = source.name.as_str() == target_table; + + for (fk_col, _) in &fks_to_target { + let is_unique = source.constraints.iter().any(|c| { + matches!(c, TableConstraint::Unique { columns, .. } + if columns.len() == 1 && columns[0].as_str() == *fk_col) + }); + + let needs_name = multi_fk || is_self_ref; + let relation_name = if needs_name { + let rel_field = infer_relation_field_name(fk_col); + let source_pascal = to_pascal_case(&source.name); + let rel_pascal = to_pascal_case(&rel_field); + Some(format!("{source_pascal}{rel_pascal}")) + } else { + None + }; + + result.push(BackRelation { + source_table: source.name.as_str().to_string(), + fk_col: fk_col.to_string(), + is_one_to_one: is_unique, + relation_name, + }); + } + } + + result +} + +pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String { + let mut lines: Vec = Vec::new(); + + if let Some(desc) = &table.description { + for line in desc.lines() { + lines.push(format!("/// {line}")); + } + } + + let model_name = to_pascal_case(&table.name); + lines.push(format!("model {model_name} {{")); + + let pk_info = extract_pk_info(&table.constraints); + let pk_columns: std::collections::HashSet<&str> = + pk_info.columns.iter().map(String::as_str).collect(); + let is_composite_pk = pk_info.columns.len() > 1; + + let unique_single: HashMap<&str, Option<&str>> = table + .constraints + .iter() + .filter_map(|c| { + if let TableConstraint::Unique { name, columns, .. } = c { + if columns.len() == 1 { + Some((columns[0].as_str(), name.as_deref())) + } else { + None + } + } else { + None + } + }) + .collect(); + + // FK lookup by column + let fk_by_col: HashMap<&str, FkInfo<'_>> = table + .constraints + .iter() + .filter_map(|c| { + if let TableConstraint::ForeignKey { + columns, + ref_table, + ref_columns, + on_delete, + on_update, + .. + } = c + { + if columns.len() == 1 { + Some(( + columns[0].as_str(), + FkInfo { + ref_table: ref_table.as_str(), + ref_cols: ref_columns.as_slice(), + on_delete: on_delete.as_ref(), + on_update: on_update.as_ref(), + }, + )) + } else { + None + } + } else { + None + } + }) + .collect(); + + // Count FKs per ref_table for disambiguation detection + let mut ref_table_fk_count: HashMap<&str, usize> = HashMap::new(); + for fk in fk_by_col.values() { + *ref_table_fk_count.entry(fk.ref_table).or_default() += 1; + } + + // Render scalar fields + inline relation fields + for col in &table.columns { + let col_name = col.name.as_str(); + let in_pk = pk_columns.contains(col_name); + let is_single_pk = in_pk && !is_composite_pk; + let auto_inc = is_single_pk && pk_info.auto_increment; + let is_unique = unique_single.get(col_name).copied(); + + if let Some(ref comment) = col.comment { + let comment = comment.replace('\n', " "); + lines.push(format!(" /// {comment}")); + } + + let (type_str, native_attr) = column_type_to_prisma(&col.r#type, col.nullable); + let mut attrs: Vec = Vec::new(); + + if is_single_pk { + attrs.push("@id".into()); + if auto_inc { + attrs.push("@default(autoincrement())".into()); + } + } + + if !auto_inc && let Some(ref default) = col.default { + attrs.push(prisma_default_attr(&default.to_sql(), &col.r#type)); + } + + if let Some(unique_name) = is_unique + && !is_single_pk + { + match unique_name { + Some(n) => attrs.push(format!("@unique(map: \"{n}\")")), + None => attrs.push("@unique".into()), + } + } + + if let Some(ref native) = native_attr { + attrs.push(native.clone()); + } + + let attrs_str = if attrs.is_empty() { + String::new() + } else { + format!(" {}", attrs.join(" ")) + }; + + lines.push(format!(" {col_name} {type_str}{attrs_str}")); + + // Emit inline relation field for FK columns + if let Some(fk) = fk_by_col.get(col_name) { + let rel_field_name = infer_relation_field_name(col_name); + let rel_model = to_pascal_case(fk.ref_table); + let rel_type = if col.nullable { + format!("{rel_model}?") + } else { + rel_model.clone() + }; + + let multi_fk = ref_table_fk_count.get(fk.ref_table).copied().unwrap_or(0) > 1; + let is_self_ref = fk.ref_table == table.name.as_str(); + let needs_name = multi_fk || is_self_ref; + + let mut rel_args: Vec = Vec::new(); + if needs_name { + let table_pascal = to_pascal_case(&table.name); + let field_pascal = to_pascal_case(&rel_field_name); + rel_args.push(format!("\"{table_pascal}{field_pascal}\"")); + } + rel_args.push(format!("fields: [{col_name}]")); + rel_args.push(format!( + "references: [{}]", + fk.ref_cols + .iter() + .map(ColumnName::as_str) + .collect::>() + .join(", ") + )); + if let Some(od) = fk.on_delete { + let action = reference_action_to_prisma(od); + rel_args.push(format!("onDelete: {action}")); + } + if let Some(ou) = fk.on_update { + let action = reference_action_to_prisma(ou); + rel_args.push(format!("onUpdate: {action}")); + } + + let rel_args_str = rel_args.join(", "); + lines.push(format!( + " {rel_field_name} {rel_type} @relation({rel_args_str})" + )); + } + } + + // Back-relations from schema context + if !schema.is_empty() { + let back_rels = collect_back_relations(&table.name, schema); + for br in &back_rels { + let (field_name, rel_type) = back_rel_field(br); + let rel_attr = match &br.relation_name { + Some(name) => format!(" @relation(\"{name}\")"), + None => String::new(), + }; + lines.push(format!(" {field_name} {rel_type}{rel_attr}")); + } + } + + // Blank line before model-level attributes + lines.push(String::new()); + + // Composite PK + if is_composite_pk { + let pk_cols = pk_info.columns.join(", "); + lines.push(format!(" @@id([{pk_cols}])")); + } + + // Composite unique constraints + for c in &table.constraints { + if let TableConstraint::Unique { name, columns, .. } = c + && columns.len() > 1 + { + let cols = columns.join(", "); + if let Some(n) = name { + lines.push(format!(" @@unique([{cols}], name: \"{n}\")")); + } else { + lines.push(format!(" @@unique([{cols}])")); + } + } + } + + // All index constraints + for c in &table.constraints { + if let TableConstraint::Index { name, columns } = c { + let cols = columns.join(", "); + if let Some(n) = name { + lines.push(format!(" @@index([{cols}], name: \"{n}\")")); + } else { + lines.push(format!(" @@index([{cols}])")); + } + } + } + + // @@map (always present since model is PascalCase but table is snake_case) + let table_name = table.name.as_str(); + lines.push(format!(" @@map(\"{table_name}\")")); + lines.push("}".into()); + + lines.join("\n") +} + +fn prisma_default_attr(default_sql: &str, col_type: &ColumnType) -> String { + if default_sql == "true" { + return "@default(true)".into(); + } + if default_sql == "false" { + return "@default(false)".into(); + } + + let lower = default_sql.to_lowercase(); + if lower.contains("now()") || lower.starts_with("current_timestamp") { + return "@default(now())".into(); + } + if lower.contains("gen_random_uuid()") + || lower.contains("uuid_generate_v4()") + || lower.contains("newid()") + { + return "@default(uuid())".into(); + } + + // Any remaining function call → dbgenerated + if default_sql.contains('(') { + let escaped = default_sql.replace('"', "\\\""); + return format!("@default(dbgenerated(\"{escaped}\"))"); + } + + // String literal with quotes — may be an enum value + if default_sql.starts_with('\'') || default_sql.starts_with('"') { + let stripped = default_sql.trim_matches(|c| c == '\'' || c == '"'); + if let ColumnType::Complex(ComplexColumnType::Enum { + values: EnumValues::String(variants), + .. + }) = col_type + && variants.iter().any(|v| v.as_str() == stripped) + { + let variant = to_screaming_snake(stripped); + return format!("@default({variant})"); + } + let s = stripped.replace('\\', "\\\\").replace('"', "\\\""); + return format!("@default(\"{s}\")"); + } + + // Numeric + if default_sql.parse::().is_ok() { + return format!("@default({default_sql})"); + } + + // Fallback + let escaped = default_sql.replace('"', "\\\""); + format!("@default(dbgenerated(\"{escaped}\"))") +} + +fn reference_action_to_prisma(action: &ReferenceAction) -> &'static str { + match action { + ReferenceAction::Cascade => "Cascade", + ReferenceAction::Restrict => "Restrict", + ReferenceAction::SetNull => "SetNull", + ReferenceAction::SetDefault => "SetDefault", + // Includes NoAction and unknown/future referential actions. + _ => "NoAction", + } +} + +fn infer_relation_field_name(fk_col: &str) -> String { + fk_col.strip_suffix("_id").unwrap_or(fk_col).to_string() +} diff --git a/crates/vespertide-exporter/src/prisma/types.rs b/crates/vespertide-exporter/src/prisma/types.rs new file mode 100644 index 0000000..d5f3ed7 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/types.rs @@ -0,0 +1,57 @@ +use vespertide_core::schema::column::{ColumnType, ComplexColumnType, SimpleColumnType}; + +use super::enums::to_pascal_case; + +pub(super) fn column_type_to_prisma(ty: &ColumnType, nullable: bool) -> (String, Option) { + let q = if nullable { "?" } else { "" }; + + match ty { + ColumnType::Simple(simple) => { + let (base, native) = match simple { + SimpleColumnType::SmallInt => ("Int", Some("@db.SmallInt")), + SimpleColumnType::Integer => ("Int", None), + SimpleColumnType::BigInt => ("BigInt", None), + SimpleColumnType::Real => ("Float", Some("@db.Real")), + SimpleColumnType::DoublePrecision => ("Float", None), + SimpleColumnType::Text => ("String", Some("@db.Text")), + SimpleColumnType::Boolean => ("Boolean", None), + SimpleColumnType::Date => ("DateTime", Some("@db.Date")), + SimpleColumnType::Time => ("DateTime", Some("@db.Time")), + SimpleColumnType::Timestamp => ("DateTime", Some("@db.Timestamp")), + SimpleColumnType::Timestamptz => ("DateTime", Some("@db.Timestamptz")), + SimpleColumnType::Interval => ("String", Some("@db.Interval")), + SimpleColumnType::Bytea => ("Bytes", None), + SimpleColumnType::Uuid => ("String", Some("@db.Uuid")), + SimpleColumnType::Json => ("Json", None), + SimpleColumnType::Inet => ("String", Some("@db.Inet")), + SimpleColumnType::Cidr => ("String", Some("@db.Cidr")), + SimpleColumnType::Macaddr => ("String", Some("@db.Macaddr")), + SimpleColumnType::Xml => ("String", Some("@db.Xml")), + // Unknown/future simple types fall back to a plain String column. + _ => ("String", None), + }; + (format!("{base}{q}"), native.map(str::to_string)) + } + ColumnType::Complex(complex) => match complex { + ComplexColumnType::Varchar { length } => { + (format!("String{q}"), Some(format!("@db.VarChar({length})"))) + } + ComplexColumnType::Char { length } => { + (format!("String{q}"), Some(format!("@db.Char({length})"))) + } + ComplexColumnType::Numeric { precision, scale } => ( + format!("Decimal{q}"), + Some(format!("@db.Decimal({precision}, {scale})")), + ), + ComplexColumnType::Custom { custom_type } => { + (format!("Unsupported(\"{custom_type}\"){q}"), None) + } + ComplexColumnType::Enum { name, .. } => { + let pascal = to_pascal_case(name); + (format!("{pascal}{q}"), None) + } + // Unknown/future complex types fall back to a plain String column. + _ => (format!("String{q}"), None), + }, + } +} From dcbff76bf515e626ef40a85ecc3bfce42d71cbc5 Mon Sep 17 00:00:00 2001 From: L33gn21 Date: Wed, 24 Jun 2026 00:24:58 +0900 Subject: [PATCH 4/4] 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 --- .../vespertide-exporter/src/prisma/enums.rs | 8 +++- crates/vespertide-exporter/src/prisma/mod.rs | 8 +++- .../vespertide-exporter/src/prisma/render.rs | 40 +++++++++++++++++-- ...snapshot@composite_constraints_Prisma.snap | 5 ++- ...index_snapshot@composite_index_Prisma.snap | 3 +- ...ot@composite_unique_constraint_Prisma.snap | 3 +- ...ique_snapshot@composite_unique_Prisma.snap | 3 +- ...h_schema_snapshots@username_fk_Prisma.snap | 2 +- ...k_snapshot@self_referencing_fk_Prisma.snap | 1 + ...es_snapshot@table_with_indexes_Prisma.snap | 3 +- 10 files changed, 62 insertions(+), 14 deletions(-) diff --git a/crates/vespertide-exporter/src/prisma/enums.rs b/crates/vespertide-exporter/src/prisma/enums.rs index d7c025b..f38c543 100644 --- a/crates/vespertide-exporter/src/prisma/enums.rs +++ b/crates/vespertide-exporter/src/prisma/enums.rs @@ -55,5 +55,11 @@ pub(super) fn to_screaming_snake(s: &str) -> String { prev_lower = false; } } - result.trim_end_matches('_').to_string() + let result = result.trim_end_matches('_').to_string(); + // Prisma identifiers cannot start with a digit; prefix with '_' to keep it valid. + if result.starts_with(|c: char| c.is_ascii_digit()) { + format!("_{result}") + } else { + result + } } diff --git a/crates/vespertide-exporter/src/prisma/mod.rs b/crates/vespertide-exporter/src/prisma/mod.rs index b24d01a..5514137 100644 --- a/crates/vespertide-exporter/src/prisma/mod.rs +++ b/crates/vespertide-exporter/src/prisma/mod.rs @@ -98,9 +98,13 @@ fn collect_table_enums(table: &TableDef) -> Vec<(&str, &EnumValues)> { result } -/// Render enum blocks + model block without schema context (no back-relations). +/// Render enum blocks + model block without schema context. +/// +/// Passes the table itself as a one-element schema so that self-referential FK +/// back-relations are always emitted (Prisma requires both sides of a relation to +/// be present in the model, including self-referential ones). pub fn render_entity(table: &TableDef) -> String { - render_entity_with_schema(table, &[]) + render_entity_with_schema(table, std::slice::from_ref(table)) } /// Render enum blocks + model block with full schema context (includes back-relations). diff --git a/crates/vespertide-exporter/src/prisma/render.rs b/crates/vespertide-exporter/src/prisma/render.rs index 5773d25..67a0389 100644 --- a/crates/vespertide-exporter/src/prisma/render.rs +++ b/crates/vespertide-exporter/src/prisma/render.rs @@ -249,7 +249,16 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String { // Emit inline relation field for FK columns if let Some(fk) = fk_by_col.get(col_name) { - let rel_field_name = infer_relation_field_name(col_name); + // `rel_name_segment` drives @relation("…") naming — must stay consistent + // with the segment computed by collect_back_relations on the other side. + let rel_name_segment = infer_relation_field_name(col_name); + // When no `_id` was stripped the inferred name equals the FK column name, + // which would collide with the scalar field. Append `_rel` to deduplicate. + let rel_field_name = if rel_name_segment == col_name { + format!("{col_name}_rel") + } else { + rel_name_segment.clone() + }; let rel_model = to_pascal_case(fk.ref_table); let rel_type = if col.nullable { format!("{rel_model}?") @@ -263,8 +272,9 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String { let mut rel_args: Vec = Vec::new(); if needs_name { + // Use rel_name_segment (pre-dedup) so the name matches back-relations. let table_pascal = to_pascal_case(&table.name); - let field_pascal = to_pascal_case(&rel_field_name); + let field_pascal = to_pascal_case(&rel_name_segment); rel_args.push(format!("\"{table_pascal}{field_pascal}\"")); } rel_args.push(format!("fields: [{col_name}]")); @@ -321,7 +331,7 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String { { let cols = columns.join(", "); if let Some(n) = name { - lines.push(format!(" @@unique([{cols}], name: \"{n}\")")); + lines.push(format!(" @@unique([{cols}], map: \"{n}\")")); } else { lines.push(format!(" @@unique([{cols}])")); } @@ -333,7 +343,7 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String { if let TableConstraint::Index { name, columns } = c { let cols = columns.join(", "); if let Some(n) = name { - lines.push(format!(" @@index([{cols}], name: \"{n}\")")); + lines.push(format!(" @@index([{cols}], map: \"{n}\")")); } else { lines.push(format!(" @@index([{cols}])")); } @@ -349,6 +359,28 @@ pub(super) fn render_model(table: &TableDef, schema: &[TableDef]) -> String { } fn prisma_default_attr(default_sql: &str, col_type: &ColumnType) -> String { + // Integer-backed enum: resolve to a variant identifier (SCREAMING_SNAKE), never a bare int. + if let ColumnType::Complex(ComplexColumnType::Enum { + values: EnumValues::Integer(int_values), + .. + }) = col_type + { + let key = default_sql.trim_matches(|c: char| c == '\'' || c == '"'); + // 1) numeric value match → variant name + if let Ok(n) = key.parse::() + && let Some(v) = int_values.iter().find(|v| v.value == n) + { + return format!("@default({})", to_screaming_snake(&v.name)); + } + // 2) exact variant-name match → variant name + if let Some(v) = int_values.iter().find(|v| v.name == key) { + return format!("@default({})", to_screaming_snake(&v.name)); + } + // 3) no match → dbgenerated fallback (valid PSL; avoids bare-int type error) + let escaped = key.replace('"', "\\\""); + return format!("@default(dbgenerated(\"{escaped}\"))"); + } + if default_sql == "true" { return "@default(true)".into(); } diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap index c439a59..94c7016 100644 --- a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_constraints_snapshot@composite_constraints_Prisma.snap @@ -1,5 +1,6 @@ --- source: crates/vespertide-exporter/src/tests/mod.rs +assertion_line: 172 expression: rendered --- model OrderItems { @@ -10,7 +11,7 @@ model OrderItems { quantity Int @@id([order_id, product_id]) - @@unique([order_id, product_id], name: "uq_order_items__order_product") - @@index([order_id], name: "ix_order_items__order_id") + @@unique([order_id, product_id], map: "uq_order_items__order_product") + @@index([order_id], map: "ix_order_items__order_id") @@map("order_items") } diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_index_snapshot@composite_index_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_index_snapshot@composite_index_Prisma.snap index f4c1907..29d0539 100644 --- a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_index_snapshot@composite_index_Prisma.snap +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_index_snapshot@composite_index_Prisma.snap @@ -1,5 +1,6 @@ --- source: crates/vespertide-exporter/src/tests/mod.rs +assertion_line: 182 expression: rendered --- model CompositeIndex { @@ -7,6 +8,6 @@ model CompositeIndex { tenant_id Int name String @db.Text - @@index([tenant_id, name], name: "idx_tenant_name") + @@index([tenant_id, name], map: "idx_tenant_name") @@map("composite_index") } diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_constraint_snapshot@composite_unique_constraint_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_constraint_snapshot@composite_unique_constraint_Prisma.snap index 2ef045c..50ed0ce 100644 --- a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_constraint_snapshot@composite_unique_constraint_Prisma.snap +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_constraint_snapshot@composite_unique_constraint_Prisma.snap @@ -1,5 +1,6 @@ --- source: crates/vespertide-exporter/src/tests/mod.rs +assertion_line: 252 expression: rendered --- model AccountAliases { @@ -7,6 +8,6 @@ model AccountAliases { tenant_id Int slug String @db.Text - @@unique([tenant_id, slug], name: "uq_account_aliases__tenant_slug") + @@unique([tenant_id, slug], map: "uq_account_aliases__tenant_slug") @@map("account_aliases") } diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_snapshot@composite_unique_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_snapshot@composite_unique_Prisma.snap index c62e246..63f9f6a 100644 --- a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_snapshot@composite_unique_Prisma.snap +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__composite_unique_snapshot@composite_unique_Prisma.snap @@ -1,5 +1,6 @@ --- source: crates/vespertide-exporter/src/tests/mod.rs +assertion_line: 177 expression: rendered --- model CompositeUnique { @@ -7,6 +8,6 @@ model CompositeUnique { tenant_id Int name String @db.Text - @@unique([tenant_id, name], name: "uq_tenant_name") + @@unique([tenant_id, name], map: "uq_tenant_name") @@map("composite_unique") } diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap index f20329d..4dcb549 100644 --- a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__render_entity_with_schema_snapshots@username_fk_Prisma.snap @@ -5,7 +5,7 @@ expression: rendered model Session { id String @id @db.Uuid username String @db.Text - username User @relation(fields: [username], references: [username]) + username_rel User @relation(fields: [username], references: [username]) @@map("session") } diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap index 36a999c..ebb5863 100644 --- a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__self_referencing_fk_snapshot@self_referencing_fk_Prisma.snap @@ -6,6 +6,7 @@ model Employees { id Int @id manager_id Int? manager Employees? @relation("EmployeesManager", fields: [manager_id], references: [id], onDelete: SetNull) + manager_employees Employees[] @relation("EmployeesManager") @@map("employees") } diff --git a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_indexes_snapshot@table_with_indexes_Prisma.snap b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_indexes_snapshot@table_with_indexes_Prisma.snap index a69cbe6..19c1740 100644 --- a/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_indexes_snapshot@table_with_indexes_Prisma.snap +++ b/crates/vespertide-exporter/src/tests/snapshots/vespertide_exporter__tests__table_with_indexes_snapshot@table_with_indexes_Prisma.snap @@ -1,5 +1,6 @@ --- source: crates/vespertide-exporter/src/tests/mod.rs +assertion_line: 131 expression: rendered --- model Articles { @@ -7,7 +8,7 @@ model Articles { title String @db.Text created_at DateTime @db.Timestamptz - @@index([created_at], name: "idx_articles_created_at") + @@index([created_at], map: "idx_articles_created_at") @@index([title]) @@map("articles") }