diff --git a/.changepacks/changepack_log_AghfQEJrLN_MntBiG3N_E.json b/.changepacks/changepack_log_AghfQEJrLN_MntBiG3N_E.json new file mode 100644 index 0000000..d2211ab --- /dev/null +++ b/.changepacks/changepack_log_AghfQEJrLN_MntBiG3N_E.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch"},"note":"Add prefix when exporting","date":"2026-01-23T19:12:30.745641400Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 40b0bf2..44aafc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2995,7 +2995,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespertide" -version = "0.1.34" +version = "0.1.35" dependencies = [ "vespertide-core", "vespertide-macro", @@ -3003,7 +3003,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.34" +version = "0.1.35" dependencies = [ "anyhow", "assert_cmd", @@ -3028,7 +3028,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.34" +version = "0.1.35" dependencies = [ "clap", "schemars", @@ -3038,7 +3038,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.34" +version = "0.1.35" dependencies = [ "rstest", "schemars", @@ -3050,7 +3050,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.34" +version = "0.1.35" dependencies = [ "insta", "rstest", @@ -3061,7 +3061,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.34" +version = "0.1.35" dependencies = [ "anyhow", "rstest", @@ -3076,7 +3076,7 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.34" +version = "0.1.35" dependencies = [ "proc-macro2", "quote", @@ -3093,11 +3093,11 @@ dependencies = [ [[package]] name = "vespertide-naming" -version = "0.1.34" +version = "0.1.35" [[package]] name = "vespertide-planner" -version = "0.1.34" +version = "0.1.35" dependencies = [ "insta", "rstest", @@ -3108,7 +3108,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.34" +version = "0.1.35" dependencies = [ "insta", "rstest", diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs index 0f5f6be..531cbf5 100644 --- a/crates/vespertide-cli/src/commands/export.rs +++ b/crates/vespertide-cli/src/commands/export.rs @@ -55,7 +55,7 @@ pub fn cmd_export(orm: OrmArg, export_dir: Option) -> Result<()> { let all_tables: Vec = normalized_models.iter().map(|(t, _)| t.clone()).collect(); // Create SeaORM exporter with config if needed - let seaorm_exporter = SeaOrmExporterWithConfig::new(config.seaorm()); + let seaorm_exporter = SeaOrmExporterWithConfig::new(config.seaorm(), config.prefix()); for (table, rel_path) in &normalized_models { let code = match orm_kind { diff --git a/crates/vespertide-exporter/src/seaorm/mod.rs b/crates/vespertide-exporter/src/seaorm/mod.rs index 66a1d3b..08565e4 100644 --- a/crates/vespertide-exporter/src/seaorm/mod.rs +++ b/crates/vespertide-exporter/src/seaorm/mod.rs @@ -12,6 +12,7 @@ pub struct SeaOrmExporter; /// SeaORM exporter with configuration support. pub struct SeaOrmExporterWithConfig<'a> { pub config: &'a SeaOrmConfig, + pub prefix: &'a str, } impl OrmExporter for SeaOrmExporter { @@ -29,12 +30,17 @@ impl OrmExporter for SeaOrmExporter { } impl<'a> SeaOrmExporterWithConfig<'a> { - pub fn new(config: &'a SeaOrmConfig) -> Self { - Self { config } + pub fn new(config: &'a SeaOrmConfig, prefix: &'a str) -> Self { + Self { config, prefix } } pub fn render_entity(&self, table: &TableDef) -> Result { - Ok(render_entity_with_config(table, &[], self.config)) + Ok(render_entity_with_config( + table, + &[], + self.config, + self.prefix, + )) } pub fn render_entity_with_schema( @@ -42,7 +48,12 @@ impl<'a> SeaOrmExporterWithConfig<'a> { table: &TableDef, schema: &[TableDef], ) -> Result { - Ok(render_entity_with_config(table, schema, self.config)) + Ok(render_entity_with_config( + table, + schema, + self.config, + self.prefix, + )) } } @@ -56,7 +67,7 @@ pub fn render_entity(table: &TableDef) -> String { /// Render a single table into SeaORM entity code with schema context for FK chain resolution. pub fn render_entity_with_schema(table: &TableDef, schema: &[TableDef]) -> String { - render_entity_with_config(table, schema, &SeaOrmConfig::default()) + render_entity_with_config(table, schema, &SeaOrmConfig::default(), "") } /// Render a single table into SeaORM entity code with schema context and configuration. @@ -64,6 +75,7 @@ pub fn render_entity_with_config( table: &TableDef, schema: &[TableDef], config: &SeaOrmConfig, + prefix: &str, ) -> String { let primary_keys = primary_key_columns(table); let composite_pk = primary_keys.len() > 1; @@ -116,7 +128,10 @@ pub fn render_entity_with_config( lines.push("#[sea_orm::model]".into()); lines.push(format!("#[derive({})]", model_derives.join(", "))); - lines.push(format!("#[sea_orm(table_name = \"{}\")]", table.name)); + lines.push(format!( + "#[sea_orm(table_name = \"{}{}\")]", + prefix, table.name + )); lines.push("pub struct Model {".into()); for column in &table.columns { @@ -2778,7 +2793,7 @@ mod tests { extra_model_derives: vec!["ModelDerive".to_string()], ..Default::default() }; - let exporter = SeaOrmExporterWithConfig::new(&config); + let exporter = SeaOrmExporterWithConfig::new(&config, ""); let table = TableDef { name: "items".into(), @@ -2810,7 +2825,7 @@ mod tests { extra_model_derives: vec![], ..Default::default() }; - let exporter = SeaOrmExporterWithConfig::new(&config); + let exporter = SeaOrmExporterWithConfig::new(&config, ""); let table = TableDef { name: "orders".into(), @@ -2858,7 +2873,7 @@ mod tests { extra_model_derives: vec!["SchemaDerive".to_string()], ..Default::default() }; - let exporter = SeaOrmExporterWithConfig::new(&config); + let exporter = SeaOrmExporterWithConfig::new(&config, ""); let table = TableDef { name: "users".into(), @@ -2891,7 +2906,7 @@ mod tests { extra_model_derives: vec![], ..Default::default() }; - let exporter = SeaOrmExporterWithConfig::new(&config); + let exporter = SeaOrmExporterWithConfig::new(&config, ""); let table = TableDef { name: "products".into(), @@ -3023,4 +3038,62 @@ mod tests { assert!(rendered.contains("/// Post content body")); assert!(rendered.contains("/// Supports markdown format")); } + + #[test] + fn test_exporter_with_prefix() { + use vespertide_core::schema::primary_key::PrimaryKeySyntax; + + let config = SeaOrmConfig::default(); + let exporter = SeaOrmExporterWithConfig::new(&config, "myapp_"); + + let table = TableDef { + name: "users".into(), + description: None, + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }; + + let result = exporter.render_entity(&table).unwrap(); + // Should have prefixed table name + assert!(result.contains("#[sea_orm(table_name = \"myapp_users\")]")); + } + + #[test] + fn test_exporter_without_prefix() { + use vespertide_core::schema::primary_key::PrimaryKeySyntax; + + let config = SeaOrmConfig::default(); + let exporter = SeaOrmExporterWithConfig::new(&config, ""); + + let table = TableDef { + name: "users".into(), + description: None, + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }; + + let result = exporter.render_entity(&table).unwrap(); + // Should have original table name without prefix + assert!(result.contains("#[sea_orm(table_name = \"users\")]")); + } } diff --git a/crates/vespertide-query/src/sql/delete_column.rs b/crates/vespertide-query/src/sql/delete_column.rs index 7d03ee4..9b3ec7f 100644 --- a/crates/vespertide-query/src/sql/delete_column.rs +++ b/crates/vespertide-query/src/sql/delete_column.rs @@ -27,48 +27,43 @@ pub fn build_delete_column( if *backend == DatabaseBackend::Sqlite && let Some(table_def) = current_schema.iter().find(|t| t.name == table) { - // Check if the column has FK or PK constraints (requires temp table approach) - let has_fk_or_pk = table_def.constraints.iter().any(|c| { - c.columns().iter().any(|col| col == column) - && matches!( - c, - TableConstraint::ForeignKey { .. } | TableConstraint::PrimaryKey { .. } - ) - }); - - if has_fk_or_pk { - // Use temp table approach for FK/PK constraints - return build_delete_column_sqlite_temp_table(table, column, table_def, column_type); - } - - // For Unique/Index constraints, just drop the index first + // Handle constraints referencing the deleted column for constraint in &table_def.constraints { - if constraint.columns().iter().any(|c| c == column) { - match constraint { - TableConstraint::Unique { name, columns } => { - let index_name = vespertide_naming::build_unique_constraint_name( - table, - columns, - name.as_deref(), - ); - let drop_idx = Index::drop() - .name(&index_name) - .table(Alias::new(table)) - .to_owned(); - stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx))); - } - TableConstraint::Index { name, columns } => { - let index_name = - vespertide_naming::build_index_name(table, columns, name.as_deref()); - let drop_idx = Index::drop() - .name(&index_name) - .table(Alias::new(table)) - .to_owned(); - stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx))); - } - // PK/FK constraints trigger temp table approach earlier; Check returns empty columns. - // This arm is defensive for future constraint types. - _ => {} + match constraint { + // Check constraints are expression-based, not column-based - skip + TableConstraint::Check { .. } => continue, + // For column-based constraints, check if they reference the deleted column + _ if !constraint.columns().iter().any(|c| c == column) => continue, + // FK/PK require temp table approach - return immediately + TableConstraint::ForeignKey { .. } | TableConstraint::PrimaryKey { .. } => { + return build_delete_column_sqlite_temp_table( + table, + column, + table_def, + column_type, + ); + } + // Unique/Index: drop the index first, then drop column below + TableConstraint::Unique { name, columns } => { + let index_name = vespertide_naming::build_unique_constraint_name( + table, + columns, + name.as_deref(), + ); + let drop_idx = Index::drop() + .name(&index_name) + .table(Alias::new(table)) + .to_owned(); + stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx))); + } + TableConstraint::Index { name, columns } => { + let index_name = + vespertide_naming::build_index_name(table, columns, name.as_deref()); + let drop_idx = Index::drop() + .name(&index_name) + .table(Alias::new(table)) + .to_owned(); + stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx))); } } } @@ -1032,4 +1027,42 @@ mod tests { pg_last_sql ); } + + #[test] + fn test_delete_column_sqlite_with_check_constraint_skipped() { + // Check constraints are expression-based, not column-based. + // They should be skipped (continue) during constraint iteration. + let schema = vec![TableDef { + name: "orders".into(), + description: None, + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("amount", ColumnType::Simple(SimpleColumnType::Integer)), + ], + constraints: vec![TableConstraint::Check { + name: "check_positive".into(), + expr: "amount > 0".into(), + }], + }]; + + // Delete amount column - Check constraint should be skipped (not trigger temp table) + let result = + build_delete_column(&DatabaseBackend::Sqlite, "orders", "amount", None, &schema); + + // Should have only 1 statement: ALTER TABLE DROP COLUMN + // (Check constraint doesn't require special handling) + assert_eq!( + result.len(), + 1, + "Check constraint should be skipped, got: {} statements", + result.len() + ); + + let sql = result[0].build(DatabaseBackend::Sqlite); + assert!( + sql.contains("DROP COLUMN"), + "Expected DROP COLUMN, got: {}", + sql + ); + } }