diff --git a/.changepacks/changepack_log_WVIEPiJN_k0LI81B8uE3A.json b/.changepacks/changepack_log_WVIEPiJN_k0LI81B8uE3A.json new file mode 100644 index 0000000..41ddacf --- /dev/null +++ b/.changepacks/changepack_log_WVIEPiJN_k0LI81B8uE3A.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch"},"note":"Fix index, unique naming issue","date":"2025-12-19T06:56:36.275571Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2ca7d93..9af0bc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2949,7 +2949,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespertide" -version = "0.1.13" +version = "0.1.14" dependencies = [ "vespertide-core", "vespertide-macro", @@ -2957,7 +2957,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.13" +version = "0.1.14" dependencies = [ "anyhow", "assert_cmd", @@ -2981,7 +2981,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.13" +version = "0.1.14" dependencies = [ "clap", "serde", @@ -2989,17 +2989,18 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.13" +version = "0.1.14" dependencies = [ "rstest", "schemars", "serde", "thiserror", + "vespertide-naming", ] [[package]] name = "vespertide-exporter" -version = "0.1.13" +version = "0.1.14" dependencies = [ "insta", "rstest", @@ -3009,7 +3010,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.13" +version = "0.1.14" dependencies = [ "anyhow", "rstest", @@ -3024,7 +3025,7 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.13" +version = "0.1.14" dependencies = [ "proc-macro2", "quote", @@ -3039,24 +3040,31 @@ dependencies = [ "vespertide-query", ] +[[package]] +name = "vespertide-naming" +version = "0.1.14" + [[package]] name = "vespertide-planner" -version = "0.1.13" +version = "0.1.14" dependencies = [ + "insta", "rstest", "thiserror", "vespertide-core", + "vespertide-naming", ] [[package]] name = "vespertide-query" -version = "0.1.13" +version = "0.1.14" dependencies = [ "insta", "rstest", "sea-query 0.32.7", "thiserror", "vespertide-core", + "vespertide-naming", "vespertide-planner", ] diff --git a/Cargo.toml b/Cargo.toml index 79a6fa4..a0ba65f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ vespertide-core = { path = "crates/vespertide-core", version = "0.1.14" } vespertide-config = { path = "crates/vespertide-config", version = "0.1.14" } vespertide-loader = { path = "crates/vespertide-loader", version = "0.1.14" } vespertide-macro = { path = "crates/vespertide-macro", version = "0.1.14" } +vespertide-naming = { path = "crates/vespertide-naming", version = "0.1.14" } vespertide-planner = { path = "crates/vespertide-planner", version = "0.1.14" } vespertide-query = { path = "crates/vespertide-query", version = "0.1.14" } vespertide-exporter = { path = "crates/vespertide-exporter", version = "0.1.14" } diff --git a/crates/vespertide-cli/src/commands/diff.rs b/crates/vespertide-cli/src/commands/diff.rs index cb55dc6..c4114d4 100644 --- a/crates/vespertide-cli/src/commands/diff.rs +++ b/crates/vespertide-cli/src/commands/diff.rs @@ -89,24 +89,6 @@ fn format_action(action: &MigrationAction) -> String { column.bright_cyan().bold() ) } - MigrationAction::AddIndex { table, index } => { - format!( - "{} {} {} {}", - "Add index:".bright_green(), - index.name.bright_cyan().bold(), - "on".bright_white(), - table.bright_cyan() - ) - } - MigrationAction::RemoveIndex { table, name } => { - format!( - "{} {} {} {}", - "Remove index:".bright_red(), - name.bright_cyan().bold(), - "from".bright_white(), - table.bright_cyan() - ) - } MigrationAction::RenameTable { from, to } => { format!( "{} {} {} {}", @@ -171,6 +153,13 @@ fn format_constraint_type(constraint: &vespertide_core::TableConstraint) -> Stri vespertide_core::TableConstraint::Check { name, expr } => { format!("{} CHECK ({})", name, expr) } + vespertide_core::TableConstraint::Index { name, columns } => { + if let Some(n) = name { + format!("{} INDEX ({})", n, columns.join(", ")) + } else { + format!("INDEX ({})", columns.join(", ")) + } + } } } @@ -230,7 +219,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; let path = models_dir.join(format!("{name}.json")); fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap(); @@ -284,19 +272,24 @@ mod tests { format!("{} {}.{}", "Modify column type:".bright_yellow(), "users".bright_cyan(), "id".bright_cyan().bold()) )] #[case( - MigrationAction::AddIndex { + MigrationAction::AddConstraint { table: "users".into(), - index: vespertide_core::IndexDef { - name: "idx".into(), + constraint: vespertide_core::TableConstraint::Index { + name: Some("idx".into()), columns: vec!["id".into()], - unique: false, }, }, - format!("{} {} {} {}", "Add index:".bright_green(), "idx".bright_cyan().bold(), "on".bright_white(), "users".bright_cyan()) + format!("{} {} {} {}", "Add constraint:".bright_green(), "idx INDEX (id)".bright_cyan().bold(), "on".bright_white(), "users".bright_cyan()) )] #[case( - MigrationAction::RemoveIndex { table: "users".into(), name: "idx".into() }, - format!("{} {} {} {}", "Remove index:".bright_red(), "idx".bright_cyan().bold(), "from".bright_white(), "users".bright_cyan()) + MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: vespertide_core::TableConstraint::Index { + name: Some("idx".into()), + columns: vec!["id".into()], + }, + }, + format!("{} {} {} {}", "Remove constraint:".bright_red(), "idx INDEX (id)".bright_cyan().bold(), "from".bright_white(), "users".bright_cyan()) )] #[case( MigrationAction::RenameTable { from: "users".into(), to: "accounts".into() }, @@ -433,4 +426,24 @@ mod tests { let result = cmd_diff(); assert!(result.is_ok()); } + + #[test] + fn test_constraint_display_unnamed_index() { + let constraint = TableConstraint::Index { + name: None, + columns: vec!["email".into(), "username".into()], + }; + let display = format_constraint_type(&constraint); + assert_eq!(display, "INDEX (email, username)"); + } + + #[test] + fn test_constraint_display_named_index() { + let constraint = TableConstraint::Index { + name: Some("ix_users_email".into()), + columns: vec!["email".into()], + }; + let display = format_constraint_type(&constraint); + assert_eq!(display, "ix_users_email INDEX (email)"); + } } diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs index d497b50..e167072 100644 --- a/crates/vespertide-cli/src/commands/export.rs +++ b/crates/vespertide-cli/src/commands/export.rs @@ -261,7 +261,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], } } diff --git a/crates/vespertide-cli/src/commands/log.rs b/crates/vespertide-cli/src/commands/log.rs index ca519cf..d119c4b 100644 --- a/crates/vespertide-cli/src/commands/log.rs +++ b/crates/vespertide-cli/src/commands/log.rs @@ -58,36 +58,38 @@ pub fn cmd_log(backend: DatabaseBackend) -> Result<()> { } for (i, pq) in plan_queries.iter().enumerate() { + let queries = match backend { + DatabaseBackend::Postgres => &pq.postgres, + DatabaseBackend::MySql => &pq.mysql, + DatabaseBackend::Sqlite => &pq.sqlite, + }; + + // Build non-empty SQL statements + let sql_statements: Vec = queries + .iter() + .map(|q| q.build(backend).trim().to_string()) + .filter(|sql| !sql.is_empty()) + .collect(); + + // Print action description println!( " {}. {}", (i + 1).to_string().bright_magenta().bold(), - match backend { - DatabaseBackend::Postgres => pq - .postgres - .iter() - .map(|q| q.build(DatabaseBackend::Postgres)) - .collect::>() - .join(";\n") - .trim() - .bright_white(), - DatabaseBackend::MySql => pq - .mysql - .iter() - .map(|q| q.build(DatabaseBackend::MySql)) - .collect::>() - .join(";\n") - .trim() - .bright_white(), - DatabaseBackend::Sqlite => pq - .sqlite - .iter() - .map(|q| q.build(DatabaseBackend::Sqlite)) - .collect::>() - .join(";\n") - .trim() - .bright_white(), - } + pq.action.to_string().bright_cyan() ); + + // Print SQL statements with sub-numbering if multiple + for (j, sql) in sql_statements.iter().enumerate() { + let prefix = if sql_statements.len() > 1 { + format!(" {}-{}.", i + 1, j + 1) + .bright_magenta() + .bold() + .to_string() + } else { + " ".to_string() + }; + println!("{} {}", prefix, sql.bright_white()); + } } println!(); @@ -226,4 +228,54 @@ mod tests { let result = cmd_log(DatabaseBackend::Sqlite); assert!(result.is_ok()); } + + #[test] + #[serial_test::serial] + fn cmd_log_with_multiple_sql_statements() { + use vespertide_core::schema::primary_key::PrimaryKeySyntax; + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; + + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let cfg = VespertideConfig::default(); + write_config(&cfg); + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + + // Create a migration with ModifyColumnType for SQLite, which generates multiple SQL statements + let plan = MigrationPlan { + comment: Some("modify column type".into()), + created_at: Some("2024-01-01T00:00:00Z".into()), + version: 1, + actions: vec![ + MigrationAction::CreateTable { + table: "users".into(), + 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![], + }, + MigrationAction::ModifyColumnType { + table: "users".into(), + column: "id".into(), + new_type: ColumnType::Simple(SimpleColumnType::BigInt), + }, + ], + }; + let path = cfg.migrations_dir().join("0001_modify_column_type.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + // SQLite backend will generate multiple SQL statements for ModifyColumnType (table recreation) + // This exercises line 84 where sql_statements.len() > 1 + let result = cmd_log(DatabaseBackend::Sqlite); + assert!(result.is_ok()); + } } diff --git a/crates/vespertide-cli/src/commands/new.rs b/crates/vespertide-cli/src/commands/new.rs index 86a0b06..b829570 100644 --- a/crates/vespertide-cli/src/commands/new.rs +++ b/crates/vespertide-cli/src/commands/new.rs @@ -32,7 +32,6 @@ pub fn cmd_new(name: String, format: Option) -> Result<()> { name: name.clone(), columns: Vec::new(), constraints: Vec::new(), - indexes: Vec::new(), }; match format { diff --git a/crates/vespertide-cli/src/commands/revision.rs b/crates/vespertide-cli/src/commands/revision.rs index 77a1faf..22e62df 100644 --- a/crates/vespertide-cli/src/commands/revision.rs +++ b/crates/vespertide-cli/src/commands/revision.rs @@ -178,7 +178,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; let path = models_dir.join(format!("{name}.json")); fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap(); diff --git a/crates/vespertide-cli/src/commands/sql.rs b/crates/vespertide-cli/src/commands/sql.rs index 51341e7..5bce752 100644 --- a/crates/vespertide-cli/src/commands/sql.rs +++ b/crates/vespertide-cli/src/commands/sql.rs @@ -163,7 +163,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; let path = models_dir.join(format!("{name}.json")); fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap(); @@ -399,12 +398,11 @@ mod tests { }], constraints: vec![], }, - MigrationAction::AddIndex { + MigrationAction::AddConstraint { table: "users".into(), - index: vespertide_core::IndexDef { - name: "idx_id".into(), + constraint: TableConstraint::Index { + name: Some("idx_id".into()), columns: vec!["id".into()], - unique: false, }, }, ], @@ -463,7 +461,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }]; let result = emit_sql(&plan, DatabaseBackend::Sqlite, ¤t_schema); diff --git a/crates/vespertide-cli/src/commands/status.rs b/crates/vespertide-cli/src/commands/status.rs index 8243ea6..0518600 100644 --- a/crates/vespertide-cli/src/commands/status.rs +++ b/crates/vespertide-cli/src/commands/status.rs @@ -75,13 +75,19 @@ pub fn cmd_status() -> Result<()> { current_models.len().to_string().bright_yellow() ); for model in ¤t_models { + // Count Index constraints + let index_count = model + .constraints + .iter() + .filter(|c| matches!(c, vespertide_core::TableConstraint::Index { .. })) + .count(); println!( " {} {} ({} {}, {} {})", "-".bright_white(), model.name.bright_green(), model.columns.len().to_string().bright_blue(), "columns".bright_white(), - model.indexes.len().to_string().bright_blue(), + index_count.to_string().bright_blue(), "indexes".bright_white() ); } @@ -193,7 +199,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; let path = models_dir.join(format!("{name}.json")); fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap(); diff --git a/crates/vespertide-cli/src/utils.rs b/crates/vespertide-cli/src/utils.rs index 7775bd7..dbaa616 100644 --- a/crates/vespertide-cli/src/utils.rs +++ b/crates/vespertide-cli/src/utils.rs @@ -171,7 +171,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; fs::write("models/users.yaml", serde_yaml::to_string(&table).unwrap()).unwrap(); @@ -207,7 +206,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; let content = serde_json::to_string_pretty(&table).unwrap(); fs::write("models/subdir/subtable.json", content).unwrap(); @@ -301,7 +299,6 @@ mod tests { foreign_key: Some(ForeignKeySyntax::String("invalid_format".into())), }], constraints: vec![], - indexes: vec![], }; fs::write( "models/orders.json", diff --git a/crates/vespertide-core/Cargo.toml b/crates/vespertide-core/Cargo.toml index 65443b0..487865c 100644 --- a/crates/vespertide-core/Cargo.toml +++ b/crates/vespertide-core/Cargo.toml @@ -12,6 +12,7 @@ description = "Data models for tables, columns, constraints, indexes, and migrat serde = { version = "1", features = ["derive"] } schemars = { version = "1.1" } thiserror = "2" +vespertide-naming = { workspace = true } [dev-dependencies] rstest = "0.26" diff --git a/crates/vespertide-core/src/action.rs b/crates/vespertide-core/src/action.rs index 7d362cf..ff21146 100644 --- a/crates/vespertide-core/src/action.rs +++ b/crates/vespertide-core/src/action.rs @@ -1,6 +1,4 @@ -use crate::schema::{ - ColumnDef, ColumnName, ColumnType, IndexDef, IndexName, TableConstraint, TableName, -}; +use crate::schema::{ColumnDef, ColumnName, ColumnType, TableConstraint, TableName}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt; @@ -46,14 +44,6 @@ pub enum MigrationAction { column: ColumnName, new_type: ColumnType, }, - AddIndex { - table: TableName, - index: IndexDef, - }, - RemoveIndex { - table: TableName, - name: IndexName, - }, AddConstraint { table: TableName, constraint: TableConstraint, @@ -92,12 +82,6 @@ impl fmt::Display for MigrationAction { MigrationAction::ModifyColumnType { table, column, .. } => { write!(f, "ModifyColumnType: {}.{}", table, column) } - MigrationAction::AddIndex { table, index } => { - write!(f, "AddIndex: {}.{}", table, index.name) - } - MigrationAction::RemoveIndex { name, .. } => { - write!(f, "RemoveIndex: {}", name) - } MigrationAction::AddConstraint { table, constraint } => { let constraint_name = match constraint { TableConstraint::PrimaryKey { .. } => "PRIMARY KEY", @@ -116,6 +100,12 @@ impl fmt::Display for MigrationAction { TableConstraint::Check { name, .. } => { return write!(f, "AddConstraint: {}.{} (CHECK)", table, name); } + TableConstraint::Index { name, .. } => { + if let Some(n) = name { + return write!(f, "AddConstraint: {}.{} (INDEX)", table, n); + } + "INDEX" + } }; write!(f, "AddConstraint: {}.{}", table, constraint_name) } @@ -137,6 +127,12 @@ impl fmt::Display for MigrationAction { TableConstraint::Check { name, .. } => { return write!(f, "RemoveConstraint: {}.{} (CHECK)", table, name); } + TableConstraint::Index { name, .. } => { + if let Some(n) = name { + return write!(f, "RemoveConstraint: {}.{} (INDEX)", table, n); + } + "INDEX" + } }; write!(f, "RemoveConstraint: {}.{}", table, constraint_name) } @@ -159,7 +155,7 @@ impl fmt::Display for MigrationAction { #[cfg(test)] mod tests { use super::*; - use crate::schema::{IndexDef, ReferenceAction, SimpleColumnType}; + use crate::schema::{ReferenceAction, SimpleColumnType}; use rstest::rstest; fn default_column() -> ColumnDef { @@ -222,23 +218,45 @@ mod tests { }, "ModifyColumnType: users.age" )] - #[case::add_index( - MigrationAction::AddIndex { + #[case::add_constraint_index_with_name( + MigrationAction::AddConstraint { + table: "users".into(), + constraint: TableConstraint::Index { + name: Some("ix_users__email".into()), + columns: vec!["email".into()], + }, + }, + "AddConstraint: users.ix_users__email (INDEX)" + )] + #[case::add_constraint_index_without_name( + MigrationAction::AddConstraint { table: "users".into(), - index: IndexDef { - name: "idx_email".into(), + constraint: TableConstraint::Index { + name: None, columns: vec!["email".into()], - unique: false, }, }, - "AddIndex: users.idx_email" + "AddConstraint: users.INDEX" )] - #[case::remove_index( - MigrationAction::RemoveIndex { + #[case::remove_constraint_index_with_name( + MigrationAction::RemoveConstraint { table: "users".into(), - name: "idx_email".into(), + constraint: TableConstraint::Index { + name: Some("ix_users__email".into()), + columns: vec!["email".into()], + }, + }, + "RemoveConstraint: users.ix_users__email (INDEX)" + )] + #[case::remove_constraint_index_without_name( + MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Index { + name: None, + columns: vec!["email".into()], + }, }, - "RemoveIndex: idx_email" + "RemoveConstraint: users.INDEX" )] #[case::rename_table( MigrationAction::RenameTable { diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index a6d45f5..d3da8cb 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -12,11 +12,17 @@ pub struct ColumnDef { pub name: ColumnName, pub r#type: ColumnType, pub nullable: bool, + #[serde(skip_serializing_if = "Option::is_none")] pub default: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub comment: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub primary_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub unique: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub index: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub foreign_key: Option, } diff --git a/crates/vespertide-core/src/schema/constraint.rs b/crates/vespertide-core/src/schema/constraint.rs index 64b77ee..13cdc43 100644 --- a/crates/vespertide-core/src/schema/constraint.rs +++ b/crates/vespertide-core/src/schema/constraint.rs @@ -15,10 +15,12 @@ pub enum TableConstraint { columns: Vec, }, Unique { + #[serde(skip_serializing_if = "Option::is_none")] name: Option, columns: Vec, }, ForeignKey { + #[serde(skip_serializing_if = "Option::is_none")] name: Option, columns: Vec, ref_table: TableName, @@ -30,4 +32,9 @@ pub enum TableConstraint { name: String, expr: String, }, + Index { + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + columns: Vec, + }, } diff --git a/crates/vespertide-core/src/schema/table.rs b/crates/vespertide-core/src/schema/table.rs index 63ce663..16d7f7a 100644 --- a/crates/vespertide-core/src/schema/table.rs +++ b/crates/vespertide-core/src/schema/table.rs @@ -1,1413 +1,1452 @@ -use schemars::JsonSchema; - -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; - -use crate::schema::{ - StrOrBoolOrArray, column::ColumnDef, constraint::TableConstraint, - foreign_key::ForeignKeySyntax, index::IndexDef, names::TableName, - primary_key::PrimaryKeySyntax, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TableValidationError { - DuplicateIndexColumn { - index_name: String, - column_name: String, - }, - InvalidForeignKeyFormat { - column_name: String, - value: String, - }, -} - -impl std::fmt::Display for TableValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TableValidationError::DuplicateIndexColumn { - index_name, - column_name, - } => { - write!( - f, - "Duplicate index '{}' on column '{}': the same index name cannot be applied to the same column multiple times", - index_name, column_name - ) - } - TableValidationError::InvalidForeignKeyFormat { column_name, value } => { - write!( - f, - "Invalid foreign key format '{}' on column '{}': expected 'table.column' format", - value, column_name - ) - } - } - } -} - -impl std::error::Error for TableValidationError {} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct TableDef { - pub name: TableName, - pub columns: Vec, - pub constraints: Vec, - pub indexes: Vec, -} - -impl TableDef { - /// Normalizes inline column constraints (primary_key, unique, index, foreign_key) - /// into table-level constraints and indexes. - /// Returns a new TableDef with all inline constraints converted to table-level. - /// - /// # Errors - /// - /// Returns an error if the same index name is applied to the same column multiple times. - pub fn normalize(&self) -> Result { - let mut constraints = self.constraints.clone(); - let mut indexes = self.indexes.clone(); - - // Collect columns with inline primary_key and check for auto_increment - let mut pk_columns: Vec = Vec::new(); - let mut pk_auto_increment = false; - - for col in &self.columns { - if let Some(ref pk) = col.primary_key { - match pk { - PrimaryKeySyntax::Bool(true) => { - pk_columns.push(col.name.clone()); - } - PrimaryKeySyntax::Bool(false) => {} - PrimaryKeySyntax::Object(pk_def) => { - pk_columns.push(col.name.clone()); - if pk_def.auto_increment { - pk_auto_increment = true; - } - } - } - } - } - - // Add primary key constraint if any columns have inline pk and no existing pk constraint. - if !pk_columns.is_empty() { - let has_pk_constraint = constraints - .iter() - .any(|c| matches!(c, TableConstraint::PrimaryKey { .. })); - - if !has_pk_constraint { - constraints.push(TableConstraint::PrimaryKey { - auto_increment: pk_auto_increment, - columns: pk_columns, - }); - } - } - - // Process inline unique and index for each column - for col in &self.columns { - // Handle inline unique - if let Some(ref unique_val) = col.unique { - match unique_val { - StrOrBoolOrArray::Str(name) => { - let constraint_name = Some(name.clone()); - - // Check if this unique constraint already exists - let exists = constraints.iter().any(|c| { - if let TableConstraint::Unique { - name: c_name, - columns, - } = c - { - c_name.as_ref() == Some(name) - && columns.len() == 1 - && columns[0] == col.name - } else { - false - } - }); - - if !exists { - constraints.push(TableConstraint::Unique { - name: constraint_name, - columns: vec![col.name.clone()], - }); - } - } - StrOrBoolOrArray::Bool(true) => { - let exists = constraints.iter().any(|c| { - if let TableConstraint::Unique { - name: None, - columns, - } = c - { - columns.len() == 1 && columns[0] == col.name - } else { - false - } - }); - - if !exists { - constraints.push(TableConstraint::Unique { - name: None, - columns: vec![col.name.clone()], - }); - } - } - StrOrBoolOrArray::Bool(false) => continue, - StrOrBoolOrArray::Array(names) => { - // Array format: each element is a constraint name - // This column will be part of all these named constraints - for constraint_name in names { - // Check if constraint with this name already exists - if let Some(existing) = constraints.iter_mut().find(|c| { - if let TableConstraint::Unique { name: Some(n), .. } = c { - n == constraint_name - } else { - false - } - }) { - // Add this column to existing composite constraint - if let TableConstraint::Unique { columns, .. } = existing - && !columns.contains(&col.name) - { - columns.push(col.name.clone()); - } - } else { - // Create new constraint with this column - constraints.push(TableConstraint::Unique { - name: Some(constraint_name.clone()), - columns: vec![col.name.clone()], - }); - } - } - } - } - } - - // Handle inline foreign_key - if let Some(ref fk_syntax) = col.foreign_key { - // Convert ForeignKeySyntax to ForeignKeyDef - let (ref_table, ref_columns, on_delete, on_update) = match fk_syntax { - ForeignKeySyntax::String(s) => { - // Parse "table.column" format - let parts: Vec<&str> = s.split('.').collect(); - if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { - return Err(TableValidationError::InvalidForeignKeyFormat { - column_name: col.name.clone(), - value: s.clone(), - }); - } - (parts[0].to_string(), vec![parts[1].to_string()], None, None) - } - ForeignKeySyntax::Object(fk_def) => ( - fk_def.ref_table.clone(), - fk_def.ref_columns.clone(), - fk_def.on_delete.clone(), - fk_def.on_update.clone(), - ), - }; - - // Check if this foreign key already exists - let exists = constraints.iter().any(|c| { - if let TableConstraint::ForeignKey { columns, .. } = c { - columns.len() == 1 && columns[0] == col.name - } else { - false - } - }); - - if !exists { - constraints.push(TableConstraint::ForeignKey { - name: None, - columns: vec![col.name.clone()], - ref_table, - ref_columns, - on_delete, - on_update, - }); - } - } - } - - // Group columns by index name to create composite indexes - // Use a HashMap to group, but preserve column order by tracking first occurrence - let mut index_groups: HashMap> = HashMap::new(); - let mut index_order: Vec = Vec::new(); // Preserve order of first occurrence - // Track which columns are already in each index from inline definitions to detect duplicates - // Only track inline definitions, not existing table-level indexes (they can be extended) - let mut inline_index_column_tracker: HashMap> = HashMap::new(); - - for col in &self.columns { - if let Some(ref index_val) = col.index { - match index_val { - StrOrBoolOrArray::Str(name) => { - // Named index - group by name - let index_name = name.clone(); - - // Check for duplicate - only check inline definitions, not existing table-level indexes - if let Some(columns) = inline_index_column_tracker.get(name.as_str()) - && columns.contains(col.name.as_str()) - { - return Err(TableValidationError::DuplicateIndexColumn { - index_name: name.clone(), - column_name: col.name.clone(), - }); - } - - if !index_groups.contains_key(&index_name) { - index_order.push(index_name.clone()); - } - - index_groups - .entry(index_name.clone()) - .or_default() - .push(col.name.clone()); - - inline_index_column_tracker - .entry(index_name) - .or_default() - .insert(col.name.clone()); - } - StrOrBoolOrArray::Bool(true) => { - // Auto-generated index name - let index_name = format!("idx_{}_{}", self.name, col.name); - - // Check for duplicate (auto-generated names are unique per column, so this shouldn't happen) - // But we check anyway for consistency - only check inline definitions - if let Some(columns) = inline_index_column_tracker.get(index_name.as_str()) - && columns.contains(col.name.as_str()) - { - return Err(TableValidationError::DuplicateIndexColumn { - index_name: index_name.clone(), - column_name: col.name.clone(), - }); - } - - if !index_groups.contains_key(&index_name) { - index_order.push(index_name.clone()); - } - - index_groups - .entry(index_name.clone()) - .or_default() - .push(col.name.clone()); - - inline_index_column_tracker - .entry(index_name) - .or_default() - .insert(col.name.clone()); - } - StrOrBoolOrArray::Bool(false) => continue, - StrOrBoolOrArray::Array(names) => { - // Array format: each element is an index name - // This column will be part of all these named indexes - // Check for duplicates within the array - let mut seen_in_array = HashSet::new(); - for index_name in names { - // Check for duplicate within the same array - if seen_in_array.contains(index_name.as_str()) { - return Err(TableValidationError::DuplicateIndexColumn { - index_name: index_name.clone(), - column_name: col.name.clone(), - }); - } - seen_in_array.insert(index_name.clone()); - - // Check for duplicate across different inline definitions - // Only check inline definitions, not existing table-level indexes - if let Some(columns) = - inline_index_column_tracker.get(index_name.as_str()) - && columns.contains(col.name.as_str()) - { - return Err(TableValidationError::DuplicateIndexColumn { - index_name: index_name.clone(), - column_name: col.name.clone(), - }); - } - - if !index_groups.contains_key(index_name.as_str()) { - index_order.push(index_name.clone()); - } - - index_groups - .entry(index_name.clone()) - .or_default() - .push(col.name.clone()); - - inline_index_column_tracker - .entry(index_name.clone()) - .or_default() - .insert(col.name.clone()); - } - } - } - } - } - - // Create indexes from grouped columns in order - for index_name in index_order { - let columns = index_groups.get(&index_name).unwrap().clone(); - - // Check if this index already exists (by name only, not by column match) - // Multiple indexes can have the same columns but different names - let exists = indexes.iter().any(|i| i.name == index_name); - - if !exists { - indexes.push(IndexDef { - name: index_name, - columns, - unique: false, - }); - } - } - - Ok(TableDef { - name: self.name.clone(), - columns: self.columns.clone(), - constraints, - indexes, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::schema::column::{ColumnType, SimpleColumnType}; - use crate::schema::foreign_key::{ForeignKeyDef, ForeignKeySyntax}; - use crate::schema::primary_key::PrimaryKeySyntax; - use crate::schema::reference::ReferenceAction; - use crate::schema::str_or_bool::StrOrBoolOrArray; - - fn col(name: &str, ty: ColumnType) -> ColumnDef { - ColumnDef { - name: name.to_string(), - r#type: ty, - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - } - } - - #[test] - fn normalize_inline_primary_key() { - let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); - id_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); - - let table = TableDef { - name: "users".into(), - columns: vec![ - id_col, - col("name", ColumnType::Simple(SimpleColumnType::Text)), - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - assert!(matches!( - &normalized.constraints[0], - TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string()] - )); - } - - #[test] - fn normalize_multiple_inline_primary_keys() { - let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); - id_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); - - let mut tenant_col = col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)); - tenant_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); - - let table = TableDef { - name: "users".into(), - columns: vec![id_col, tenant_col], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - assert!(matches!( - &normalized.constraints[0], - TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string(), "tenant_id".to_string()] - )); - } - - #[test] - fn normalize_does_not_duplicate_existing_pk() { - let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); - id_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); - - let table = TableDef { - name: "users".into(), - columns: vec![id_col], - constraints: vec![TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - } - - #[test] - fn normalize_ignores_primary_key_false() { - let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); - id_col.primary_key = Some(PrimaryKeySyntax::Bool(false)); - - let table = TableDef { - name: "users".into(), - columns: vec![ - id_col, - col("name", ColumnType::Simple(SimpleColumnType::Text)), - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // primary_key: false should be ignored, so no primary key constraint should be added - assert_eq!(normalized.constraints.len(), 0); - } - - #[test] - fn normalize_inline_unique_bool() { - let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); - email_col.unique = Some(StrOrBoolOrArray::Bool(true)); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - email_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - assert!(matches!( - &normalized.constraints[0], - TableConstraint::Unique { name: None, columns } if columns == &["email".to_string()] - )); - } - - #[test] - fn normalize_inline_unique_with_name() { - let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); - email_col.unique = Some(StrOrBoolOrArray::Str("uq_users_email".into())); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - email_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - assert!(matches!( - &normalized.constraints[0], - TableConstraint::Unique { name: Some(n), columns } - if n == "uq_users_email" && columns == &["email".to_string()] - )); - } - - #[test] - fn normalize_inline_index_bool() { - let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text)); - name_col.index = Some(StrOrBoolOrArray::Bool(true)); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - name_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.indexes.len(), 1); - assert_eq!(normalized.indexes[0].name, "idx_users_name"); - assert_eq!(normalized.indexes[0].columns, vec!["name".to_string()]); - assert!(!normalized.indexes[0].unique); - } - - #[test] - fn normalize_inline_index_with_name() { - let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text)); - name_col.index = Some(StrOrBoolOrArray::Str("custom_idx_name".into())); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - name_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.indexes.len(), 1); - assert_eq!(normalized.indexes[0].name, "custom_idx_name"); - } - - #[test] - fn normalize_inline_foreign_key() { - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef { - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: Some(ReferenceAction::Cascade), - on_update: None, - })); - - let table = TableDef { - name: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - user_id_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - assert!(matches!( - &normalized.constraints[0], - TableConstraint::ForeignKey { - name: None, - columns, - ref_table, - ref_columns, - on_delete: Some(ReferenceAction::Cascade), - on_update: None, - } if columns == &["user_id".to_string()] - && ref_table == "users" - && ref_columns == &["id".to_string()] - )); - } - - #[test] - fn normalize_all_inline_constraints() { - let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); - id_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); - - let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); - email_col.unique = Some(StrOrBoolOrArray::Bool(true)); - - let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text)); - name_col.index = Some(StrOrBoolOrArray::Bool(true)); - - let mut user_id_col = col("org_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef { - ref_table: "orgs".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - })); - - let table = TableDef { - name: "users".into(), - columns: vec![id_col, email_col, name_col, user_id_col], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // Should have: PrimaryKey, Unique, ForeignKey - assert_eq!(normalized.constraints.len(), 3); - // Should have: 1 index - assert_eq!(normalized.indexes.len(), 1); - } - - #[test] - fn normalize_composite_index_from_string_name() { - let mut updated_at_col = col( - "updated_at", - ColumnType::Simple(SimpleColumnType::Timestamp), - ); - updated_at_col.index = Some(StrOrBoolOrArray::Str("tuple".into())); - - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.index = Some(StrOrBoolOrArray::Str("tuple".into())); - - let table = TableDef { - name: "post".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - updated_at_col, - user_id_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.indexes.len(), 1); - assert_eq!(normalized.indexes[0].name, "tuple"); - assert_eq!( - normalized.indexes[0].columns, - vec!["updated_at".to_string(), "user_id".to_string()] - ); - assert!(!normalized.indexes[0].unique); - } - - #[test] - fn normalize_multiple_different_indexes() { - let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); - col1.index = Some(StrOrBoolOrArray::Str("idx_a".into())); - - let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text)); - col2.index = Some(StrOrBoolOrArray::Str("idx_a".into())); - - let mut col3 = col("col3", ColumnType::Simple(SimpleColumnType::Text)); - col3.index = Some(StrOrBoolOrArray::Str("idx_b".into())); - - let mut col4 = col("col4", ColumnType::Simple(SimpleColumnType::Text)); - col4.index = Some(StrOrBoolOrArray::Bool(true)); - - let table = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1, - col2, - col3, - col4, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.indexes.len(), 3); - - // Check idx_a composite index - let idx_a = normalized - .indexes - .iter() - .find(|i| i.name == "idx_a") - .unwrap(); - assert_eq!(idx_a.columns, vec!["col1".to_string(), "col2".to_string()]); - - // Check idx_b single column index - let idx_b = normalized - .indexes - .iter() - .find(|i| i.name == "idx_b") - .unwrap(); - assert_eq!(idx_b.columns, vec!["col3".to_string()]); - - // Check auto-generated index for col4 - let idx_col4 = normalized - .indexes - .iter() - .find(|i| i.name == "idx_test_col4") - .unwrap(); - assert_eq!(idx_col4.columns, vec!["col4".to_string()]); - } - - #[test] - fn normalize_false_values_are_ignored() { - let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); - email_col.unique = Some(StrOrBoolOrArray::Bool(false)); - email_col.index = Some(StrOrBoolOrArray::Bool(false)); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - email_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 0); - assert_eq!(normalized.indexes.len(), 0); - } - - #[test] - fn normalize_table_without_primary_key() { - // Test normalize with a table that has no primary key columns - // This should cover lines 67-69, 72-73, and 93 (pk_columns.is_empty() branch) - let table = TableDef { - name: "users".into(), - columns: vec![ - col("name", ColumnType::Simple(SimpleColumnType::Text)), - col("email", ColumnType::Simple(SimpleColumnType::Text)), - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // Should not add any primary key constraint - assert_eq!(normalized.constraints.len(), 0); - assert_eq!(normalized.indexes.len(), 0); - } - - #[test] - fn normalize_multiple_indexes_from_same_array() { - // Multiple columns with same array of index names should create multiple composite indexes - let mut updated_at_col = col( - "updated_at", - ColumnType::Simple(SimpleColumnType::Timestamp), - ); - updated_at_col.index = Some(StrOrBoolOrArray::Array(vec![ - "tuple".into(), - "tuple2".into(), - ])); - - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.index = Some(StrOrBoolOrArray::Array(vec![ - "tuple".into(), - "tuple2".into(), - ])); - - let table = TableDef { - name: "post".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - updated_at_col, - user_id_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // Should have: tuple (composite: updated_at, user_id), tuple2 (composite: updated_at, user_id) - assert_eq!(normalized.indexes.len(), 2); - - let tuple_idx = normalized - .indexes - .iter() - .find(|i| i.name == "tuple") - .unwrap(); - let mut sorted_cols = tuple_idx.columns.clone(); - sorted_cols.sort(); - assert_eq!( - sorted_cols, - vec!["updated_at".to_string(), "user_id".to_string()] - ); - - let tuple2_idx = normalized - .indexes - .iter() - .find(|i| i.name == "tuple2") - .unwrap(); - let mut sorted_cols2 = tuple2_idx.columns.clone(); - sorted_cols2.sort(); - assert_eq!( - sorted_cols2, - vec!["updated_at".to_string(), "user_id".to_string()] - ); - } - - #[test] - fn normalize_inline_unique_with_array_existing_constraint() { - // Test Array format where constraint already exists - should add column to existing - let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); - col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()])); - - let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text)); - col2.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()])); - - let table = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1, - col2, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - let unique_constraint = &normalized.constraints[0]; - assert!(matches!( - unique_constraint, - TableConstraint::Unique { name: Some(n), columns: _ } - if n == "uq_group" - )); - if let TableConstraint::Unique { columns, .. } = unique_constraint { - let mut sorted_cols = columns.clone(); - sorted_cols.sort(); - assert_eq!(sorted_cols, vec!["col1".to_string(), "col2".to_string()]); - } - } - - #[test] - fn normalize_inline_unique_with_array_column_already_in_constraint() { - // Test Array format where column is already in constraint - should not duplicate - let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); - col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()])); - - let table = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1.clone(), - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized1 = table.normalize().unwrap(); - assert_eq!(normalized1.constraints.len(), 1); - - // Add same column again - should not create duplicate - let table2 = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1, - ], - constraints: normalized1.constraints.clone(), - indexes: vec![], - }; - - let normalized2 = table2.normalize().unwrap(); - assert_eq!(normalized2.constraints.len(), 1); - if let TableConstraint::Unique { columns, .. } = &normalized2.constraints[0] { - assert_eq!(columns.len(), 1); - assert_eq!(columns[0], "col1"); - } - } - - #[test] - fn normalize_inline_unique_str_already_exists() { - // Test that existing unique constraint with same name and column is not duplicated - let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); - email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into())); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - email_col, - ], - constraints: vec![TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // Should not duplicate the constraint - let unique_constraints: Vec<_> = normalized - .constraints - .iter() - .filter(|c| matches!(c, TableConstraint::Unique { .. })) - .collect(); - assert_eq!(unique_constraints.len(), 1); - } - - #[test] - fn normalize_inline_unique_bool_already_exists() { - // Test that existing unnamed unique constraint with same column is not duplicated - let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); - email_col.unique = Some(StrOrBoolOrArray::Bool(true)); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - email_col, - ], - constraints: vec![TableConstraint::Unique { - name: None, - columns: vec!["email".into()], - }], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // Should not duplicate the constraint - let unique_constraints: Vec<_> = normalized - .constraints - .iter() - .filter(|c| matches!(c, TableConstraint::Unique { .. })) - .collect(); - assert_eq!(unique_constraints.len(), 1); - } - - #[test] - fn normalize_inline_foreign_key_already_exists() { - // Test that existing foreign key constraint is not duplicated - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef { - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - })); - - let table = TableDef { - name: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - user_id_col, - ], - constraints: vec![TableConstraint::ForeignKey { - name: None, - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // Should not duplicate the foreign key - let fk_constraints: Vec<_> = normalized - .constraints - .iter() - .filter(|c| matches!(c, TableConstraint::ForeignKey { .. })) - .collect(); - assert_eq!(fk_constraints.len(), 1); - } - - #[test] - fn normalize_duplicate_index_same_column_str() { - // Same index name applied to the same column multiple times should error - // This tests inline index duplicate, not table-level index - let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); - col1.index = Some(StrOrBoolOrArray::Str("idx1".into())); - - let table = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1.clone(), - { - // Same column with same index name again - let mut c = col1.clone(); - c.index = Some(StrOrBoolOrArray::Str("idx1".into())); - c - }, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::DuplicateIndexColumn { - index_name, - column_name, - }) = result - { - assert_eq!(index_name, "idx1"); - assert_eq!(column_name, "col1"); - } else { - panic!("Expected DuplicateIndexColumn error"); - } - } - - #[test] - fn normalize_duplicate_index_same_column_array() { - // Same index name in array applied to the same column should error - let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); - col1.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into(), "idx1".into()])); - - let table = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::DuplicateIndexColumn { - index_name, - column_name, - }) = result - { - assert_eq!(index_name, "idx1"); - assert_eq!(column_name, "col1"); - } else { - panic!("Expected DuplicateIndexColumn error"); - } - } - - #[test] - fn normalize_duplicate_index_same_column_multiple_definitions() { - // Same index name applied to the same column in different ways should error - let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); - col1.index = Some(StrOrBoolOrArray::Str("idx1".into())); - - let table = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1.clone(), - { - let mut c = col1.clone(); - c.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into()])); - c - }, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::DuplicateIndexColumn { - index_name, - column_name, - }) = result - { - assert_eq!(index_name, "idx1"); - assert_eq!(column_name, "col1"); - } else { - panic!("Expected DuplicateIndexColumn error"); - } - } - - #[test] - fn test_table_validation_error_display() { - let error = TableValidationError::DuplicateIndexColumn { - index_name: "idx_test".into(), - column_name: "col1".into(), - }; - let error_msg = format!("{}", error); - assert!(error_msg.contains("idx_test")); - assert!(error_msg.contains("col1")); - assert!(error_msg.contains("Duplicate index")); - } - - #[test] - fn normalize_inline_unique_str_with_different_constraint_type() { - // Test that other constraint types don't match in the exists check - let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); - email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into())); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - email_col, - ], - constraints: vec![ - // Add a PrimaryKey constraint (different type) - should not match - TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }, - ], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // Should have: PrimaryKey (existing) + Unique (new) - assert_eq!(normalized.constraints.len(), 2); - } - - #[test] - fn normalize_inline_unique_array_with_different_constraint_type() { - // Test that other constraint types don't match in the exists check for Array case - let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); - col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()])); - - let table = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1, - ], - constraints: vec![ - // Add a PrimaryKey constraint (different type) - should not match - TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }, - ], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - // Should have: PrimaryKey (existing) + Unique (new) - assert_eq!(normalized.constraints.len(), 2); - } - - #[test] - fn normalize_duplicate_index_bool_true_same_column() { - // Test that Bool(true) with duplicate on same column errors - let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); - col1.index = Some(StrOrBoolOrArray::Bool(true)); - - let table = TableDef { - name: "test".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col1.clone(), - { - // Same column with Bool(true) again - let mut c = col1.clone(); - c.index = Some(StrOrBoolOrArray::Bool(true)); - c - }, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::DuplicateIndexColumn { - index_name, - column_name, - }) = result - { - assert!(index_name.contains("idx_test")); - assert!(index_name.contains("col1")); - assert_eq!(column_name, "col1"); - } else { - panic!("Expected DuplicateIndexColumn error"); - } - } - - #[test] - fn normalize_inline_foreign_key_string_syntax() { - // Test ForeignKeySyntax::String with valid "table.column" format - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.id".into())); - - let table = TableDef { - name: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - user_id_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - assert!(matches!( - &normalized.constraints[0], - TableConstraint::ForeignKey { - name: None, - columns, - ref_table, - ref_columns, - on_delete: None, - on_update: None, - } if columns == &["user_id".to_string()] - && ref_table == "users" - && ref_columns == &["id".to_string()] - )); - } - - #[test] - fn normalize_inline_foreign_key_invalid_format_no_dot() { - // Test ForeignKeySyntax::String with invalid format (no dot) - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.foreign_key = Some(ForeignKeySyntax::String("usersid".into())); - - let table = TableDef { - name: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - user_id_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result { - assert_eq!(column_name, "user_id"); - assert_eq!(value, "usersid"); - } else { - panic!("Expected InvalidForeignKeyFormat error"); - } - } - - #[test] - fn normalize_inline_foreign_key_invalid_format_empty_table() { - // Test ForeignKeySyntax::String with empty table part - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.foreign_key = Some(ForeignKeySyntax::String(".id".into())); - - let table = TableDef { - name: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - user_id_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result { - assert_eq!(column_name, "user_id"); - assert_eq!(value, ".id"); - } else { - panic!("Expected InvalidForeignKeyFormat error"); - } - } - - #[test] - fn normalize_inline_foreign_key_invalid_format_empty_column() { - // Test ForeignKeySyntax::String with empty column part - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.".into())); - - let table = TableDef { - name: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - user_id_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result { - assert_eq!(column_name, "user_id"); - assert_eq!(value, "users."); - } else { - panic!("Expected InvalidForeignKeyFormat error"); - } - } - - #[test] - fn normalize_inline_foreign_key_invalid_format_too_many_parts() { - // Test ForeignKeySyntax::String with too many parts - let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); - user_id_col.foreign_key = Some(ForeignKeySyntax::String("schema.users.id".into())); - - let table = TableDef { - name: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - user_id_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result { - assert_eq!(column_name, "user_id"); - assert_eq!(value, "schema.users.id"); - } else { - panic!("Expected InvalidForeignKeyFormat error"); - } - } - - #[test] - fn normalize_inline_primary_key_with_auto_increment() { - use crate::schema::primary_key::PrimaryKeyDef; - - let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); - id_col.primary_key = Some(PrimaryKeySyntax::Object(PrimaryKeyDef { - auto_increment: true, - columns: vec![], // columns is ignored for inline definition - })); - - let table = TableDef { - name: "users".into(), - columns: vec![ - id_col, - col("name", ColumnType::Simple(SimpleColumnType::Text)), - ], - constraints: vec![], - indexes: vec![], - }; - - let normalized = table.normalize().unwrap(); - assert_eq!(normalized.constraints.len(), 1); - assert!(matches!( - &normalized.constraints[0], - TableConstraint::PrimaryKey { auto_increment: true, columns } if columns == &["id".to_string()] - )); - } - - #[test] - fn normalize_duplicate_inline_index_on_same_column() { - // This test triggers the DuplicateIndexColumn error (lines 251-253) - // by having the same column appear twice in the same named index group - use crate::schema::str_or_bool::StrOrBoolOrArray; - - // Create a column that references the same index name twice (via Array) - let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); - email_col.index = Some(StrOrBoolOrArray::Array(vec![ - "idx_email".into(), - "idx_email".into(), // Duplicate reference - ])); - - let table = TableDef { - name: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - email_col, - ], - constraints: vec![], - indexes: vec![], - }; - - let result = table.normalize(); - assert!(result.is_err()); - if let Err(TableValidationError::DuplicateIndexColumn { - index_name, - column_name, - }) = result - { - assert_eq!(index_name, "idx_email"); - assert_eq!(column_name, "email"); - } else { - panic!("Expected DuplicateIndexColumn error, got: {:?}", result); - } - } - - #[test] - fn test_invalid_foreign_key_format_error_display() { - let error = TableValidationError::InvalidForeignKeyFormat { - column_name: "user_id".into(), - value: "invalid".into(), - }; - let error_msg = format!("{}", error); - assert!(error_msg.contains("user_id")); - assert!(error_msg.contains("invalid")); - assert!(error_msg.contains("table.column")); - } -} +use schemars::JsonSchema; + +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; + +use crate::schema::{ + StrOrBoolOrArray, column::ColumnDef, constraint::TableConstraint, + foreign_key::ForeignKeySyntax, names::TableName, primary_key::PrimaryKeySyntax, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TableValidationError { + DuplicateIndexColumn { + index_name: String, + column_name: String, + }, + InvalidForeignKeyFormat { + column_name: String, + value: String, + }, +} + +impl std::fmt::Display for TableValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TableValidationError::DuplicateIndexColumn { + index_name, + column_name, + } => { + write!( + f, + "Duplicate index '{}' on column '{}': the same index name cannot be applied to the same column multiple times", + index_name, column_name + ) + } + TableValidationError::InvalidForeignKeyFormat { column_name, value } => { + write!( + f, + "Invalid foreign key format '{}' on column '{}': expected 'table.column' format", + value, column_name + ) + } + } + } +} + +impl std::error::Error for TableValidationError {} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct TableDef { + pub name: TableName, + pub columns: Vec, + pub constraints: Vec, +} + +impl TableDef { + /// Normalizes inline column constraints (primary_key, unique, index, foreign_key) + /// into table-level constraints. + /// Returns a new TableDef with all inline constraints converted to table-level. + /// + /// # Errors + /// + /// Returns an error if the same index name is applied to the same column multiple times. + pub fn normalize(&self) -> Result { + let mut constraints = self.constraints.clone(); + + // Collect columns with inline primary_key and check for auto_increment + let mut pk_columns: Vec = Vec::new(); + let mut pk_auto_increment = false; + + for col in &self.columns { + if let Some(ref pk) = col.primary_key { + match pk { + PrimaryKeySyntax::Bool(true) => { + pk_columns.push(col.name.clone()); + } + PrimaryKeySyntax::Bool(false) => {} + PrimaryKeySyntax::Object(pk_def) => { + pk_columns.push(col.name.clone()); + if pk_def.auto_increment { + pk_auto_increment = true; + } + } + } + } + } + + // Add primary key constraint if any columns have inline pk and no existing pk constraint. + if !pk_columns.is_empty() { + let has_pk_constraint = constraints + .iter() + .any(|c| matches!(c, TableConstraint::PrimaryKey { .. })); + + if !has_pk_constraint { + constraints.push(TableConstraint::PrimaryKey { + auto_increment: pk_auto_increment, + columns: pk_columns, + }); + } + } + + // Process inline unique and index for each column + for col in &self.columns { + // Handle inline unique + if let Some(ref unique_val) = col.unique { + match unique_val { + StrOrBoolOrArray::Str(name) => { + let constraint_name = Some(name.clone()); + + // Check if this unique constraint already exists + let exists = constraints.iter().any(|c| { + if let TableConstraint::Unique { + name: c_name, + columns, + } = c + { + c_name.as_ref() == Some(name) + && columns.len() == 1 + && columns[0] == col.name + } else { + false + } + }); + + if !exists { + constraints.push(TableConstraint::Unique { + name: constraint_name, + columns: vec![col.name.clone()], + }); + } + } + StrOrBoolOrArray::Bool(true) => { + let exists = constraints.iter().any(|c| { + if let TableConstraint::Unique { + name: None, + columns, + } = c + { + columns.len() == 1 && columns[0] == col.name + } else { + false + } + }); + + if !exists { + constraints.push(TableConstraint::Unique { + name: None, + columns: vec![col.name.clone()], + }); + } + } + StrOrBoolOrArray::Bool(false) => continue, + StrOrBoolOrArray::Array(names) => { + // Array format: each element is a constraint name + // This column will be part of all these named constraints + for constraint_name in names { + // Check if constraint with this name already exists + if let Some(existing) = constraints.iter_mut().find(|c| { + if let TableConstraint::Unique { name: Some(n), .. } = c { + n == constraint_name + } else { + false + } + }) { + // Add this column to existing composite constraint + if let TableConstraint::Unique { columns, .. } = existing + && !columns.contains(&col.name) + { + columns.push(col.name.clone()); + } + } else { + // Create new constraint with this column + constraints.push(TableConstraint::Unique { + name: Some(constraint_name.clone()), + columns: vec![col.name.clone()], + }); + } + } + } + } + } + + // Handle inline foreign_key + if let Some(ref fk_syntax) = col.foreign_key { + // Convert ForeignKeySyntax to ForeignKeyDef + let (ref_table, ref_columns, on_delete, on_update) = match fk_syntax { + ForeignKeySyntax::String(s) => { + // Parse "table.column" format + let parts: Vec<&str> = s.split('.').collect(); + if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { + return Err(TableValidationError::InvalidForeignKeyFormat { + column_name: col.name.clone(), + value: s.clone(), + }); + } + (parts[0].to_string(), vec![parts[1].to_string()], None, None) + } + ForeignKeySyntax::Object(fk_def) => ( + fk_def.ref_table.clone(), + fk_def.ref_columns.clone(), + fk_def.on_delete.clone(), + fk_def.on_update.clone(), + ), + }; + + // Check if this foreign key already exists + let exists = constraints.iter().any(|c| { + if let TableConstraint::ForeignKey { columns, .. } = c { + columns.len() == 1 && columns[0] == col.name + } else { + false + } + }); + + if !exists { + constraints.push(TableConstraint::ForeignKey { + name: None, + columns: vec![col.name.clone()], + ref_table, + ref_columns, + on_delete, + on_update, + }); + } + } + } + + // Group columns by index name to create composite indexes + // Use a HashMap to group, but preserve column order by tracking first occurrence + let mut index_groups: HashMap> = HashMap::new(); + let mut index_order: Vec = Vec::new(); // Preserve order of first occurrence + // Track which columns are already in each index from inline definitions to detect duplicates + // Only track inline definitions, not existing table-level indexes (they can be extended) + let mut inline_index_column_tracker: HashMap> = HashMap::new(); + + for col in &self.columns { + if let Some(ref index_val) = col.index { + match index_val { + StrOrBoolOrArray::Str(name) => { + // Named index - group by name + let index_name = name.clone(); + + // Check for duplicate - only check inline definitions, not existing table-level indexes + if let Some(columns) = inline_index_column_tracker.get(name.as_str()) + && columns.contains(col.name.as_str()) + { + return Err(TableValidationError::DuplicateIndexColumn { + index_name: name.clone(), + column_name: col.name.clone(), + }); + } + + if !index_groups.contains_key(&index_name) { + index_order.push(index_name.clone()); + } + + index_groups + .entry(index_name.clone()) + .or_default() + .push(col.name.clone()); + + inline_index_column_tracker + .entry(index_name) + .or_default() + .insert(col.name.clone()); + } + StrOrBoolOrArray::Bool(true) => { + // Use special marker for auto-generated indexes (without custom name) + // We use the column name as a unique key to group, but will use None for the constraint name + // This allows SQL generation to auto-generate the name based on naming conventions + let group_key = format!("__auto_{}", col.name); + + // Check for duplicate - only check inline definitions + if let Some(columns) = inline_index_column_tracker.get(group_key.as_str()) + && columns.contains(col.name.as_str()) + { + return Err(TableValidationError::DuplicateIndexColumn { + index_name: group_key.clone(), + column_name: col.name.clone(), + }); + } + + if !index_groups.contains_key(&group_key) { + index_order.push(group_key.clone()); + } + + index_groups + .entry(group_key.clone()) + .or_default() + .push(col.name.clone()); + + inline_index_column_tracker + .entry(group_key) + .or_default() + .insert(col.name.clone()); + } + StrOrBoolOrArray::Bool(false) => continue, + StrOrBoolOrArray::Array(names) => { + // Array format: each element is an index name + // This column will be part of all these named indexes + // Check for duplicates within the array + let mut seen_in_array = HashSet::new(); + for index_name in names { + // Check for duplicate within the same array + if seen_in_array.contains(index_name.as_str()) { + return Err(TableValidationError::DuplicateIndexColumn { + index_name: index_name.clone(), + column_name: col.name.clone(), + }); + } + seen_in_array.insert(index_name.clone()); + + // Check for duplicate across different inline definitions + // Only check inline definitions, not existing table-level indexes + if let Some(columns) = + inline_index_column_tracker.get(index_name.as_str()) + && columns.contains(col.name.as_str()) + { + return Err(TableValidationError::DuplicateIndexColumn { + index_name: index_name.clone(), + column_name: col.name.clone(), + }); + } + + if !index_groups.contains_key(index_name.as_str()) { + index_order.push(index_name.clone()); + } + + index_groups + .entry(index_name.clone()) + .or_default() + .push(col.name.clone()); + + inline_index_column_tracker + .entry(index_name.clone()) + .or_default() + .insert(col.name.clone()); + } + } + } + } + } + + // Create indexes from grouped columns in order + for index_name in index_order { + let columns = index_groups.get(&index_name).unwrap().clone(); + + // Determine if this is an auto-generated index (from index: true) + // or a named index (from index: "name") + let constraint_name = if index_name.starts_with("__auto_") { + // Auto-generated index - use None so SQL generation can create the name + None + } else { + // Named index - preserve the custom name + Some(index_name.clone()) + }; + + // Check if this index already exists + let exists = constraints.iter().any(|c| { + if let TableConstraint::Index { + name, + columns: cols, + } = c + { + // Match by name if both have names, otherwise match by columns + match (&constraint_name, name) { + (Some(n1), Some(n2)) => n1 == n2, + (None, None) => cols == &columns, + _ => false, + } + } else { + false + } + }); + + if !exists { + constraints.push(TableConstraint::Index { + name: constraint_name, + columns, + }); + } + } + + Ok(TableDef { + name: self.name.clone(), + columns: self.columns.clone(), + constraints, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::column::{ColumnType, SimpleColumnType}; + use crate::schema::foreign_key::{ForeignKeyDef, ForeignKeySyntax}; + use crate::schema::primary_key::PrimaryKeySyntax; + use crate::schema::reference::ReferenceAction; + use crate::schema::str_or_bool::StrOrBoolOrArray; + + fn col(name: &str, ty: ColumnType) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + #[test] + fn normalize_inline_primary_key() { + let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); + id_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); + + let table = TableDef { + name: "users".into(), + columns: vec![ + id_col, + col("name", ColumnType::Simple(SimpleColumnType::Text)), + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + assert!(matches!( + &normalized.constraints[0], + TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string()] + )); + } + + #[test] + fn normalize_multiple_inline_primary_keys() { + let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); + id_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); + + let mut tenant_col = col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)); + tenant_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); + + let table = TableDef { + name: "users".into(), + columns: vec![id_col, tenant_col], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + assert!(matches!( + &normalized.constraints[0], + TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string(), "tenant_id".to_string()] + )); + } + + #[test] + fn normalize_does_not_duplicate_existing_pk() { + let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); + id_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); + + let table = TableDef { + name: "users".into(), + columns: vec![id_col], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + } + + #[test] + fn normalize_ignores_primary_key_false() { + let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); + id_col.primary_key = Some(PrimaryKeySyntax::Bool(false)); + + let table = TableDef { + name: "users".into(), + columns: vec![ + id_col, + col("name", ColumnType::Simple(SimpleColumnType::Text)), + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + // primary_key: false should be ignored, so no primary key constraint should be added + assert_eq!(normalized.constraints.len(), 0); + } + + #[test] + fn normalize_inline_unique_bool() { + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.unique = Some(StrOrBoolOrArray::Bool(true)); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + email_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + assert!(matches!( + &normalized.constraints[0], + TableConstraint::Unique { name: None, columns } if columns == &["email".to_string()] + )); + } + + #[test] + fn normalize_inline_unique_with_name() { + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.unique = Some(StrOrBoolOrArray::Str("uq_users_email".into())); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + email_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + assert!(matches!( + &normalized.constraints[0], + TableConstraint::Unique { name: Some(n), columns } + if n == "uq_users_email" && columns == &["email".to_string()] + )); + } + + #[test] + fn normalize_inline_index_bool() { + let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text)); + name_col.index = Some(StrOrBoolOrArray::Bool(true)); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + name_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + // Count Index constraints + let indexes: Vec<_> = normalized + .constraints + .iter() + .filter(|c| matches!(c, TableConstraint::Index { .. })) + .collect(); + assert_eq!(indexes.len(), 1); + // Auto-generated indexes (from index: true) should have name: None + // SQL generation will create the actual name based on naming conventions + assert!(matches!( + indexes[0], + TableConstraint::Index { name: None, columns } + if columns == &["name".to_string()] + )); + } + + #[test] + fn normalize_inline_index_with_name() { + let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text)); + name_col.index = Some(StrOrBoolOrArray::Str("custom_idx_name".into())); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + name_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + let indexes: Vec<_> = normalized + .constraints + .iter() + .filter(|c| matches!(c, TableConstraint::Index { .. })) + .collect(); + assert_eq!(indexes.len(), 1); + assert!(matches!( + indexes[0], + TableConstraint::Index { name: Some(n), .. } + if n == "custom_idx_name" + )); + } + + #[test] + fn normalize_inline_foreign_key() { + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef { + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: Some(ReferenceAction::Cascade), + on_update: None, + })); + + let table = TableDef { + name: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + user_id_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + assert!(matches!( + &normalized.constraints[0], + TableConstraint::ForeignKey { + name: None, + columns, + ref_table, + ref_columns, + on_delete: Some(ReferenceAction::Cascade), + on_update: None, + } if columns == &["user_id".to_string()] + && ref_table == "users" + && ref_columns == &["id".to_string()] + )); + } + + #[test] + fn normalize_all_inline_constraints() { + let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); + id_col.primary_key = Some(PrimaryKeySyntax::Bool(true)); + + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.unique = Some(StrOrBoolOrArray::Bool(true)); + + let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text)); + name_col.index = Some(StrOrBoolOrArray::Bool(true)); + + let mut user_id_col = col("org_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef { + ref_table: "orgs".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + })); + + let table = TableDef { + name: "users".into(), + columns: vec![id_col, email_col, name_col, user_id_col], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + // Should have: PrimaryKey, Unique, ForeignKey, Index + // Count non-Index constraints + let non_index_constraints: Vec<_> = normalized + .constraints + .iter() + .filter(|c| !matches!(c, TableConstraint::Index { .. })) + .collect(); + assert_eq!(non_index_constraints.len(), 3); + // Should have: 1 index + let indexes: Vec<_> = normalized + .constraints + .iter() + .filter(|c| matches!(c, TableConstraint::Index { .. })) + .collect(); + assert_eq!(indexes.len(), 1); + } + + #[test] + fn normalize_composite_index_from_string_name() { + let mut updated_at_col = col( + "updated_at", + ColumnType::Simple(SimpleColumnType::Timestamp), + ); + updated_at_col.index = Some(StrOrBoolOrArray::Str("tuple".into())); + + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.index = Some(StrOrBoolOrArray::Str("tuple".into())); + + let table = TableDef { + name: "post".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + updated_at_col, + user_id_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + let indexes: Vec<_> = normalized + .constraints + .iter() + .filter_map(|c| { + if let TableConstraint::Index { name, columns } = c { + Some((name.clone(), columns.clone())) + } else { + None + } + }) + .collect(); + assert_eq!(indexes.len(), 1); + assert_eq!(indexes[0].0, Some("tuple".to_string())); + assert_eq!( + indexes[0].1, + vec!["updated_at".to_string(), "user_id".to_string()] + ); + } + + #[test] + fn normalize_multiple_different_indexes() { + let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); + col1.index = Some(StrOrBoolOrArray::Str("idx_a".into())); + + let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text)); + col2.index = Some(StrOrBoolOrArray::Str("idx_a".into())); + + let mut col3 = col("col3", ColumnType::Simple(SimpleColumnType::Text)); + col3.index = Some(StrOrBoolOrArray::Str("idx_b".into())); + + let mut col4 = col("col4", ColumnType::Simple(SimpleColumnType::Text)); + col4.index = Some(StrOrBoolOrArray::Bool(true)); + + let table = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1, + col2, + col3, + col4, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + let indexes: Vec<_> = normalized + .constraints + .iter() + .filter_map(|c| { + if let TableConstraint::Index { name, columns } = c { + Some((name.clone(), columns.clone())) + } else { + None + } + }) + .collect(); + assert_eq!(indexes.len(), 3); + + // Check idx_a composite index + let idx_a = indexes + .iter() + .find(|(n, _)| n == &Some("idx_a".to_string())) + .unwrap(); + assert_eq!(idx_a.1, vec!["col1".to_string(), "col2".to_string()]); + + // Check idx_b single column index + let idx_b = indexes + .iter() + .find(|(n, _)| n == &Some("idx_b".to_string())) + .unwrap(); + assert_eq!(idx_b.1, vec!["col3".to_string()]); + + // Check auto-generated index for col4 (should have name: None) + let idx_col4 = indexes.iter().find(|(n, _)| n.is_none()).unwrap(); + assert_eq!(idx_col4.1, vec!["col4".to_string()]); + } + + #[test] + fn normalize_false_values_are_ignored() { + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.unique = Some(StrOrBoolOrArray::Bool(false)); + email_col.index = Some(StrOrBoolOrArray::Bool(false)); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + email_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 0); + } + + #[test] + fn normalize_table_without_primary_key() { + // Test normalize with a table that has no primary key columns + // This should cover lines 67-69, 72-73, and 93 (pk_columns.is_empty() branch) + let table = TableDef { + name: "users".into(), + columns: vec![ + col("name", ColumnType::Simple(SimpleColumnType::Text)), + col("email", ColumnType::Simple(SimpleColumnType::Text)), + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + // Should not add any primary key constraint + assert_eq!(normalized.constraints.len(), 0); + } + + #[test] + fn normalize_multiple_indexes_from_same_array() { + // Multiple columns with same array of index names should create multiple composite indexes + let mut updated_at_col = col( + "updated_at", + ColumnType::Simple(SimpleColumnType::Timestamp), + ); + updated_at_col.index = Some(StrOrBoolOrArray::Array(vec![ + "tuple".into(), + "tuple2".into(), + ])); + + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.index = Some(StrOrBoolOrArray::Array(vec![ + "tuple".into(), + "tuple2".into(), + ])); + + let table = TableDef { + name: "post".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + updated_at_col, + user_id_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + // Should have: tuple (composite: updated_at, user_id), tuple2 (composite: updated_at, user_id) + let indexes: Vec<_> = normalized + .constraints + .iter() + .filter_map(|c| { + if let TableConstraint::Index { name, columns } = c { + Some((name.clone(), columns.clone())) + } else { + None + } + }) + .collect(); + assert_eq!(indexes.len(), 2); + + let tuple_idx = indexes + .iter() + .find(|(n, _)| n == &Some("tuple".to_string())) + .unwrap(); + let mut sorted_cols = tuple_idx.1.clone(); + sorted_cols.sort(); + assert_eq!( + sorted_cols, + vec!["updated_at".to_string(), "user_id".to_string()] + ); + + let tuple2_idx = indexes + .iter() + .find(|(n, _)| n == &Some("tuple2".to_string())) + .unwrap(); + let mut sorted_cols2 = tuple2_idx.1.clone(); + sorted_cols2.sort(); + assert_eq!( + sorted_cols2, + vec!["updated_at".to_string(), "user_id".to_string()] + ); + } + + #[test] + fn normalize_inline_unique_with_array_existing_constraint() { + // Test Array format where constraint already exists - should add column to existing + let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); + col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()])); + + let mut col2 = col("col2", ColumnType::Simple(SimpleColumnType::Text)); + col2.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()])); + + let table = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1, + col2, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + let unique_constraint = &normalized.constraints[0]; + assert!(matches!( + unique_constraint, + TableConstraint::Unique { name: Some(n), columns: _ } + if n == "uq_group" + )); + if let TableConstraint::Unique { columns, .. } = unique_constraint { + let mut sorted_cols = columns.clone(); + sorted_cols.sort(); + assert_eq!(sorted_cols, vec!["col1".to_string(), "col2".to_string()]); + } + } + + #[test] + fn normalize_inline_unique_with_array_column_already_in_constraint() { + // Test Array format where column is already in constraint - should not duplicate + let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); + col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()])); + + let table = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1.clone(), + ], + constraints: vec![], + }; + + let normalized1 = table.normalize().unwrap(); + assert_eq!(normalized1.constraints.len(), 1); + + // Add same column again - should not create duplicate + let table2 = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1, + ], + constraints: normalized1.constraints.clone(), + }; + + let normalized2 = table2.normalize().unwrap(); + assert_eq!(normalized2.constraints.len(), 1); + if let TableConstraint::Unique { columns, .. } = &normalized2.constraints[0] { + assert_eq!(columns.len(), 1); + assert_eq!(columns[0], "col1"); + } + } + + #[test] + fn normalize_inline_unique_str_already_exists() { + // Test that existing unique constraint with same name and column is not duplicated + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into())); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + email_col, + ], + constraints: vec![TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }], + }; + + let normalized = table.normalize().unwrap(); + // Should not duplicate the constraint + let unique_constraints: Vec<_> = normalized + .constraints + .iter() + .filter(|c| matches!(c, TableConstraint::Unique { .. })) + .collect(); + assert_eq!(unique_constraints.len(), 1); + } + + #[test] + fn normalize_inline_unique_bool_already_exists() { + // Test that existing unnamed unique constraint with same column is not duplicated + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.unique = Some(StrOrBoolOrArray::Bool(true)); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + email_col, + ], + constraints: vec![TableConstraint::Unique { + name: None, + columns: vec!["email".into()], + }], + }; + + let normalized = table.normalize().unwrap(); + // Should not duplicate the constraint + let unique_constraints: Vec<_> = normalized + .constraints + .iter() + .filter(|c| matches!(c, TableConstraint::Unique { .. })) + .collect(); + assert_eq!(unique_constraints.len(), 1); + } + + #[test] + fn normalize_inline_foreign_key_already_exists() { + // Test that existing foreign key constraint is not duplicated + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef { + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + })); + + let table = TableDef { + name: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + user_id_col, + ], + constraints: vec![TableConstraint::ForeignKey { + name: None, + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }], + }; + + let normalized = table.normalize().unwrap(); + // Should not duplicate the foreign key + let fk_constraints: Vec<_> = normalized + .constraints + .iter() + .filter(|c| matches!(c, TableConstraint::ForeignKey { .. })) + .collect(); + assert_eq!(fk_constraints.len(), 1); + } + + #[test] + fn normalize_duplicate_index_same_column_str() { + // Same index name applied to the same column multiple times should error + // This tests inline index duplicate, not table-level index + let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); + col1.index = Some(StrOrBoolOrArray::Str("idx1".into())); + + let table = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1.clone(), + { + // Same column with same index name again + let mut c = col1.clone(); + c.index = Some(StrOrBoolOrArray::Str("idx1".into())); + c + }, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::DuplicateIndexColumn { + index_name, + column_name, + }) = result + { + assert_eq!(index_name, "idx1"); + assert_eq!(column_name, "col1"); + } else { + panic!("Expected DuplicateIndexColumn error"); + } + } + + #[test] + fn normalize_duplicate_index_same_column_array() { + // Same index name in array applied to the same column should error + let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); + col1.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into(), "idx1".into()])); + + let table = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::DuplicateIndexColumn { + index_name, + column_name, + }) = result + { + assert_eq!(index_name, "idx1"); + assert_eq!(column_name, "col1"); + } else { + panic!("Expected DuplicateIndexColumn error"); + } + } + + #[test] + fn normalize_duplicate_index_same_column_multiple_definitions() { + // Same index name applied to the same column in different ways should error + let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); + col1.index = Some(StrOrBoolOrArray::Str("idx1".into())); + + let table = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1.clone(), + { + let mut c = col1.clone(); + c.index = Some(StrOrBoolOrArray::Array(vec!["idx1".into()])); + c + }, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::DuplicateIndexColumn { + index_name, + column_name, + }) = result + { + assert_eq!(index_name, "idx1"); + assert_eq!(column_name, "col1"); + } else { + panic!("Expected DuplicateIndexColumn error"); + } + } + + #[test] + fn test_table_validation_error_display() { + let error = TableValidationError::DuplicateIndexColumn { + index_name: "idx_test".into(), + column_name: "col1".into(), + }; + let error_msg = format!("{}", error); + assert!(error_msg.contains("idx_test")); + assert!(error_msg.contains("col1")); + assert!(error_msg.contains("Duplicate index")); + } + + #[test] + fn normalize_inline_unique_str_with_different_constraint_type() { + // Test that other constraint types don't match in the exists check + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.unique = Some(StrOrBoolOrArray::Str("uq_email".into())); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + email_col, + ], + constraints: vec![ + // Add a PrimaryKey constraint (different type) - should not match + TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }, + ], + }; + + let normalized = table.normalize().unwrap(); + // Should have: PrimaryKey (existing) + Unique (new) + assert_eq!(normalized.constraints.len(), 2); + } + + #[test] + fn normalize_inline_unique_array_with_different_constraint_type() { + // Test that other constraint types don't match in the exists check for Array case + let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); + col1.unique = Some(StrOrBoolOrArray::Array(vec!["uq_group".into()])); + + let table = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1, + ], + constraints: vec![ + // Add a PrimaryKey constraint (different type) - should not match + TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }, + ], + }; + + let normalized = table.normalize().unwrap(); + // Should have: PrimaryKey (existing) + Unique (new) + assert_eq!(normalized.constraints.len(), 2); + } + + #[test] + fn normalize_duplicate_index_bool_true_same_column() { + // Test that Bool(true) with duplicate on same column errors + let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text)); + col1.index = Some(StrOrBoolOrArray::Bool(true)); + + let table = TableDef { + name: "test".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col1.clone(), + { + // Same column with Bool(true) again + let mut c = col1.clone(); + c.index = Some(StrOrBoolOrArray::Bool(true)); + c + }, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::DuplicateIndexColumn { + index_name, + column_name, + }) = result + { + // The group key for auto-generated indexes is "__auto_{column}" + assert!(index_name.contains("__auto_")); + assert!(index_name.contains("col1")); + assert_eq!(column_name, "col1"); + } else { + panic!("Expected DuplicateIndexColumn error"); + } + } + + #[test] + fn normalize_inline_foreign_key_string_syntax() { + // Test ForeignKeySyntax::String with valid "table.column" format + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.id".into())); + + let table = TableDef { + name: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + user_id_col, + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + assert!(matches!( + &normalized.constraints[0], + TableConstraint::ForeignKey { + name: None, + columns, + ref_table, + ref_columns, + on_delete: None, + on_update: None, + } if columns == &["user_id".to_string()] + && ref_table == "users" + && ref_columns == &["id".to_string()] + )); + } + + #[test] + fn normalize_inline_foreign_key_invalid_format_no_dot() { + // Test ForeignKeySyntax::String with invalid format (no dot) + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.foreign_key = Some(ForeignKeySyntax::String("usersid".into())); + + let table = TableDef { + name: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + user_id_col, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result { + assert_eq!(column_name, "user_id"); + assert_eq!(value, "usersid"); + } else { + panic!("Expected InvalidForeignKeyFormat error"); + } + } + + #[test] + fn normalize_inline_foreign_key_invalid_format_empty_table() { + // Test ForeignKeySyntax::String with empty table part + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.foreign_key = Some(ForeignKeySyntax::String(".id".into())); + + let table = TableDef { + name: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + user_id_col, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result { + assert_eq!(column_name, "user_id"); + assert_eq!(value, ".id"); + } else { + panic!("Expected InvalidForeignKeyFormat error"); + } + } + + #[test] + fn normalize_inline_foreign_key_invalid_format_empty_column() { + // Test ForeignKeySyntax::String with empty column part + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.foreign_key = Some(ForeignKeySyntax::String("users.".into())); + + let table = TableDef { + name: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + user_id_col, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result { + assert_eq!(column_name, "user_id"); + assert_eq!(value, "users."); + } else { + panic!("Expected InvalidForeignKeyFormat error"); + } + } + + #[test] + fn normalize_inline_foreign_key_invalid_format_too_many_parts() { + // Test ForeignKeySyntax::String with too many parts + let mut user_id_col = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + user_id_col.foreign_key = Some(ForeignKeySyntax::String("schema.users.id".into())); + + let table = TableDef { + name: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + user_id_col, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::InvalidForeignKeyFormat { column_name, value }) = result { + assert_eq!(column_name, "user_id"); + assert_eq!(value, "schema.users.id"); + } else { + panic!("Expected InvalidForeignKeyFormat error"); + } + } + + #[test] + fn normalize_inline_primary_key_with_auto_increment() { + use crate::schema::primary_key::PrimaryKeyDef; + + let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer)); + id_col.primary_key = Some(PrimaryKeySyntax::Object(PrimaryKeyDef { + auto_increment: true, + columns: vec![], // columns is ignored for inline definition + })); + + let table = TableDef { + name: "users".into(), + columns: vec![ + id_col, + col("name", ColumnType::Simple(SimpleColumnType::Text)), + ], + constraints: vec![], + }; + + let normalized = table.normalize().unwrap(); + assert_eq!(normalized.constraints.len(), 1); + assert!(matches!( + &normalized.constraints[0], + TableConstraint::PrimaryKey { auto_increment: true, columns } if columns == &["id".to_string()] + )); + } + + #[test] + fn normalize_duplicate_inline_index_on_same_column() { + // This test triggers the DuplicateIndexColumn error (lines 251-253) + // by having the same column appear twice in the same named index group + use crate::schema::str_or_bool::StrOrBoolOrArray; + + // Create a column that references the same index name twice (via Array) + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.index = Some(StrOrBoolOrArray::Array(vec![ + "idx_email".into(), + "idx_email".into(), // Duplicate reference + ])); + + let table = TableDef { + name: "users".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + email_col, + ], + constraints: vec![], + }; + + let result = table.normalize(); + assert!(result.is_err()); + if let Err(TableValidationError::DuplicateIndexColumn { + index_name, + column_name, + }) = result + { + assert_eq!(index_name, "idx_email"); + assert_eq!(column_name, "email"); + } else { + panic!("Expected DuplicateIndexColumn error, got: {:?}", result); + } + } + + #[test] + fn test_invalid_foreign_key_format_error_display() { + let error = TableValidationError::InvalidForeignKeyFormat { + column_name: "user_id".into(), + value: "invalid".into(), + }; + let error_msg = format!("{}", error); + assert!(error_msg.contains("user_id")); + assert!(error_msg.contains("invalid")); + assert!(error_msg.contains("table.column")); + } +} diff --git a/crates/vespertide-exporter/src/orm.rs b/crates/vespertide-exporter/src/orm.rs index 08443d2..f753b63 100644 --- a/crates/vespertide-exporter/src/orm.rs +++ b/crates/vespertide-exporter/src/orm.rs @@ -72,7 +72,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], } } diff --git a/crates/vespertide-exporter/src/seaorm/mod.rs b/crates/vespertide-exporter/src/seaorm/mod.rs index 27d7c37..4fd5eb1 100644 --- a/crates/vespertide-exporter/src/seaorm/mod.rs +++ b/crates/vespertide-exporter/src/seaorm/mod.rs @@ -2,8 +2,7 @@ use std::collections::HashSet; use crate::orm::OrmExporter; use vespertide_core::{ - ColumnDef, ColumnType, ComplexColumnType, EnumValues, IndexDef, NumValue, TableConstraint, - TableDef, + ColumnDef, ColumnType, ComplexColumnType, EnumValues, NumValue, TableConstraint, TableDef, }; pub struct SeaOrmExporter; @@ -34,12 +33,11 @@ pub fn render_entity(table: &TableDef) -> String { pub fn render_entity_with_schema(table: &TableDef, schema: &[TableDef]) -> String { let primary_keys = primary_key_columns(table); let composite_pk = primary_keys.len() > 1; - let indexes = &table.indexes; let relation_fields = relation_field_defs_with_schema(table, schema); // Build sets of columns with single-column unique constraints and indexes let unique_columns = single_column_unique_set(&table.constraints); - let indexed_columns = single_column_index_set(indexes); + let indexed_columns = single_column_index_set(&table.constraints); let mut lines: Vec = Vec::new(); lines.push("use sea_orm::entity::prelude::*;".into()); @@ -83,7 +81,7 @@ pub fn render_entity_with_schema(table: &TableDef, schema: &[TableDef]) -> Strin // Indexes (relations expressed as belongs_to fields above) lines.push(String::new()); - render_indexes(&mut lines, indexes); + render_indexes(&mut lines, &table.constraints); lines.push("impl ActiveModelBehavior for ActiveModel {}".into()); @@ -105,12 +103,14 @@ fn single_column_unique_set(constraints: &[TableConstraint]) -> HashSet unique_cols } -/// Build a set of column names that have single-column indexes. -fn single_column_index_set(indexes: &[IndexDef]) -> HashSet { +/// Build a set of column names that have single-column indexes from constraints. +fn single_column_index_set(constraints: &[TableConstraint]) -> HashSet { let mut indexed_cols = HashSet::new(); - for index in indexes { - if index.columns.len() == 1 { - indexed_cols.insert(index.columns[0].clone()); + for constraint in constraints { + if let TableConstraint::Index { columns, .. } = constraint + && columns.len() == 1 + { + indexed_cols.insert(columns[0].clone()); } } indexed_cols @@ -549,18 +549,27 @@ fn fk_attr_value(cols: &[String]) -> String { } } -fn render_indexes(lines: &mut Vec, indexes: &[IndexDef]) { - if indexes.is_empty() { +fn render_indexes(lines: &mut Vec, constraints: &[TableConstraint]) { + let index_constraints: Vec<_> = constraints + .iter() + .filter_map(|c| { + if let TableConstraint::Index { name, columns } = c { + Some((name, columns)) + } else { + None + } + }) + .collect(); + + if index_constraints.is_empty() { return; } lines.push(String::new()); lines.push("// Index definitions (SeaORM uses Statement builders externally)".into()); - for idx in indexes { - let cols = idx.columns.join(", "); - lines.push(format!( - "// {} on [{}] unique={}", - idx.name, cols, idx.unique - )); + for (name, columns) in index_constraints { + let cols = columns.join(", "); + let idx_name = name.clone().unwrap_or_else(|| "(unnamed)".to_string()); + lines.push(format!("// {} on [{}]", idx_name, cols)); } } @@ -690,24 +699,22 @@ fn to_pascal_case(s: &str) -> String { mod helper_tests { use super::*; use rstest::rstest; - use vespertide_core::{ColumnType, ComplexColumnType, IndexDef, SimpleColumnType}; + use vespertide_core::{ColumnType, ComplexColumnType, SimpleColumnType}; #[test] fn test_render_indexes() { let mut lines = Vec::new(); - let indexes = vec![ - IndexDef { - name: "idx_users_email".into(), + let constraints = vec![ + TableConstraint::Index { + name: Some("idx_users_email".into()), columns: vec!["email".into()], - unique: false, }, - IndexDef { - name: "idx_users_name_email".into(), + TableConstraint::Index { + name: Some("idx_users_name_email".into()), columns: vec!["name".into(), "email".into()], - unique: true, }, ]; - render_indexes(&mut lines, &indexes); + render_indexes(&mut lines, &constraints); assert!(!lines.is_empty()); assert!(lines.iter().any(|l| l.contains("idx_users_email"))); assert!(lines.iter().any(|l| l.contains("idx_users_name_email"))); @@ -926,7 +933,6 @@ mod helper_tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; let schema = vec![media]; @@ -956,7 +962,6 @@ mod helper_tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; // article table with FK to media @@ -1000,7 +1005,6 @@ mod helper_tests { on_update: None, }, ], - indexes: vec![], }; let schema = vec![media, article]; @@ -1027,7 +1031,6 @@ mod helper_tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }; let schema = vec![media]; @@ -1067,7 +1070,6 @@ mod helper_tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; // article table with FK to media @@ -1111,7 +1113,6 @@ mod helper_tests { on_update: None, }, ], - indexes: vec![], }; // article_user table with FK to article.media_id @@ -1155,7 +1156,6 @@ mod helper_tests { on_update: None, }, ], - indexes: vec![], }; let schema = vec![media, article.clone(), article_user.clone()]; @@ -1205,7 +1205,6 @@ mod helper_tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; // level_b with FK to level_a @@ -1236,7 +1235,6 @@ mod helper_tests { on_update: None, }, ], - indexes: vec![], }; // level_c with FK to level_b @@ -1267,7 +1265,6 @@ mod helper_tests { on_update: None, }, ], - indexes: vec![], }; let schema = vec![level_a, level_b, level_c]; @@ -1299,7 +1296,6 @@ mod helper_tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; // post table with FK to user (not PK, so has_many) @@ -1343,7 +1339,6 @@ mod helper_tests { on_update: None, }, ], - indexes: vec![], }; let schema = vec![user.clone(), post]; @@ -1380,7 +1375,6 @@ mod helper_tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; // profile table with FK to user that is also the PK (one-to-one) @@ -1424,7 +1418,6 @@ mod helper_tests { on_update: None, }, ], - indexes: vec![], }; let schema = vec![user.clone(), profile]; @@ -1461,7 +1454,6 @@ mod helper_tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; // settings table with unique FK to user (one-to-one via UNIQUE constraint) @@ -1509,7 +1501,6 @@ mod helper_tests { columns: vec!["user_id".into()], }, ], - indexes: vec![], }; let schema = vec![user.clone(), settings]; @@ -1541,7 +1532,6 @@ mod tests { ColumnDef { name: "display_name".into(), r#type: ColumnType::Simple(SimpleColumnType::Text), nullable: true, default: None, comment: None, primary_key: None, unique: None, index: None, foreign_key: None }, ], constraints: vec![TableConstraint::PrimaryKey { auto_increment: false, columns: vec!["id".into()] }], - indexes: vec![], })] #[case("composite_pk", TableDef { name: "accounts".into(), @@ -1550,7 +1540,6 @@ mod tests { ColumnDef { name: "tenant_id".into(), r#type: ColumnType::Simple(SimpleColumnType::BigInt), nullable: false, default: None, comment: None, primary_key: None, unique: None, index: None, foreign_key: None }, ], constraints: vec![TableConstraint::PrimaryKey { auto_increment: false, columns: vec!["id".into(), "tenant_id".into()] }], - indexes: vec![], })] #[case("fk_single", TableDef { name: "posts".into(), @@ -1570,7 +1559,6 @@ mod tests { on_update: None, }, ], - indexes: vec![], })] #[case("fk_composite", TableDef { name: "invoices".into(), @@ -1590,7 +1578,6 @@ mod tests { on_update: None, }, ], - indexes: vec![], })] #[case("inline_pk", TableDef { name: "users".into(), @@ -1599,7 +1586,6 @@ mod tests { ColumnDef { name: "email".into(), r#type: ColumnType::Simple(SimpleColumnType::Text), nullable: false, default: None, comment: None, primary_key: None, unique: Some(vespertide_core::StrOrBoolOrArray::Bool(true)), index: None, foreign_key: None }, ], constraints: vec![], - indexes: vec![], })] #[case("pk_and_fk_together", { use vespertide_core::schema::foreign_key::{ForeignKeyDef, ForeignKeySyntax}; @@ -1685,7 +1671,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], }; // Normalize to convert inline constraints to table-level table = table.normalize().unwrap(); @@ -1711,7 +1696,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], })] #[case("enum_nullable", TableDef { name: "tasks".into(), @@ -1733,7 +1717,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], })] #[case("enum_multiple_columns", TableDef { name: "products".into(), @@ -1769,7 +1752,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], })] #[case("enum_shared", TableDef { name: "documents".into(), @@ -1805,7 +1787,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], })] #[case("enum_special_values", TableDef { name: "events".into(), @@ -1827,7 +1808,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], })] #[case("unique_and_indexed", TableDef { name: "users".into(), @@ -1841,9 +1821,7 @@ mod tests { constraints: vec![ TableConstraint::Unique { name: None, columns: vec!["email".into()] }, TableConstraint::Unique { name: Some("uq_username".into()), columns: vec!["username".into()] }, - ], - indexes: vec![ - IndexDef { name: "idx_department".into(), columns: vec!["department".into()], unique: false }, + TableConstraint::Index { name: Some("idx_department".into()), columns: vec!["department".into()] }, ], })] #[case("enum_with_default", TableDef { @@ -1868,7 +1846,6 @@ mod tests { ColumnDef { name: "is_archived".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 }, ], constraints: vec![], - indexes: vec![], })] fn render_entity_snapshots(#[case] name: &str, #[case] table: TableDef) { let rendered = render_entity(&table); @@ -1900,7 +1877,6 @@ mod tests { auto_increment: false, columns: pk_cols.into_iter().map(String::from).collect(), }], - indexes: vec![], } } @@ -1928,7 +1904,6 @@ mod tests { name: name.into(), columns, constraints, - indexes: vec![], } } @@ -2156,7 +2131,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }; let rendered = render_entity(&table); assert!(rendered.contains("default_value = 0.00")); @@ -2225,7 +2199,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], } } 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 index ced030f..650a48d 100644 --- 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 @@ -29,6 +29,6 @@ pub struct Model { // 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 +// (unnamed) on [article_id] +// (unnamed) on [user_id] impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_unique_and_indexed.snap b/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_unique_and_indexed.snap index 305140c..5d36fab 100644 --- a/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_unique_and_indexed.snap +++ b/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_unique_and_indexed.snap @@ -23,5 +23,5 @@ pub struct Model { // Index definitions (SeaORM uses Statement builders externally) -// idx_department on [department] unique=false +// idx_department on [department] impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs index 8d1b73d..8beb727 100644 --- a/crates/vespertide-loader/src/models.rs +++ b/crates/vespertide-loader/src/models.rs @@ -16,23 +16,22 @@ pub fn load_models(config: &VespertideConfig) -> Result> { let mut tables = Vec::new(); load_models_recursive(models_dir, &mut tables)?; - // Normalize tables to convert inline constraints (primary_key, foreign_key, etc.) to table-level constraints - // This must happen before validation so that foreign key references can be checked - let normalized_tables: Vec = tables - .into_iter() - .map(|t| { - t.normalize() - .map_err(|e| anyhow::anyhow!("Failed to normalize table '{}': {}", t.name, e)) - }) - .collect::, _>>()?; + // Validate schema integrity using normalized version + // But return the original tables to preserve inline constraints + if !tables.is_empty() { + let normalized_tables: Vec = tables + .iter() + .map(|t| { + t.normalize() + .map_err(|e| anyhow::anyhow!("Failed to normalize table '{}': {}", t.name, e)) + }) + .collect::, _>>()?; - // Validate schema integrity before returning - if !normalized_tables.is_empty() { validate_schema(&normalized_tables) .map_err(|e| anyhow::anyhow!("schema validation failed: {}", e))?; } - Ok(normalized_tables) + Ok(tables) } /// Recursively walk directory and load model files. @@ -236,7 +235,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; fs::write("models/users.yaml", serde_yaml::to_string(&table).unwrap()).unwrap(); @@ -272,7 +270,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - indexes: vec![], }; let content = serde_json::to_string_pretty(&table).unwrap(); fs::write("models/subdir/subtable.json", content).unwrap(); @@ -307,7 +304,6 @@ mod tests { foreign_key: Some(ForeignKeySyntax::String("invalid_format".into())), }], constraints: vec![], - indexes: vec![], }; fs::write( "models/orders.json", @@ -342,7 +338,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }; fs::write( models_dir.join("users.json"), @@ -411,7 +406,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }; fs::write( models_dir.join("users.yaml"), @@ -447,7 +441,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }; fs::write( models_dir.join("users.yml"), @@ -484,7 +477,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }; fs::write( subdir.join("subtable.json"), @@ -551,7 +543,6 @@ mod tests { foreign_key: Some(ForeignKeySyntax::String("invalid_format".into())), }], constraints: vec![], - indexes: vec![], }; fs::write( models_dir.join("orders.json"), diff --git a/crates/vespertide-naming/Cargo.toml b/crates/vespertide-naming/Cargo.toml new file mode 100644 index 0000000..a3421ad --- /dev/null +++ b/crates/vespertide-naming/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "vespertide-naming" +version = "0.1.14" +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +description = "Naming conventions and helpers for vespertide database schema management" + +[dependencies] diff --git a/crates/vespertide-naming/src/lib.rs b/crates/vespertide-naming/src/lib.rs new file mode 100644 index 0000000..548c978 --- /dev/null +++ b/crates/vespertide-naming/src/lib.rs @@ -0,0 +1,114 @@ +//! Naming conventions and helpers for vespertide database schema management. +//! +//! This crate provides consistent naming functions for database objects like +//! indexes, constraints, and foreign keys. It has no dependencies and can be +//! used by any other vespertide crate. + +/// Generate index name from table name, columns, and optional user-provided key. +/// Always includes table name to avoid conflicts across tables. +/// Uses double underscore to separate table name from the rest. +/// Format: ix_{table}__{key} or ix_{table}__{col1}_{col2}... +pub fn build_index_name(table: &str, columns: &[String], key: Option<&str>) -> String { + match key { + Some(k) => format!("ix_{}__{}", table, k), + None => format!("ix_{}__{}", table, columns.join("_")), + } +} + +/// Generate unique constraint name from table name, columns, and optional user-provided key. +/// Always includes table name to avoid conflicts across tables. +/// Uses double underscore to separate table name from the rest. +/// Format: uq_{table}__{key} or uq_{table}__{col1}_{col2}... +pub fn build_unique_constraint_name(table: &str, columns: &[String], key: Option<&str>) -> String { + match key { + Some(k) => format!("uq_{}__{}", table, k), + None => format!("uq_{}__{}", table, columns.join("_")), + } +} + +/// Generate foreign key constraint name from table name, columns, and optional user-provided key. +/// Always includes table name to avoid conflicts across tables. +/// Uses double underscore to separate table name from the rest. +/// Format: fk_{table}__{key} or fk_{table}__{col1}_{col2}... +pub fn build_foreign_key_name(table: &str, columns: &[String], key: Option<&str>) -> String { + match key { + Some(k) => format!("fk_{}__{}", table, k), + None => format!("fk_{}__{}", table, columns.join("_")), + } +} + +/// Generate CHECK constraint name for SQLite enum column. +/// Uses double underscore to separate table name from the rest. +/// Format: chk_{table}__{column} +pub fn build_check_constraint_name(table: &str, column: &str) -> String { + format!("chk_{}__{}", table, column) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_index_name_with_key() { + assert_eq!( + build_index_name("users", &["email".into()], Some("email_idx")), + "ix_users__email_idx" + ); + } + + #[test] + fn test_build_index_name_without_key() { + assert_eq!( + build_index_name("users", &["email".into()], None), + "ix_users__email" + ); + } + + #[test] + fn test_build_index_name_multiple_columns() { + assert_eq!( + build_index_name("users", &["first_name".into(), "last_name".into()], None), + "ix_users__first_name_last_name" + ); + } + + #[test] + fn test_build_unique_constraint_name_with_key() { + assert_eq!( + build_unique_constraint_name("users", &["email".into()], Some("email_unique")), + "uq_users__email_unique" + ); + } + + #[test] + fn test_build_unique_constraint_name_without_key() { + assert_eq!( + build_unique_constraint_name("users", &["email".into()], None), + "uq_users__email" + ); + } + + #[test] + fn test_build_foreign_key_name_with_key() { + assert_eq!( + build_foreign_key_name("posts", &["user_id".into()], Some("fk_user")), + "fk_posts__fk_user" + ); + } + + #[test] + fn test_build_foreign_key_name_without_key() { + assert_eq!( + build_foreign_key_name("posts", &["user_id".into()], None), + "fk_posts__user_id" + ); + } + + #[test] + fn test_build_check_constraint_name() { + assert_eq!( + build_check_constraint_name("users", "status"), + "chk_users__status" + ); + } +} diff --git a/crates/vespertide-planner/Cargo.toml b/crates/vespertide-planner/Cargo.toml index cd25130..6bea182 100644 --- a/crates/vespertide-planner/Cargo.toml +++ b/crates/vespertide-planner/Cargo.toml @@ -10,7 +10,9 @@ description = "Replays applied migrations to rebuild a baseline, then diffs agai [dependencies] vespertide-core = { workspace = true } +vespertide-naming = { workspace = true } thiserror = "2" [dev-dependencies] rstest = "0.26" +insta = "1.44" diff --git a/crates/vespertide-planner/src/apply.rs b/crates/vespertide-planner/src/apply.rs index 9badf85..095bdd6 100644 --- a/crates/vespertide-planner/src/apply.rs +++ b/crates/vespertide-planner/src/apply.rs @@ -1,4 +1,4 @@ -use vespertide_core::{IndexDef, MigrationAction, TableConstraint, TableDef}; +use vespertide_core::{MigrationAction, TableConstraint, TableDef}; use crate::error::PlannerError; @@ -20,7 +20,6 @@ pub fn apply_action( name: table.clone(), columns: columns.clone(), constraints: constraints.clone(), - indexes: Vec::new(), }); Ok(()) } @@ -64,7 +63,6 @@ pub fn apply_action( .ok_or_else(|| PlannerError::ColumnNotFound(table.clone(), from.clone()))?; col.name = to.clone(); rename_column_in_constraints(&mut tbl.constraints, from, to); - rename_column_in_indexes(&mut tbl.indexes, from, to); Ok(()) } MigrationAction::DeleteColumn { table, column } => { @@ -78,7 +76,6 @@ pub fn apply_action( Err(PlannerError::ColumnNotFound(table.clone(), column.clone())) } else { drop_column_from_constraints(&mut tbl.constraints, column); - drop_column_from_indexes(&mut tbl.indexes, column); Ok(()) } } @@ -99,59 +96,6 @@ pub fn apply_action( col.r#type = new_type.clone(); Ok(()) } - MigrationAction::AddIndex { table, index } => { - let tbl = schema - .iter_mut() - .find(|t| t.name == *table) - .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?; - tbl.indexes.push(index.clone()); - Ok(()) - } - MigrationAction::RemoveIndex { table, name } => { - let tbl = schema - .iter_mut() - .find(|t| t.name == *table) - .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?; - let before = tbl.indexes.len(); - tbl.indexes.retain(|i| i.name != *name); - - // Also clear inline index on column if index name matches the auto-generated pattern - // Pattern: idx_{table}_{column} for Bool(true) or the name itself for Str(name) - let prefix = format!("idx_{}_", table); - if let Some(col_name) = name.strip_prefix(&prefix) { - // This is an auto-generated index name - clear the inline index on that column - if let Some(col) = tbl.columns.iter_mut().find(|c| c.name == col_name) { - col.index = None; - } - } - // Also check if any column has a named index matching this name - for col in &mut tbl.columns { - if let Some(ref idx_val) = col.index { - match idx_val { - vespertide_core::StrOrBoolOrArray::Str(idx_name) if idx_name == name => { - col.index = None; - } - vespertide_core::StrOrBoolOrArray::Array(names) => { - let filtered: Vec<_> = - names.iter().filter(|n| *n != name).cloned().collect(); - if filtered.is_empty() { - col.index = None; - } else if filtered.len() < names.len() { - col.index = - Some(vespertide_core::StrOrBoolOrArray::Array(filtered)); - } - } - _ => {} - } - } - } - - if tbl.indexes.len() == before { - Err(PlannerError::IndexNotFound(table.clone(), name.clone())) - } else { - Ok(()) - } - } MigrationAction::RenameTable { from, to } => { if schema.iter().any(|t| t.name == *to) { Err(PlannerError::TableExists(to.clone())) @@ -179,6 +123,108 @@ pub fn apply_action( .find(|t| t.name == *table) .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?; tbl.constraints.retain(|c| c != constraint); + + // Also clear inline column fields that correspond to the removed constraint + // This ensures normalize() won't re-add the constraint from inline fields + match constraint { + TableConstraint::Unique { name, columns } => { + // For unnamed single-column unique constraints, clear the column's inline unique + if name.is_none() + && columns.len() == 1 + && let Some(col) = tbl.columns.iter_mut().find(|c| c.name == columns[0]) + { + col.unique = None; + } + // For named constraints, clear inline unique references to this constraint name + if let Some(constraint_name) = name { + for col in &mut tbl.columns { + if let Some(vespertide_core::StrOrBoolOrArray::Array(names)) = + &mut col.unique + { + names.retain(|n| n != constraint_name); + if names.is_empty() { + col.unique = None; + } + } else if let Some(vespertide_core::StrOrBoolOrArray::Str(n)) = + &col.unique + && n == constraint_name + { + col.unique = None; + } + } + } + } + TableConstraint::PrimaryKey { columns, .. } => { + // Clear inline primary_key for columns in this constraint + for col_name in columns { + if let Some(col) = tbl.columns.iter_mut().find(|c| &c.name == col_name) { + col.primary_key = None; + } + } + } + TableConstraint::ForeignKey { columns, .. } => { + // Clear inline foreign_key for columns in this constraint + for col_name in columns { + if let Some(col) = tbl.columns.iter_mut().find(|c| &c.name == col_name) { + col.foreign_key = None; + } + } + } + TableConstraint::Check { .. } => { + // Check constraints don't have inline representation + } + TableConstraint::Index { name, columns } => { + // Clear inline index on columns when removing an index constraint + // Check if this index name was auto-generated for a single column + for col in &mut tbl.columns { + let auto_name = vespertide_naming::build_index_name( + table, + std::slice::from_ref(&col.name), + None, + ); + if name.as_ref() == Some(&auto_name) { + col.index = None; + break; + } + } + // Also check for single-column unnamed indexes + if name.is_none() + && columns.len() == 1 + && let Some(col) = tbl.columns.iter_mut().find(|c| c.name == columns[0]) + { + col.index = None; + } + // Check for named index matching inline field + if let Some(constraint_name) = name { + for col in &mut tbl.columns { + if let Some(ref idx_val) = col.index { + match idx_val { + vespertide_core::StrOrBoolOrArray::Str(idx_name) + if idx_name == constraint_name => + { + col.index = None; + } + vespertide_core::StrOrBoolOrArray::Array(names) => { + let filtered: Vec<_> = names + .iter() + .filter(|n| *n != constraint_name) + .cloned() + .collect(); + if filtered.is_empty() { + col.index = None; + } else if filtered.len() < names.len() { + col.index = Some( + vespertide_core::StrOrBoolOrArray::Array(filtered), + ); + } + } + _ => {} + } + } + } + } + } + } Ok(()) } } @@ -218,15 +264,12 @@ fn rename_column_in_constraints(constraints: &mut [TableConstraint], from: &str, } } TableConstraint::Check { .. } => {} - } - } -} - -fn rename_column_in_indexes(indexes: &mut [IndexDef], from: &str, to: &str) { - for idx in indexes { - for c in idx.columns.iter_mut() { - if c == from { - *c = to.to_string(); + TableConstraint::Index { columns, .. } => { + for c in columns.iter_mut() { + if c == from { + *c = to.to_string(); + } + } } } } @@ -252,13 +295,13 @@ fn drop_column_from_constraints(constraints: &mut Vec, column: !columns.is_empty() && !ref_columns.is_empty() } TableConstraint::Check { .. } => true, + TableConstraint::Index { columns, .. } => { + columns.retain(|c| c != column); + !columns.is_empty() + } }); } -fn drop_column_from_indexes(indexes: &mut Vec, column: &str) { - indexes.retain(|idx| !idx.columns.iter().any(|c| c == column)); -} - #[cfg(test)] mod tests { use super::*; @@ -279,17 +322,11 @@ mod tests { } } - fn table( - name: &str, - columns: Vec, - constraints: Vec, - indexes: Vec, - ) -> TableDef { + fn table(name: &str, columns: Vec, constraints: Vec) -> TableDef { TableDef { name: name.to_string(), columns, constraints, - indexes, } } @@ -299,7 +336,6 @@ mod tests { TableNotFound, ColumnExists, ColumnNotFound, - IndexNotFound, } fn assert_err_kind(err: crate::error::PlannerError, kind: ErrKind) { @@ -308,14 +344,13 @@ mod tests { (crate::error::PlannerError::TableNotFound(_), ErrKind::TableNotFound) => {} (crate::error::PlannerError::ColumnExists(_, _), ErrKind::ColumnExists) => {} (crate::error::PlannerError::ColumnNotFound(_, _), ErrKind::ColumnNotFound) => {} - (crate::error::PlannerError::IndexNotFound(_, _), ErrKind::IndexNotFound) => {} (other, expected) => panic!("unexpected error {other:?}, expected {:?}", expected), } } #[rstest] #[case( - vec![table("users", vec![], vec![], vec![])], + vec![table("users", vec![], vec![])], MigrationAction::CreateTable { table: "users".into(), columns: vec![], @@ -334,7 +369,6 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], vec![] )], MigrationAction::AddColumn { @@ -348,7 +382,6 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], vec![] )], MigrationAction::DeleteColumn { @@ -357,23 +390,10 @@ mod tests { }, ErrKind::ColumnNotFound )] - #[case( - vec![table( - "users", - vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], - vec![] - )], - MigrationAction::RemoveIndex { - table: "users".into(), - name: "idx".into() - }, - ErrKind::IndexNotFound - )] #[case( vec![ - table("old", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![]), - table("new", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![]), + table("old", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![]), + table("new", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![]), ], MigrationAction::RenameTable { from: "old".into(), @@ -390,11 +410,10 @@ mod tests { assert_err_kind(err, expected); } - fn idx(name: &str, columns: Vec<&str>, unique: bool) -> IndexDef { - IndexDef { - name: name.to_string(), + fn idx(name: &str, columns: Vec<&str>) -> TableConstraint { + TableConstraint::Index { + name: Some(name.to_string()), columns: columns.into_iter().map(|s| s.to_string()).collect(), - unique, } } @@ -446,10 +465,8 @@ mod tests { name: "ck_old".into(), expr: "old IS NOT NULL".into(), }, - ], - vec![ - idx("idx_old", vec!["old"], false), - idx("idx_ref", vec!["ref_id"], false), + idx("idx_old", vec!["old"]), + idx("idx_ref", vec!["ref_id"]), ], )], actions: vec![ @@ -490,10 +507,8 @@ mod tests { name: "ck_old".into(), expr: "old IS NOT NULL".into(), }, - ], - vec![ - idx("idx_old", vec!["old"], false), - idx("idx_ref", vec!["renamed"], false), + idx("idx_old", vec!["old"]), + idx("idx_ref", vec!["renamed"]), ], )], })] @@ -519,8 +534,8 @@ mod tests { name: "ck_old".into(), expr: "old IS NOT NULL".into(), }, + idx("idx_old", vec!["old"]), ], - vec![idx("idx_old", vec!["old"], false)], )], actions: vec![MigrationAction::DeleteColumn { table: "users".into(), @@ -536,7 +551,6 @@ mod tests { expr: "old IS NOT NULL".into(), }, ], - vec![], )], })] #[case(SuccessCase { @@ -544,7 +558,6 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], actions: vec![ MigrationAction::ModifyColumnType { @@ -552,20 +565,19 @@ mod tests { column: "id".into(), new_type: ColumnType::Simple(SimpleColumnType::Text), }, - MigrationAction::AddIndex { + MigrationAction::AddConstraint { table: "users".into(), - index: idx("idx_id", vec!["id"], true), + constraint: idx("idx_id", vec!["id"]), }, - MigrationAction::RemoveIndex { + MigrationAction::RemoveConstraint { table: "users".into(), - name: "idx_id".into(), + constraint: idx("idx_id", vec!["id"]), }, ], expected: vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Text))], vec![], - vec![], )], })] #[case(SuccessCase { @@ -573,7 +585,6 @@ mod tests { "old", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], actions: vec![MigrationAction::RenameTable { from: "old".into(), @@ -583,11 +594,10 @@ mod tests { "new", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], })] #[case(SuccessCase { - initial: vec![table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![])], + initial: vec![table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![])], actions: vec![MigrationAction::AddConstraint { table: "users".into(), constraint: TableConstraint::PrimaryKey { @@ -602,7 +612,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - vec![], )], })] #[case(SuccessCase { @@ -613,7 +622,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - vec![], )], actions: vec![MigrationAction::RemoveConstraint { table: "users".into(), @@ -622,14 +630,14 @@ mod tests { columns: vec!["id".into()], }, }], - expected: vec![table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![])], + expected: vec![table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![])], })] #[case(SuccessCase { - initial: vec![table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![])], + initial: vec![table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![])], actions: vec![MigrationAction::RawSql { sql: "SELECT 1;".to_string(), }], - expected: vec![table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![])], + expected: vec![table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![])], })] fn apply_action_success_cases(#[case] case: SuccessCase) { let mut schema = case.initial; @@ -659,8 +667,8 @@ mod tests { name: "ck_old".into(), expr: "old > 0".into(), }, + idx("idx_old", vec!["old", "keep"]), ], - vec![idx("idx_old", vec!["old", "keep"], false)], "old", "new", vec![ @@ -681,8 +689,8 @@ mod tests { name: "ck_old".into(), expr: "old > 0".into(), }, - ], - vec![idx("idx_old", vec!["new", "keep"], false)] + idx("idx_old", vec!["new", "keep"]), + ] )] #[case( vec![ @@ -691,8 +699,8 @@ mod tests { name: "ck_id".into(), expr: "id > 0".into(), }, + idx("idx_id", vec!["id"]), ], - vec![idx("idx_id", vec!["id"], false)], "missing", "new", vec![ @@ -701,54 +709,49 @@ mod tests { name: "ck_id".into(), expr: "id > 0".into(), }, - ], - vec![idx("idx_id", vec!["id"], false)] + idx("idx_id", vec!["id"]), + ] )] - fn rename_helpers_update_constraints_and_indexes( + fn rename_helpers_update_constraints( #[case] mut constraints: Vec, - #[case] mut indexes: Vec, #[case] from: &str, #[case] to: &str, #[case] expected_constraints: Vec, - #[case] expected_indexes: Vec, ) { rename_column_in_constraints(&mut constraints, from, to); - rename_column_in_indexes(&mut indexes, from, to); assert_eq!(constraints, expected_constraints); - assert_eq!(indexes, expected_indexes); } - // Tests for RemoveIndex clearing inline index on columns + // Tests for RemoveConstraint (Index) clearing inline index on columns #[test] - fn remove_index_clears_inline_index_bool() { - // Column with inline index: true creates idx_{table}_{column} pattern + fn remove_index_constraint_clears_inline_index_bool() { + // Column with inline index: true creates ix_{table}__{column} pattern let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text)); col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Bool(true)); let mut schema = vec![table( "users", vec![col_with_index], - vec![], - vec![idx("idx_users_email", vec!["email"], false)], + vec![idx("ix_users__email", vec!["email"])], )]; apply_action( &mut schema, - &MigrationAction::RemoveIndex { + &MigrationAction::RemoveConstraint { table: "users".into(), - name: "idx_users_email".into(), + constraint: idx("ix_users__email", vec!["email"]), }, ) .unwrap(); - // Index should be removed from indexes array - assert!(schema[0].indexes.is_empty()); + // Index should be removed from constraints + assert!(schema[0].constraints.is_empty()); // Inline index on column should also be cleared assert!(schema[0].columns[0].index.is_none()); } #[test] - fn remove_index_clears_inline_index_str() { + fn remove_index_constraint_clears_inline_index_str() { // Column with inline index: "custom_idx_name" let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text)); col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Str( @@ -758,25 +761,24 @@ mod tests { let mut schema = vec![table( "users", vec![col_with_index], - vec![], - vec![idx("custom_idx_name", vec!["email"], false)], + vec![idx("custom_idx_name", vec!["email"])], )]; apply_action( &mut schema, - &MigrationAction::RemoveIndex { + &MigrationAction::RemoveConstraint { table: "users".into(), - name: "custom_idx_name".into(), + constraint: idx("custom_idx_name", vec!["email"]), }, ) .unwrap(); - assert!(schema[0].indexes.is_empty()); + assert!(schema[0].constraints.is_empty()); assert!(schema[0].columns[0].index.is_none()); } #[test] - fn remove_index_clears_inline_index_array_partial() { + fn remove_index_constraint_clears_inline_index_array_partial() { // Column with inline index: ["idx_a", "idx_b"] let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text)); col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Array(vec![ @@ -787,25 +789,20 @@ mod tests { let mut schema = vec![table( "users", vec![col_with_index], - vec![], - vec![ - idx("idx_a", vec!["email"], false), - idx("idx_b", vec!["email"], false), - ], + vec![idx("idx_a", vec!["email"]), idx("idx_b", vec!["email"])], )]; // Remove only idx_a apply_action( &mut schema, - &MigrationAction::RemoveIndex { + &MigrationAction::RemoveConstraint { table: "users".into(), - name: "idx_a".into(), + constraint: idx("idx_a", vec!["email"]), }, ) .unwrap(); - assert_eq!(schema[0].indexes.len(), 1); - assert_eq!(schema[0].indexes[0].name, "idx_b"); + assert_eq!(schema[0].constraints.len(), 1); // inline index should only have idx_b remaining assert_eq!( schema[0].columns[0].index, @@ -816,7 +813,7 @@ mod tests { } #[test] - fn remove_index_clears_inline_index_array_all() { + fn remove_index_constraint_clears_inline_index_array_all() { // Column with inline index: ["idx_single"] let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text)); col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Array(vec![ @@ -826,53 +823,268 @@ mod tests { let mut schema = vec![table( "users", vec![col_with_index], - vec![], - vec![idx("idx_single", vec!["email"], false)], + vec![idx("idx_single", vec!["email"])], )]; apply_action( &mut schema, - &MigrationAction::RemoveIndex { + &MigrationAction::RemoveConstraint { table: "users".into(), - name: "idx_single".into(), + constraint: idx("idx_single", vec!["email"]), }, ) .unwrap(); - assert!(schema[0].indexes.is_empty()); + assert!(schema[0].constraints.is_empty()); // When array becomes empty, inline index should be None assert!(schema[0].columns[0].index.is_none()); } #[test] - fn remove_index_with_inline_bool_non_matching_name() { - // Column with inline index: true, but index name doesn't match idx_{table}_{column} pattern - // This tests the `_ => {}` branch (line 144) where Bool(true) doesn't match Str or Array + fn remove_index_constraint_with_inline_bool_non_matching_name() { + // Column with inline index: true, but index name doesn't match ix_{table}__{column} pattern let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text)); col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Bool(true)); let mut schema = vec![table( "users", vec![col_with_index], - vec![], - vec![idx("custom_email_idx", vec!["email"], false)], // not idx_users_email + vec![idx("custom_email_idx", vec!["email"])], )]; apply_action( &mut schema, - &MigrationAction::RemoveIndex { + &MigrationAction::RemoveConstraint { table: "users".into(), - name: "custom_email_idx".into(), + constraint: idx("custom_email_idx", vec!["email"]), }, ) .unwrap(); - // Index removed from array - assert!(schema[0].indexes.is_empty()); - // Inline index NOT cleared because name didn't match pattern and Bool(true) hits _ branch + // Index removed from constraints + assert!(schema[0].constraints.is_empty()); + // Inline index NOT cleared because name didn't match pattern assert_eq!( schema[0].columns[0].index, Some(vespertide_core::StrOrBoolOrArray::Bool(true)) ); } + + #[test] + fn remove_unique_constraint_clears_inline_unique_array() { + // Column with inline unique: ["uq_email", "uq_users_email"] + let mut col_with_unique = col("email", ColumnType::Simple(SimpleColumnType::Text)); + col_with_unique.unique = Some(vespertide_core::StrOrBoolOrArray::Array(vec![ + "uq_email".to_string(), + "uq_users_email".to_string(), + ])); + + let mut schema = vec![table( + "users", + vec![col_with_unique], + vec![TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }], + )]; + + apply_action( + &mut schema, + &MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }, + }, + ) + .unwrap(); + + // Constraint removed + assert!(schema[0].constraints.is_empty()); + // "uq_email" removed from array, "uq_users_email" remains + assert_eq!( + schema[0].columns[0].unique, + Some(vespertide_core::StrOrBoolOrArray::Array(vec![ + "uq_users_email".to_string() + ])) + ); + } + + #[test] + fn remove_unique_constraint_clears_inline_unique_array_last_item() { + // Column with inline unique: ["uq_email"] (only one item in array) + let mut col_with_unique = col("email", ColumnType::Simple(SimpleColumnType::Text)); + col_with_unique.unique = Some(vespertide_core::StrOrBoolOrArray::Array(vec![ + "uq_email".to_string(), + ])); + + let mut schema = vec![table( + "users", + vec![col_with_unique], + vec![TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }], + )]; + + apply_action( + &mut schema, + &MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }, + }, + ) + .unwrap(); + + // Constraint removed + assert!(schema[0].constraints.is_empty()); + // Array becomes empty, so unique should be None + assert!(schema[0].columns[0].unique.is_none()); + } + + #[test] + fn remove_unique_constraint_clears_inline_unique_str() { + // Column with inline unique: "uq_email" + let mut col_with_unique = col("email", ColumnType::Simple(SimpleColumnType::Text)); + col_with_unique.unique = Some(vespertide_core::StrOrBoolOrArray::Str( + "uq_email".to_string(), + )); + + let mut schema = vec![table( + "users", + vec![col_with_unique], + vec![TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }], + )]; + + apply_action( + &mut schema, + &MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }, + }, + ) + .unwrap(); + + // Constraint removed + assert!(schema[0].constraints.is_empty()); + // Inline unique cleared + assert!(schema[0].columns[0].unique.is_none()); + } + + #[test] + fn remove_foreign_key_constraint_clears_inline_fk() { + use vespertide_core::schema::foreign_key::{ForeignKeyDef, ForeignKeySyntax}; + // Column with inline foreign_key + let mut col_with_fk = col("user_id", ColumnType::Simple(SimpleColumnType::Integer)); + col_with_fk.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef { + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + })); + + let mut schema = vec![table( + "posts", + vec![col_with_fk], + vec![TableConstraint::ForeignKey { + name: Some("fk_posts_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }], + )]; + + apply_action( + &mut schema, + &MigrationAction::RemoveConstraint { + table: "posts".into(), + constraint: TableConstraint::ForeignKey { + name: Some("fk_posts_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }, + }, + ) + .unwrap(); + + // Constraint removed + assert!(schema[0].constraints.is_empty()); + // Inline foreign_key cleared + assert!(schema[0].columns[0].foreign_key.is_none()); + } + + #[test] + fn remove_check_constraint() { + let mut schema = vec![table( + "users", + vec![col("age", ColumnType::Simple(SimpleColumnType::Integer))], + vec![TableConstraint::Check { + name: "check_age".into(), + expr: "age >= 18".into(), + }], + )]; + + apply_action( + &mut schema, + &MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Check { + name: "check_age".into(), + expr: "age >= 18".into(), + }, + }, + ) + .unwrap(); + + // Constraint removed + assert!(schema[0].constraints.is_empty()); + } + + #[test] + fn remove_unnamed_index_single_column() { + // Column with inline index: true + let mut col_with_index = col("email", ColumnType::Simple(SimpleColumnType::Text)); + col_with_index.index = Some(vespertide_core::StrOrBoolOrArray::Bool(true)); + + let mut schema = vec![table( + "users", + vec![col_with_index], + vec![TableConstraint::Index { + name: None, + columns: vec!["email".into()], + }], + )]; + + apply_action( + &mut schema, + &MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Index { + name: None, + columns: vec!["email".into()], + }, + }, + ) + .unwrap(); + + // Constraint removed + assert!(schema[0].constraints.is_empty()); + // Inline index cleared + assert!(schema[0].columns[0].index.is_none()); + } } diff --git a/crates/vespertide-planner/src/diff.rs b/crates/vespertide-planner/src/diff.rs index 3765570..65edb2c 100644 --- a/crates/vespertide-planner/src/diff.rs +++ b/crates/vespertide-planner/src/diff.rs @@ -222,12 +222,12 @@ fn sort_delete_tables(actions: &mut [MigrationAction], all_tables: &BTreeMap<&st } /// Diff two schema snapshots into a migration plan. -/// Both schemas are normalized to convert inline column constraints -/// (primary_key, unique, index, foreign_key) to table-level constraints. +/// Schemas are normalized for comparison purposes, but the original (non-normalized) +/// tables are used in migration actions to preserve inline constraint definitions. pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result { let mut actions: Vec = Vec::new(); - // Normalize both schemas to ensure inline constraints are converted to table-level + // Normalize both schemas for comparison (to ensure inline and table-level constraints are treated equally) let from_normalized: Vec = from .iter() .map(|t| { @@ -252,12 +252,16 @@ pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result, _>>()?; // Use BTreeMap for consistent ordering + // Normalized versions for comparison let from_map: BTreeMap<_, _> = from_normalized .iter() .map(|t| (t.name.as_str(), t)) .collect(); let to_map: BTreeMap<_, _> = to_normalized.iter().map(|t| (t.name.as_str(), t)).collect(); + // Original (non-normalized) versions for migration storage + let to_original_map: BTreeMap<_, _> = to.iter().map(|t| (t.name.as_str(), t)).collect(); + // Drop tables that disappeared. for name in from_map.keys() { if !to_map.contains_key(name) { @@ -318,36 +322,7 @@ pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result = from_tbl - .indexes - .iter() - .map(|i| (i.name.as_str(), i)) - .collect(); - let to_indexes: BTreeMap<_, _> = to_tbl - .indexes - .iter() - .map(|i| (i.name.as_str(), i)) - .collect(); - - for idx in from_indexes.keys() { - if !to_indexes.contains_key(idx) { - actions.push(MigrationAction::RemoveIndex { - table: (*name).to_string(), - name: (*idx).to_string(), - }); - } - } - for (idx, def) in &to_indexes { - if !from_indexes.contains_key(idx) { - actions.push(MigrationAction::AddIndex { - table: (*name).to_string(), - index: (*def).clone(), - }); - } - } - - // Constraints - compare and detect additions/removals + // Constraints - compare and detect additions/removals (includes indexes) for from_constraint in &from_tbl.constraints { if !to_tbl.constraints.contains(from_constraint) { actions.push(MigrationAction::RemoveConstraint { @@ -368,6 +343,7 @@ pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result = to_map .iter() @@ -378,17 +354,13 @@ pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result Result ColumnDef { ColumnDef { @@ -426,13 +401,18 @@ mod tests { name: &str, columns: Vec, constraints: Vec, - indexes: Vec, ) -> TableDef { TableDef { name: name.to_string(), columns, constraints, - indexes, + } + } + + fn idx(name: &str, columns: Vec<&str>) -> TableConstraint { + TableConstraint::Index { + name: Some(name.to_string()), + columns: columns.into_iter().map(|s| s.to_string()).collect(), } } @@ -442,7 +422,6 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], vec![table( "users", @@ -450,12 +429,7 @@ mod tests { col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text)), ], - vec![], - vec![IndexDef { - name: "idx_users_name".into(), - columns: vec!["name".into()], - unique: false, - }], + vec![idx("ix_users__name", vec!["name"])], )], vec![ MigrationAction::AddColumn { @@ -463,13 +437,9 @@ mod tests { column: Box::new(col("name", ColumnType::Simple(SimpleColumnType::Text))), fill_with: None, }, - MigrationAction::AddIndex { + MigrationAction::AddConstraint { table: "users".into(), - index: IndexDef { - name: "idx_users_name".into(), - columns: vec!["name".into()], - unique: false, - }, + constraint: idx("ix_users__name", vec!["name"]), }, ] )] @@ -478,38 +448,24 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], vec![], vec![MigrationAction::DeleteTable { table: "users".into() }] )] - #[case::add_table( + #[case::add_table_with_index( vec![], vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], - vec![IndexDef { - name: "idx_users_id".into(), - columns: vec!["id".into()], - unique: true, - }], + vec![idx("idx_users_id", vec!["id"])], )], vec![ MigrationAction::CreateTable { table: "users".into(), columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - constraints: vec![], - }, - MigrationAction::AddIndex { - table: "users".into(), - index: IndexDef { - name: "idx_users_id".into(), - columns: vec!["id".into()], - unique: true, - }, + constraints: vec![idx("idx_users_id", vec!["id"])], }, ] )] @@ -518,13 +474,11 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text))], vec![], - vec![], )], vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], vec![MigrationAction::DeleteColumn { table: "users".into(), @@ -536,13 +490,11 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Text))], vec![], - vec![], )], vec![MigrationAction::ModifyColumnType { table: "users".into(), @@ -554,22 +506,16 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], - vec![IndexDef { - name: "idx_users_id".into(), - columns: vec!["id".into()], - unique: false, - }], + vec![idx("idx_users_id", vec!["id"])], )], vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], - vec![MigrationAction::RemoveIndex { + vec![MigrationAction::RemoveConstraint { table: "users".into(), - name: "idx_users_id".into(), + constraint: idx("idx_users_id", vec!["id"]), }] )] #[case::add_index_existing_table( @@ -577,25 +523,15 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], - vec![IndexDef { - name: "idx_users_id".into(), - columns: vec!["id".into()], - unique: true, - }], + vec![idx("idx_users_id", vec!["id"])], )], - vec![MigrationAction::AddIndex { + vec![MigrationAction::AddConstraint { table: "users".into(), - index: IndexDef { - name: "idx_users_id".into(), - columns: vec!["id".into()], - unique: true, - }, + constraint: idx("idx_users_id", vec!["id"]), }] )] fn diff_schemas_detects_additions( @@ -634,7 +570,6 @@ mod tests { }), )], vec![], - vec![], )]; let to = vec![table( @@ -664,7 +599,6 @@ mod tests { }), )], vec![], - vec![], )]; let plan = diff_schemas(&from, &to).unwrap(); @@ -688,7 +622,6 @@ mod tests { }), )], vec![], - vec![], )]; let to = vec![table( @@ -705,7 +638,6 @@ mod tests { }), )], vec![], - vec![], )]; let plan = diff_schemas(&from, &to).unwrap(); @@ -798,18 +730,23 @@ mod tests { col("name", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], )], ) .unwrap(); + // Inline PK should be preserved in column definition assert_eq!(plan.actions.len(), 1); - if let MigrationAction::CreateTable { constraints, .. } = &plan.actions[0] { - assert_eq!(constraints.len(), 1); - assert!(matches!( - &constraints[0], - TableConstraint::PrimaryKey { columns, .. } if columns == &["id".to_string()] - )); + if let MigrationAction::CreateTable { + columns, + constraints, + .. + } = &plan.actions[0] + { + // Constraints should be empty (inline PK not moved here) + assert_eq!(constraints.len(), 0); + // Check that the column has inline PK + let id_col = columns.iter().find(|c| c.name == "id").unwrap(); + assert!(id_col.primary_key.is_some()); } else { panic!("Expected CreateTable action"); } @@ -826,17 +763,25 @@ mod tests { col_with_unique("email", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], )], ) .unwrap(); + // Inline unique should be preserved in column definition assert_eq!(plan.actions.len(), 1); - if let MigrationAction::CreateTable { constraints, .. } = &plan.actions[0] { - assert_eq!(constraints.len(), 1); + if let MigrationAction::CreateTable { + columns, + constraints, + .. + } = &plan.actions[0] + { + // Constraints should be empty (inline unique not moved here) + assert_eq!(constraints.len(), 0); + // Check that the column has inline unique + let email_col = columns.iter().find(|c| c.name == "email").unwrap(); assert!(matches!( - &constraints[0], - TableConstraint::Unique { name: None, columns } if columns == &["email".to_string()] + email_col.unique, + Some(StrOrBoolOrArray::Bool(true)) )); } else { panic!("Expected CreateTable action"); @@ -854,22 +799,25 @@ mod tests { col_with_index("name", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], )], ) .unwrap(); - // Should have CreateTable + AddIndex - assert_eq!(plan.actions.len(), 2); - assert!(matches!( - &plan.actions[0], - MigrationAction::CreateTable { .. } - )); - if let MigrationAction::AddIndex { index, .. } = &plan.actions[1] { - assert_eq!(index.name, "idx_users_name"); - assert_eq!(index.columns, vec!["name".to_string()]); + // Inline index should be preserved in column definition, not moved to constraints + assert_eq!(plan.actions.len(), 1); + if let MigrationAction::CreateTable { + columns, + constraints, + .. + } = &plan.actions[0] + { + // Constraints should be empty (inline index not moved here) + assert_eq!(constraints.len(), 0); + // Check that the column has inline index + let name_col = columns.iter().find(|c| c.name == "name").unwrap(); + assert!(matches!(name_col.index, Some(StrOrBoolOrArray::Bool(true)))); } else { - panic!("Expected AddIndex action"); + panic!("Expected CreateTable action"); } } @@ -889,21 +837,23 @@ mod tests { ), ], vec![], - vec![], )], ) .unwrap(); + // Inline FK should be preserved in column definition assert_eq!(plan.actions.len(), 1); - if let MigrationAction::CreateTable { constraints, .. } = &plan.actions[0] { - assert_eq!(constraints.len(), 1); - assert!(matches!( - &constraints[0], - TableConstraint::ForeignKey { columns, ref_table, ref_columns, .. } - if columns == &["user_id".to_string()] - && ref_table == "users" - && ref_columns == &["id".to_string()] - )); + if let MigrationAction::CreateTable { + columns, + constraints, + .. + } = &plan.actions[0] + { + // Constraints should be empty (inline FK not moved here) + assert_eq!(constraints.len(), 0); + // Check that the column has inline FK + let user_id_col = columns.iter().find(|c| c.name == "user_id").unwrap(); + assert!(user_id_col.foreign_key.is_some()); } else { panic!("Expected CreateTable action"); } @@ -912,6 +862,7 @@ mod tests { #[test] fn add_index_via_inline_constraint() { // Existing table without index -> table with inline index + // Inline index (Bool(true)) is normalized to a named table-level constraint let plan = diff_schemas( &[table( "users", @@ -920,7 +871,6 @@ mod tests { col("name", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], )], &[table( "users", @@ -929,18 +879,22 @@ mod tests { col_with_index("name", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], )], ) .unwrap(); + // Should generate AddConstraint with name: None (auto-generated indexes) assert_eq!(plan.actions.len(), 1); - if let MigrationAction::AddIndex { table, index } = &plan.actions[0] { + if let MigrationAction::AddConstraint { table, constraint } = &plan.actions[0] { assert_eq!(table, "users"); - assert_eq!(index.name, "idx_users_name"); - assert_eq!(index.columns, vec!["name".to_string()]); + if let TableConstraint::Index { name, columns } = constraint { + assert_eq!(name, &None); // Auto-generated indexes use None + assert_eq!(columns, &vec!["name".to_string()]); + } else { + panic!("Expected Index constraint, got {:?}", constraint); + } } else { - panic!("Expected AddIndex action, got {:?}", plan.actions[0]); + panic!("Expected AddConstraint action, got {:?}", plan.actions[0]); } } @@ -970,23 +924,40 @@ mod tests { "users", vec![id_col, email_col, name_col, org_id_col], vec![], - vec![], )], ) .unwrap(); - // Should have CreateTable + AddIndex - assert_eq!(plan.actions.len(), 2); + // All inline constraints should be preserved in column definitions + assert_eq!(plan.actions.len(), 1); + + if let MigrationAction::CreateTable { + columns, + constraints, + .. + } = &plan.actions[0] + { + // Constraints should be empty (all inline) + assert_eq!(constraints.len(), 0); + + // Check each column has its inline constraint + let id_col = columns.iter().find(|c| c.name == "id").unwrap(); + assert!(id_col.primary_key.is_some()); - if let MigrationAction::CreateTable { constraints, .. } = &plan.actions[0] { - // Should have: PrimaryKey, Unique, ForeignKey (3 constraints) - assert_eq!(constraints.len(), 3); + let email_col = columns.iter().find(|c| c.name == "email").unwrap(); + assert!(matches!( + email_col.unique, + Some(StrOrBoolOrArray::Bool(true)) + )); + + let name_col = columns.iter().find(|c| c.name == "name").unwrap(); + assert!(matches!(name_col.index, Some(StrOrBoolOrArray::Bool(true)))); + + let org_id_col = columns.iter().find(|c| c.name == "org_id").unwrap(); + assert!(org_id_col.foreign_key.is_some()); } else { panic!("Expected CreateTable action"); } - - // Check for AddIndex action - assert!(matches!(&plan.actions[1], MigrationAction::AddIndex { .. })); } #[test] @@ -999,7 +970,6 @@ mod tests { col("email", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], )]; let to_schema = vec![table( @@ -1012,7 +982,6 @@ mod tests { name: Some("uq_users_email".into()), columns: vec!["email".into()], }], - vec![], )]; let plan = diff_schemas(&from_schema, &to_schema).unwrap(); @@ -1042,7 +1011,6 @@ mod tests { name: Some("uq_users_email".into()), columns: vec!["email".into()], }], - vec![], )]; let to_schema = vec![table( @@ -1052,7 +1020,6 @@ mod tests { col("email", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], )]; let plan = diff_schemas(&from_schema, &to_schema).unwrap(); @@ -1091,7 +1058,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], }; let result = diff_schemas(&[], &[table]); @@ -1123,7 +1089,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], }; // 'from' schema has the invalid table @@ -1163,7 +1128,6 @@ mod tests { on_delete: None, on_update: None, }], - indexes: vec![], } } @@ -1172,7 +1136,6 @@ mod tests { name: name.to_string(), columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], constraints: vec![], - indexes: vec![], } } @@ -1363,7 +1326,6 @@ mod tests { on_delete: None, on_update: None, }], - indexes: vec![], }; let table_b = TableDef { @@ -1380,7 +1342,6 @@ mod tests { on_delete: None, on_update: None, }], - indexes: vec![], }; let result = diff_schemas(&[], &[table_a, table_b]); @@ -1437,13 +1398,11 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], ), table( "posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], ), ]; @@ -1456,7 +1415,6 @@ mod tests { col("name", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], ), ]; @@ -1551,21 +1509,18 @@ mod tests { name: "user".to_string(), columns: vec![col_pk("id")], constraints: vec![], - indexes: vec![], }; let product = TableDef { name: "product".to_string(), columns: vec![col_pk("id")], constraints: vec![], - indexes: vec![], }; let project = TableDef { name: "project".to_string(), columns: vec![col_pk("id"), col_inline_fk("user_id", "user")], constraints: vec![], - indexes: vec![], }; let code = TableDef { @@ -1577,7 +1532,6 @@ mod tests { col_inline_fk("project_id", "project"), ], constraints: vec![], - indexes: vec![], }; let order = TableDef { @@ -1590,14 +1544,12 @@ mod tests { col_inline_fk("code_id", "code"), ], constraints: vec![], - indexes: vec![], }; let payment = TableDef { name: "payment".to_string(), columns: vec![col_pk("id"), col_inline_fk("order_id", "order")], constraints: vec![], - indexes: vec![], }; // Pass in arbitrary order - should NOT return circular dependency error @@ -1691,7 +1643,6 @@ mod tests { name: "user".to_string(), columns: vec![col_pk("id")], constraints: vec![], - indexes: vec![], }; let code = TableDef { @@ -1702,7 +1653,6 @@ mod tests { col_inline_fk("used_by_user_id", "user"), // Second FK to same table ], constraints: vec![], - indexes: vec![], }; // This should NOT return circular dependency error even with duplicate FK refs @@ -1728,4 +1678,240 @@ mod tests { assert!(user_pos < code_pos, "user must come before code"); } } + + mod diff_tables { + use insta::assert_debug_snapshot; + + use super::*; + + #[test] + fn create_table_with_inline_index() { + let base = [table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(false)), + foreign_key: None, + }, + ColumnDef { + name: "name".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: Some(StrOrBoolOrArray::Bool(true)), + index: Some(StrOrBoolOrArray::Bool(true)), + foreign_key: None, + }, + ], + vec![], + )]; + let plan = diff_schemas(&[], &base).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert_debug_snapshot!(plan.actions); + + let plan = diff_schemas( + &base, + &[table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(false)), + foreign_key: None, + }, + ColumnDef { + name: "name".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: Some(StrOrBoolOrArray::Bool(true)), + index: Some(StrOrBoolOrArray::Bool(false)), + foreign_key: None, + }, + ], + vec![], + )], + ) + .unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert_debug_snapshot!(plan.actions); + } + + #[rstest] + #[case( + "add_index", + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + 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, + }, + ], + vec![], + )], + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(true)), + foreign_key: None, + }, + ], + vec![], + )], + )] + #[case( + "remove_index", + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(true)), + foreign_key: None, + }, + ], + vec![], + )], + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(false)), + foreign_key: None, + }, + ], + vec![], + )], + )] + #[case( + "add_named_index", + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + 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, + }, + ], + vec![], + )], + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Str("hello".to_string())), + foreign_key: None, + }, + ], + vec![], + )], + )] + #[case( + "remove_named_index", + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Str("hello".to_string())), + foreign_key: None, + }, + ], + vec![], + )], + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + 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, + }, + ], + vec![], + )], + )] + fn diff_tables(#[case] name: &str, #[case] base: Vec, #[case] to: Vec) { + use insta::with_settings; + + let plan = diff_schemas(&base, &to).unwrap(); + with_settings!({ snapshot_suffix => name }, { + assert_debug_snapshot!(plan.actions); + }); + } + } } diff --git a/crates/vespertide-planner/src/plan.rs b/crates/vespertide-planner/src/plan.rs index e28b41a..864b369 100644 --- a/crates/vespertide-planner/src/plan.rs +++ b/crates/vespertide-planner/src/plan.rs @@ -58,13 +58,11 @@ mod tests { name: &str, columns: Vec, constraints: Vec, - indexes: Vec, ) -> TableDef { TableDef { name: name.to_string(), columns, constraints, - indexes, } } @@ -88,7 +86,6 @@ mod tests { col("name", ColumnType::Simple(SimpleColumnType::Text)), ], vec![], - vec![], )]; let plan = plan_next_migration(&target_schema, &applied).unwrap(); diff --git a/crates/vespertide-planner/src/schema.rs b/crates/vespertide-planner/src/schema.rs index ac1534c..64cad33 100644 --- a/crates/vespertide-planner/src/schema.rs +++ b/crates/vespertide-planner/src/schema.rs @@ -1,163 +1,227 @@ -use vespertide_core::{MigrationPlan, TableDef}; - -use crate::apply::apply_action; -use crate::error::PlannerError; - -/// Derive a schema snapshot from existing migration plans. -pub fn schema_from_plans(plans: &[MigrationPlan]) -> Result, PlannerError> { - let mut schema: Vec = Vec::new(); - for plan in plans { - for action in &plan.actions { - apply_action(&mut schema, action)?; - } - } - Ok(schema) -} - -#[cfg(test)] -mod tests { - use super::*; - use rstest::rstest; - use vespertide_core::{ - ColumnDef, ColumnType, IndexDef, MigrationAction, SimpleColumnType, TableConstraint, - }; - - fn col(name: &str, ty: ColumnType) -> ColumnDef { - ColumnDef { - name: name.to_string(), - r#type: ty, - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - } - } - - fn table( - name: &str, - columns: Vec, - constraints: Vec, - indexes: Vec, - ) -> TableDef { - TableDef { - name: name.to_string(), - columns, - constraints, - indexes, - } - } - - #[rstest] - #[case::create_only( - vec![MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - constraints: vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], - }], - }], - table( - "users", - vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], - vec![], - ) - )] - #[case::create_and_add_column( - vec![ - MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - constraints: vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], - }], - }, - MigrationPlan { - comment: None, - created_at: None, - version: 2, - actions: vec![MigrationAction::AddColumn { - table: "users".into(), - column: Box::new(col("name", ColumnType::Simple(SimpleColumnType::Text))), - fill_with: None, - }], - }, - ], - table( - "users", - vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col("name", ColumnType::Simple(SimpleColumnType::Text)), - ], - vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], - vec![], - ) - )] - #[case::create_add_column_and_index( - vec![ - MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - constraints: vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], - }], - }, - MigrationPlan { - comment: None, - created_at: None, - version: 2, - actions: vec![MigrationAction::AddColumn { - table: "users".into(), - column: Box::new(col("name", ColumnType::Simple(SimpleColumnType::Text))), - fill_with: None, - }], - }, - MigrationPlan { - comment: None, - created_at: None, - version: 3, - actions: vec![MigrationAction::AddIndex { - table: "users".into(), - index: IndexDef { - name: "idx_users_name".into(), - columns: vec!["name".into()], - unique: false, - }, - }], - }, - ], - table( - "users", - vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col("name", ColumnType::Simple(SimpleColumnType::Text)), - ], - vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], - vec![IndexDef { - name: "idx_users_name".into(), - columns: vec!["name".into()], - unique: false, - }], - ) - )] - fn schema_from_plans_applies_actions( - #[case] plans: Vec, - #[case] expected_users: TableDef, - ) { - let schema = schema_from_plans(&plans).unwrap(); - let users = schema.iter().find(|t| t.name == "users").unwrap(); - assert_eq!(users, &expected_users); - } -} +use vespertide_core::{MigrationPlan, TableDef}; + +use crate::apply::apply_action; +use crate::error::PlannerError; + +/// Derive a schema snapshot from existing migration plans. +pub fn schema_from_plans(plans: &[MigrationPlan]) -> Result, PlannerError> { + let mut schema: Vec = Vec::new(); + for plan in plans { + for action in &plan.actions { + apply_action(&mut schema, action)?; + } + } + Ok(schema) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + use vespertide_core::{ + ColumnDef, ColumnType, MigrationAction, SimpleColumnType, TableConstraint, + }; + + fn col(name: &str, ty: ColumnType) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + fn table(name: &str, columns: Vec, constraints: Vec) -> TableDef { + TableDef { + name: name.to_string(), + columns, + constraints, + } + } + + #[rstest] + #[case::create_only( + vec![MigrationPlan { + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + constraints: vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], + }], + }], + table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], + ) + )] + #[case::create_and_add_column( + vec![ + MigrationPlan { + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + constraints: vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], + }], + }, + MigrationPlan { + comment: None, + created_at: None, + version: 2, + actions: vec![MigrationAction::AddColumn { + table: "users".into(), + column: Box::new(col("name", ColumnType::Simple(SimpleColumnType::Text))), + fill_with: None, + }], + }, + ], + table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("name", ColumnType::Simple(SimpleColumnType::Text)), + ], + vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], + ) + )] + #[case::create_add_column_and_index( + vec![ + MigrationPlan { + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + constraints: vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], + }], + }, + MigrationPlan { + comment: None, + created_at: None, + version: 2, + actions: vec![MigrationAction::AddColumn { + table: "users".into(), + column: Box::new(col("name", ColumnType::Simple(SimpleColumnType::Text))), + fill_with: None, + }], + }, + MigrationPlan { + comment: None, + created_at: None, + version: 3, + actions: vec![MigrationAction::AddConstraint { + table: "users".into(), + constraint: TableConstraint::Index { + name: Some("ix_users__name".into()), + columns: vec!["name".into()], + }, + }], + }, + ], + table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("name", ColumnType::Simple(SimpleColumnType::Text)), + ], + vec![ + TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }, + TableConstraint::Index { + name: Some("ix_users__name".into()), + columns: vec!["name".into()], + }, + ], + ) + )] + fn schema_from_plans_applies_actions( + #[case] plans: Vec, + #[case] expected_users: TableDef, + ) { + let schema = schema_from_plans(&plans).unwrap(); + let users = schema.iter().find(|t| t.name == "users").unwrap(); + assert_eq!(users, &expected_users); + } + + /// Test that RemoveConstraint works when table was created with both + /// inline unique column AND table-level unique constraint for the same column + #[test] + fn remove_constraint_with_inline_and_table_level_unique() { + use vespertide_core::StrOrBoolOrArray; + + // Simulate migration 0001: CreateTable with both inline unique and table-level constraint + let create_plan = MigrationPlan { + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "email".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: Some(StrOrBoolOrArray::Bool(true)), // inline unique + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::Unique { + name: None, + columns: vec!["email".into()], + }], // table-level unique (duplicate!) + }], + }; + + // Migration 0002: RemoveConstraint + let remove_plan = MigrationPlan { + comment: None, + created_at: None, + version: 2, + actions: vec![MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: None, + columns: vec!["email".into()], + }, + }], + }; + + let schema = schema_from_plans(&[create_plan, remove_plan]).unwrap(); + let users = schema.iter().find(|t| t.name == "users").unwrap(); + + println!("Constraints after apply: {:?}", users.constraints); + println!("Column unique field: {:?}", users.columns[0].unique); + + // After apply_action: + // - constraints is empty (RemoveConstraint removed the table-level one) + // - but column still has unique: Some(Bool(true))! + + // Now simulate what diff_schemas does - it normalizes the baseline + let normalized = users.clone().normalize().unwrap(); + println!("Constraints after normalize: {:?}", normalized.constraints); + + // After normalize: + // - inline unique (column.unique = true) is converted to table-level constraint + // - So we'd still have one unique constraint! + + // This is the bug: diff_schemas normalizes both baseline and target, + // but the baseline still has inline unique that gets re-added. + assert!( + normalized.constraints.is_empty(), + "Expected no constraints after normalize, but got: {:?}", + normalized.constraints + ); + } +} diff --git a/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__create_table_with_inline_index-2.snap b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__create_table_with_inline_index-2.snap new file mode 100644 index 0000000..63244ee --- /dev/null +++ b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__create_table_with_inline_index-2.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-planner/src/diff.rs +expression: plan.actions +--- +[ + RemoveConstraint { + table: "users", + constraint: Index { + name: None, + columns: [ + "name", + ], + }, + }, +] diff --git a/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__create_table_with_inline_index.snap b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__create_table_with_inline_index.snap new file mode 100644 index 0000000..f37377d --- /dev/null +++ b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__create_table_with_inline_index.snap @@ -0,0 +1,54 @@ +--- +source: crates/vespertide-planner/src/diff.rs +expression: plan.actions +--- +[ + CreateTable { + table: "users", + columns: [ + ColumnDef { + name: "id", + type: Simple( + Integer, + ), + nullable: false, + default: None, + comment: None, + primary_key: Some( + Bool( + true, + ), + ), + unique: None, + index: Some( + Bool( + false, + ), + ), + foreign_key: None, + }, + ColumnDef { + name: "name", + type: Simple( + Text, + ), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: Some( + Bool( + true, + ), + ), + index: Some( + Bool( + true, + ), + ), + foreign_key: None, + }, + ], + constraints: [], + }, +] diff --git a/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@add_index.snap b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@add_index.snap new file mode 100644 index 0000000..b9738d7 --- /dev/null +++ b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@add_index.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-planner/src/diff.rs +expression: plan.actions +--- +[ + AddConstraint { + table: "users", + constraint: Index { + name: None, + columns: [ + "id", + ], + }, + }, +] diff --git a/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@add_named_index.snap b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@add_named_index.snap new file mode 100644 index 0000000..3b15984 --- /dev/null +++ b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@add_named_index.snap @@ -0,0 +1,17 @@ +--- +source: crates/vespertide-planner/src/diff.rs +expression: plan.actions +--- +[ + AddConstraint { + table: "users", + constraint: Index { + name: Some( + "hello", + ), + columns: [ + "id", + ], + }, + }, +] diff --git a/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@remove_index.snap b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@remove_index.snap new file mode 100644 index 0000000..d37d1e8 --- /dev/null +++ b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@remove_index.snap @@ -0,0 +1,15 @@ +--- +source: crates/vespertide-planner/src/diff.rs +expression: plan.actions +--- +[ + RemoveConstraint { + table: "users", + constraint: Index { + name: None, + columns: [ + "id", + ], + }, + }, +] diff --git a/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@remove_named_index.snap b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@remove_named_index.snap new file mode 100644 index 0000000..05be7e5 --- /dev/null +++ b/crates/vespertide-planner/src/snapshots/vespertide_planner__diff__tests__diff_tables__diff_tables@remove_named_index.snap @@ -0,0 +1,17 @@ +--- +source: crates/vespertide-planner/src/diff.rs +expression: plan.actions +--- +[ + RemoveConstraint { + table: "users", + constraint: Index { + name: Some( + "hello", + ), + columns: [ + "id", + ], + }, + }, +] diff --git a/crates/vespertide-planner/src/validate.rs b/crates/vespertide-planner/src/validate.rs index 670547d..b25d245 100644 --- a/crates/vespertide-planner/src/validate.rs +++ b/crates/vespertide-planner/src/validate.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use vespertide_core::{ - ColumnDef, ColumnType, ComplexColumnType, EnumValues, IndexDef, MigrationAction, MigrationPlan, + ColumnDef, ColumnType, ComplexColumnType, EnumValues, MigrationAction, MigrationPlan, TableConstraint, TableDef, }; @@ -66,16 +66,11 @@ fn validate_table( validate_column(column, &table.name)?; } - // Validate constraints + // Validate constraints (including indexes) for constraint in &table.constraints { validate_constraint(constraint, &table.name, &table_columns, table_map)?; } - // Validate indexes - for index in &table.indexes { - validate_index(index, &table.name, &table_columns)?; - } - Ok(()) } @@ -236,30 +231,25 @@ fn validate_constraint( TableConstraint::Check { .. } => { // Check constraints are just expressions, no validation needed } - } - - Ok(()) -} - -fn validate_index( - index: &IndexDef, - table_name: &str, - table_columns: &HashSet<&str>, -) -> Result<(), PlannerError> { - if index.columns.is_empty() { - return Err(PlannerError::EmptyConstraintColumns( - table_name.to_string(), - format!("Index({})", index.name), - )); - } + TableConstraint::Index { name, columns } => { + if columns.is_empty() { + let index_name = name.clone().unwrap_or_else(|| "(unnamed)".to_string()); + return Err(PlannerError::EmptyConstraintColumns( + table_name.to_string(), + format!("Index({})", index_name), + )); + } - for col in &index.columns { - if !table_columns.contains(col.as_str()) { - return Err(PlannerError::IndexColumnNotFound( - table_name.to_string(), - index.name.clone(), - col.clone(), - )); + for col in columns { + if !table_columns.contains(col.as_str()) { + let index_name = name.clone().unwrap_or_else(|| "(unnamed)".to_string()); + return Err(PlannerError::IndexColumnNotFound( + table_name.to_string(), + index_name, + col.clone(), + )); + } + } } } @@ -294,7 +284,7 @@ mod tests { use super::*; use rstest::rstest; use vespertide_core::{ - ColumnDef, ColumnType, ComplexColumnType, EnumValues, IndexDef, NumValue, SimpleColumnType, + ColumnDef, ColumnType, ComplexColumnType, EnumValues, NumValue, SimpleColumnType, TableConstraint, }; @@ -312,17 +302,18 @@ mod tests { } } - fn table( - name: &str, - columns: Vec, - constraints: Vec, - indexes: Vec, - ) -> TableDef { + fn table(name: &str, columns: Vec, constraints: Vec) -> TableDef { TableDef { name: name.to_string(), columns, constraints, - indexes, + } + } + + fn idx(name: &str, columns: Vec<&str>) -> TableConstraint { + TableConstraint::Index { + name: Some(name.to_string()), + columns: columns.into_iter().map(|s| s.to_string()).collect(), } } @@ -367,14 +358,13 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], - vec![], )], None )] #[case::duplicate_table( vec![ - table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![]), - table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![]), + table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![]), + table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![]), ], Some(is_duplicate as fn(&PlannerError) -> bool) )] @@ -390,13 +380,12 @@ mod tests { on_delete: None, on_update: None, }], - vec![], )], Some(is_fk_table as fn(&PlannerError) -> bool) )] #[case::fk_missing_column( vec![ - table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])], vec![]), + table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])]), table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], @@ -408,14 +397,13 @@ mod tests { on_delete: None, on_update: None, }], - vec![], ), ], Some(is_fk_column as fn(&PlannerError) -> bool) )] #[case::fk_local_missing_column( vec![ - table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])], vec![]), + table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])]), table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], @@ -427,7 +415,6 @@ mod tests { on_delete: None, on_update: None, }], - vec![], ), ], Some(is_constraint_column as fn(&PlannerError) -> bool) @@ -438,7 +425,6 @@ mod tests { "posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])], - vec![], ), table( "users", @@ -451,7 +437,6 @@ mod tests { on_delete: None, on_update: None, }], - vec![], ), ], None @@ -460,12 +445,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![pk(vec!["id"])], - vec![IndexDef { - name: "idx_name".into(), - columns: vec!["nonexistent".into()], - unique: false, - }], + vec![pk(vec!["id"]), idx("idx_name", vec!["nonexistent"])], )], Some(is_index_column as fn(&PlannerError) -> bool) )] @@ -474,7 +454,6 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["nonexistent".into()] }], - vec![], )], Some(is_constraint_column as fn(&PlannerError) -> bool) )] @@ -486,7 +465,6 @@ mod tests { name: Some("u".into()), columns: vec![], }], - vec![], )], Some(is_empty_columns as fn(&PlannerError) -> bool) )] @@ -498,7 +476,6 @@ mod tests { name: None, columns: vec!["missing".into()], }], - vec![], )], Some(is_constraint_column as fn(&PlannerError) -> bool) )] @@ -507,7 +484,6 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec![] }], - vec![], )], Some(is_empty_columns as fn(&PlannerError) -> bool) )] @@ -517,7 +493,6 @@ mod tests { "posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])], - vec![], ), table( "users", @@ -530,7 +505,6 @@ mod tests { on_delete: None, on_update: None, }], - vec![], ), ], Some(is_fk_column as fn(&PlannerError) -> bool) @@ -547,7 +521,6 @@ mod tests { on_delete: None, on_update: None, }], - vec![], )], Some(is_empty_columns as fn(&PlannerError) -> bool) )] @@ -557,7 +530,6 @@ mod tests { "posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])], - vec![], ), table( "users", @@ -570,7 +542,6 @@ mod tests { on_delete: None, on_update: None, }], - vec![], ), ], Some(is_empty_columns as fn(&PlannerError) -> bool) @@ -579,11 +550,9 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![pk(vec!["id"])], - vec![IndexDef { - name: "idx".into(), + vec![pk(vec!["id"]), TableConstraint::Index { + name: Some("idx".into()), columns: vec![], - unique: false, }], )], Some(is_empty_columns as fn(&PlannerError) -> bool) @@ -592,12 +561,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text))], - vec![pk(vec!["id"])], - vec![IndexDef { - name: "idx_name".into(), - columns: vec!["name".into()], - unique: false, - }], + vec![pk(vec!["id"]), idx("idx_name", vec!["name"])], )], None )] @@ -609,7 +573,6 @@ mod tests { name: "ck".into(), expr: "id > 0".into(), }], - vec![], )], None )] @@ -618,7 +581,6 @@ mod tests { "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], - vec![], )], Some(is_missing_pk as fn(&PlannerError) -> bool) )] @@ -781,7 +743,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - vec![], )]; let result = validate_schema(&schema); @@ -828,7 +789,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - vec![], )]; let result = validate_schema(&schema); @@ -875,7 +835,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - vec![], )]; let result = validate_schema(&schema); @@ -933,7 +892,6 @@ mod tests { auto_increment: false, columns: vec!["id".into()], }], - vec![], )]; let result = validate_schema(&schema); diff --git a/crates/vespertide-query/Cargo.toml b/crates/vespertide-query/Cargo.toml index 29241f9..03a17db 100644 --- a/crates/vespertide-query/Cargo.toml +++ b/crates/vespertide-query/Cargo.toml @@ -10,6 +10,7 @@ description = "Converts migration actions into SQL statements with bind paramete [dependencies] vespertide-core = { workspace = true } +vespertide-naming = { workspace = true } vespertide-planner = { workspace = true } thiserror = "2" sea-query = "0.32" diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs index e2596b8..797bb85 100644 --- a/crates/vespertide-query/src/sql/add_column.rs +++ b/crates/vespertide-query/src/sql/add_column.rs @@ -117,18 +117,23 @@ pub fn build_add_column( BuiltQuery::DropTable(Box::new(Table::drop().table(Alias::new(table)).to_owned())); let rename_query = build_rename_table(&temp_table, table); + // Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + for constraint in &table_def.constraints { + if let vespertide_core::TableConstraint::Index { name, columns } = constraint { + let index_name = vespertide_naming::build_index_name( + table, + columns, + name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in columns { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); - } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut stmts = vec![create_query, insert_query, drop_query, rename_query]; @@ -279,7 +284,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }]; let result = build_add_column(&backend, "users", &column, fill_with, ¤t_schema).unwrap(); @@ -351,7 +355,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }]; let result = build_add_column( &DatabaseBackend::Sqlite, @@ -398,7 +401,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }]; let result = build_add_column( &DatabaseBackend::Sqlite, @@ -420,7 +422,7 @@ mod tests { #[test] fn test_add_column_sqlite_with_indexes() { - use vespertide_core::IndexDef; + use vespertide_core::TableConstraint; let column = ColumnDef { name: "nickname".into(), @@ -446,11 +448,9 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_id".into(), + constraints: vec![TableConstraint::Index { + name: Some("idx_id".into()), columns: vec!["id".into()], - unique: false, }], }]; let result = build_add_column( @@ -472,60 +472,6 @@ mod tests { assert!(sql.contains("idx_id")); } - #[test] - fn test_add_column_sqlite_with_unique_index() { - use vespertide_core::IndexDef; - - let column = ColumnDef { - name: "nickname".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }; - let current_schema = vec![TableDef { - name: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_email".into(), - columns: vec!["email".into()], - unique: true, - }], - }]; - let result = build_add_column( - &DatabaseBackend::Sqlite, - "users", - &column, - None, - ¤t_schema, - ); - assert!(result.is_ok()); - let queries = result.unwrap(); - let sql = queries - .iter() - .map(|q| q.build(DatabaseBackend::Sqlite)) - .collect::>() - .join("\n"); - // Should recreate unique index - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_email")); - } - #[rstest] #[case::add_column_with_enum_type_postgres(DatabaseBackend::Postgres)] #[case::add_column_with_enum_type_mysql(DatabaseBackend::MySql)] @@ -563,7 +509,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }]; let result = build_add_column(&backend, "users", &column, None, ¤t_schema); assert!(result.is_ok()); diff --git a/crates/vespertide-query/src/sql/add_constraint.rs b/crates/vespertide-query/src/sql/add_constraint.rs index 02b42bb..e6e9132 100644 --- a/crates/vespertide-query/src/sql/add_constraint.rs +++ b/crates/vespertide-query/src/sql/add_constraint.rs @@ -112,19 +112,27 @@ pub fn build_add_constraint( // 4. Rename temporary table let rename_query = build_rename_table(&temp_table, table); - // 5. Recreate indexes + // 5. Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + for c in &table_def.constraints { + if let TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = c + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); - } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut queries = vec![create_query, insert_query, drop_query, rename_query]; @@ -153,10 +161,14 @@ pub fn build_add_constraint( } TableConstraint::Unique { name, columns } => { // SQLite does not support ALTER TABLE ... ADD CONSTRAINT UNIQUE - let mut idx = Index::create().table(Alias::new(table)).unique().to_owned(); - if let Some(n) = name { - idx = idx.name(n).to_owned(); - } + // Always generate a proper name: uq_{table}_{key} or uq_{table}_{columns} + let index_name = + super::helpers::build_unique_constraint_name(table, columns, name.as_deref()); + let mut idx = Index::create() + .table(Alias::new(table)) + .name(&index_name) + .unique() + .to_owned(); for col in columns { idx = idx.col(Alias::new(col)).to_owned(); } @@ -228,19 +240,27 @@ pub fn build_add_constraint( // 4. Rename temporary table to original name let rename_query = build_rename_table(&temp_table, table); - // 5. Recreate indexes (if any) + // 5. Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + for c in &table_def.constraints { + if let TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = c + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); - } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut queries = vec![create_query, insert_query, drop_query, rename_query]; @@ -248,10 +268,13 @@ pub fn build_add_constraint( Ok(queries) } else { // Build foreign key using ForeignKey::create + let fk_name = vespertide_naming::build_foreign_key_name( + table, + columns, + name.as_deref(), + ); let mut fk = ForeignKey::create(); - if let Some(n) = name { - fk = fk.name(n).to_owned(); - } + fk = fk.name(&fk_name).to_owned(); fk = fk.from_tbl(Alias::new(table)).to_owned(); for col in columns { fk = fk.from_col(Alias::new(col)).to_owned(); @@ -269,6 +292,18 @@ pub fn build_add_constraint( Ok(vec![BuiltQuery::CreateForeignKey(Box::new(fk))]) } } + TableConstraint::Index { name, columns } => { + // Index constraints are simple CREATE INDEX statements for all backends + let index_name = vespertide_naming::build_index_name(table, columns, name.as_deref()); + let mut idx = Index::create() + .table(Alias::new(table)) + .name(&index_name) + .to_owned(); + for col in columns { + idx = idx.col(Alias::new(col)).to_owned(); + } + Ok(vec![BuiltQuery::CreateIndex(Box::new(idx))]) + } TableConstraint::Check { name, expr } => { // SQLite does not support ALTER TABLE ... ADD CONSTRAINT CHECK if *backend == DatabaseBackend::Sqlite { @@ -328,19 +363,27 @@ pub fn build_add_constraint( // 4. Rename temporary table to original name let rename_query = build_rename_table(&temp_table, table); - // 5. Recreate indexes (if any) + // 5. Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + for c in &table_def.constraints { + if let TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = c + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); - } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut queries = vec![create_query, insert_query, drop_query, rename_query]; @@ -394,17 +437,17 @@ mod tests { #[case::add_constraint_unique_named_postgres( "add_constraint_unique_named_postgres", DatabaseBackend::Postgres, - &["CREATE UNIQUE INDEX \"uq_email\" ON \"users\" (\"email\")"] + &["CREATE UNIQUE INDEX \"uq_users__uq_email\" ON \"users\" (\"email\")"] )] #[case::add_constraint_unique_named_mysql( "add_constraint_unique_named_mysql", DatabaseBackend::MySql, - &["CREATE UNIQUE INDEX `uq_email` ON `users` (`email`)"] + &["CREATE UNIQUE INDEX `uq_users__uq_email` ON `users` (`email`)"] )] #[case::add_constraint_unique_named_sqlite( "add_constraint_unique_named_sqlite", DatabaseBackend::Sqlite, - &["CREATE UNIQUE INDEX \"uq_email\" ON \"users\" (\"email\")"] + &["CREATE UNIQUE INDEX \"uq_users__uq_email\" ON \"users\" (\"email\")"] )] #[case::add_constraint_foreign_key_postgres( "add_constraint_foreign_key_postgres", @@ -526,7 +569,6 @@ mod tests { ] }, constraints: vec![], - indexes: vec![], }]; let result = build_add_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); @@ -586,7 +628,6 @@ mod tests { name: "chk_id".into(), expr: "id > 0".into(), }], - indexes: vec![], }]; let result = build_add_constraint( &DatabaseBackend::Sqlite, @@ -607,8 +648,6 @@ mod tests { #[test] fn test_add_constraint_primary_key_sqlite_with_indexes() { - use vespertide_core::IndexDef; - let constraint = TableConstraint::PrimaryKey { columns: vec!["id".into()], auto_increment: false, @@ -626,11 +665,9 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_id".into(), + constraints: vec![TableConstraint::Index { + name: Some("idx_id".into()), columns: vec!["id".into()], - unique: false, }], }]; let result = build_add_constraint( @@ -652,9 +689,9 @@ mod tests { } #[test] - fn test_add_constraint_primary_key_sqlite_with_unique_index() { - use vespertide_core::IndexDef; - + fn test_add_constraint_primary_key_sqlite_with_unique_constraint() { + // Note: Unique indexes are now TableConstraint::Unique, not Index + // Index constraints don't have a unique flag - use Unique constraint instead let constraint = TableConstraint::PrimaryKey { columns: vec!["id".into()], auto_increment: false, @@ -672,11 +709,9 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_email".into(), + constraints: vec![TableConstraint::Unique { + name: Some("uq_email".into()), columns: vec!["email".into()], - unique: true, }], }]; let result = build_add_constraint( @@ -692,9 +727,8 @@ mod tests { .map(|q| q.build(DatabaseBackend::Sqlite)) .collect::>() .join("\n"); - // Should recreate unique index - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_email")); + // Unique constraint should be in CREATE TABLE statement (for SQLite temp table approach) + assert!(sql.contains("CREATE TABLE")); } #[test] @@ -746,7 +780,6 @@ mod tests { name: "chk_user_id".into(), expr: "user_id > 0".into(), }], - indexes: vec![], }]; let result = build_add_constraint( &DatabaseBackend::Sqlite, @@ -767,8 +800,6 @@ mod tests { #[test] fn test_add_constraint_foreign_key_sqlite_with_indexes() { - use vespertide_core::IndexDef; - let constraint = TableConstraint::ForeignKey { name: Some("fk_user".into()), columns: vec!["user_id".into()], @@ -790,11 +821,9 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_user_id".into(), + constraints: vec![TableConstraint::Index { + name: Some("idx_user_id".into()), columns: vec!["user_id".into()], - unique: false, }], }]; let result = build_add_constraint( @@ -816,9 +845,8 @@ mod tests { } #[test] - fn test_add_constraint_foreign_key_sqlite_with_unique_index() { - use vespertide_core::IndexDef; - + fn test_add_constraint_foreign_key_sqlite_with_unique_constraint() { + // Note: Unique indexes are now TableConstraint::Unique let constraint = TableConstraint::ForeignKey { name: Some("fk_user".into()), columns: vec!["user_id".into()], @@ -840,11 +868,9 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_user_id".into(), + constraints: vec![TableConstraint::Unique { + name: Some("uq_user_id".into()), columns: vec!["user_id".into()], - unique: true, }], }]; let result = build_add_constraint( @@ -860,9 +886,8 @@ mod tests { .map(|q| q.build(DatabaseBackend::Sqlite)) .collect::>() .join("\n"); - // Should recreate unique index - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_user_id")); + // Unique constraint should be in CREATE TABLE statement + assert!(sql.contains("CREATE TABLE")); } #[test] @@ -904,7 +929,6 @@ mod tests { foreign_key: None, }], constraints: vec![], // No existing CHECK constraints - indexes: vec![], }]; let result = build_add_constraint( &DatabaseBackend::Sqlite, @@ -946,7 +970,6 @@ mod tests { foreign_key: None, }], constraints: vec![], // No existing CHECK constraints - indexes: vec![], }]; let result = build_add_constraint( &DatabaseBackend::Sqlite, @@ -992,7 +1015,6 @@ mod tests { foreign_key: None, }], constraints: vec![], // No existing CHECK constraints - indexes: vec![], }]; let result = build_add_constraint( &DatabaseBackend::Sqlite, @@ -1014,8 +1036,6 @@ mod tests { #[test] fn test_add_constraint_check_sqlite_with_indexes() { - use vespertide_core::IndexDef; - let constraint = TableConstraint::Check { name: "chk_age".into(), expr: "age > 0".into(), @@ -1033,11 +1053,9 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_age".into(), + constraints: vec![TableConstraint::Index { + name: Some("idx_age".into()), columns: vec!["age".into()], - unique: false, }], }]; let result = build_add_constraint( @@ -1059,9 +1077,8 @@ mod tests { } #[test] - fn test_add_constraint_check_sqlite_with_unique_index() { - use vespertide_core::IndexDef; - + fn test_add_constraint_check_sqlite_with_unique_constraint() { + // Note: Unique indexes are now TableConstraint::Unique let constraint = TableConstraint::Check { name: "chk_age".into(), expr: "age > 0".into(), @@ -1079,11 +1096,9 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_age".into(), + constraints: vec![TableConstraint::Unique { + name: Some("uq_age".into()), columns: vec!["age".into()], - unique: true, }], }]; let result = build_add_constraint( @@ -1099,9 +1114,8 @@ mod tests { .map(|q| q.build(DatabaseBackend::Sqlite)) .collect::>() .join("\n"); - // Should recreate unique index - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_age")); + // Unique constraint should be in CREATE TABLE statement + assert!(sql.contains("CREATE TABLE")); } #[test] diff --git a/crates/vespertide-query/src/sql/add_index.rs b/crates/vespertide-query/src/sql/add_index.rs deleted file mode 100644 index ebb6c9e..0000000 --- a/crates/vespertide-query/src/sql/add_index.rs +++ /dev/null @@ -1,88 +0,0 @@ -use sea_query::{Alias, Index}; - -use vespertide_core::IndexDef; - -use super::types::BuiltQuery; - -pub fn build_add_index(table: &str, index: &IndexDef) -> BuiltQuery { - let mut stmt = Index::create() - .name(&index.name) - .table(Alias::new(table)) - .to_owned(); - - for col in &index.columns { - stmt = stmt.col(Alias::new(col)).to_owned(); - } - - if index.unique { - stmt = stmt.unique().to_owned(); - } - - BuiltQuery::CreateIndex(Box::new(stmt)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sql::types::DatabaseBackend; - use insta::{assert_snapshot, with_settings}; - use rstest::rstest; - use vespertide_core::IndexDef; - - #[rstest] - #[case::add_index_postgres( - "add_index_postgres", - DatabaseBackend::Postgres, - &["CREATE INDEX \"idx_email\" ON \"users\" (\"email\")"] - )] - #[case::add_index_mysql( - "add_index_mysql", - DatabaseBackend::MySql, - &["CREATE INDEX `idx_email` ON `users` (`email`)"] - )] - #[case::add_index_sqlite( - "add_index_sqlite", - DatabaseBackend::Sqlite, - &["CREATE INDEX \"idx_email\" ON \"users\" (\"email\")"] - )] - #[case::add_unique_index_postgres( - "add_unique_index_postgres", - DatabaseBackend::Postgres, - &["CREATE UNIQUE INDEX \"idx_email\" ON \"users\" (\"email\")"] - )] - #[case::add_unique_index_mysql( - "add_unique_index_mysql", - DatabaseBackend::MySql, - &["CREATE UNIQUE INDEX `idx_email` ON `users` (`email`)"] - )] - #[case::add_unique_index_sqlite( - "add_unique_index_sqlite", - DatabaseBackend::Sqlite, - &["CREATE UNIQUE INDEX \"idx_email\" ON \"users\" (\"email\")"] - )] - fn test_add_index( - #[case] title: &str, - #[case] backend: DatabaseBackend, - #[case] expected: &[&str], - ) { - let index = IndexDef { - name: "idx_email".into(), - columns: vec!["email".into()], - unique: title.contains("unique"), - }; - let result = build_add_index("users", &index); - let sql = result.build(backend); - for exp in expected { - assert!( - sql.contains(exp), - "Expected SQL to contain '{}', got: {}", - exp, - sql - ); - } - - with_settings!({ snapshot_suffix => format!("add_index_{}", title) }, { - assert_snapshot!(sql); - }); - } -} diff --git a/crates/vespertide-query/src/sql/create_table.rs b/crates/vespertide-query/src/sql/create_table.rs index 1894da6..38e0023 100644 --- a/crates/vespertide-query/src/sql/create_table.rs +++ b/crates/vespertide-query/src/sql/create_table.rs @@ -30,10 +30,9 @@ pub(crate) fn build_create_table_for_backend( col.primary_key(); } - // Check for inline unique constraint - if column.unique.is_some() { - col.unique_key(); - } + // NOTE: We do NOT add inline unique constraints here. + // All unique constraints are handled as separate CREATE UNIQUE INDEX statements + // so they have proper names and can be dropped later. stmt = stmt.col(col).to_owned(); } @@ -59,10 +58,13 @@ pub(crate) fn build_create_table_for_backend( // For MySQL, we can add unique index directly in CREATE TABLE // For Postgres and SQLite, we'll handle it separately in build_create_table if matches!(backend, DatabaseBackend::MySql) { - let mut idx = Index::create().unique().to_owned(); - if let Some(n) = name { - idx = idx.name(n).to_owned(); - } + // Always generate a proper name: uq_{table}_{key} or uq_{table}_{columns} + let index_name = super::helpers::build_unique_constraint_name( + table, + unique_cols, + name.as_deref(), + ); + let mut idx = Index::create().name(&index_name).unique().to_owned(); for col in unique_cols { idx = idx.col(Alias::new(col)).to_owned(); } @@ -79,10 +81,10 @@ pub(crate) fn build_create_table_for_backend( on_delete, on_update, } => { - let mut fk = ForeignKey::create(); - if let Some(n) = name { - fk = fk.name(n).to_owned(); - } + // Always generate a proper name: fk_{table}_{key} or fk_{table}_{columns} + let fk_name = + super::helpers::build_foreign_key_name(table, fk_cols, name.as_deref()); + let mut fk = ForeignKey::create().name(&fk_name).to_owned(); fk = fk.from_tbl(Alias::new(table)).to_owned(); for col in fk_cols { fk = fk.from_col(Alias::new(col)).to_owned(); @@ -104,6 +106,10 @@ pub(crate) fn build_create_table_for_backend( // This would need to be handled as raw SQL or post-creation ALTER let _ = (name, expr); } + TableConstraint::Index { .. } => { + // Indexes are added separately after CREATE TABLE as CREATE INDEX statements + // They will be handled in build_create_table + } } } @@ -116,6 +122,21 @@ pub fn build_create_table( columns: &[ColumnDef], constraints: &[TableConstraint], ) -> Result, QueryError> { + // Normalize the table to convert inline constraints to table-level + // This ensures we don't have duplicate constraints if both inline and table-level are defined + let table_def = vespertide_core::TableDef { + name: table.to_string(), + columns: columns.to_vec(), + constraints: constraints.to_vec(), + }; + let normalized = table_def + .normalize() + .map_err(|e| QueryError::Other(format!("Failed to normalize table '{}': {}", table, e)))?; + + // Use normalized columns and constraints for SQL generation + let columns = &normalized.columns; + let constraints = &normalized.constraints; + let mut queries = Vec::new(); // Create enum types first (PostgreSQL only) @@ -180,10 +201,17 @@ pub fn build_create_table( columns: unique_cols, } = constraint { - let mut idx = Index::create().table(Alias::new(table)).unique().to_owned(); - if let Some(n) = name { - idx = idx.name(n).to_owned(); - } + // Always generate a proper name: uq_{table}_{key} or uq_{table}_{columns} + let index_name = super::helpers::build_unique_constraint_name( + table, + unique_cols, + name.as_deref(), + ); + let mut idx = Index::create() + .table(Alias::new(table)) + .name(&index_name) + .unique() + .to_owned(); for col in unique_cols { idx = idx.col(Alias::new(col)).to_owned(); } @@ -192,6 +220,26 @@ pub fn build_create_table( } } + // Add Index constraints as CREATE INDEX statements (for all backends) + for constraint in constraints { + if let TableConstraint::Index { + name, + columns: index_cols, + } = constraint + { + // Always generate a proper name: ix_{table}_{key} or ix_{table}_{columns} + let index_name = super::helpers::build_index_name(table, index_cols, name.as_deref()); + let mut idx = Index::create() + .table(Alias::new(table)) + .name(&index_name) + .to_owned(); + for col in index_cols { + idx = idx.col(Alias::new(col)).to_owned(); + } + queries.push(BuiltQuery::CreateIndex(Box::new(idx))); + } + } + Ok(queries) } @@ -268,7 +316,8 @@ mod tests { #[case::inline_unique_mysql(DatabaseBackend::MySql)] #[case::inline_unique_sqlite(DatabaseBackend::Sqlite)] fn test_create_table_with_inline_unique(#[case] backend: DatabaseBackend) { - // Test inline unique constraint (line 32) + // Test that inline unique constraint is converted to table-level during normalization. + // build_create_table now normalizes the table, so inline unique becomes a CREATE UNIQUE INDEX. use vespertide_core::schema::str_or_bool::StrOrBoolOrArray; let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); @@ -281,6 +330,7 @@ mod tests { col("id", ColumnType::Simple(SimpleColumnType::Integer)), email_col, ], + // No explicit table-level unique constraint passed, but normalize will create one from inline &[], ) .unwrap(); @@ -289,7 +339,13 @@ mod tests { .map(|q| q.build(backend)) .collect::>() .join("\n"); - assert!(sql.contains("UNIQUE")); + + // After normalization, inline unique should produce UNIQUE constraint in SQL + assert!( + sql.contains("UNIQUE") || sql.to_uppercase().contains("UNIQUE"), + "Normalized unique constraint should be in SQL, but not found: {}", + sql + ); with_settings!({ snapshot_suffix => format!("create_table_with_inline_unique_{:?}", backend) }, { assert_snapshot!(sql); }); diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 2ed0430..5951f76 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -238,10 +238,15 @@ pub fn is_enum_type(column_type: &ColumnType) -> bool { ) } -/// Generate CHECK constraint name for SQLite enum column -/// Format: chk_{table}_{column} +// Re-export naming functions from vespertide-naming +pub use vespertide_naming::{ + build_check_constraint_name, build_foreign_key_name, build_index_name, + build_unique_constraint_name, +}; + +/// Alias for build_check_constraint_name for SQLite enum columns pub fn build_sqlite_enum_check_name(table: &str, column: &str) -> String { - format!("chk_{}_{}", table, column) + build_check_constraint_name(table, column) } /// Generate CHECK constraint expression for SQLite enum column diff --git a/crates/vespertide-query/src/sql/mod.rs b/crates/vespertide-query/src/sql/mod.rs index dc95705..a4396ef 100644 --- a/crates/vespertide-query/src/sql/mod.rs +++ b/crates/vespertide-query/src/sql/mod.rs @@ -1,6 +1,5 @@ pub mod add_column; pub mod add_constraint; -pub mod add_index; pub mod create_table; pub mod delete_column; pub mod delete_table; @@ -8,7 +7,6 @@ pub mod helpers; pub mod modify_column_type; pub mod raw_sql; pub mod remove_constraint; -pub mod remove_index; pub mod rename_column; pub mod rename_table; pub mod types; @@ -20,12 +18,11 @@ use crate::error::QueryError; use vespertide_core::{MigrationAction, TableDef}; use self::{ - add_column::build_add_column, add_constraint::build_add_constraint, add_index::build_add_index, + add_column::build_add_column, add_constraint::build_add_constraint, create_table::build_create_table, delete_column::build_delete_column, delete_table::build_delete_table, modify_column_type::build_modify_column_type, raw_sql::build_raw_sql, remove_constraint::build_remove_constraint, - remove_index::build_remove_index, rename_column::build_rename_column, - rename_table::build_rename_table, + rename_column::build_rename_column, rename_table::build_rename_table, }; pub fn build_action_queries( @@ -68,10 +65,6 @@ pub fn build_action_queries( new_type, } => build_modify_column_type(backend, table, column, new_type, current_schema), - MigrationAction::AddIndex { table, index } => Ok(vec![build_add_index(table, index)]), - - MigrationAction::RemoveIndex { table, name } => Ok(vec![build_remove_index(table, name)]), - MigrationAction::RenameTable { from, to } => Ok(vec![build_rename_table(from, to)]), MigrationAction::RawSql { sql } => Ok(vec![build_raw_sql(sql.clone())]), @@ -404,7 +397,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }]; let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); assert!(!result.is_empty()); @@ -421,14 +413,17 @@ mod tests { } #[rstest] - #[case::remove_index_postgres(DatabaseBackend::Postgres)] - #[case::remove_index_mysql(DatabaseBackend::MySql)] - #[case::remove_index_sqlite(DatabaseBackend::Sqlite)] - fn test_build_action_queries_remove_index(#[case] backend: DatabaseBackend) { - // Test MigrationAction::RemoveIndex (line 67) - let action = MigrationAction::RemoveIndex { + #[case::remove_index_constraint_postgres(DatabaseBackend::Postgres)] + #[case::remove_index_constraint_mysql(DatabaseBackend::MySql)] + #[case::remove_index_constraint_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_remove_index_constraint(#[case] backend: DatabaseBackend) { + // Test MigrationAction::RemoveConstraint with Index variant + let action = MigrationAction::RemoveConstraint { table: "users".into(), - name: "idx_email".into(), + constraint: TableConstraint::Index { + name: Some("idx_email".into()), + columns: vec!["email".into()], + }, }; let result = build_action_queries(&backend, &action, &[]).unwrap(); assert_eq!(result.len(), 1); @@ -436,7 +431,7 @@ mod tests { assert!(sql.contains("DROP INDEX")); assert!(sql.contains("idx_email")); - with_settings!({ snapshot_suffix => format!("remove_index_{:?}", backend) }, { + with_settings!({ snapshot_suffix => format!("remove_index_constraint_{:?}", backend) }, { assert_snapshot!(sql); }); } @@ -503,7 +498,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], }]; let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); assert!(!result.is_empty()); @@ -562,7 +556,6 @@ mod tests { name: Some("uq_email".into()), columns: vec!["email".into()], }], - indexes: vec![], }]; let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); assert!(!result.is_empty()); @@ -613,7 +606,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }]; let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); assert!(!result.is_empty()); @@ -631,17 +623,16 @@ mod tests { } #[rstest] - #[case::add_index_postgres(DatabaseBackend::Postgres)] - #[case::add_index_mysql(DatabaseBackend::MySql)] - #[case::add_index_sqlite(DatabaseBackend::Sqlite)] - fn test_build_action_queries_add_index(#[case] backend: DatabaseBackend) { - // Test MigrationAction::AddIndex (line 65) - let action = MigrationAction::AddIndex { + #[case::add_index_constraint_postgres(DatabaseBackend::Postgres)] + #[case::add_index_constraint_mysql(DatabaseBackend::MySql)] + #[case::add_index_constraint_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_add_index_constraint(#[case] backend: DatabaseBackend) { + // Test MigrationAction::AddConstraint with Index variant + let action = MigrationAction::AddConstraint { table: "users".into(), - index: vespertide_core::IndexDef { - name: "idx_email".into(), + constraint: TableConstraint::Index { + name: Some("idx_email".into()), columns: vec!["email".into()], - unique: false, }, }; let result = build_action_queries(&backend, &action, &[]).unwrap(); @@ -650,7 +641,7 @@ mod tests { assert!(sql.contains("CREATE INDEX")); assert!(sql.contains("idx_email")); - with_settings!({ snapshot_suffix => format!("add_index_{:?}", backend) }, { + with_settings!({ snapshot_suffix => format!("add_index_constraint_{:?}", backend) }, { assert_snapshot!(sql); }); } @@ -673,4 +664,511 @@ mod tests { assert_snapshot!(sql); }); } + + // Comprehensive index naming tests + #[rstest] + #[case::add_index_with_custom_name_postgres( + DatabaseBackend::Postgres, + "hello", + vec!["email", "password"] + )] + #[case::add_index_with_custom_name_mysql( + DatabaseBackend::MySql, + "hello", + vec!["email", "password"] + )] + #[case::add_index_with_custom_name_sqlite( + DatabaseBackend::Sqlite, + "hello", + vec!["email", "password"] + )] + #[case::add_index_single_column_postgres( + DatabaseBackend::Postgres, + "email_idx", + vec!["email"] + )] + #[case::add_index_single_column_mysql( + DatabaseBackend::MySql, + "email_idx", + vec!["email"] + )] + #[case::add_index_single_column_sqlite( + DatabaseBackend::Sqlite, + "email_idx", + vec!["email"] + )] + fn test_add_index_with_custom_name( + #[case] backend: DatabaseBackend, + #[case] index_name: &str, + #[case] columns: Vec<&str>, + ) { + // Test that custom index names follow ix_table__name pattern + let action = MigrationAction::AddConstraint { + table: "user".into(), + constraint: TableConstraint::Index { + name: Some(index_name.into()), + columns: columns.iter().map(|s| s.to_string()).collect(), + }, + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + let sql = result[0].build(backend); + + // Should use ix_table__name pattern + let expected_name = format!("ix_user__{}", index_name); + assert!( + sql.contains(&expected_name), + "Expected index name '{}' in SQL: {}", + expected_name, + sql + ); + + with_settings!({ snapshot_suffix => format!("add_index_custom_{}_{:?}", index_name, backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::add_unnamed_index_single_column_postgres( + DatabaseBackend::Postgres, + vec!["email"] + )] + #[case::add_unnamed_index_single_column_mysql( + DatabaseBackend::MySql, + vec!["email"] + )] + #[case::add_unnamed_index_single_column_sqlite( + DatabaseBackend::Sqlite, + vec!["email"] + )] + #[case::add_unnamed_index_multiple_columns_postgres( + DatabaseBackend::Postgres, + vec!["email", "password"] + )] + #[case::add_unnamed_index_multiple_columns_mysql( + DatabaseBackend::MySql, + vec!["email", "password"] + )] + #[case::add_unnamed_index_multiple_columns_sqlite( + DatabaseBackend::Sqlite, + vec!["email", "password"] + )] + fn test_add_unnamed_index(#[case] backend: DatabaseBackend, #[case] columns: Vec<&str>) { + // Test that unnamed indexes follow ix_table__col1_col2 pattern + let action = MigrationAction::AddConstraint { + table: "user".into(), + constraint: TableConstraint::Index { + name: None, + columns: columns.iter().map(|s| s.to_string()).collect(), + }, + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + let sql = result[0].build(backend); + + // Should use ix_table__col1_col2... pattern + let expected_name = format!("ix_user__{}", columns.join("_")); + assert!( + sql.contains(&expected_name), + "Expected index name '{}' in SQL: {}", + expected_name, + sql + ); + + with_settings!({ snapshot_suffix => format!("add_unnamed_index_{}_{:?}", columns.join("_"), backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_index_with_custom_name_postgres( + DatabaseBackend::Postgres, + "hello", + vec!["email", "password"] + )] + #[case::remove_index_with_custom_name_mysql( + DatabaseBackend::MySql, + "hello", + vec!["email", "password"] + )] + #[case::remove_index_with_custom_name_sqlite( + DatabaseBackend::Sqlite, + "hello", + vec!["email", "password"] + )] + fn test_remove_index_with_custom_name( + #[case] backend: DatabaseBackend, + #[case] index_name: &str, + #[case] columns: Vec<&str>, + ) { + // Test that removing custom index uses ix_table__name pattern + let action = MigrationAction::RemoveConstraint { + table: "user".into(), + constraint: TableConstraint::Index { + name: Some(index_name.into()), + columns: columns.iter().map(|s| s.to_string()).collect(), + }, + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + let sql = result[0].build(backend); + + // Should use ix_table__name pattern + let expected_name = format!("ix_user__{}", index_name); + assert!( + sql.contains(&expected_name), + "Expected index name '{}' in SQL: {}", + expected_name, + sql + ); + + with_settings!({ snapshot_suffix => format!("remove_index_custom_{}_{:?}", index_name, backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_unnamed_index_single_column_postgres( + DatabaseBackend::Postgres, + vec!["email"] + )] + #[case::remove_unnamed_index_single_column_mysql( + DatabaseBackend::MySql, + vec!["email"] + )] + #[case::remove_unnamed_index_single_column_sqlite( + DatabaseBackend::Sqlite, + vec!["email"] + )] + #[case::remove_unnamed_index_multiple_columns_postgres( + DatabaseBackend::Postgres, + vec!["email", "password"] + )] + #[case::remove_unnamed_index_multiple_columns_mysql( + DatabaseBackend::MySql, + vec!["email", "password"] + )] + #[case::remove_unnamed_index_multiple_columns_sqlite( + DatabaseBackend::Sqlite, + vec!["email", "password"] + )] + fn test_remove_unnamed_index(#[case] backend: DatabaseBackend, #[case] columns: Vec<&str>) { + // Test that removing unnamed indexes uses ix_table__col1_col2 pattern + let action = MigrationAction::RemoveConstraint { + table: "user".into(), + constraint: TableConstraint::Index { + name: None, + columns: columns.iter().map(|s| s.to_string()).collect(), + }, + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + let sql = result[0].build(backend); + + // Should use ix_table__col1_col2... pattern + let expected_name = format!("ix_user__{}", columns.join("_")); + assert!( + sql.contains(&expected_name), + "Expected index name '{}' in SQL: {}", + expected_name, + sql + ); + + with_settings!({ snapshot_suffix => format!("remove_unnamed_index_{}_{:?}", columns.join("_"), backend) }, { + assert_snapshot!(sql); + }); + } + + // Comprehensive unique constraint naming tests + #[rstest] + #[case::add_unique_with_custom_name_postgres( + DatabaseBackend::Postgres, + "email_unique", + vec!["email"] + )] + #[case::add_unique_with_custom_name_mysql( + DatabaseBackend::MySql, + "email_unique", + vec!["email"] + )] + #[case::add_unique_with_custom_name_sqlite( + DatabaseBackend::Sqlite, + "email_unique", + vec!["email"] + )] + fn test_add_unique_with_custom_name( + #[case] backend: DatabaseBackend, + #[case] constraint_name: &str, + #[case] columns: Vec<&str>, + ) { + // Test that custom unique constraint names follow uq_table__name pattern + let action = MigrationAction::AddConstraint { + table: "user".into(), + constraint: TableConstraint::Unique { + name: Some(constraint_name.into()), + columns: columns.iter().map(|s| s.to_string()).collect(), + }, + }; + + let current_schema = vec![TableDef { + name: "user".into(), + columns: vec![ColumnDef { + name: "email".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }]; + + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should use uq_table__name pattern + let expected_name = format!("uq_user__{}", constraint_name); + assert!( + sql.contains(&expected_name), + "Expected unique constraint name '{}' in SQL: {}", + expected_name, + sql + ); + + with_settings!({ snapshot_suffix => format!("add_unique_custom_{}_{:?}", constraint_name, backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::add_unnamed_unique_single_column_postgres( + DatabaseBackend::Postgres, + vec!["email"] + )] + #[case::add_unnamed_unique_single_column_mysql( + DatabaseBackend::MySql, + vec!["email"] + )] + #[case::add_unnamed_unique_single_column_sqlite( + DatabaseBackend::Sqlite, + vec!["email"] + )] + #[case::add_unnamed_unique_multiple_columns_postgres( + DatabaseBackend::Postgres, + vec!["email", "username"] + )] + #[case::add_unnamed_unique_multiple_columns_mysql( + DatabaseBackend::MySql, + vec!["email", "username"] + )] + #[case::add_unnamed_unique_multiple_columns_sqlite( + DatabaseBackend::Sqlite, + vec!["email", "username"] + )] + fn test_add_unnamed_unique(#[case] backend: DatabaseBackend, #[case] columns: Vec<&str>) { + // Test that unnamed unique constraints follow uq_table__col1_col2 pattern + let action = MigrationAction::AddConstraint { + table: "user".into(), + constraint: TableConstraint::Unique { + name: None, + columns: columns.iter().map(|s| s.to_string()).collect(), + }, + }; + + let schema_columns: Vec = columns + .iter() + .map(|col| ColumnDef { + name: col.to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }) + .collect(); + + let current_schema = vec![TableDef { + name: "user".into(), + columns: schema_columns, + constraints: vec![], + }]; + + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should use uq_table__col1_col2... pattern + let expected_name = format!("uq_user__{}", columns.join("_")); + assert!( + sql.contains(&expected_name), + "Expected unique constraint name '{}' in SQL: {}", + expected_name, + sql + ); + + with_settings!({ snapshot_suffix => format!("add_unnamed_unique_{}_{:?}", columns.join("_"), backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_unique_with_custom_name_postgres( + DatabaseBackend::Postgres, + "email_unique", + vec!["email"] + )] + #[case::remove_unique_with_custom_name_mysql( + DatabaseBackend::MySql, + "email_unique", + vec!["email"] + )] + #[case::remove_unique_with_custom_name_sqlite( + DatabaseBackend::Sqlite, + "email_unique", + vec!["email"] + )] + fn test_remove_unique_with_custom_name( + #[case] backend: DatabaseBackend, + #[case] constraint_name: &str, + #[case] columns: Vec<&str>, + ) { + // Test that removing custom unique constraint uses uq_table__name pattern + let constraint = TableConstraint::Unique { + name: Some(constraint_name.into()), + columns: columns.iter().map(|s| s.to_string()).collect(), + }; + + let current_schema = vec![TableDef { + name: "user".into(), + columns: vec![ColumnDef { + name: "email".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![constraint.clone()], + }]; + + let action = MigrationAction::RemoveConstraint { + table: "user".into(), + constraint, + }; + + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should use uq_table__name pattern (for Postgres/MySQL, not SQLite which rebuilds table) + if backend != DatabaseBackend::Sqlite { + let expected_name = format!("uq_user__{}", constraint_name); + assert!( + sql.contains(&expected_name), + "Expected unique constraint name '{}' in SQL: {}", + expected_name, + sql + ); + } + + with_settings!({ snapshot_suffix => format!("remove_unique_custom_{}_{:?}", constraint_name, backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_unnamed_unique_single_column_postgres( + DatabaseBackend::Postgres, + vec!["email"] + )] + #[case::remove_unnamed_unique_single_column_mysql( + DatabaseBackend::MySql, + vec!["email"] + )] + #[case::remove_unnamed_unique_single_column_sqlite( + DatabaseBackend::Sqlite, + vec!["email"] + )] + #[case::remove_unnamed_unique_multiple_columns_postgres( + DatabaseBackend::Postgres, + vec!["email", "username"] + )] + #[case::remove_unnamed_unique_multiple_columns_mysql( + DatabaseBackend::MySql, + vec!["email", "username"] + )] + #[case::remove_unnamed_unique_multiple_columns_sqlite( + DatabaseBackend::Sqlite, + vec!["email", "username"] + )] + fn test_remove_unnamed_unique(#[case] backend: DatabaseBackend, #[case] columns: Vec<&str>) { + // Test that removing unnamed unique constraints uses uq_table__col1_col2 pattern + let constraint = TableConstraint::Unique { + name: None, + columns: columns.iter().map(|s| s.to_string()).collect(), + }; + + let schema_columns: Vec = columns + .iter() + .map(|col| ColumnDef { + name: col.to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }) + .collect(); + + let current_schema = vec![TableDef { + name: "user".into(), + columns: schema_columns, + constraints: vec![constraint.clone()], + }]; + + let action = MigrationAction::RemoveConstraint { + table: "user".into(), + constraint, + }; + + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should use uq_table__col1_col2... pattern (for Postgres/MySQL, not SQLite which rebuilds table) + if backend != DatabaseBackend::Sqlite { + let expected_name = format!("uq_user__{}", columns.join("_")); + assert!( + sql.contains(&expected_name), + "Expected unique constraint name '{}' in SQL: {}", + expected_name, + sql + ); + } + + with_settings!({ snapshot_suffix => format!("remove_unnamed_unique_{}_{:?}", columns.join("_"), backend) }, { + assert_snapshot!(sql); + }); + } } diff --git a/crates/vespertide-query/src/sql/modify_column_type.rs b/crates/vespertide-query/src/sql/modify_column_type.rs index 1f2f036..397e7ad 100644 --- a/crates/vespertide-query/src/sql/modify_column_type.rs +++ b/crates/vespertide-query/src/sql/modify_column_type.rs @@ -79,19 +79,23 @@ pub fn build_modify_column_type( // 4. Rename temporary table to original name let rename_query = build_rename_table(&temp_table, table); - // 5. Recreate indexes (if any) + // 5. Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); - } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); + for constraint in &table_def.constraints { + if let vespertide_core::TableConstraint::Index { name, columns } = constraint { + let index_name = vespertide_naming::build_index_name( + table, + columns, + name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in columns { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut queries = vec![create_query, insert_query, drop_query, rename_query]; @@ -290,7 +294,6 @@ mod tests { }, ], constraints: vec![], - indexes: vec![], }]; let result = build_modify_column_type( @@ -358,7 +361,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }]; let result = build_modify_column_type( &DatabaseBackend::Sqlite, @@ -390,8 +392,8 @@ mod tests { DatabaseBackend::Sqlite )] fn test_modify_column_type_with_index(#[case] title: &str, #[case] backend: DatabaseBackend) { - // Test modify column type with indexes (lines 85-88, 90-91, 93-94) - use vespertide_core::IndexDef; + // Test modify column type with indexes + use vespertide_core::TableConstraint; let current_schema = vec![TableDef { name: "users".into(), @@ -419,11 +421,9 @@ mod tests { foreign_key: None, }, ], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_age".into(), + constraints: vec![TableConstraint::Index { + name: Some("idx_age".into()), columns: vec!["age".into()], - unique: false, }], }]; @@ -454,24 +454,24 @@ mod tests { } #[rstest] - #[case::modify_column_type_with_unique_index_postgres( - "modify_column_type_with_unique_index_postgres", + #[case::modify_column_type_with_unique_constraint_postgres( + "modify_column_type_with_unique_constraint_postgres", DatabaseBackend::Postgres )] - #[case::modify_column_type_with_unique_index_mysql( - "modify_column_type_with_unique_index_mysql", + #[case::modify_column_type_with_unique_constraint_mysql( + "modify_column_type_with_unique_constraint_mysql", DatabaseBackend::MySql )] - #[case::modify_column_type_with_unique_index_sqlite( - "modify_column_type_with_unique_index_sqlite", + #[case::modify_column_type_with_unique_constraint_sqlite( + "modify_column_type_with_unique_constraint_sqlite", DatabaseBackend::Sqlite )] - fn test_modify_column_type_with_unique_index( + fn test_modify_column_type_with_unique_constraint( #[case] title: &str, #[case] backend: DatabaseBackend, ) { - // Test modify column type with unique index (lines 85-88, 90-91, 93-94) - use vespertide_core::IndexDef; + // Test modify column type with unique constraint + use vespertide_core::TableConstraint; let current_schema = vec![TableDef { name: "users".into(), @@ -499,11 +499,9 @@ mod tests { foreign_key: None, }, ], - constraints: vec![], - indexes: vec![IndexDef { - name: "idx_email".into(), + constraints: vec![TableConstraint::Unique { + name: Some("uq_email".into()), columns: vec!["email".into()], - unique: true, }], }]; @@ -522,13 +520,12 @@ mod tests { .collect::>() .join(";\n"); - // For SQLite, should recreate unique index + // For SQLite, unique constraint should be in CREATE TABLE statement if matches!(backend, DatabaseBackend::Sqlite) { - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_email")); + assert!(sql.contains("CREATE TABLE")); } - with_settings!({ snapshot_suffix => format!("modify_column_type_with_unique_index_{}", title) }, { + with_settings!({ snapshot_suffix => format!("modify_column_type_with_unique_constraint_{}", title) }, { assert_snapshot!(sql); }); } @@ -716,7 +713,6 @@ mod tests { foreign_key: None, }], constraints: vec![], - indexes: vec![], }]; let result = diff --git a/crates/vespertide-query/src/sql/remove_constraint.rs b/crates/vespertide-query/src/sql/remove_constraint.rs index 1bf9b73..02fb374 100644 --- a/crates/vespertide-query/src/sql/remove_constraint.rs +++ b/crates/vespertide-query/src/sql/remove_constraint.rs @@ -69,19 +69,27 @@ pub fn build_remove_constraint( // 4. Rename temporary table to original name let rename_query = build_rename_table(&temp_table, table); - // 5. Recreate indexes (if any) + // 5. Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); - } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); + for constraint in &table_def.constraints { + if let TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = constraint + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut queries = vec![create_query, insert_query, drop_query, rename_query]; @@ -177,21 +185,27 @@ pub fn build_remove_constraint( // 4. Rename temporary table to original name let rename_query = build_rename_table(&temp_table, table); - // 5. Recreate indexes (if any) - // Note: We need to filter out indexes that might be associated with the unique constraint if any - // But TableDef separates constraints and indexes. + // 5. Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); - } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); + for c in &table_def.constraints { + if let TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = c + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut queries = vec![create_query, insert_query, drop_query, rename_query]; @@ -204,11 +218,11 @@ pub fn build_remove_constraint( // However, PostgreSQL expects DROP CONSTRAINT, so we need to use Table::alter() // Since drop_constraint() doesn't exist, we'll use Index::drop() for now // Note: This may not match PostgreSQL's DROP CONSTRAINT syntax - let constraint_name = if let Some(n) = name { - n.clone() - } else { - format!("{}_{}_key", table, columns.join("_")) - }; + let constraint_name = vespertide_naming::build_unique_constraint_name( + table, + columns, + name.as_deref(), + ); // Try using Table::alter() with drop_constraint if available // If not, use Index::drop() as fallback // For PostgreSQL, we need DROP CONSTRAINT, but sea_query doesn't support this @@ -303,19 +317,27 @@ pub fn build_remove_constraint( // 4. Rename temporary table to original name let rename_query = build_rename_table(&temp_table, table); - // 5. Recreate indexes (if any) + // 5. Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); - } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); + for c in &table_def.constraints { + if let TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = c + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut queries = vec![create_query, insert_query, drop_query, rename_query]; @@ -323,11 +345,11 @@ pub fn build_remove_constraint( Ok(queries) } else { // Build foreign key drop using ForeignKey::drop() - let constraint_name = if let Some(n) = name { - n.clone() - } else { - format!("{}_{}_fkey", table, columns.join("_")) - }; + let constraint_name = vespertide_naming::build_foreign_key_name( + table, + columns, + name.as_deref(), + ); let fk_drop = ForeignKey::drop() .name(&constraint_name) .table(Alias::new(table)) @@ -335,6 +357,21 @@ pub fn build_remove_constraint( Ok(vec![BuiltQuery::DropForeignKey(Box::new(fk_drop))]) } } + TableConstraint::Index { name, columns } => { + // Index constraints are simple DROP INDEX statements for all backends + let index_name = if let Some(n) = name { + // Use naming convention for named indexes + vespertide_naming::build_index_name(table, columns, Some(n)) + } else { + // Generate name from table and columns for unnamed indexes + vespertide_naming::build_index_name(table, columns, None) + }; + let idx_drop = sea_query::Index::drop() + .table(Alias::new(table)) + .name(&index_name) + .to_owned(); + Ok(vec![BuiltQuery::DropIndex(Box::new(idx_drop))]) + } TableConstraint::Check { name, .. } => { // SQLite does not support ALTER TABLE ... DROP CONSTRAINT CHECK if *backend == DatabaseBackend::Sqlite { @@ -396,19 +433,27 @@ pub fn build_remove_constraint( // 4. Rename temporary table to original name let rename_query = build_rename_table(&temp_table, table); - // 5. Recreate indexes (if any) + // 5. Recreate indexes from Index constraints let mut index_queries = Vec::new(); - for index in &table_def.indexes { - let mut idx_stmt = sea_query::Index::create(); - idx_stmt = idx_stmt.name(&index.name).to_owned(); - for col_name in &index.columns { - idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); - } - if index.unique { - idx_stmt = idx_stmt.unique().to_owned(); + for c in &table_def.constraints { + if let TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = c + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } - idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); - index_queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); } let mut queries = vec![create_query, insert_query, drop_query, rename_query]; @@ -454,12 +499,12 @@ mod tests { #[case::remove_constraint_unique_named_postgres( "remove_constraint_unique_named_postgres", DatabaseBackend::Postgres, - &["DROP CONSTRAINT \"uq_email\""] + &["DROP CONSTRAINT \"uq_users__uq_email\""] )] #[case::remove_constraint_unique_named_mysql( "remove_constraint_unique_named_mysql", DatabaseBackend::MySql, - &["DROP INDEX `uq_email`"] + &["DROP INDEX `uq_users__uq_email`"] )] #[case::remove_constraint_unique_named_sqlite( "remove_constraint_unique_named_sqlite", @@ -469,12 +514,12 @@ mod tests { #[case::remove_constraint_foreign_key_named_postgres( "remove_constraint_foreign_key_named_postgres", DatabaseBackend::Postgres, - &["DROP CONSTRAINT \"fk_user\""] + &["DROP CONSTRAINT \"fk_users__fk_user\""] )] #[case::remove_constraint_foreign_key_named_mysql( "remove_constraint_foreign_key_named_mysql", DatabaseBackend::MySql, - &["DROP FOREIGN KEY `fk_user`"] + &["DROP FOREIGN KEY `fk_users__fk_user`"] )] #[case::remove_constraint_foreign_key_named_sqlite( "remove_constraint_foreign_key_named_sqlite", @@ -595,7 +640,6 @@ mod tests { }] }, constraints: vec![constraint.clone()], - indexes: vec![], }]; let result = @@ -638,9 +682,7 @@ mod tests { #[case::remove_primary_key_with_index_mysql(DatabaseBackend::MySql)] #[case::remove_primary_key_with_index_sqlite(DatabaseBackend::Sqlite)] fn test_remove_constraint_primary_key_with_index(#[case] backend: DatabaseBackend) { - // Test PrimaryKey removal with indexes (lines 75-78, 83-84) - use vespertide_core::IndexDef; - + // Test PrimaryKey removal with indexes let constraint = TableConstraint::PrimaryKey { columns: vec!["id".into()], auto_increment: false, @@ -658,12 +700,13 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![constraint.clone()], - indexes: vec![IndexDef { - name: "idx_id".into(), - columns: vec!["id".into()], - unique: false, - }], + constraints: vec![ + constraint.clone(), + TableConstraint::Index { + name: Some("idx_id".into()), + columns: vec!["id".into()], + }, + ], }]; let result = @@ -676,7 +719,7 @@ mod tests { if matches!(backend, DatabaseBackend::Sqlite) { assert!(sql.contains("CREATE INDEX")); - assert!(sql.contains("idx_id")); + assert!(sql.contains("ix_users__idx_id")); } with_settings!({ snapshot_suffix => format!("remove_primary_key_with_index_{:?}", backend) }, { @@ -685,13 +728,11 @@ mod tests { } #[rstest] - #[case::remove_primary_key_with_unique_index_postgres(DatabaseBackend::Postgres)] - #[case::remove_primary_key_with_unique_index_mysql(DatabaseBackend::MySql)] - #[case::remove_primary_key_with_unique_index_sqlite(DatabaseBackend::Sqlite)] - fn test_remove_constraint_primary_key_with_unique_index(#[case] backend: DatabaseBackend) { - // Test PrimaryKey removal with unique index (lines 75-78, 80-81, 83-84) - use vespertide_core::IndexDef; - + #[case::remove_primary_key_with_unique_constraint_postgres(DatabaseBackend::Postgres)] + #[case::remove_primary_key_with_unique_constraint_mysql(DatabaseBackend::MySql)] + #[case::remove_primary_key_with_unique_constraint_sqlite(DatabaseBackend::Sqlite)] + fn test_remove_constraint_primary_key_with_unique_constraint(#[case] backend: DatabaseBackend) { + // Test PrimaryKey removal with unique constraint let constraint = TableConstraint::PrimaryKey { columns: vec!["id".into()], auto_increment: false, @@ -709,12 +750,13 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![constraint.clone()], - indexes: vec![IndexDef { - name: "idx_email".into(), - columns: vec!["email".into()], - unique: true, - }], + constraints: vec![ + constraint.clone(), + TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }, + ], }]; let result = @@ -726,11 +768,11 @@ mod tests { .join("\n"); if matches!(backend, DatabaseBackend::Sqlite) { - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_email")); + // Unique constraint should be in the temp table definition + assert!(sql.contains("CREATE TABLE")); } - with_settings!({ snapshot_suffix => format!("remove_primary_key_with_unique_index_{:?}", backend) }, { + with_settings!({ snapshot_suffix => format!("remove_primary_key_with_unique_constraint_{:?}", backend) }, { assert_snapshot!(sql); }); } @@ -790,7 +832,6 @@ mod tests { }, ], constraints: vec![constraint.clone()], - indexes: vec![], }]; let result = @@ -816,9 +857,7 @@ mod tests { #[case::remove_unique_with_index_mysql(DatabaseBackend::MySql)] #[case::remove_unique_with_index_sqlite(DatabaseBackend::Sqlite)] fn test_remove_constraint_unique_with_index(#[case] backend: DatabaseBackend) { - // Test Unique removal with indexes (lines 185-188, 193-194) - use vespertide_core::IndexDef; - + // Test Unique removal with indexes let constraint = TableConstraint::Unique { name: Some("uq_email".into()), columns: vec!["email".into()], @@ -849,12 +888,13 @@ mod tests { foreign_key: None, }, ], - constraints: vec![constraint.clone()], - indexes: vec![IndexDef { - name: "idx_id".into(), - columns: vec!["id".into()], - unique: false, - }], + constraints: vec![ + constraint.clone(), + TableConstraint::Index { + name: Some("idx_id".into()), + columns: vec!["id".into()], + }, + ], }]; let result = @@ -867,7 +907,7 @@ mod tests { if matches!(backend, DatabaseBackend::Sqlite) { assert!(sql.contains("CREATE INDEX")); - assert!(sql.contains("idx_id")); + assert!(sql.contains("ix_users__idx_id")); } with_settings!({ snapshot_suffix => format!("remove_unique_with_index_{:?}", backend) }, { @@ -876,13 +916,13 @@ mod tests { } #[rstest] - #[case::remove_unique_with_unique_index_postgres(DatabaseBackend::Postgres)] - #[case::remove_unique_with_unique_index_mysql(DatabaseBackend::MySql)] - #[case::remove_unique_with_unique_index_sqlite(DatabaseBackend::Sqlite)] - fn test_remove_constraint_unique_with_unique_index(#[case] backend: DatabaseBackend) { - // Test Unique removal with unique index (lines 185-188, 190-191, 193-194) - use vespertide_core::IndexDef; - + #[case::remove_unique_with_other_unique_constraint_postgres(DatabaseBackend::Postgres)] + #[case::remove_unique_with_other_unique_constraint_mysql(DatabaseBackend::MySql)] + #[case::remove_unique_with_other_unique_constraint_sqlite(DatabaseBackend::Sqlite)] + fn test_remove_constraint_unique_with_other_unique_constraint( + #[case] backend: DatabaseBackend, + ) { + // Test Unique removal with another unique constraint let constraint = TableConstraint::Unique { name: Some("uq_email".into()), columns: vec!["email".into()], @@ -913,12 +953,13 @@ mod tests { foreign_key: None, }, ], - constraints: vec![constraint.clone()], - indexes: vec![IndexDef { - name: "idx_name".into(), - columns: vec!["name".into()], - unique: true, - }], + constraints: vec![ + constraint.clone(), + TableConstraint::Unique { + name: Some("uq_name".into()), + columns: vec!["name".into()], + }, + ], }]; let result = @@ -930,11 +971,11 @@ mod tests { .join("\n"); if matches!(backend, DatabaseBackend::Sqlite) { - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_name")); + // The remaining unique constraint should be preserved + assert!(sql.contains("CREATE TABLE")); } - with_settings!({ snapshot_suffix => format!("remove_unique_with_unique_index_{:?}", backend) }, { + with_settings!({ snapshot_suffix => format!("remove_unique_with_other_unique_constraint_{:?}", backend) }, { assert_snapshot!(sql); }); } @@ -1002,7 +1043,6 @@ mod tests { }, ], constraints: vec![constraint.clone()], - indexes: vec![], }]; let result = @@ -1028,9 +1068,7 @@ mod tests { #[case::remove_foreign_key_with_index_mysql(DatabaseBackend::MySql)] #[case::remove_foreign_key_with_index_sqlite(DatabaseBackend::Sqlite)] fn test_remove_constraint_foreign_key_with_index(#[case] backend: DatabaseBackend) { - // Test ForeignKey removal with indexes (lines 309-312, 317-318) - use vespertide_core::IndexDef; - + // Test ForeignKey removal with indexes let constraint = TableConstraint::ForeignKey { name: Some("fk_user".into()), columns: vec!["user_id".into()], @@ -1065,12 +1103,13 @@ mod tests { foreign_key: None, }, ], - constraints: vec![constraint.clone()], - indexes: vec![IndexDef { - name: "idx_user_id".into(), - columns: vec!["user_id".into()], - unique: false, - }], + constraints: vec![ + constraint.clone(), + TableConstraint::Index { + name: Some("idx_user_id".into()), + columns: vec!["user_id".into()], + }, + ], }]; let result = @@ -1092,13 +1131,11 @@ mod tests { } #[rstest] - #[case::remove_foreign_key_with_unique_index_postgres(DatabaseBackend::Postgres)] - #[case::remove_foreign_key_with_unique_index_mysql(DatabaseBackend::MySql)] - #[case::remove_foreign_key_with_unique_index_sqlite(DatabaseBackend::Sqlite)] - fn test_remove_constraint_foreign_key_with_unique_index(#[case] backend: DatabaseBackend) { - // Test ForeignKey removal with unique index (lines 309-312, 314-315, 317-318) - use vespertide_core::IndexDef; - + #[case::remove_foreign_key_with_unique_constraint_postgres(DatabaseBackend::Postgres)] + #[case::remove_foreign_key_with_unique_constraint_mysql(DatabaseBackend::MySql)] + #[case::remove_foreign_key_with_unique_constraint_sqlite(DatabaseBackend::Sqlite)] + fn test_remove_constraint_foreign_key_with_unique_constraint(#[case] backend: DatabaseBackend) { + // Test ForeignKey removal with unique constraint let constraint = TableConstraint::ForeignKey { name: Some("fk_user".into()), columns: vec!["user_id".into()], @@ -1133,12 +1170,13 @@ mod tests { foreign_key: None, }, ], - constraints: vec![constraint.clone()], - indexes: vec![IndexDef { - name: "idx_user_id".into(), - columns: vec!["user_id".into()], - unique: true, - }], + constraints: vec![ + constraint.clone(), + TableConstraint::Unique { + name: Some("uq_user_id".into()), + columns: vec!["user_id".into()], + }, + ], }]; let result = @@ -1150,11 +1188,11 @@ mod tests { .join("\n"); if matches!(backend, DatabaseBackend::Sqlite) { - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_user_id")); + // Unique constraint should be preserved in the temp table + assert!(sql.contains("CREATE TABLE")); } - with_settings!({ snapshot_suffix => format!("remove_foreign_key_with_unique_index_{:?}", backend) }, { + with_settings!({ snapshot_suffix => format!("remove_foreign_key_with_unique_constraint_{:?}", backend) }, { assert_snapshot!(sql); }); } @@ -1182,9 +1220,7 @@ mod tests { #[case::remove_check_with_index_mysql(DatabaseBackend::MySql)] #[case::remove_check_with_index_sqlite(DatabaseBackend::Sqlite)] fn test_remove_constraint_check_with_index(#[case] backend: DatabaseBackend) { - // Test Check removal with indexes (lines 402-405, 410-411) - use vespertide_core::IndexDef; - + // Test Check removal with indexes let constraint = TableConstraint::Check { name: "chk_age".into(), expr: "age > 0".into(), @@ -1215,12 +1251,13 @@ mod tests { foreign_key: None, }, ], - constraints: vec![constraint.clone()], - indexes: vec![IndexDef { - name: "idx_age".into(), - columns: vec!["age".into()], - unique: false, - }], + constraints: vec![ + constraint.clone(), + TableConstraint::Index { + name: Some("idx_age".into()), + columns: vec!["age".into()], + }, + ], }]; let result = @@ -1242,13 +1279,11 @@ mod tests { } #[rstest] - #[case::remove_check_with_unique_index_postgres(DatabaseBackend::Postgres)] - #[case::remove_check_with_unique_index_mysql(DatabaseBackend::MySql)] - #[case::remove_check_with_unique_index_sqlite(DatabaseBackend::Sqlite)] - fn test_remove_constraint_check_with_unique_index(#[case] backend: DatabaseBackend) { - // Test Check removal with unique index (lines 402-405, 407-408, 410-411) - use vespertide_core::IndexDef; - + #[case::remove_check_with_unique_constraint_postgres(DatabaseBackend::Postgres)] + #[case::remove_check_with_unique_constraint_mysql(DatabaseBackend::MySql)] + #[case::remove_check_with_unique_constraint_sqlite(DatabaseBackend::Sqlite)] + fn test_remove_constraint_check_with_unique_constraint(#[case] backend: DatabaseBackend) { + // Test Check removal with unique constraint let constraint = TableConstraint::Check { name: "chk_age".into(), expr: "age > 0".into(), @@ -1279,12 +1314,13 @@ mod tests { foreign_key: None, }, ], - constraints: vec![constraint.clone()], - indexes: vec![IndexDef { - name: "idx_age".into(), - columns: vec!["age".into()], - unique: true, - }], + constraints: vec![ + constraint.clone(), + TableConstraint::Unique { + name: Some("uq_age".into()), + columns: vec!["age".into()], + }, + ], }]; let result = @@ -1296,11 +1332,11 @@ mod tests { .join("\n"); if matches!(backend, DatabaseBackend::Sqlite) { - assert!(sql.contains("CREATE UNIQUE INDEX")); - assert!(sql.contains("idx_age")); + // Unique constraint should be preserved in the temp table + assert!(sql.contains("CREATE TABLE")); } - with_settings!({ snapshot_suffix => format!("remove_check_with_unique_index_{:?}", backend) }, { + with_settings!({ snapshot_suffix => format!("remove_check_with_unique_constraint_{:?}", backend) }, { assert_snapshot!(sql); }); } @@ -1352,7 +1388,6 @@ mod tests { expr: "email IS NOT NULL".into(), }, ], - indexes: vec![], }]; let result = @@ -1426,7 +1461,6 @@ mod tests { expr: "user_id > 0".into(), }, ], - indexes: vec![], }]; let result = @@ -1492,7 +1526,6 @@ mod tests { }, constraint.clone(), ], - indexes: vec![], }]; let result = @@ -1510,4 +1543,51 @@ mod tests { assert_snapshot!(sql); }); } + + #[rstest] + #[case::remove_index_with_custom_inline_name_postgres(DatabaseBackend::Postgres)] + #[case::remove_index_with_custom_inline_name_mysql(DatabaseBackend::MySql)] + #[case::remove_index_with_custom_inline_name_sqlite(DatabaseBackend::Sqlite)] + fn test_remove_constraint_index_with_custom_inline_name(#[case] backend: DatabaseBackend) { + // Test Index removal with a custom name from inline index field + // This tests the scenario where index: "custom_idx_name" is used + let constraint = TableConstraint::Index { + name: Some("custom_idx_email".into()), + columns: vec!["email".into()], + }; + + let schema = vec![TableDef { + name: "users".to_string(), + columns: vec![ColumnDef { + name: "email".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: Some(vespertide_core::StrOrBoolOrArray::Str( + "custom_idx_email".into(), + )), + foreign_key: None, + }], + constraints: vec![], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, &schema); + assert!(result.is_ok()); + let sql = result + .unwrap() + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should use the custom index name + assert!(sql.contains("custom_idx_email")); + + with_settings!({ snapshot_suffix => format!("remove_index_custom_name_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } } diff --git a/crates/vespertide-query/src/sql/remove_index.rs b/crates/vespertide-query/src/sql/remove_index.rs deleted file mode 100644 index a26a67a..0000000 --- a/crates/vespertide-query/src/sql/remove_index.rs +++ /dev/null @@ -1,57 +0,0 @@ -use sea_query::{Alias, Index}; - -use super::types::BuiltQuery; - -pub fn build_remove_index(table: &str, name: &str) -> BuiltQuery { - let stmt = Index::drop() - .name(name) - // MySQL requires ON ; other backends accept this form - .table(Alias::new(table)) - .to_owned(); - BuiltQuery::DropIndex(Box::new(stmt)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sql::types::DatabaseBackend; - use insta::{assert_snapshot, with_settings}; - use rstest::rstest; - - #[rstest] - #[case::remove_index_postgres( - "remove_index_postgres", - DatabaseBackend::Postgres, - &["DROP INDEX \"idx_email\""] - )] - #[case::remove_index_mysql( - "remove_index_mysql", - DatabaseBackend::MySql, - &["DROP INDEX `idx_email` ON `users`"] - )] - #[case::remove_index_sqlite( - "remove_index_sqlite", - DatabaseBackend::Sqlite, - &["\"idx_email\""] - )] - fn test_remove_index( - #[case] title: &str, - #[case] backend: DatabaseBackend, - #[case] expected: &[&str], - ) { - let result = build_remove_index("users", "idx_email"); - let sql = result.build(backend); - for exp in expected { - assert!( - sql.contains(exp), - "Expected SQL to contain '{}', got: {}", - exp, - sql - ); - } - - with_settings!({ snapshot_suffix => format!("remove_index_{}", title) }, { - assert_snapshot!(sql); - }); - } -} diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_enum_type@add_column_with_enum_type_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_enum_type@add_column_with_enum_type_Sqlite.snap index 04ab18c..6e8dfdf 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_enum_type@add_column_with_enum_type_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_enum_type@add_column_with_enum_type_Sqlite.snap @@ -2,7 +2,7 @@ source: crates/vespertide-query/src/sql/add_column.rs expression: sql --- -CREATE TABLE "users_temp" ( "id" integer NOT NULL, "status" enum_text , CONSTRAINT "chk_users_status" CHECK ("status" IN ('active', 'inactive'))); +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "status" enum_text , CONSTRAINT "chk_users__status" CHECK ("status" IN ('active', 'inactive'))); INSERT INTO "users_temp" ("id", "status") SELECT "id", NULL AS "status" FROM "users"; DROP TABLE "users"; ALTER TABLE "users_temp" RENAME TO "users" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_mysql.snap index 8dfb6b7..b236e03 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_mysql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/add_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -ALTER TABLE `users` ADD CONSTRAINT `fk_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +ALTER TABLE `users` ADD CONSTRAINT `fk_users__fk_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_postgres.snap index f955ca9..3f2b8b1 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/add_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -ALTER TABLE "users" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT +ALTER TABLE "users" ADD CONSTRAINT "fk_users__fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_mysql.snap index 6203446..89b3302 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_mysql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/add_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE UNIQUE INDEX `uq_email` ON `users` (`email`) +CREATE UNIQUE INDEX `uq_users__uq_email` ON `users` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_postgres.snap index b3185dc..63d0178 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/add_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE UNIQUE INDEX "uq_email" ON "users" ("email") +CREATE UNIQUE INDEX "uq_users__uq_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_sqlite.snap index b3185dc..63d0178 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_sqlite.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/add_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE UNIQUE INDEX "uq_email" ON "users" ("email") +CREATE UNIQUE INDEX "uq_users__uq_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_mysql.snap deleted file mode 100644 index bb16fa6..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/add_index.rs -expression: sql ---- -CREATE INDEX `idx_email` ON `users` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_postgres.snap deleted file mode 100644 index b87bc63..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/add_index.rs -expression: sql ---- -CREATE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_sqlite.snap deleted file mode 100644 index b87bc63..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/add_index.rs -expression: sql ---- -CREATE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_mysql.snap deleted file mode 100644 index 1b87bf4..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/add_index.rs -expression: sql ---- -CREATE UNIQUE INDEX `idx_email` ON `users` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_postgres.snap deleted file mode 100644 index 5815d42..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/add_index.rs -expression: sql ---- -CREATE UNIQUE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_sqlite.snap deleted file mode 100644 index 5815d42..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/add_index.rs -expression: sql ---- -CREATE UNIQUE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_enum_column@create_table_with_enum_column_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_enum_column@create_table_with_enum_column_Sqlite.snap index dbd4d7c..62ce56a 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_enum_column@create_table_with_enum_column_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_enum_column@create_table_with_enum_column_Sqlite.snap @@ -3,4 +3,4 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- ; -CREATE TABLE "users" ( "id" integer NOT NULL, "status" enum_text NOT NULL DEFAULT 'active', PRIMARY KEY ("id") , CONSTRAINT "chk_users_status" CHECK ("status" IN ('active', 'inactive', 'pending'))) +CREATE TABLE "users" ( "id" integer NOT NULL, "status" enum_text NOT NULL DEFAULT 'active', PRIMARY KEY ("id") , CONSTRAINT "chk_users__status" CHECK ("status" IN ('active', 'inactive', 'pending'))) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_MySql.snap index c3a51cf..47b2bfe 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- -CREATE TABLE `users` ( `id` int, `email` text UNIQUE ) +CREATE TABLE `users` ( `id` int, `email` text, UNIQUE KEY `uq_users__email` (`email`) ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_Postgres.snap index 37564b5..68c0163 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_Postgres.snap @@ -2,4 +2,5 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- -CREATE TABLE "users" ( "id" integer, "email" text UNIQUE ) +CREATE TABLE "users" ( "id" integer, "email" text ) +CREATE UNIQUE INDEX "uq_users__email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_Sqlite.snap index 37564b5..68c0163 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_inline_unique@create_table_with_inline_unique_Sqlite.snap @@ -2,4 +2,5 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- -CREATE TABLE "users" ( "id" integer, "email" text UNIQUE ) +CREATE TABLE "users" ( "id" integer, "email" text ) +CREATE UNIQUE INDEX "uq_users__email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_MySql.snap index 3bb00fe..e1c3d14 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- -CREATE TABLE `users` ( `id` int, `email` text, UNIQUE KEY `uq_email` (`email`) ) +CREATE TABLE `users` ( `id` int, `email` text, UNIQUE KEY `uq_users__uq_email` (`email`) ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_Postgres.snap index eb8f4da..f8b1bc1 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_Postgres.snap @@ -3,4 +3,4 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- CREATE TABLE "users" ( "id" integer, "email" text ) -CREATE UNIQUE INDEX "uq_email" ON "users" ("email") +CREATE UNIQUE INDEX "uq_users__uq_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_Sqlite.snap index eb8f4da..f8b1bc1 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique@create_table_with_table_level_unique_Sqlite.snap @@ -3,4 +3,4 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- CREATE TABLE "users" ( "id" integer, "email" text ) -CREATE UNIQUE INDEX "uq_email" ON "users" ("email") +CREATE UNIQUE INDEX "uq_users__uq_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_MySql.snap index d0ed37f..47b2bfe 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- -CREATE TABLE `users` ( `id` int, `email` text, UNIQUE KEY (`email`) ) +CREATE TABLE `users` ( `id` int, `email` text, UNIQUE KEY `uq_users__email` (`email`) ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_Postgres.snap index 8cca4cf..68c0163 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_Postgres.snap @@ -3,4 +3,4 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- CREATE TABLE "users" ( "id" integer, "email" text ) -CREATE UNIQUE INDEX ON "users" ("email") +CREATE UNIQUE INDEX "uq_users__email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_Sqlite.snap index 8cca4cf..68c0163 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table_with_table_level_unique_no_name@create_table_with_table_level_unique_no_name_Sqlite.snap @@ -3,4 +3,4 @@ source: crates/vespertide-query/src/sql/create_table.rs expression: sql --- CREATE TABLE "users" ( "id" integer, "email" text ) -CREATE UNIQUE INDEX ON "users" ("email") +CREATE UNIQUE INDEX "uq_users__email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_index@modify_column_type_with_index_modify_column_type_with_index_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_index@modify_column_type_with_index_modify_column_type_with_index_sqlite.snap index 56591f0..3822e8a 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_index@modify_column_type_with_index_modify_column_type_with_index_sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_index@modify_column_type_with_index_modify_column_type_with_index_sqlite.snap @@ -6,4 +6,4 @@ CREATE TABLE "users_temp" ( "id" integer NOT NULL, "age" bigint ); INSERT INTO "users_temp" ("id", "age") SELECT "id", "age" FROM "users"; DROP TABLE "users"; ALTER TABLE "users_temp" RENAME TO "users"; -CREATE INDEX "idx_age" ON "users" ("age") +CREATE INDEX "ix_users__idx_age" ON "users" ("age") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_constraint@modify_column_type_with_unique_constraint_modify_column_type_with_unique_constraint_mysql.snap similarity index 100% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_constraint@modify_column_type_with_unique_constraint_modify_column_type_with_unique_constraint_mysql.snap diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_constraint@modify_column_type_with_unique_constraint_modify_column_type_with_unique_constraint_postgres.snap similarity index 100% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_constraint@modify_column_type_with_unique_constraint_modify_column_type_with_unique_constraint_postgres.snap diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_constraint@modify_column_type_with_unique_constraint_modify_column_type_with_unique_constraint_sqlite.snap similarity index 72% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_constraint@modify_column_type_with_unique_constraint_modify_column_type_with_unique_constraint_sqlite.snap index 4ca6b18..742b4a4 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type_with_unique_constraint@modify_column_type_with_unique_constraint_modify_column_type_with_unique_constraint_sqlite.snap @@ -5,5 +5,4 @@ expression: sql CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" varchar(255) ); INSERT INTO "users_temp" ("id", "email") SELECT "id", "email" FROM "users"; DROP TABLE "users"; -ALTER TABLE "users_temp" RENAME TO "users"; -CREATE UNIQUE INDEX "idx_email" ON "users" ("email") +ALTER TABLE "users_temp" RENAME TO "users" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_mysql.snap index 2c22e94..63e70dc 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_mysql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -ALTER TABLE `users` DROP FOREIGN KEY `fk_user` +ALTER TABLE `users` DROP FOREIGN KEY `fk_users__fk_user` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_postgres.snap index bc8d252..943f7de 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -ALTER TABLE "users" DROP CONSTRAINT "fk_user" +ALTER TABLE "users" DROP CONSTRAINT "fk_users__fk_user" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_mysql.snap index 07d23b1..ed5b2b6 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_mysql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -ALTER TABLE `users` DROP INDEX `uq_email` +ALTER TABLE `users` DROP INDEX `uq_users__uq_email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_postgres.snap index 4157065..73f9e8f 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -ALTER TABLE "users" DROP CONSTRAINT "uq_email" +ALTER TABLE "users" DROP CONSTRAINT "uq_users__uq_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_Sqlite.snap index 9997adf..d12673d 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_Sqlite.snap @@ -6,4 +6,4 @@ CREATE TABLE "users_temp" ( "id" integer NOT NULL, "age" integer ) INSERT INTO "users_temp" ("id", "age") SELECT "id", "age" FROM "users" DROP TABLE "users" ALTER TABLE "users_temp" RENAME TO "users" -CREATE INDEX "idx_age" ON "users" ("age") +CREATE INDEX "ix_users__idx_age" ON "users" ("age") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_index@remove_check_with_unique_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_constraint@remove_check_with_unique_constraint_MySql.snap similarity index 100% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_index@remove_check_with_unique_index_MySql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_constraint@remove_check_with_unique_constraint_MySql.snap diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_index@remove_check_with_unique_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_constraint@remove_check_with_unique_constraint_Postgres.snap similarity index 100% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_index@remove_check_with_unique_index_Postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_constraint@remove_check_with_unique_constraint_Postgres.snap diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_index@remove_check_with_unique_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_constraint@remove_check_with_unique_constraint_Sqlite.snap similarity index 85% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_index@remove_check_with_unique_index_Sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_constraint@remove_check_with_unique_constraint_Sqlite.snap index f5e9dc0..bd54566 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_index@remove_check_with_unique_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_unique_constraint@remove_check_with_unique_constraint_Sqlite.snap @@ -6,4 +6,3 @@ CREATE TABLE "users_temp" ( "id" integer NOT NULL, "age" integer ) INSERT INTO "users_temp" ("id", "age") SELECT "id", "age" FROM "users" DROP TABLE "users" ALTER TABLE "users_temp" RENAME TO "users" -CREATE UNIQUE INDEX "idx_age" ON "users" ("age") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_MySql.snap index 83b2d94..fc9a03d 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE `posts` DROP FOREIGN KEY `fk_user` +ALTER TABLE `posts` DROP FOREIGN KEY `fk_posts__fk_user` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_Postgres.snap index 2e42d24..b0e5a2f 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE "posts" DROP CONSTRAINT "fk_user" +ALTER TABLE "posts" DROP CONSTRAINT "fk_posts__fk_user" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_Sqlite.snap index be1f539..d2391a6 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_index@remove_foreign_key_with_index_Sqlite.snap @@ -6,4 +6,4 @@ CREATE TABLE "posts_temp" ( "id" integer NOT NULL, "user_id" integer ) INSERT INTO "posts_temp" ("id", "user_id") SELECT "id", "user_id" FROM "posts" DROP TABLE "posts" ALTER TABLE "posts_temp" RENAME TO "posts" -CREATE INDEX "idx_user_id" ON "posts" ("user_id") +CREATE INDEX "ix_posts__idx_user_id" ON "posts" ("user_id") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_other_constraints@remove_foreign_key_with_other_constraints_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_other_constraints@remove_foreign_key_with_other_constraints_MySql.snap index 83b2d94..fc9a03d 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_other_constraints@remove_foreign_key_with_other_constraints_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_other_constraints@remove_foreign_key_with_other_constraints_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE `posts` DROP FOREIGN KEY `fk_user` +ALTER TABLE `posts` DROP FOREIGN KEY `fk_posts__fk_user` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_other_constraints@remove_foreign_key_with_other_constraints_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_other_constraints@remove_foreign_key_with_other_constraints_Postgres.snap index 2e42d24..b0e5a2f 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_other_constraints@remove_foreign_key_with_other_constraints_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_other_constraints@remove_foreign_key_with_other_constraints_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE "posts" DROP CONSTRAINT "fk_user" +ALTER TABLE "posts" DROP CONSTRAINT "fk_posts__fk_user" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_MySql.snap new file mode 100644 index 0000000..fc9a03d --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `posts` DROP FOREIGN KEY `fk_posts__fk_user` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_Postgres.snap new file mode 100644 index 0000000..b0e5a2f --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "posts" DROP CONSTRAINT "fk_posts__fk_user" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_Sqlite.snap similarity index 83% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_Sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_Sqlite.snap index c24749a..021f502 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_constraint@remove_foreign_key_with_unique_constraint_Sqlite.snap @@ -6,4 +6,3 @@ CREATE TABLE "posts_temp" ( "id" integer NOT NULL, "user_id" integer ) INSERT INTO "posts_temp" ("id", "user_id") SELECT "id", "user_id" FROM "posts" DROP TABLE "posts" ALTER TABLE "posts_temp" RENAME TO "posts" -CREATE UNIQUE INDEX "idx_user_id" ON "posts" ("user_id") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_MySql.snap index f4e00cc..2bcdced 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE `posts` DROP FOREIGN KEY `posts_user_id_fkey` +ALTER TABLE `posts` DROP FOREIGN KEY `fk_posts__user_id` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_Postgres.snap index 14e2fe9..68aabb9 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE "posts" DROP CONSTRAINT "posts_user_id_fkey" +ALTER TABLE "posts" DROP CONSTRAINT "fk_posts__user_id" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_MySql.snap similarity index 62% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_MySql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_MySql.snap index 83b2d94..a029afe 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE `posts` DROP FOREIGN KEY `fk_user` +DROP INDEX `ix_users__custom_idx_email` ON `users` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_Postgres.snap similarity index 66% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_MySql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_Postgres.snap index 9e60d72..cd16f77 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE `users` DROP INDEX `uq_email` +DROP INDEX "ix_users__custom_idx_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_Sqlite.snap similarity index 64% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_Postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_Sqlite.snap index 2e42d24..cd16f77 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_index_with_custom_inline_name@remove_index_custom_name_Sqlite.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE "posts" DROP CONSTRAINT "fk_user" +DROP INDEX "ix_users__custom_idx_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_index@remove_primary_key_with_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_index@remove_primary_key_with_index_Sqlite.snap index d7f46fa..2f07216 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_index@remove_primary_key_with_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_index@remove_primary_key_with_index_Sqlite.snap @@ -6,4 +6,4 @@ CREATE TABLE "users_temp" ( "id" integer NOT NULL ) INSERT INTO "users_temp" ("id") SELECT "id" FROM "users" DROP TABLE "users" ALTER TABLE "users_temp" RENAME TO "users" -CREATE INDEX "idx_id" ON "users" ("id") +CREATE INDEX "ix_users__idx_id" ON "users" ("id") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_index@remove_primary_key_with_unique_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_constraint@remove_primary_key_with_unique_constraint_MySql.snap similarity index 100% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_index@remove_primary_key_with_unique_index_MySql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_constraint@remove_primary_key_with_unique_constraint_MySql.snap diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_index@remove_primary_key_with_unique_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_constraint@remove_primary_key_with_unique_constraint_Postgres.snap similarity index 100% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_index@remove_primary_key_with_unique_index_Postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_constraint@remove_primary_key_with_unique_constraint_Postgres.snap diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_index@remove_primary_key_with_unique_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_constraint@remove_primary_key_with_unique_constraint_Sqlite.snap similarity index 82% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_index@remove_primary_key_with_unique_index_Sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_constraint@remove_primary_key_with_unique_constraint_Sqlite.snap index a7a5af5..9cb9e58 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_index@remove_primary_key_with_unique_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_unique_constraint@remove_primary_key_with_unique_constraint_Sqlite.snap @@ -6,4 +6,3 @@ CREATE TABLE "users_temp" ( "id" integer NOT NULL ) INSERT INTO "users_temp" ("id") SELECT "id" FROM "users" DROP TABLE "users" ALTER TABLE "users_temp" RENAME TO "users" -CREATE UNIQUE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_MySql.snap index 9e60d72..d66f0ab 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE `users` DROP INDEX `uq_email` +ALTER TABLE `users` DROP INDEX `uq_users__uq_email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Postgres.snap index affe793..5bd21b3 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE "users" DROP CONSTRAINT "uq_email" +ALTER TABLE "users" DROP CONSTRAINT "uq_users__uq_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Sqlite.snap index 438f2dc..674b1b3 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Sqlite.snap @@ -6,4 +6,4 @@ CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text ) INSERT INTO "users_temp" ("id", "email") SELECT "id", "email" FROM "users" DROP TABLE "users" ALTER TABLE "users_temp" RENAME TO "users" -CREATE INDEX "idx_id" ON "users" ("id") +CREATE INDEX "ix_users__idx_id" ON "users" ("id") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_MySql.snap index 9e60d72..d66f0ab 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE `users` DROP INDEX `uq_email` +ALTER TABLE `users` DROP INDEX `uq_users__uq_email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_Postgres.snap index affe793..5bd21b3 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE "users" DROP CONSTRAINT "uq_email" +ALTER TABLE "users" DROP CONSTRAINT "uq_users__uq_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_MySql.snap similarity index 62% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_Postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_MySql.snap index affe793..d66f0ab 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE "users" DROP CONSTRAINT "uq_email" +ALTER TABLE `users` DROP INDEX `uq_users__uq_email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_Postgres.snap new file mode 100644 index 0000000..5bd21b3 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "uq_users__uq_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_Sqlite.snap similarity index 84% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_Sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_Sqlite.snap index f14ae31..673a40b 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_unique_index@remove_unique_with_unique_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_unique_constraint@remove_unique_with_other_unique_constraint_Sqlite.snap @@ -6,4 +6,3 @@ CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text ) INSERT INTO "users_temp" ("id", "email") SELECT "id", "email" FROM "users" DROP TABLE "users" ALTER TABLE "users_temp" RENAME TO "users" -CREATE UNIQUE INDEX "idx_name" ON "users" ("name") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_MySql.snap index 710074b..3fe9fe7 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE `users` DROP INDEX `users_email_key` +ALTER TABLE `users` DROP INDEX `uq_users__email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_Postgres.snap index cc202bb..aca0508 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/remove_constraint.rs expression: sql --- -ALTER TABLE "users" DROP CONSTRAINT "users_email_key" +ALTER TABLE "users" DROP CONSTRAINT "uq_users__email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_mysql.snap deleted file mode 100644 index 27b6995..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/remove_index.rs -expression: sql ---- -DROP INDEX `idx_email` ON `users` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_postgres.snap deleted file mode 100644 index 539199b..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/remove_index.rs -expression: sql ---- -DROP INDEX "idx_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_sqlite.snap deleted file mode 100644 index 539199b..0000000 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql/remove_index.rs -expression: sql ---- -DROP INDEX "idx_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_MySql.snap new file mode 100644 index 0000000..a5226cd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX `ix_user__email_idx` ON `user` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_Postgres.snap new file mode 100644 index 0000000..00eb36a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_user__email_idx" ON "user" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_Sqlite.snap new file mode 100644 index 0000000..00eb36a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_email_idx_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_user__email_idx" ON "user" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_MySql.snap new file mode 100644 index 0000000..77e3340 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX `ix_user__hello` ON `user` (`email`, `password`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_Postgres.snap new file mode 100644 index 0000000..11d18bc --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_user__hello" ON "user" ("email", "password") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_Sqlite.snap new file mode 100644 index 0000000..11d18bc --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_index_with_custom_name@add_index_custom_hello_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_user__hello" ON "user" ("email", "password") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_MySql.snap new file mode 100644 index 0000000..140af23 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX `uq_user__email_unique` ON `user` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_Postgres.snap new file mode 100644 index 0000000..8b5d56c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX "uq_user__email_unique" ON "user" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_Sqlite.snap new file mode 100644 index 0000000..8b5d56c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unique_with_custom_name@add_unique_custom_email_unique_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX "uq_user__email_unique" ON "user" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_MySql.snap new file mode 100644 index 0000000..548732b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX `ix_user__email` ON `user` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_Postgres.snap new file mode 100644 index 0000000..69d7ffc --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_user__email" ON "user" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_Sqlite.snap new file mode 100644 index 0000000..69d7ffc --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_user__email" ON "user" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_MySql.snap new file mode 100644 index 0000000..98ed707 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX `ix_user__email_password` ON `user` (`email`, `password`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_Postgres.snap new file mode 100644 index 0000000..d5fe39a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_user__email_password" ON "user" ("email", "password") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_Sqlite.snap new file mode 100644 index 0000000..d5fe39a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_index@add_unnamed_index_email_password_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_user__email_password" ON "user" ("email", "password") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_MySql.snap new file mode 100644 index 0000000..a993666 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX `uq_user__email` ON `user` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_Postgres.snap new file mode 100644 index 0000000..21492f2 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX "uq_user__email" ON "user" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_Sqlite.snap new file mode 100644 index 0000000..21492f2 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX "uq_user__email" ON "user" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_MySql.snap new file mode 100644 index 0000000..8314838 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX `uq_user__email_username` ON `user` (`email`, `username`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_Postgres.snap new file mode 100644 index 0000000..8b06cb2 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX "uq_user__email_username" ON "user" ("email", "username") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_Sqlite.snap new file mode 100644 index 0000000..8b06cb2 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__add_unnamed_unique@add_unnamed_unique_email_username_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX "uq_user__email_username" ON "user" ("email", "username") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_MySql.snap index 60dfde9..08718be 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -CREATE UNIQUE INDEX `uq_email` ON `users` (`email`) +CREATE UNIQUE INDEX `uq_users__uq_email` ON `users` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Postgres.snap index 2b6e885..5fd341f 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -CREATE UNIQUE INDEX "uq_email" ON "users" ("email") +CREATE UNIQUE INDEX "uq_users__uq_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Sqlite.snap index 2b6e885..5fd341f 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Sqlite.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -CREATE UNIQUE INDEX "uq_email" ON "users" ("email") +CREATE UNIQUE INDEX "uq_users__uq_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_MySql.snap new file mode 100644 index 0000000..e4d1af6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX `ix_users__idx_email` ON `users` (`email`) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_Postgres.snap new file mode 100644 index 0000000..8c09b5b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_users__idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_Sqlite.snap new file mode 100644 index 0000000..8c09b5b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index_constraint@add_index_constraint_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "ix_users__idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_MySql.snap index 7546253..d94f0f0 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -ALTER TABLE `users` DROP INDEX `uq_email` +ALTER TABLE `users` DROP INDEX `uq_users__uq_email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_Postgres.snap index d9a4473..db5db4e 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -ALTER TABLE "users" DROP CONSTRAINT "uq_email" +ALTER TABLE "users" DROP CONSTRAINT "uq_users__uq_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_MySql.snap similarity index 60% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_MySql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_MySql.snap index 3c427d5..ab3edc3 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -CREATE INDEX `idx_email` ON `users` (`email`) +DROP INDEX `ix_users__idx_email` ON `users` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_Postgres.snap similarity index 67% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_MySql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_Postgres.snap index f053b40..74edded 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_MySql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -DROP INDEX `idx_email` ON `users` +DROP INDEX "ix_users__idx_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_Sqlite.snap new file mode 100644 index 0000000..74edded --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index_constraint@remove_index_constraint_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX "ix_users__idx_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_mysql.snap index 527e4a6..6c5f792 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_mysql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE TABLE "posts" ( "id" integer, "user_id" integer, CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT ) +CREATE TABLE "posts" ( "id" integer, "user_id" integer, CONSTRAINT "fk_posts__fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_postgres.snap index 527e4a6..6c5f792 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE TABLE "posts" ( "id" integer, "user_id" integer, CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT ) +CREATE TABLE "posts" ( "id" integer, "user_id" integer, CONSTRAINT "fk_posts__fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_sqlite.snap index 527e4a6..6c5f792 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_sqlite.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE TABLE "posts" ( "id" integer, "user_id" integer, CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT ) +CREATE TABLE "posts" ( "id" integer, "user_id" integer, CONSTRAINT "fk_posts__fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_mysql.snap index f9b6e45..0962100 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_mysql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE TABLE "users" ( "id" integer NOT NULL PRIMARY KEY ) +CREATE TABLE "users" ( "id" integer NOT NULL, PRIMARY KEY ("id") ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_postgres.snap index f9b6e45..0962100 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE TABLE "users" ( "id" integer NOT NULL PRIMARY KEY ) +CREATE TABLE "users" ( "id" integer NOT NULL, PRIMARY KEY ("id") ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_sqlite.snap index f9b6e45..0962100 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_sqlite.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -CREATE TABLE "users" ( "id" integer NOT NULL PRIMARY KEY ) +CREATE TABLE "users" ( "id" integer NOT NULL, PRIMARY KEY ("id") ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_MySql.snap new file mode 100644 index 0000000..57dfeff --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX `ix_user__hello` ON `user` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_Postgres.snap similarity index 71% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_Postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_Postgres.snap index bb04a9e..2891d35 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_Postgres.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -DROP INDEX "idx_email" +DROP INDEX "ix_user__hello" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_Sqlite.snap similarity index 71% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_Sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_Sqlite.snap index bb04a9e..2891d35 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_index_with_custom_name@remove_index_custom_hello_Sqlite.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -DROP INDEX "idx_email" +DROP INDEX "ix_user__hello" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_MySql.snap new file mode 100644 index 0000000..3728402 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `user` DROP INDEX `uq_user__email_unique` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_Postgres.snap new file mode 100644 index 0000000..54d2f46 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "user" DROP CONSTRAINT "uq_user__email_unique" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_Sqlite.snap new file mode 100644 index 0000000..4ef8469 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unique_with_custom_name@remove_unique_custom_email_unique_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE TABLE "user_temp" ( "email" text ) +INSERT INTO "user_temp" ("email") SELECT "email" FROM "user" +DROP TABLE "user" +ALTER TABLE "user_temp" RENAME TO "user" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_MySql.snap new file mode 100644 index 0000000..3754d5e --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX `ix_user__email` ON `user` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_Postgres.snap new file mode 100644 index 0000000..b7e64e9 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX "ix_user__email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_Sqlite.snap new file mode 100644 index 0000000..b7e64e9 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX "ix_user__email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_MySql.snap similarity index 60% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_Postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_MySql.snap index 1b1f0b5..987a951 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_Postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -CREATE INDEX "idx_email" ON "users" ("email") +DROP INDEX `ix_user__email_password` ON `user` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_Postgres.snap new file mode 100644 index 0000000..6996bc6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX "ix_user__email_password" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_Sqlite.snap new file mode 100644 index 0000000..6996bc6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_index@remove_unnamed_index_email_password_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX "ix_user__email_password" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_MySql.snap similarity index 60% rename from crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_Sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_MySql.snap index 1b1f0b5..73b08e1 100644 --- a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_Sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_MySql.snap @@ -2,4 +2,4 @@ source: crates/vespertide-query/src/sql/mod.rs expression: sql --- -CREATE INDEX "idx_email" ON "users" ("email") +ALTER TABLE `user` DROP INDEX `uq_user__email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_Postgres.snap new file mode 100644 index 0000000..979fc76 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "user" DROP CONSTRAINT "uq_user__email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_Sqlite.snap new file mode 100644 index 0000000..4ef8469 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE TABLE "user_temp" ( "email" text ) +INSERT INTO "user_temp" ("email") SELECT "email" FROM "user" +DROP TABLE "user" +ALTER TABLE "user_temp" RENAME TO "user" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_MySql.snap new file mode 100644 index 0000000..2051fa8 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `user` DROP INDEX `uq_user__email_username` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_Postgres.snap new file mode 100644 index 0000000..e576388 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "user" DROP CONSTRAINT "uq_user__email_username" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_Sqlite.snap new file mode 100644 index 0000000..7b8543d --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__remove_unnamed_unique@remove_unnamed_unique_email_username_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE TABLE "user_temp" ( "email" text, "username" text ) +INSERT INTO "user_temp" ("email", "username") SELECT "email", "username" FROM "user" +DROP TABLE "user" +ALTER TABLE "user_temp" RENAME TO "user" diff --git a/crates/vespertide-query/tests/enum_migration_test.rs b/crates/vespertide-query/tests/enum_migration_test.rs index 7b52f9e..77487db 100644 --- a/crates/vespertide-query/tests/enum_migration_test.rs +++ b/crates/vespertide-query/tests/enum_migration_test.rs @@ -57,7 +57,6 @@ fn test_enum_value_change_generates_correct_sql() { }, ], constraints: vec![], - indexes: vec![], }]; let queries = build_plan_queries(&plan, &baseline_schema).unwrap(); diff --git a/examples/app/migrations/0001_init.json b/examples/app/migrations/0001_init.json index c476ba8..aa1c84d 100644 --- a/examples/app/migrations/0001_init.json +++ b/examples/app/migrations/0001_init.json @@ -4,24 +4,16 @@ { "columns": [ { - "comment": null, "default": "gen_random_uuid()", - "foreign_key": null, - "index": null, "name": "id", "nullable": false, "primary_key": true, - "type": "uuid", - "unique": null + "type": "uuid" }, { - "comment": null, - "default": null, - "foreign_key": null, "index": true, "name": "email", "nullable": false, - "primary_key": null, "type": { "kind": "varchar", "length": 255 @@ -29,149 +21,70 @@ "unique": true }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "password", "nullable": false, - "primary_key": null, "type": { "kind": "varchar", "length": 255 - }, - "unique": null + } }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "name", "nullable": false, - "primary_key": null, "type": { "kind": "varchar", "length": 100 - }, - "unique": null + } }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "profile_image", "nullable": true, - "primary_key": null, - "type": "text", - "unique": null + "type": "text" }, { - "comment": null, "default": "now()", - "foreign_key": null, - "index": null, "name": "created_at", "nullable": false, - "primary_key": null, - "type": "timestamptz", - "unique": null + "type": "timestamptz" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "updated_at", "nullable": true, - "primary_key": null, - "type": "timestamptz", - "unique": null - } - ], - "constraints": [ - { - "auto_increment": false, - "columns": [ - "id" - ], - "type": "primary_key" - }, - { - "columns": [ - "email" - ], - "name": null, - "type": "unique" + "type": "timestamptz" } ], + "constraints": [], "table": "user", "type": "create_table" }, - { - "index": { - "columns": [ - "email" - ], - "name": "idx_user_email", - "unique": false - }, - "table": "user", - "type": "add_index" - }, { "columns": [ { - "comment": null, "default": "gen_random_uuid()", - "foreign_key": null, - "index": null, "name": "id", "nullable": false, "primary_key": true, - "type": "uuid", - "unique": null + "type": "uuid" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "name", "nullable": false, - "primary_key": null, "type": { "kind": "varchar", "length": 100 - }, - "unique": null + } }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "description", "nullable": true, - "primary_key": null, - "type": "text", - "unique": null + "type": "text" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "logo", "nullable": true, - "primary_key": null, - "type": "text", - "unique": null + "type": "text" }, { - "comment": null, - "default": null, "foreign_key": { "on_delete": "cascade", "on_update": null, @@ -183,92 +96,27 @@ "index": true, "name": "owner_id", "nullable": false, - "primary_key": null, - "type": "uuid", - "unique": null + "type": "uuid" }, { - "comment": null, "default": "now()", - "foreign_key": null, - "index": null, "name": "created_at", "nullable": false, - "primary_key": null, - "type": "timestamptz", - "unique": null + "type": "timestamptz" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "updated_at", "nullable": true, - "primary_key": null, - "type": "timestamptz", - "unique": null - } - ], - "constraints": [ - { - "auto_increment": false, - "columns": [ - "id" - ], - "type": "primary_key" - }, - { - "columns": [ - "owner_id" - ], - "name": null, - "on_delete": "cascade", - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "user", - "type": "foreign_key" + "type": "timestamptz" } ], + "constraints": [], "table": "media", "type": "create_table" }, - { - "index": { - "columns": [ - "owner_id" - ], - "name": "idx_media_owner_id", - "unique": false - }, - "table": "media", - "type": "add_index" - }, { "columns": [ { - "comment": null, - "default": null, - "foreign_key": { - "on_delete": "cascade", - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "user" - }, - "index": true, - "name": "user_id", - "nullable": false, - "primary_key": true, - "type": "uuid", - "unique": null - }, - { - "comment": null, - "default": null, "foreign_key": { "on_delete": "cascade", "on_update": null, @@ -277,305 +125,138 @@ ], "ref_table": "media" }, - "index": true, "name": "media_id", "nullable": false, "primary_key": true, - "type": "uuid", - "unique": null + "type": "uuid" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": true, - "name": "role", - "nullable": false, - "primary_key": null, - "type": { - "kind": "varchar", - "length": 20 - }, - "unique": null - }, - { - "comment": null, - "default": "now()", - "foreign_key": null, - "index": null, - "name": "created_at", - "nullable": false, - "primary_key": null, - "type": "timestamptz", - "unique": null - } - ], - "constraints": [ - { - "auto_increment": false, - "columns": [ - "user_id", - "media_id" - ], - "type": "primary_key" - }, - { - "columns": [ - "user_id" - ], - "name": null, - "on_delete": "cascade", - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "user", - "type": "foreign_key" - }, - { - "columns": [ - "media_id" - ], - "name": null, - "on_delete": "cascade", - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "media", - "type": "foreign_key" - } - ], - "table": "user_media_role", - "type": "create_table" - }, - { - "index": { - "columns": [ - "user_id" - ], - "name": "idx_user_media_role_user_id", - "unique": false - }, - "table": "user_media_role", - "type": "add_index" - }, - { - "index": { - "columns": [ - "media_id" - ], - "name": "idx_user_media_role_media_id", - "unique": false - }, - "table": "user_media_role", - "type": "add_index" - }, - { - "index": { - "columns": [ - "role" - ], - "name": "idx_user_media_role_role", - "unique": false - }, - "table": "user_media_role", - "type": "add_index" - }, - { - "columns": [ - { - "comment": null, - "default": "gen_random_uuid()", - "foreign_key": null, - "index": null, "name": "id", "nullable": false, "primary_key": true, - "type": "uuid", - "unique": null + "type": "big_int" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "title", "nullable": false, - "primary_key": null, "type": { "kind": "varchar", "length": 500 - }, - "unique": null + } }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "content", "nullable": false, - "primary_key": null, - "type": "text", - "unique": null + "type": "text" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "summary", "nullable": true, - "primary_key": null, - "type": "text", - "unique": null + "type": "text" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "thumbnail", "nullable": true, - "primary_key": null, - "type": "text", - "unique": null + "type": "text" }, { - "comment": null, - "default": null, - "foreign_key": { - "on_delete": "cascade", - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "media" - }, - "index": true, - "name": "media_id", - "nullable": false, - "primary_key": null, - "type": "uuid", - "unique": null - }, - { - "comment": null, "default": "'draft'", - "foreign_key": null, "index": true, "name": "status", "nullable": false, - "primary_key": null, "type": { - "kind": "varchar", - "length": 20 - }, - "unique": null + "kind": "enum", + "name": "article_status", + "values": [ + "draft", + "review", + "published", + "archived" + ] + } }, { - "comment": null, - "default": null, - "foreign_key": null, "index": true, "name": "published_at", "nullable": true, - "primary_key": null, - "type": "timestamptz", - "unique": null + "type": "timestamptz" }, { - "comment": null, "default": "now()", - "foreign_key": null, - "index": null, "name": "created_at", "nullable": false, - "primary_key": null, - "type": "timestamptz", - "unique": null + "type": "timestamptz" }, { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, "name": "updated_at", "nullable": true, - "primary_key": null, - "type": "timestamptz", - "unique": null + "type": "timestamptz" } ], - "constraints": [ + "constraints": [], + "table": "article", + "type": "create_table" + }, + { + "columns": [ { - "expr": "status IN ('draft', 'review', 'published', 'archived')", - "name": "chk_article_status", - "type": "check" + "foreign_key": { + "on_delete": "cascade", + "on_update": null, + "ref_columns": [ + "id" + ], + "ref_table": "user" + }, + "index": true, + "name": "user_id", + "nullable": false, + "primary_key": true, + "type": "uuid" + }, + { + "foreign_key": { + "on_delete": "cascade", + "on_update": null, + "ref_columns": [ + "id" + ], + "ref_table": "media" + }, + "index": true, + "name": "media_id", + "nullable": false, + "primary_key": true, + "type": "uuid" }, { - "auto_increment": false, - "columns": [ - "id" - ], - "type": "primary_key" + "index": true, + "name": "role", + "nullable": false, + "type": { + "kind": "enum", + "name": "media_role", + "values": [ + "owner", + "editor", + "reporter" + ] + } }, { - "columns": [ - "media_id" - ], - "name": null, - "on_delete": "cascade", - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "media", - "type": "foreign_key" + "default": "now()", + "name": "created_at", + "nullable": false, + "type": "timestamptz" } ], - "table": "article", + "constraints": [], + "table": "user_media_role", "type": "create_table" }, - { - "index": { - "columns": [ - "media_id" - ], - "name": "idx_article_media_id", - "unique": false - }, - "table": "article", - "type": "add_index" - }, - { - "index": { - "columns": [ - "status" - ], - "name": "idx_article_status", - "unique": false - }, - "table": "article", - "type": "add_index" - }, - { - "index": { - "columns": [ - "published_at" - ], - "name": "idx_article_published_at", - "unique": false - }, - "table": "article", - "type": "add_index" - }, { "columns": [ { - "comment": null, - "default": null, "foreign_key": { "on_delete": "cascade", "on_update": null, @@ -584,16 +265,12 @@ ], "ref_table": "article" }, - "index": true, "name": "article_id", "nullable": false, "primary_key": true, - "type": "uuid", - "unique": null + "type": "big_int" }, { - "comment": null, - "default": null, "foreign_key": { "on_delete": "cascade", "on_update": null, @@ -606,109 +283,40 @@ "name": "user_id", "nullable": false, "primary_key": true, - "type": "uuid", - "unique": null + "type": "uuid" }, { - "comment": null, "default": "1", - "foreign_key": null, - "index": null, "name": "author_order", "nullable": false, - "primary_key": null, - "type": "integer", - "unique": null + "type": "integer" }, { - "comment": null, "default": "'contributor'", - "foreign_key": null, - "index": null, "name": "role", "nullable": false, - "primary_key": null, "type": { - "kind": "varchar", - "length": 20 - }, - "unique": null + "kind": "enum", + "name": "article_user_role", + "values": [ + "lead", + "contributor" + ] + } }, { - "comment": null, "default": "now()", - "foreign_key": null, - "index": null, "name": "created_at", "nullable": false, - "primary_key": null, - "type": "timestamptz", - "unique": null - } - ], - "constraints": [ - { - "auto_increment": false, - "columns": [ - "article_id", - "user_id" - ], - "type": "primary_key" - }, - { - "columns": [ - "article_id" - ], - "name": null, - "on_delete": "cascade", - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "article", - "type": "foreign_key" - }, - { - "columns": [ - "user_id" - ], - "name": null, - "on_delete": "cascade", - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "user", - "type": "foreign_key" + "type": "timestamptz" } ], + "constraints": [], "table": "article_user", "type": "create_table" - }, - { - "index": { - "columns": [ - "article_id" - ], - "name": "idx_article_user_article_id", - "unique": false - }, - "table": "article_user", - "type": "add_index" - }, - { - "index": { - "columns": [ - "user_id" - ], - "name": "idx_article_user_user_id", - "unique": false - }, - "table": "article_user", - "type": "add_index" } ], - "comment": "init", - "created_at": "2025-12-18T03:27:15Z", + "comment": "Init", + "created_at": "2025-12-19T04:26:58Z", "version": 1 } \ No newline at end of file diff --git a/examples/app/models/article.json b/examples/app/models/article.json index ff1f113..5b73314 100644 --- a/examples/app/models/article.json +++ b/examples/app/models/article.json @@ -64,6 +64,5 @@ "nullable": true } ], - "constraints": [], - "indexes": [] + "constraints": [] } diff --git a/examples/app/models/article_user.json b/examples/app/models/article_user.json index 5ca8279..1410956 100644 --- a/examples/app/models/article_user.json +++ b/examples/app/models/article_user.json @@ -44,6 +44,5 @@ "default": "now()" } ], - "constraints": [], - "indexes": [] + "constraints": [] } diff --git a/examples/app/models/media.json b/examples/app/models/media.json index 43552b6..4f0c4e2 100644 --- a/examples/app/models/media.json +++ b/examples/app/models/media.json @@ -47,6 +47,5 @@ "nullable": true } ], - "constraints": [], - "indexes": [] + "constraints": [] } diff --git a/examples/app/models/user.json b/examples/app/models/user.json index 7221fb1..ba49947 100644 --- a/examples/app/models/user.json +++ b/examples/app/models/user.json @@ -43,6 +43,5 @@ "nullable": true } ], - "constraints": [], - "indexes": [] + "constraints": [] } diff --git a/examples/app/models/user_media_role.json b/examples/app/models/user_media_role.json index eeeffed..9b1253a 100644 --- a/examples/app/models/user_media_role.json +++ b/examples/app/models/user_media_role.json @@ -39,6 +39,5 @@ "default": "now()" } ], - "constraints": [], - "indexes": [] + "constraints": [] } diff --git a/schemas/migration.schema.json b/schemas/migration.schema.json index 5ce8899..366fc33 100644 --- a/schemas/migration.schema.json +++ b/schemas/migration.schema.json @@ -280,28 +280,6 @@ } ] }, - "IndexDef": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "unique": { - "type": "boolean" - } - }, - "required": [ - "name", - "columns", - "unique" - ] - }, "MigrationAction": { "oneOf": [ { @@ -445,46 +423,6 @@ "new_type" ] }, - { - "type": "object", - "properties": { - "index": { - "$ref": "#/$defs/IndexDef" - }, - "table": { - "type": "string" - }, - "type": { - "type": "string", - "const": "add_index" - } - }, - "required": [ - "type", - "table", - "index" - ] - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "table": { - "type": "string" - }, - "type": { - "type": "string", - "const": "remove_index" - } - }, - "required": [ - "type", - "table", - "name" - ] - }, { "type": "object", "properties": { @@ -784,6 +722,31 @@ "name", "expr" ] + }, + { + "type": "object", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "const": "index" + } + }, + "required": [ + "type", + "columns" + ] } ] } diff --git a/schemas/model.schema.json b/schemas/model.schema.json index 8530034..7c8de24 100644 --- a/schemas/model.schema.json +++ b/schemas/model.schema.json @@ -15,12 +15,6 @@ "$ref": "#/$defs/TableConstraint" } }, - "indexes": { - "type": "array", - "items": { - "$ref": "#/$defs/IndexDef" - } - }, "name": { "type": "string" } @@ -28,8 +22,7 @@ "required": [ "name", "columns", - "constraints", - "indexes" + "constraints" ], "$defs": { "ColumnDef": { @@ -279,28 +272,6 @@ } ] }, - "IndexDef": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "unique": { - "type": "boolean" - } - }, - "required": [ - "name", - "columns", - "unique" - ] - }, "NumValue": { "description": "Integer enum variant with name and numeric value", "type": "object", @@ -522,6 +493,31 @@ "name", "expr" ] + }, + { + "type": "object", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "const": "index" + } + }, + "required": [ + "type", + "columns" + ] } ] }