Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_AghfQEJrLN_MntBiG3N_E.json
Original file line number Diff line number Diff line change
@@ -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"}
20 changes: 10 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/vespertide-cli/src/commands/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub fn cmd_export(orm: OrmArg, export_dir: Option<PathBuf>) -> Result<()> {
let all_tables: Vec<TableDef> = 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 {
Expand Down
93 changes: 83 additions & 10 deletions crates/vespertide-exporter/src/seaorm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,20 +30,30 @@ 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<String, String> {
Ok(render_entity_with_config(table, &[], self.config))
Ok(render_entity_with_config(
table,
&[],
self.config,
self.prefix,
))
}

pub fn render_entity_with_schema(
&self,
table: &TableDef,
schema: &[TableDef],
) -> Result<String, String> {
Ok(render_entity_with_config(table, schema, self.config))
Ok(render_entity_with_config(
table,
schema,
self.config,
self.prefix,
))
}
}

Expand All @@ -56,14 +67,15 @@ 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.
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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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\")]"));
}
}
115 changes: 74 additions & 41 deletions crates/vespertide-query/src/sql/delete_column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}
}
Expand Down Expand Up @@ -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
);
}
}