diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs index da274037..90ef1705 100644 --- a/crates/vespertide-cli/src/commands/export.rs +++ b/crates/vespertide-cli/src/commands/export.rs @@ -7,7 +7,10 @@ use futures::future::try_join_all; 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::utils::load_config; @@ -17,6 +20,7 @@ pub enum OrmArg { Sqlalchemy, Sqlmodel, Jpa, + Prisma, } impl From for Orm { @@ -26,6 +30,7 @@ impl From for Orm { OrmArg::Sqlalchemy => Orm::SqlAlchemy, OrmArg::Sqlmodel => Orm::SqlModel, OrmArg::Jpa => Orm::Jpa, + OrmArg::Prisma => Orm::Prisma, } } } @@ -49,6 +54,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?; @@ -203,6 +213,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?; @@ -295,6 +306,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) { @@ -395,6 +407,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 0b7a4822..b862cb29 100644 --- a/crates/vespertide-config/src/config.rs +++ b/crates/vespertide-config/src/config.rs @@ -77,6 +77,50 @@ impl SeaOrmConfig { } } +/// Prisma-specific export configuration. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +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))] @@ -98,6 +142,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)] @@ -120,6 +167,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(), } } @@ -171,6 +219,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 1ec2e86a..e517dd30 100644 --- a/crates/vespertide-config/src/lib.rs +++ b/crates/vespertide-config/src/lib.rs @@ -2,7 +2,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 22da1da2..02e8dc62 100644 --- a/crates/vespertide-exporter/src/lib.rs +++ b/crates/vespertide-exporter/src/lib.rs @@ -1,14 +1,16 @@ //! 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; +pub mod prisma; pub mod seaorm; pub mod sqlalchemy; pub mod sqlmodel; 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 cf16fd98..7d343abb 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), } } @@ -87,6 +90,7 @@ mod tests { #[case("sqlalchemy", Orm::SqlAlchemy)] #[case("sqlmodel", Orm::SqlModel)] #[case("jpa", Orm::Jpa)] + #[case("prisma", Orm::Prisma)] fn test_render_entity_snapshots(#[case] name: &str, #[case] orm: Orm) { let table = simple_table(); let result = render_entity(orm, &table); @@ -101,6 +105,7 @@ mod tests { #[case("sqlalchemy", Orm::SqlAlchemy)] #[case("sqlmodel", Orm::SqlModel)] #[case("jpa", Orm::Jpa)] + #[case("prisma", Orm::Prisma)] fn test_render_entity_with_schema_snapshots(#[case] name: &str, #[case] orm: Orm) { let table = simple_table(); 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 00000000..937205d5 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/mod.rs @@ -0,0 +1,1376 @@ +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::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") +} + +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.clone(), auto_increment: *auto_increment }; + } + } + PkInfo { columns: Vec::new(), auto_increment: false } +} + +struct FkInfo<'a> { + ref_table: &'a str, + ref_cols: &'a [String], + 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, &[String])> = 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.clone(), + 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")), + }; + (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) + } + }, + } +} + +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 { name, 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", + } +} + +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() +} + + +#[cfg(test)] +mod tests { + use super::*; + use insta::assert_snapshot; + use rstest::rstest; + use vespertide_core::schema::column::NumValue; + use vespertide_core::{ColumnDef, DefaultValue, TableDef}; + + // ── Helpers ────────────────────────────────────────────────────────────── + + fn col(name: &str, ty: ColumnType) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + fn col_null(name: &str, ty: ColumnType) -> ColumnDef { + ColumnDef { nullable: true, ..col(name, ty) } + } + + fn col_with_default(name: &str, ty: ColumnType, default: DefaultValue) -> ColumnDef { + ColumnDef { default: Some(default), ..col(name, ty) } + } + + fn col_with_comment(name: &str, ty: ColumnType, comment: &str) -> ColumnDef { + ColumnDef { comment: Some(comment.to_string()), ..col(name, ty) } + } + + fn pk(cols: &[&str], auto_inc: bool) -> TableConstraint { + TableConstraint::PrimaryKey { + auto_increment: auto_inc, + columns: cols.iter().map(|s| s.to_string()).collect(), + } + } + + fn uniq(name: Option<&str>, cols: &[&str]) -> TableConstraint { + TableConstraint::Unique { + name: name.map(str::to_string), + columns: cols.iter().map(|s| s.to_string()).collect(), + } + } + + fn idx(name: Option<&str>, cols: &[&str]) -> TableConstraint { + TableConstraint::Index { + name: name.map(str::to_string), + columns: cols.iter().map(|s| s.to_string()).collect(), + } + } + + fn fk( + cols: &[&str], + ref_table: &str, + ref_cols: &[&str], + on_delete: Option, + on_update: Option, + ) -> TableConstraint { + TableConstraint::ForeignKey { + name: None, + columns: cols.iter().map(|s| s.to_string()).collect(), + ref_table: ref_table.to_string(), + ref_columns: ref_cols.iter().map(|s| s.to_string()).collect(), + on_delete, + on_update, + } + } + + fn table(name: &str, desc: Option<&str>, cols: Vec, constraints: Vec) -> TableDef { + TableDef { + name: name.to_string(), + description: desc.map(str::to_string), + columns: cols, + constraints, + } + } + + fn render_schema_all(schema: &[TableDef]) -> String { + schema + .iter() + .map(|t| render_entity_with_schema(t, schema)) + .collect::>() + .join("\n\n") + } + + // ── 1-1. Column type × nullable matrix ────────────────────────────────── + + #[test] + fn test_column_type_matrix() { + let cases: Vec<(&str, ColumnType)> = vec![ + ("small_int", ColumnType::Simple(SimpleColumnType::SmallInt)), + ("integer", ColumnType::Simple(SimpleColumnType::Integer)), + ("big_int", ColumnType::Simple(SimpleColumnType::BigInt)), + ("real", ColumnType::Simple(SimpleColumnType::Real)), + ("double_precision", ColumnType::Simple(SimpleColumnType::DoublePrecision)), + ("text", ColumnType::Simple(SimpleColumnType::Text)), + ("boolean", ColumnType::Simple(SimpleColumnType::Boolean)), + ("date", ColumnType::Simple(SimpleColumnType::Date)), + ("time", ColumnType::Simple(SimpleColumnType::Time)), + ("timestamp", ColumnType::Simple(SimpleColumnType::Timestamp)), + ("timestamptz", ColumnType::Simple(SimpleColumnType::Timestamptz)), + ("interval", ColumnType::Simple(SimpleColumnType::Interval)), + ("bytea", ColumnType::Simple(SimpleColumnType::Bytea)), + ("uuid", ColumnType::Simple(SimpleColumnType::Uuid)), + ("json", ColumnType::Simple(SimpleColumnType::Json)), + ("inet", ColumnType::Simple(SimpleColumnType::Inet)), + ("cidr", ColumnType::Simple(SimpleColumnType::Cidr)), + ("macaddr", ColumnType::Simple(SimpleColumnType::Macaddr)), + ("xml", ColumnType::Simple(SimpleColumnType::Xml)), + ("varchar_255", ColumnType::Complex(ComplexColumnType::Varchar { length: 255 })), + ("char_10", ColumnType::Complex(ComplexColumnType::Char { length: 10 })), + ("numeric_10_2", ColumnType::Complex(ComplexColumnType::Numeric { precision: 10, scale: 2 })), + ("custom_citext", ColumnType::Complex(ComplexColumnType::Custom { custom_type: "citext".to_string() })), + ("enum_string", ColumnType::Complex(ComplexColumnType::Enum { + name: "my_status".to_string(), + values: EnumValues::String(vec!["active".to_string(), "inactive".to_string()]), + })), + ("enum_integer", ColumnType::Complex(ComplexColumnType::Enum { + name: "my_level".to_string(), + values: EnumValues::Integer(vec![ + NumValue { name: "Low".to_string(), value: 0 }, + NumValue { name: "High".to_string(), value: 1 }, + ]), + })), + ]; + + let mut output = String::new(); + for (label, ty) in cases { + for (nullable, suffix) in [(false, "not_null"), (true, "null")] { + let val_col = if nullable { col_null("val", ty.clone()) } else { col("val", ty.clone()) }; + let t = table( + "items", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), val_col], + vec![pk(&["id"], false)], + ); + output.push_str(&format!("--- {}_{} ---\n{}\n\n", label, suffix, render_entity(&t))); + } + } + assert_snapshot!(output); + } + + // ── 1-2. PK variants ──────────────────────────────────────────────────── + + #[test] + fn test_pk_single_autoincrement() { + let t = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text))], + vec![pk(&["id"], true)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_pk_single_no_autoincrement() { + let t = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Uuid)), col("name", ColumnType::Simple(SimpleColumnType::Text))], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_pk_composite() { + let t = table( + "user_roles", + None, + vec![ + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("role_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(&["user_id", "role_id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_pk_none() { + let t = table( + "logs", + None, + vec![ + col("message", ColumnType::Simple(SimpleColumnType::Text)), + col("created_at", ColumnType::Simple(SimpleColumnType::Timestamptz)), + ], + vec![], + ); + assert_snapshot!(render_entity(&t)); + } + + // ── 1-3. Unique constraints ────────────────────────────────────────────── + + #[test] + fn test_unique_single_named() { + let t = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("email", ColumnType::Simple(SimpleColumnType::Text))], + vec![pk(&["id"], true), uniq(Some("uq_users__email"), &["email"])], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_unique_single_unnamed() { + let t = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("email", ColumnType::Simple(SimpleColumnType::Text))], + vec![pk(&["id"], true), uniq(None, &["email"])], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_unique_composite_named() { + let t = table( + "memberships", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("org_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(&["id"], true), uniq(Some("uq_memberships__user_id_org_id"), &["user_id", "org_id"])], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_unique_composite_unnamed() { + let t = table( + "memberships", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("org_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(&["id"], true), uniq(None, &["user_id", "org_id"])], + ); + assert_snapshot!(render_entity(&t)); + } + + // ── 1-4. Index ─────────────────────────────────────────────────────────── + + #[test] + fn test_index_single_named() { + let t = table( + "articles", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("created_at", ColumnType::Simple(SimpleColumnType::Timestamptz))], + vec![pk(&["id"], false), idx(Some("ix_articles__created_at"), &["created_at"])], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_index_single_unnamed() { + let t = table( + "articles", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("title", ColumnType::Simple(SimpleColumnType::Text))], + vec![pk(&["id"], false), idx(None, &["title"])], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_index_composite() { + let t = table( + "events", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("created_at", ColumnType::Simple(SimpleColumnType::Timestamptz)), + ], + vec![pk(&["id"], false), idx(Some("ix_events__user_id_created_at"), &["user_id", "created_at"])], + ); + assert_snapshot!(render_entity(&t)); + } + + // ── 1-5. Default values ────────────────────────────────────────────────── + + #[rstest] + #[case::bool_true("bool_true", ColumnType::Simple(SimpleColumnType::Boolean), DefaultValue::Bool(true))] + #[case::bool_false("bool_false", ColumnType::Simple(SimpleColumnType::Boolean), DefaultValue::Bool(false))] + #[case::now("now", ColumnType::Simple(SimpleColumnType::Timestamptz), DefaultValue::String("now()".into()))] + #[case::current_timestamp("current_timestamp", ColumnType::Simple(SimpleColumnType::Timestamptz), DefaultValue::String("CURRENT_TIMESTAMP".into()))] + #[case::gen_random_uuid("gen_random_uuid", ColumnType::Simple(SimpleColumnType::Uuid), DefaultValue::String("gen_random_uuid()".into()))] + #[case::uuid_generate_v4("uuid_generate_v4", ColumnType::Simple(SimpleColumnType::Uuid), DefaultValue::String("uuid_generate_v4()".into()))] + #[case::arbitrary_fn("arbitrary_fn", ColumnType::Simple(SimpleColumnType::Integer), DefaultValue::String("nextval('my_seq')".into()))] + #[case::string_literal("string_literal", ColumnType::Simple(SimpleColumnType::Text), DefaultValue::String("'foo'".into()))] + #[case::integer_literal("integer_literal", ColumnType::Simple(SimpleColumnType::Integer), DefaultValue::Integer(42))] + #[case::fallback_keyword("fallback_keyword", ColumnType::Simple(SimpleColumnType::Date), DefaultValue::String("CURRENT_DATE".into()))] + fn test_default_attr_variants( + #[case] label: &str, + #[case] ty: ColumnType, + #[case] default: DefaultValue, + ) { + let t = table( + "items", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col_with_default("val", ty, default)], + vec![pk(&["id"], false)], + ); + assert_snapshot!(label, render_entity(&t)); + } + + // ── 1-6. Enum ──────────────────────────────────────────────────────────── + + #[test] + fn test_enum_string_screaming_snake_match() { + let ty = ColumnType::Complex(ComplexColumnType::Enum { + name: "status".to_string(), + values: EnumValues::String(vec!["ACTIVE".to_string(), "INACTIVE".to_string()]), + }); + let t = table( + "accounts", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("status", ty)], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_enum_string_mapped() { + let ty = ColumnType::Complex(ComplexColumnType::Enum { + name: "order_status".to_string(), + values: EnumValues::String(vec!["pending".to_string(), "shipped".to_string(), "delivered".to_string()]), + }); + let t = table( + "orders", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("status", ty)], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_enum_integer() { + let ty = ColumnType::Complex(ComplexColumnType::Enum { + name: "priority_level".to_string(), + values: EnumValues::Integer(vec![ + NumValue { name: "Low".to_string(), value: 0 }, + NumValue { name: "Medium".to_string(), value: 1 }, + NumValue { name: "High".to_string(), value: 2 }, + ]), + }); + let t = table( + "tasks", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("priority", ty)], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_enum_nullable() { + let ty = ColumnType::Complex(ComplexColumnType::Enum { + name: "order_status".to_string(), + values: EnumValues::String(vec!["pending".to_string(), "shipped".to_string()]), + }); + let t = table( + "orders", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col_null("status", ty)], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_enum_default_value() { + let ty = ColumnType::Complex(ComplexColumnType::Enum { + name: "order_status".to_string(), + values: EnumValues::String(vec!["pending".to_string(), "shipped".to_string()]), + }); + let t = table( + "orders", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default("status", ty, DefaultValue::String("'pending'".into())), + ], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_enum_dedup_multiple_columns() { + let enum_ty = || ColumnType::Complex(ComplexColumnType::Enum { + name: "role_type".to_string(), + values: EnumValues::String(vec!["admin".to_string(), "member".to_string()]), + }); + let t = table( + "org_members", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("role", enum_ty()), + col("backup_role", enum_ty()), + ], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + // ── 1-7. Description / Comment ─────────────────────────────────────────── + + #[test] + fn test_description_present() { + let t = table( + "users", + Some("User accounts table"), + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_description_none() { + let t = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_description_multiline() { + let t = table( + "users", + Some("First line\nSecond line\nThird line"), + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_column_comment_present() { + let t = table( + "users", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), "User's email address"), + ], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_column_comment_multiline() { + // Production code replaces '\n' with ' ' in column comments — snapshot locks in that behavior + let t = table( + "users", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), "line1\nline2"), + ], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + // ── 1-8. @@map ─────────────────────────────────────────────────────────── + + #[test] + fn test_always_emits_map() { + let t = table( + "user_profiles", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + // ── Layer 2 — Relations ────────────────────────────────────────────────── + + #[test] + fn test_has_many_basic() { + let users = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], true)], + ); + let posts = table( + "posts", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("title", ColumnType::Simple(SimpleColumnType::Text)), + ], + vec![pk(&["id"], true), fk(&["user_id"], "users", &["id"], None, None)], + ); + let schema = vec![users, posts]; + assert_snapshot!(render_schema_all(&schema)); + } + + #[test] + fn test_has_one_unique_fk() { + let users = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], true)], + ); + let profiles = table( + "profiles", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![ + pk(&["id"], true), + uniq(None, &["user_id"]), + fk(&["user_id"], "users", &["id"], None, None), + ], + ); + let schema = vec![users, profiles]; + assert_snapshot!(render_schema_all(&schema)); + } + + #[test] + fn test_nullable_fk() { + let users = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], true)], + ); + let posts = table( + "posts", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_null("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(&["id"], true), fk(&["user_id"], "users", &["id"], None, None)], + ); + let schema = vec![users, posts]; + assert_snapshot!(render_schema_all(&schema)); + } + + #[test] + fn test_multiple_fk_same_table() { + let users = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], true)], + ); + let posts = table( + "posts", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("author_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("reviewer_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![ + pk(&["id"], true), + fk(&["author_id"], "users", &["id"], None, None), + fk(&["reviewer_id"], "users", &["id"], None, None), + ], + ); + let schema = vec![users, posts]; + assert_snapshot!(render_schema_all(&schema)); + } + + #[test] + fn test_self_reference_fk() { + let categories = table( + "categories", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_null("parent_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("name", ColumnType::Simple(SimpleColumnType::Text)), + ], + vec![pk(&["id"], true), fk(&["parent_id"], "categories", &["id"], None, None)], + ); + let schema = vec![categories]; + assert_snapshot!(render_schema_all(&schema)); + } + + #[rstest] + #[case::cascade("cascade", ReferenceAction::Cascade, None)] + #[case::restrict("restrict", ReferenceAction::Restrict, None)] + #[case::set_null("set_null", ReferenceAction::SetNull, None)] + #[case::set_default("set_default", ReferenceAction::SetDefault, None)] + #[case::no_action("no_action", ReferenceAction::NoAction, None)] + #[case::both("both", ReferenceAction::Cascade, Some(ReferenceAction::Restrict))] + fn test_reference_actions( + #[case] label: &str, + #[case] on_delete: ReferenceAction, + #[case] on_update: Option, + ) { + let users = table( + "users", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], true)], + ); + let posts = table( + "posts", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(&["id"], true), fk(&["user_id"], "users", &["id"], Some(on_delete), on_update)], + ); + let schema = vec![users, posts]; + assert_snapshot!(label, render_schema_all(&schema)); + } + + #[test] + fn test_composite_fk_ignored() { + let orgs = table( + "orgs", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(&["id", "user_id"], false)], + ); + let memberships = table( + "memberships", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("org_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("member_user_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![ + pk(&["id"], false), + fk(&["org_id", "member_user_id"], "orgs", &["id", "user_id"], None, None), + ], + ); + let schema = vec![orgs, memberships]; + assert_snapshot!(render_schema_all(&schema)); + } + + #[rstest] + #[case::posts("posts")] + #[case::categories("categories")] + #[case::boxes("boxes")] + #[case::users("users")] + fn test_back_relations(#[case] child_table: &str) { + let parent = table( + "refs", + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], true)], + ); + let child = table( + child_table, + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("ref_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(&["id"], true), fk(&["ref_id"], "refs", &["id"], None, None)], + ); + let schema = vec![parent, child]; + assert_snapshot!(child_table, render_schema_all(&schema)); + } + + // ── Layer 3 — Edge cases ───────────────────────────────────────────────── + + #[test] + fn test_reserved_identifier_table_name() { + let reserved = ["select", "order", "model", "enum", "type", "group"]; + let mut output = String::new(); + for name in reserved { + let t = table( + name, + None, + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], false)], + ); + output.push_str(&format!("--- {} ---\n{}\n\n", name, render_entity(&t))); + } + assert_snapshot!(output); + } + + #[test] + fn test_reserved_identifier_column_name() { + let reserved_cols = ["default", "unique", "relation", "map", "index"]; + let mut cols = vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))]; + cols.extend(reserved_cols.iter().map(|name| col(name, ColumnType::Simple(SimpleColumnType::Text)))); + let t = table("things", None, cols, vec![pk(&["id"], false)]); + assert_snapshot!(render_entity(&t)); + } + + #[test] + fn test_description_special_chars() { + let cases: &[(&str, &str)] = &[ + ("newline", "line1\nline2"), + ("double_quote", "has \"quotes\""), + ("closing_brace", "has } brace"), + ]; + let mut output = String::new(); + for (label, desc) in cases { + let t = table( + "things", + Some(desc), + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![pk(&["id"], false)], + ); + output.push_str(&format!("--- {} ---\n{}\n\n", label, render_entity(&t))); + } + assert_snapshot!(output); + } + + #[test] + fn test_default_value_with_quote() { + let t = table( + "items", + None, + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default("name", ColumnType::Simple(SimpleColumnType::Text), DefaultValue::String("'val with \\\" quote'".into())), + ], + vec![pk(&["id"], false)], + ); + assert_snapshot!(render_entity(&t)); + } + + // ── Utility unit tests (preserved) ────────────────────────────────────── + + #[rstest] + #[case("hello_world", "HelloWorld")] + #[case("user_id", "UserId")] + #[case("simple", "Simple")] + fn test_to_pascal_case(#[case] input: &str, #[case] expected: &str) { + assert_eq!(to_pascal_case(input), expected); + } + + #[rstest] + #[case("pending", "PENDING")] + #[case("shipped", "SHIPPED")] + #[case("order_status", "ORDER_STATUS")] + fn test_to_screaming_snake(#[case] input: &str, #[case] expected: &str) { + assert_eq!(to_screaming_snake(input), expected); + } + + fn pluralize(s: &str) -> String { + if s.ends_with('s') || s.ends_with('x') || s.ends_with("ch") || s.ends_with("sh") { + format!("{}es", s) + } else if s.ends_with('y') + && !matches!(s.chars().rev().nth(1), Some('a' | 'e' | 'o' | 'u')) + { + format!("{}ies", &s[..s.len() - 1]) + } else { + format!("{}s", s) + } + } + + #[rstest] + #[case("post", "posts")] + #[case("user", "users")] + #[case("article", "articles")] + #[case("box", "boxes")] + #[case("category", "categories")] + fn test_pluralize(#[case] input: &str, #[case] expected: &str) { + assert_eq!(pluralize(input), expected); + } +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__always_emits_map.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__always_emits_map.snap new file mode 100644 index 00000000..c51e2087 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__always_emits_map.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model UserProfiles { + id Int @id + + @@map("user_profiles") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__arbitrary_fn.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__arbitrary_fn.snap new file mode 100644 index 00000000..60a0c859 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__arbitrary_fn.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val Int @default(dbgenerated("nextval('my_seq')")) + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__bool_false.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__bool_false.snap new file mode 100644 index 00000000..57e484fa --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__bool_false.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val Boolean @default(false) + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__bool_true.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__bool_true.snap new file mode 100644 index 00000000..9190d341 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__bool_true.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val Boolean @default(true) + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__both.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__both.snap new file mode 100644 index 00000000..d98f14d1 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__both.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + user_id Int + user Users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Restrict) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__boxes.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__boxes.snap new file mode 100644 index 00000000..3352dc8e --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__boxes.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Refs { + id Int @id @default(autoincrement()) + boxes Boxes[] + + @@map("refs") +} + +model Boxes { + id Int @id @default(autoincrement()) + ref_id Int + ref Refs @relation(fields: [ref_id], references: [id]) + + @@map("boxes") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__cascade.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__cascade.snap new file mode 100644 index 00000000..85f0c5b1 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__cascade.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + user_id Int + user Users @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__categories.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__categories.snap new file mode 100644 index 00000000..4550209e --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__categories.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Refs { + id Int @id @default(autoincrement()) + categories Categories[] + + @@map("refs") +} + +model Categories { + id Int @id @default(autoincrement()) + ref_id Int + ref Refs @relation(fields: [ref_id], references: [id]) + + @@map("categories") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_comment_multiline.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_comment_multiline.snap new file mode 100644 index 00000000..ae110329 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_comment_multiline.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Users { + id Int @id + /// line1 line2 + email String @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_comment_present.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_comment_present.snap new file mode 100644 index 00000000..21460d9a --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_comment_present.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Users { + id Int @id + /// User's email address + email String @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_type_matrix.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_type_matrix.snap new file mode 100644 index 00000000..1d34ac1f --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__column_type_matrix.snap @@ -0,0 +1,423 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: output +--- +--- small_int_not_null --- +model Items { + id Int @id + val Int @db.SmallInt + + @@map("items") +} + +--- small_int_null --- +model Items { + id Int @id + val Int? @db.SmallInt + + @@map("items") +} + +--- integer_not_null --- +model Items { + id Int @id + val Int + + @@map("items") +} + +--- integer_null --- +model Items { + id Int @id + val Int? + + @@map("items") +} + +--- big_int_not_null --- +model Items { + id Int @id + val BigInt + + @@map("items") +} + +--- big_int_null --- +model Items { + id Int @id + val BigInt? + + @@map("items") +} + +--- real_not_null --- +model Items { + id Int @id + val Float @db.Real + + @@map("items") +} + +--- real_null --- +model Items { + id Int @id + val Float? @db.Real + + @@map("items") +} + +--- double_precision_not_null --- +model Items { + id Int @id + val Float + + @@map("items") +} + +--- double_precision_null --- +model Items { + id Int @id + val Float? + + @@map("items") +} + +--- text_not_null --- +model Items { + id Int @id + val String @db.Text + + @@map("items") +} + +--- text_null --- +model Items { + id Int @id + val String? @db.Text + + @@map("items") +} + +--- boolean_not_null --- +model Items { + id Int @id + val Boolean + + @@map("items") +} + +--- boolean_null --- +model Items { + id Int @id + val Boolean? + + @@map("items") +} + +--- date_not_null --- +model Items { + id Int @id + val DateTime @db.Date + + @@map("items") +} + +--- date_null --- +model Items { + id Int @id + val DateTime? @db.Date + + @@map("items") +} + +--- time_not_null --- +model Items { + id Int @id + val DateTime @db.Time + + @@map("items") +} + +--- time_null --- +model Items { + id Int @id + val DateTime? @db.Time + + @@map("items") +} + +--- timestamp_not_null --- +model Items { + id Int @id + val DateTime @db.Timestamp + + @@map("items") +} + +--- timestamp_null --- +model Items { + id Int @id + val DateTime? @db.Timestamp + + @@map("items") +} + +--- timestamptz_not_null --- +model Items { + id Int @id + val DateTime @db.Timestamptz + + @@map("items") +} + +--- timestamptz_null --- +model Items { + id Int @id + val DateTime? @db.Timestamptz + + @@map("items") +} + +--- interval_not_null --- +model Items { + id Int @id + val String @db.Interval + + @@map("items") +} + +--- interval_null --- +model Items { + id Int @id + val String? @db.Interval + + @@map("items") +} + +--- bytea_not_null --- +model Items { + id Int @id + val Bytes + + @@map("items") +} + +--- bytea_null --- +model Items { + id Int @id + val Bytes? + + @@map("items") +} + +--- uuid_not_null --- +model Items { + id Int @id + val String @db.Uuid + + @@map("items") +} + +--- uuid_null --- +model Items { + id Int @id + val String? @db.Uuid + + @@map("items") +} + +--- json_not_null --- +model Items { + id Int @id + val Json + + @@map("items") +} + +--- json_null --- +model Items { + id Int @id + val Json? + + @@map("items") +} + +--- inet_not_null --- +model Items { + id Int @id + val String @db.Inet + + @@map("items") +} + +--- inet_null --- +model Items { + id Int @id + val String? @db.Inet + + @@map("items") +} + +--- cidr_not_null --- +model Items { + id Int @id + val String @db.Cidr + + @@map("items") +} + +--- cidr_null --- +model Items { + id Int @id + val String? @db.Cidr + + @@map("items") +} + +--- macaddr_not_null --- +model Items { + id Int @id + val String @db.Macaddr + + @@map("items") +} + +--- macaddr_null --- +model Items { + id Int @id + val String? @db.Macaddr + + @@map("items") +} + +--- xml_not_null --- +model Items { + id Int @id + val String @db.Xml + + @@map("items") +} + +--- xml_null --- +model Items { + id Int @id + val String? @db.Xml + + @@map("items") +} + +--- varchar_255_not_null --- +model Items { + id Int @id + val String @db.VarChar(255) + + @@map("items") +} + +--- varchar_255_null --- +model Items { + id Int @id + val String? @db.VarChar(255) + + @@map("items") +} + +--- char_10_not_null --- +model Items { + id Int @id + val String @db.Char(10) + + @@map("items") +} + +--- char_10_null --- +model Items { + id Int @id + val String? @db.Char(10) + + @@map("items") +} + +--- numeric_10_2_not_null --- +model Items { + id Int @id + val Decimal @db.Decimal(10, 2) + + @@map("items") +} + +--- numeric_10_2_null --- +model Items { + id Int @id + val Decimal? @db.Decimal(10, 2) + + @@map("items") +} + +--- custom_citext_not_null --- +model Items { + id Int @id + val Unsupported("citext") + + @@map("items") +} + +--- custom_citext_null --- +model Items { + id Int @id + val Unsupported("citext")? + + @@map("items") +} + +--- enum_string_not_null --- +enum MyStatus { + ACTIVE @map("active") + INACTIVE @map("inactive") +} + +model Items { + id Int @id + val MyStatus + + @@map("items") +} + +--- enum_string_null --- +enum MyStatus { + ACTIVE @map("active") + INACTIVE @map("inactive") +} + +model Items { + id Int @id + val MyStatus? + + @@map("items") +} + +--- enum_integer_not_null --- +enum MyLevel { + LOW // = 0 + HIGH // = 1 +} + +model Items { + id Int @id + val MyLevel + + @@map("items") +} + +--- enum_integer_null --- +enum MyLevel { + LOW // = 0 + HIGH // = 1 +} + +model Items { + id Int @id + val MyLevel? + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__composite_fk_ignored.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__composite_fk_ignored.snap new file mode 100644 index 00000000..d7591adf --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__composite_fk_ignored.snap @@ -0,0 +1,19 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Orgs { + id Int + user_id Int + + @@id([id, user_id]) + @@map("orgs") +} + +model Memberships { + id Int @id + org_id Int + member_user_id Int + + @@map("memberships") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__current_timestamp.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__current_timestamp.snap new file mode 100644 index 00000000..5ab2f39e --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__current_timestamp.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val DateTime @default(now()) @db.Timestamptz + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__default_value_with_quote.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__default_value_with_quote.snap new file mode 100644 index 00000000..2f539313 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__default_value_with_quote.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + name String @default("val with \\\" quote") @db.Text + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_multiline.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_multiline.snap new file mode 100644 index 00000000..57d2820d --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_multiline.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +/// First line +/// Second line +/// Third line +model Users { + id Int @id + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_none.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_none.snap new file mode 100644 index 00000000..fd132bb8 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_none.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Users { + id Int @id + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_present.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_present.snap new file mode 100644 index 00000000..f660078a --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_present.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +/// User accounts table +model Users { + id Int @id + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_special_chars.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_special_chars.snap new file mode 100644 index 00000000..e2b1feb5 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__description_special_chars.snap @@ -0,0 +1,28 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: output +--- +--- newline --- +/// line1 +/// line2 +model Things { + id Int @id + + @@map("things") +} + +--- double_quote --- +/// has "quotes" +model Things { + id Int @id + + @@map("things") +} + +--- closing_brace --- +/// has } brace +model Things { + id Int @id + + @@map("things") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_dedup_multiple_columns.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_dedup_multiple_columns.snap new file mode 100644 index 00000000..5b93f515 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_dedup_multiple_columns.snap @@ -0,0 +1,16 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +enum RoleType { + ADMIN @map("admin") + MEMBER @map("member") +} + +model OrgMembers { + id Int @id + role RoleType + backup_role RoleType + + @@map("org_members") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_default_value.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_default_value.snap new file mode 100644 index 00000000..f78edb6e --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_default_value.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +enum OrderStatus { + PENDING @map("pending") + SHIPPED @map("shipped") +} + +model Orders { + id Int @id + status OrderStatus @default(PENDING) + + @@map("orders") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_integer.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_integer.snap new file mode 100644 index 00000000..cf406b54 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_integer.snap @@ -0,0 +1,16 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +enum PriorityLevel { + LOW // = 0 + MEDIUM // = 1 + HIGH // = 2 +} + +model Tasks { + id Int @id + priority PriorityLevel + + @@map("tasks") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_nullable.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_nullable.snap new file mode 100644 index 00000000..7c03f33d --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_nullable.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +enum OrderStatus { + PENDING @map("pending") + SHIPPED @map("shipped") +} + +model Orders { + id Int @id + status OrderStatus? + + @@map("orders") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_string_mapped.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_string_mapped.snap new file mode 100644 index 00000000..cbc8a8ce --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_string_mapped.snap @@ -0,0 +1,16 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +enum OrderStatus { + PENDING @map("pending") + SHIPPED @map("shipped") + DELIVERED @map("delivered") +} + +model Orders { + id Int @id + status OrderStatus + + @@map("orders") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_string_screaming_snake_match.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_string_screaming_snake_match.snap new file mode 100644 index 00000000..6f373ae2 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__enum_string_screaming_snake_match.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +enum Status { + ACTIVE + INACTIVE +} + +model Accounts { + id Int @id + status Status + + @@map("accounts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__fallback_keyword.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__fallback_keyword.snap new file mode 100644 index 00000000..90385374 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__fallback_keyword.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val DateTime @default(dbgenerated("CURRENT_DATE")) @db.Date + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__gen_random_uuid.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__gen_random_uuid.snap new file mode 100644 index 00000000..66b5ef94 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__gen_random_uuid.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val String @default(uuid()) @db.Uuid + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__has_many_basic.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__has_many_basic.snap new file mode 100644 index 00000000..e4e177db --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__has_many_basic.snap @@ -0,0 +1,19 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + user_id Int + user Users @relation(fields: [user_id], references: [id]) + title String @db.Text + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__has_one_unique_fk.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__has_one_unique_fk.snap new file mode 100644 index 00000000..fab6fa07 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__has_one_unique_fk.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + profiles Profiles? + + @@map("users") +} + +model Profiles { + id Int @id @default(autoincrement()) + user_id Int @unique + user Users @relation(fields: [user_id], references: [id]) + + @@map("profiles") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_composite.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_composite.snap new file mode 100644 index 00000000..5a344de9 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_composite.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Events { + id Int @id + user_id Int + created_at DateTime @db.Timestamptz + + @@index([user_id, created_at], name: "ix_events__user_id_created_at") + @@map("events") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_single_named.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_single_named.snap new file mode 100644 index 00000000..1339c8cc --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_single_named.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Articles { + id Int @id + created_at DateTime @db.Timestamptz + + @@index([created_at], name: "ix_articles__created_at") + @@map("articles") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_single_unnamed.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_single_unnamed.snap new file mode 100644 index 00000000..cb57ebb9 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__index_single_unnamed.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Articles { + id Int @id + title String @db.Text + + @@index([title]) + @@map("articles") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__integer_literal.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__integer_literal.snap new file mode 100644 index 00000000..fae18a6b --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__integer_literal.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val Int @default(42) + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__multiple_fk_same_table.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__multiple_fk_same_table.snap new file mode 100644 index 00000000..b24a1fa8 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__multiple_fk_same_table.snap @@ -0,0 +1,21 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + author_posts Posts[] @relation("PostsAuthor") + reviewer_posts Posts[] @relation("PostsReviewer") + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + author_id Int + author Users @relation("PostsAuthor", fields: [author_id], references: [id]) + reviewer_id Int + reviewer Users @relation("PostsReviewer", fields: [reviewer_id], references: [id]) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__no_action.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__no_action.snap new file mode 100644 index 00000000..1d5fa761 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__no_action.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + user_id Int + user Users @relation(fields: [user_id], references: [id], onDelete: NoAction) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__now.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__now.snap new file mode 100644 index 00000000..5ab2f39e --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__now.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val DateTime @default(now()) @db.Timestamptz + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__nullable_fk.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__nullable_fk.snap new file mode 100644 index 00000000..f6cf236f --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__nullable_fk.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + user_id Int? + user Users? @relation(fields: [user_id], references: [id]) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_composite.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_composite.snap new file mode 100644 index 00000000..96b48708 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_composite.snap @@ -0,0 +1,11 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model UserRoles { + user_id Int + role_id Int + + @@id([user_id, role_id]) + @@map("user_roles") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_none.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_none.snap new file mode 100644 index 00000000..dcf691c2 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_none.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Logs { + message String @db.Text + created_at DateTime @db.Timestamptz + + @@map("logs") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_single_autoincrement.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_single_autoincrement.snap new file mode 100644 index 00000000..6dbc2f1f --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_single_autoincrement.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Users { + id Int @id @default(autoincrement()) + name String @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_single_no_autoincrement.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_single_no_autoincrement.snap new file mode 100644 index 00000000..4716f576 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__pk_single_no_autoincrement.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Users { + id String @id @db.Uuid + name String @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__posts.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__posts.snap new file mode 100644 index 00000000..bd4b4542 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__posts.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Refs { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("refs") +} + +model Posts { + id Int @id @default(autoincrement()) + ref_id Int + ref Refs @relation(fields: [ref_id], references: [id]) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__reserved_identifier_column_name.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__reserved_identifier_column_name.snap new file mode 100644 index 00000000..02fa5afe --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__reserved_identifier_column_name.snap @@ -0,0 +1,14 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Things { + id Int @id + default String @db.Text + unique String @db.Text + relation String @db.Text + map String @db.Text + index String @db.Text + + @@map("things") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__reserved_identifier_table_name.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__reserved_identifier_table_name.snap new file mode 100644 index 00000000..ba9ef4d1 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__reserved_identifier_table_name.snap @@ -0,0 +1,45 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: output +--- +--- select --- +model Select { + id Int @id + + @@map("select") +} + +--- order --- +model Order { + id Int @id + + @@map("order") +} + +--- model --- +model Model { + id Int @id + + @@map("model") +} + +--- enum --- +model Enum { + id Int @id + + @@map("enum") +} + +--- type --- +model Type { + id Int @id + + @@map("type") +} + +--- group --- +model Group { + id Int @id + + @@map("group") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__restrict.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__restrict.snap new file mode 100644 index 00000000..5971af88 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__restrict.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + user_id Int + user Users @relation(fields: [user_id], references: [id], onDelete: Restrict) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__self_reference_fk.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__self_reference_fk.snap new file mode 100644 index 00000000..03df5e10 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__self_reference_fk.snap @@ -0,0 +1,13 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Categories { + id Int @id @default(autoincrement()) + parent_id Int? + parent Categories? @relation("CategoriesParent", fields: [parent_id], references: [id]) + name String @db.Text + parent_categories Categories[] @relation("CategoriesParent") + + @@map("categories") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__set_default.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__set_default.snap new file mode 100644 index 00000000..0a1d72ec --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__set_default.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + user_id Int + user Users @relation(fields: [user_id], references: [id], onDelete: SetDefault) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__set_null.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__set_null.snap new file mode 100644 index 00000000..614d7733 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__set_null.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Users { + id Int @id @default(autoincrement()) + posts Posts[] + + @@map("users") +} + +model Posts { + id Int @id @default(autoincrement()) + user_id Int + user Users @relation(fields: [user_id], references: [id], onDelete: SetNull) + + @@map("posts") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__string_literal.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__string_literal.snap new file mode 100644 index 00000000..d08234d0 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__string_literal.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val String @default("foo") @db.Text + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_composite_named.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_composite_named.snap new file mode 100644 index 00000000..6f0796c0 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_composite_named.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Memberships { + id Int @id @default(autoincrement()) + user_id Int + org_id Int + + @@unique([user_id, org_id], name: "uq_memberships__user_id_org_id") + @@map("memberships") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_composite_unnamed.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_composite_unnamed.snap new file mode 100644 index 00000000..aa0b8ab5 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_composite_unnamed.snap @@ -0,0 +1,12 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Memberships { + id Int @id @default(autoincrement()) + user_id Int + org_id Int + + @@unique([user_id, org_id]) + @@map("memberships") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_single_named.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_single_named.snap new file mode 100644 index 00000000..bb693ec6 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_single_named.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Users { + id Int @id @default(autoincrement()) + email String @unique(map: "uq_users__email") @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_single_unnamed.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_single_unnamed.snap new file mode 100644 index 00000000..2c160565 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__unique_single_unnamed.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Users { + id Int @id @default(autoincrement()) + email String @unique @db.Text + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__users.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__users.snap new file mode 100644 index 00000000..00a71683 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__users.snap @@ -0,0 +1,18 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_schema_all(&schema) +--- +model Refs { + id Int @id @default(autoincrement()) + users Users[] + + @@map("refs") +} + +model Users { + id Int @id @default(autoincrement()) + ref_id Int + ref Refs @relation(fields: [ref_id], references: [id]) + + @@map("users") +} diff --git a/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__uuid_generate_v4.snap b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__uuid_generate_v4.snap new file mode 100644 index 00000000..66b5ef94 --- /dev/null +++ b/crates/vespertide-exporter/src/prisma/snapshots/vespertide_exporter__prisma__tests__uuid_generate_v4.snap @@ -0,0 +1,10 @@ +--- +source: crates/vespertide-exporter/src/prisma/mod.rs +expression: render_entity(&t) +--- +model Items { + id Int @id + val String @default(uuid()) @db.Uuid + + @@map("items") +} diff --git a/crates/vespertide-exporter/src/snapshots/vespertide_exporter__orm__tests__render_entity_snapshots@prisma.snap b/crates/vespertide-exporter/src/snapshots/vespertide_exporter__orm__tests__render_entity_snapshots@prisma.snap new file mode 100644 index 00000000..f07a3938 --- /dev/null +++ b/crates/vespertide-exporter/src/snapshots/vespertide_exporter__orm__tests__render_entity_snapshots@prisma.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-exporter/src/orm.rs +expression: result.unwrap() +--- +model Test { + id Int @id + + @@map("test") +} diff --git a/crates/vespertide-exporter/src/snapshots/vespertide_exporter__orm__tests__render_entity_with_schema_snapshots@prisma.snap b/crates/vespertide-exporter/src/snapshots/vespertide_exporter__orm__tests__render_entity_with_schema_snapshots@prisma.snap new file mode 100644 index 00000000..f07a3938 --- /dev/null +++ b/crates/vespertide-exporter/src/snapshots/vespertide_exporter__orm__tests__render_entity_with_schema_snapshots@prisma.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-exporter/src/orm.rs +expression: result.unwrap() +--- +model Test { + id Int @id + + @@map("test") +}