diff --git a/.changepacks/changepack_log_xIyk1IGTLmUTH4QC65hmf.json b/.changepacks/changepack_log_xIyk1IGTLmUTH4QC65hmf.json new file mode 100644 index 0000000..1fe6f76 --- /dev/null +++ b/.changepacks/changepack_log_xIyk1IGTLmUTH4QC65hmf.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch"},"note":"Implement fk with pk","date":"2025-12-14T16:47:09.610964300Z"} \ No newline at end of file diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs index c3c8d28..9900492 100644 --- a/crates/vespertide-cli/src/commands/export.rs +++ b/crates/vespertide-cli/src/commands/export.rs @@ -32,6 +32,17 @@ pub fn cmd_export(orm: OrmArg, export_dir: Option) -> Result<()> { let config = load_config()?; let models = load_models_recursive(config.models_dir()).context("load models recursively")?; + // Normalize tables to convert inline constraints (primary_key, foreign_key, etc.) to table-level constraints + let normalized_models: Vec<(TableDef, PathBuf)> = models + .into_iter() + .map(|(table, rel_path)| { + table + .normalize() + .map_err(|e| anyhow::anyhow!("Failed to normalize table '{}': {}", table.name, e)) + .map(|normalized| (normalized, rel_path)) + }) + .collect::, _>>()?; + let target_root = resolve_export_dir(export_dir, &config); if !target_root.exists() { fs::create_dir_all(&target_root) @@ -40,7 +51,7 @@ pub fn cmd_export(orm: OrmArg, export_dir: Option) -> Result<()> { let orm_kind: Orm = orm.into(); - for (table, rel_path) in &models { + for (table, rel_path) in &normalized_models { let code = render_entity(orm_kind, table).map_err(|e| anyhow::anyhow!(e))?; let out_path = build_output_path(&target_root, rel_path, orm_kind); if let Some(parent) = out_path.parent() { diff --git a/crates/vespertide-core/src/schema/reference.rs b/crates/vespertide-core/src/schema/reference.rs index 5dcdda3..c1f51c2 100644 --- a/crates/vespertide-core/src/schema/reference.rs +++ b/crates/vespertide-core/src/schema/reference.rs @@ -2,6 +2,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] pub enum ReferenceAction { Cascade, Restrict, diff --git a/crates/vespertide-exporter/src/seaorm/mod.rs b/crates/vespertide-exporter/src/seaorm/mod.rs index fd27b8f..e95d9b1 100644 --- a/crates/vespertide-exporter/src/seaorm/mod.rs +++ b/crates/vespertide-exporter/src/seaorm/mod.rs @@ -466,6 +466,96 @@ mod tests { constraints: vec![], indexes: vec![], })] + #[case("pk_and_fk_together", { + use vespertide_core::schema::foreign_key::{ForeignKeyDef, ForeignKeySyntax}; + use vespertide_core::schema::reference::ReferenceAction; + let mut table = TableDef { + name: "article_user".into(), + columns: vec![ + ColumnDef { + name: "article_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Uuid), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(vespertide_core::StrOrBoolOrArray::Bool(true)), + foreign_key: Some(ForeignKeySyntax::Object(ForeignKeyDef { + ref_table: "article".into(), + ref_columns: vec!["id".into()], + on_delete: Some(ReferenceAction::Cascade), + on_update: None, + })), + }, + ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Uuid), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(vespertide_core::StrOrBoolOrArray::Bool(true)), + foreign_key: Some(ForeignKeySyntax::Object(ForeignKeyDef { + ref_table: "user".into(), + ref_columns: vec!["id".into()], + on_delete: Some(ReferenceAction::Cascade), + on_update: None, + })), + }, + ColumnDef { + name: "author_order".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: Some("1".into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ColumnDef { + name: "role".into(), + r#type: ColumnType::Complex(vespertide_core::ComplexColumnType::Varchar { length: 20 }), + nullable: false, + default: Some("'contributor'".into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ColumnDef { + name: "is_lead".into(), + r#type: ColumnType::Simple(SimpleColumnType::Boolean), + nullable: false, + default: Some("false".into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ColumnDef { + name: "created_at".into(), + r#type: ColumnType::Simple(SimpleColumnType::Timestamptz), + nullable: false, + default: Some("now()".into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![], + indexes: vec![], + }; + // Normalize to convert inline constraints to table-level + table = table.normalize().unwrap(); + table + })] fn render_entity_snapshots(#[case] name: &str, #[case] table: TableDef) { let rendered = render_entity(&table); with_settings!({ snapshot_suffix => format!("params_{}", name) }, { diff --git a/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_pk_and_fk_together.snap b/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_pk_and_fk_together.snap new file mode 100644 index 0000000..c11c450 --- /dev/null +++ b/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_pk_and_fk_together.snap @@ -0,0 +1,29 @@ +--- +source: crates/vespertide-exporter/src/seaorm/mod.rs +expression: rendered +--- +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "article_user")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub article_id: Uuid, + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: Uuid, + pub author_order: i32, + pub role: String, + pub is_lead: bool, + pub created_at: DateTimeWithTimeZone, + #[sea_orm(belongs_to, from = "article_id", to = "id")] + pub article: HasOne, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: HasOne, +} + + +// Index definitions (SeaORM uses Statement builders externally) +// idx_article_user_article_id on [article_id] unique=false +// idx_article_user_user_id on [user_id] unique=false +impl ActiveModelBehavior for ActiveModel {}