diff --git a/.changepacks/changepack_log_9NxoUKdBORNMnyS1Yml7-.json b/.changepacks/changepack_log_9NxoUKdBORNMnyS1Yml7-.json new file mode 100644 index 0000000..7d91551 --- /dev/null +++ b/.changepacks/changepack_log_9NxoUKdBORNMnyS1Yml7-.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch"},"note":"Support sqlite migration","date":"2025-12-16T14:09:11.883218100Z"} \ No newline at end of file diff --git a/.changepacks/changepack_log_oa3NjaHehmq88Zy2V57od.json b/.changepacks/changepack_log_oa3NjaHehmq88Zy2V57od.json new file mode 100644 index 0000000..5c11432 --- /dev/null +++ b/.changepacks/changepack_log_oa3NjaHehmq88Zy2V57od.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch"},"note":"Convert default value","date":"2025-12-16T14:08:56.631935300Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 316aa5b..9dccd7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -523,6 +523,19 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "der" version = "0.7.10" @@ -888,6 +901,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -2232,6 +2251,20 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "538c30747ae860d6fb88330addbbd3e0ddbe46d662d032855596d8a8ca260611" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive 1.0.0", +] + [[package]] name = "serial_test" version = "3.2.0" @@ -2243,7 +2276,18 @@ dependencies = [ "once_cell", "parking_lot", "scc", - "serial_test_derive", + "serial_test_derive 3.2.0", +] + +[[package]] +name = "serial_test_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079a83df15f85d89a68d64ae1238f142f172b1fa915d0d76b26a7cba1b659a69" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -2938,7 +2982,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespertide" -version = "0.1.7" +version = "0.1.9" dependencies = [ "vespertide-core", "vespertide-macro", @@ -2946,7 +2990,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.7" +version = "0.1.9" dependencies = [ "anyhow", "assert_cmd", @@ -2958,18 +3002,19 @@ dependencies = [ "schemars", "serde_json", "serde_yaml", - "serial_test", + "serial_test 3.2.0", "tempfile", "vespertide-config", "vespertide-core", "vespertide-exporter", + "vespertide-loader", "vespertide-planner", "vespertide-query", ] [[package]] name = "vespertide-config" -version = "0.1.7" +version = "0.1.9" dependencies = [ "clap", "serde", @@ -2977,7 +3022,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.7" +version = "0.1.9" dependencies = [ "rstest", "schemars", @@ -2987,7 +3032,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.7" +version = "0.1.9" dependencies = [ "insta", "rstest", @@ -2995,25 +3040,39 @@ dependencies = [ "vespertide-core", ] +[[package]] +name = "vespertide-loader" +version = "0.1.9" +dependencies = [ + "anyhow", + "rstest", + "serde_json", + "serde_yaml", + "serial_test 1.0.0", + "tempfile", + "vespertide-config", + "vespertide-core", + "vespertide-planner", +] + [[package]] name = "vespertide-macro" -version = "0.1.7" +version = "0.1.9" dependencies = [ "proc-macro2", "quote", - "serde_json", - "serde_yaml", "syn 2.0.111", "tempfile", "thiserror", "vespertide-config", "vespertide-core", + "vespertide-loader", "vespertide-query", ] [[package]] name = "vespertide-planner" -version = "0.1.7" +version = "0.1.9" dependencies = [ "rstest", "thiserror", @@ -3022,7 +3081,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.7" +version = "0.1.9" dependencies = [ "insta", "rstest", diff --git a/Cargo.toml b/Cargo.toml index 28871f7..e2a1a4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ documentation = "https://docs.rs/vespertide" [workspace.dependencies] vespertide-core = { path = "crates/vespertide-core", version = "0.1.9" } vespertide-config = { path = "crates/vespertide-config", version = "0.1.9" } +vespertide-loader = { path = "crates/vespertide-loader", version = "0.1.9" } vespertide-macro = { path = "crates/vespertide-macro", version = "0.1.9" } vespertide-planner = { path = "crates/vespertide-planner", version = "0.1.9" } vespertide-query = { path = "crates/vespertide-query", version = "0.1.9" } diff --git a/crates/vespertide-cli/Cargo.toml b/crates/vespertide-cli/Cargo.toml index 1c4edbc..6c5c1a8 100644 --- a/crates/vespertide-cli/Cargo.toml +++ b/crates/vespertide-cli/Cargo.toml @@ -19,6 +19,7 @@ serde_yaml = "0.9" schemars = "1.1" vespertide-config = { workspace = true } vespertide-core = { workspace = true } +vespertide-loader = { workspace = true } vespertide-planner = { workspace = true } vespertide-query = { workspace = true } vespertide-exporter = { workspace = true } diff --git a/crates/vespertide-cli/src/commands/diff.rs b/crates/vespertide-cli/src/commands/diff.rs index adc4673..7146a0a 100644 --- a/crates/vespertide-cli/src/commands/diff.rs +++ b/crates/vespertide-cli/src/commands/diff.rs @@ -184,7 +184,7 @@ mod tests { use std::path::PathBuf; use tempfile::tempdir; use vespertide_config::VespertideConfig; - use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableDef}; + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint, TableDef}; struct CwdGuard { original: PathBuf, @@ -226,7 +226,10 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], indexes: vec![], }; let path = models_dir.join(format!("{name}.json")); diff --git a/crates/vespertide-cli/src/commands/log.rs b/crates/vespertide-cli/src/commands/log.rs index 25e8f9f..f9db36f 100644 --- a/crates/vespertide-cli/src/commands/log.rs +++ b/crates/vespertide-cli/src/commands/log.rs @@ -1,11 +1,13 @@ use anyhow::Result; use colored::Colorize; +use vespertide_loader::{load_config, load_models}; use vespertide_query::{DatabaseBackend, build_plan_queries}; use crate::utils::load_migrations; -pub fn cmd_log() -> Result<()> { - let plans = load_migrations(&crate::utils::load_config()?)?; +pub fn cmd_log(backend: DatabaseBackend) -> Result<()> { + let config = load_config()?; + let plans = load_migrations(&config)?; if plans.is_empty() { println!("{}", "No migrations found.".bright_yellow()); @@ -19,7 +21,7 @@ pub fn cmd_log() -> Result<()> { plans.len().to_string().bright_yellow().bold() ); println!(); - + let current_models = load_models(&config)?; for plan in &plans { println!( "{} {}", @@ -42,22 +44,42 @@ pub fn cmd_log() -> Result<()> { plan.actions.len().to_string().bright_yellow() ); - let queries = build_plan_queries(plan) + let plan_queries = build_plan_queries(plan, ¤t_models) .map_err(|e| anyhow::anyhow!("query build error for v{}: {}", plan.version, e))?; - println!( - " {} {}", - "SQL statements:".bright_cyan().bold(), - queries.len().to_string().bright_yellow().bold() - ); - for (i, q) in queries.iter().enumerate() { + for (i, pq) in plan_queries.iter().enumerate() { println!( " {}. {}", (i + 1).to_string().bright_magenta().bold(), - q.build(DatabaseBackend::Postgres).trim().bright_white() + 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(), + } ); - println!(" {} {:?}", "binds:".bright_cyan(), q.binds()); } + println!(); } @@ -113,7 +135,7 @@ mod tests { #[test] #[serial_test::serial] - fn cmd_log_with_single_migration() { + fn cmd_log_with_single_migration_postgres() { let tmp = tempdir().unwrap(); let _guard = CwdGuard::new(&tmp.path().to_path_buf()); @@ -121,13 +143,69 @@ mod tests { write_config(&cfg); write_migration(&cfg); - let result = cmd_log(); + let result = cmd_log(DatabaseBackend::Postgres); + assert!(result.is_ok()); + } + + #[test] + #[serial_test::serial] + fn cmd_log_with_single_migration_mysql() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let cfg = VespertideConfig::default(); + write_config(&cfg); + write_migration(&cfg); + + let result = cmd_log(DatabaseBackend::MySql); + assert!(result.is_ok()); + } + + #[test] + #[serial_test::serial] + fn cmd_log_with_single_migration_sqlite() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let cfg = VespertideConfig::default(); + write_config(&cfg); + write_migration(&cfg); + + let result = cmd_log(DatabaseBackend::Sqlite); + assert!(result.is_ok()); + } + + #[test] + #[serial_test::serial] + fn cmd_log_no_migrations_postgres() { + 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(); + + let result = cmd_log(DatabaseBackend::Postgres); + assert!(result.is_ok()); + } + + #[test] + #[serial_test::serial] + fn cmd_log_no_migrations_mysql() { + 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(); + + let result = cmd_log(DatabaseBackend::MySql); assert!(result.is_ok()); } #[test] #[serial_test::serial] - fn cmd_log_no_migrations() { + fn cmd_log_no_migrations_sqlite() { let tmp = tempdir().unwrap(); let _guard = CwdGuard::new(&tmp.path().to_path_buf()); @@ -135,7 +213,7 @@ mod tests { write_config(&cfg); fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let result = cmd_log(); + let result = cmd_log(DatabaseBackend::Sqlite); assert!(result.is_ok()); } } diff --git a/crates/vespertide-cli/src/commands/revision.rs b/crates/vespertide-cli/src/commands/revision.rs index a2d0d3b..77a1faf 100644 --- a/crates/vespertide-cli/src/commands/revision.rs +++ b/crates/vespertide-cli/src/commands/revision.rs @@ -124,7 +124,7 @@ mod tests { use std::{env, fs, path::PathBuf}; use tempfile::tempdir; use vespertide_config::{FileFormat, VespertideConfig}; - use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableDef}; + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint, TableDef}; struct CwdGuard { original: PathBuf, @@ -174,7 +174,10 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], indexes: vec![], }; let path = models_dir.join(format!("{name}.json")); diff --git a/crates/vespertide-cli/src/commands/sql.rs b/crates/vespertide-cli/src/commands/sql.rs index 8d82ed4..e7b2a31 100644 --- a/crates/vespertide-cli/src/commands/sql.rs +++ b/crates/vespertide-cli/src/commands/sql.rs @@ -5,7 +5,7 @@ use vespertide_query::{DatabaseBackend, build_plan_queries}; use crate::utils::{load_config, load_migrations, load_models}; -pub fn cmd_sql() -> Result<()> { +pub fn cmd_sql(backend: DatabaseBackend) -> Result<()> { let config = load_config()?; let current_models = load_models(&config)?; let applied_plans = load_migrations(&config)?; @@ -13,10 +13,14 @@ pub fn cmd_sql() -> Result<()> { let plan = plan_next_migration(¤t_models, &applied_plans) .map_err(|e| anyhow::anyhow!("planning error: {}", e))?; - emit_sql(&plan) + emit_sql(&plan, backend, ¤t_models) } -fn emit_sql(plan: &vespertide_core::MigrationPlan) -> Result<()> { +fn emit_sql( + plan: &vespertide_core::MigrationPlan, + backend: DatabaseBackend, + current_schema: &[vespertide_core::TableDef], +) -> Result<()> { if plan.actions.is_empty() { println!( "{} {}", @@ -26,8 +30,18 @@ fn emit_sql(plan: &vespertide_core::MigrationPlan) -> Result<()> { return Ok(()); } - let queries = - build_plan_queries(plan).map_err(|e| anyhow::anyhow!("query build error: {}", e))?; + let plan_queries = build_plan_queries(plan, current_schema) + .map_err(|e| anyhow::anyhow!("query build error: {}", e))?; + + // Select queries for the specified backend + let queries: Vec<_> = plan_queries + .iter() + .flat_map(|pq| match backend { + DatabaseBackend::Postgres => &pq.postgres, + DatabaseBackend::MySql => &pq.mysql, + DatabaseBackend::Sqlite => &pq.sqlite, + }) + .collect(); println!( "{} {}", @@ -56,13 +70,31 @@ fn emit_sql(plan: &vespertide_core::MigrationPlan) -> Result<()> { ); println!(); - for (i, q) in queries.iter().enumerate() { + for (i, pq) in plan_queries.iter().enumerate() { + let queries = match backend { + DatabaseBackend::Postgres => &pq.postgres, + DatabaseBackend::MySql => &pq.mysql, + DatabaseBackend::Sqlite => &pq.sqlite, + }; println!( - "{}. {}", - (i + 1).to_string().bright_magenta().bold(), - q.build(DatabaseBackend::Postgres).trim().bright_white() + "{} {}", + "Action:".bright_cyan(), + pq.action.to_string().bright_white() ); - println!(" {} {:?}", "binds:".bright_cyan(), q.binds()); + for (j, q) in queries.iter().enumerate() { + println!( + "{}{}. {}", + (i + 1).to_string().bright_magenta().bold(), + if queries.len() > 1 { + format!("-{}", j + 1) + } else { + "".to_string() + } + .bright_magenta() + .bold(), + q.build(backend).trim().bright_white() + ); + } } Ok(()) @@ -122,7 +154,10 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], indexes: vec![], }; let path = models_dir.join(format!("{name}.json")); @@ -131,28 +166,52 @@ mod tests { #[test] #[serial] - fn cmd_sql_emits_queries() { + fn cmd_sql_emits_queries_postgres() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let _cfg = write_config(); + write_model("users"); + + let result = cmd_sql(DatabaseBackend::Postgres); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn cmd_sql_emits_queries_mysql() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let _cfg = write_config(); + write_model("users"); + + let result = cmd_sql(DatabaseBackend::MySql); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn cmd_sql_emits_queries_sqlite() { let tmp = tempdir().unwrap(); let _guard = CwdGuard::new(&tmp.path().to_path_buf()); let _cfg = write_config(); write_model("users"); - // No migrations yet -> plan will create table - let result = cmd_sql(); + let result = cmd_sql(DatabaseBackend::Sqlite); assert!(result.is_ok()); } #[test] #[serial] - fn cmd_sql_no_changes() { + fn cmd_sql_no_changes_postgres() { let tmp = tempdir().unwrap(); let _guard = CwdGuard::new(&tmp.path().to_path_buf()); let cfg = write_config(); write_model("users"); - // Create initial migration to establish baseline let plan = MigrationPlan { comment: None, created_at: None, @@ -180,13 +239,93 @@ mod tests { let path = cfg.migrations_dir().join("0001_init.json"); fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - let result = cmd_sql(); + let result = cmd_sql(DatabaseBackend::Postgres); assert!(result.is_ok()); } #[test] #[serial] - fn emit_sql_prints_created_at_and_comment() { + fn cmd_sql_no_changes_mysql() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let cfg = write_config(); + write_model("users"); + + let plan = MigrationPlan { + comment: None, + created_at: None, + 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: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }], + }; + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + let result = cmd_sql(DatabaseBackend::MySql); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn cmd_sql_no_changes_sqlite() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let cfg = write_config(); + write_model("users"); + + let plan = MigrationPlan { + comment: None, + created_at: None, + 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: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }], + }; + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + let result = cmd_sql(DatabaseBackend::Sqlite); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn emit_sql_prints_created_at_and_comment_postgres() { let plan = MigrationPlan { comment: Some("with comment".into()), created_at: Some("2024-01-02T00:00:00Z".into()), @@ -196,7 +335,133 @@ mod tests { }], }; - let result = emit_sql(&plan); + let result = emit_sql(&plan, DatabaseBackend::Postgres, &[]); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn emit_sql_prints_created_at_and_comment_mysql() { + let plan = MigrationPlan { + comment: Some("with comment".into()), + created_at: Some("2024-01-02T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }], + }; + + let result = emit_sql(&plan, DatabaseBackend::MySql, &[]); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn emit_sql_prints_created_at_and_comment_sqlite() { + let plan = MigrationPlan { + comment: Some("with comment".into()), + created_at: Some("2024-01-02T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }], + }; + + let result = emit_sql(&plan, DatabaseBackend::Sqlite, &[]); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn emit_sql_multiple_queries() { + let plan = MigrationPlan { + comment: None, + created_at: None, + 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: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }, + MigrationAction::AddIndex { + table: "users".into(), + index: vespertide_core::IndexDef { + name: "idx_id".into(), + columns: vec!["id".into()], + unique: false, + }, + }, + ], + }; + + let result = emit_sql(&plan, DatabaseBackend::Postgres, &[]); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn emit_sql_multiple_queries_per_action() { + // Test case where a single action generates multiple queries (e.g., SQLite constraint addition) + // This should trigger the queries.len() > 1 branch (line 89) + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + let _cfg = write_config(); + write_model("users"); + + // Create a migration that adds a NOT NULL column in SQLite, which generates multiple queries + let plan = MigrationPlan { + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { + table: "users".into(), + 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, + }, + fill_with: Some("default".into()), + }], + }; + + 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![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + indexes: vec![], + }]; + + let result = emit_sql(&plan, DatabaseBackend::Sqlite, ¤t_schema); assert!(result.is_ok()); } } diff --git a/crates/vespertide-cli/src/commands/status.rs b/crates/vespertide-cli/src/commands/status.rs index 259e3b4..8243ea6 100644 --- a/crates/vespertide-cli/src/commands/status.rs +++ b/crates/vespertide-cli/src/commands/status.rs @@ -144,7 +144,8 @@ mod tests { use tempfile::tempdir; use vespertide_config::VespertideConfig; use vespertide_core::{ - ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, TableDef, + ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, TableConstraint, + TableDef, }; struct CwdGuard { @@ -188,7 +189,10 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], indexes: vec![], }; let path = models_dir.join(format!("{name}.json")); diff --git a/crates/vespertide-cli/src/main.rs b/crates/vespertide-cli/src/main.rs index 1a0c5d2..2073b80 100644 --- a/crates/vespertide-cli/src/main.rs +++ b/crates/vespertide-cli/src/main.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use clap::{CommandFactory, Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; mod commands; mod utils; @@ -8,6 +8,24 @@ use commands::{ cmd_diff, cmd_export, cmd_init, cmd_log, cmd_new, cmd_revision, cmd_sql, cmd_status, }; use vespertide_config::FileFormat; +use vespertide_query::DatabaseBackend; + +#[derive(Copy, Clone, Debug, ValueEnum)] +enum BackendArg { + Postgres, + Mysql, + Sqlite, +} + +impl From for DatabaseBackend { + fn from(value: BackendArg) -> Self { + match value { + BackendArg::Postgres => DatabaseBackend::Postgres, + BackendArg::Mysql => DatabaseBackend::MySql, + BackendArg::Sqlite => DatabaseBackend::Sqlite, + } + } +} /// vespertide command-line interface. #[derive(Parser, Debug)] @@ -22,9 +40,17 @@ enum Commands { /// Show diff between applied migrations and current models. Diff, /// Show SQL statements for the pending migration plan. - Sql, + Sql { + /// Database backend for SQL generation. + #[arg(short = 'b', long = "backend", value_enum, default_value = "postgres")] + backend: BackendArg, + }, /// Show SQL per applied migration (chronological log). - Log, + Log { + /// Database backend for SQL generation. + #[arg(short = 'b', long = "backend", value_enum, default_value = "postgres")] + backend: BackendArg, + }, /// Create a new model file from template. New { /// Model name (table name). @@ -57,8 +83,8 @@ fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { Some(Commands::Diff) => cmd_diff(), - Some(Commands::Sql) => cmd_sql(), - Some(Commands::Log) => cmd_log(), + Some(Commands::Sql { backend }) => cmd_sql(backend.into()), + Some(Commands::Log { backend }) => cmd_log(backend.into()), Some(Commands::New { name, format }) => cmd_new(name, format), Some(Commands::Status) => cmd_status(), Some(Commands::Revision { message }) => cmd_revision(message), diff --git a/crates/vespertide-cli/src/utils.rs b/crates/vespertide-cli/src/utils.rs index c4549bb..7775bd7 100644 --- a/crates/vespertide-cli/src/utils.rs +++ b/crates/vespertide-cli/src/utils.rs @@ -1,130 +1,7 @@ -use std::fs; -use std::path::{Path, PathBuf}; +use vespertide_config::FileFormat; -use anyhow::{Context, Result}; -use vespertide_config::{FileFormat, VespertideConfig}; -use vespertide_core::{MigrationPlan, TableDef}; -use vespertide_planner::{validate_migration_plan, validate_schema}; - -/// Load vespertide.json config from current directory. -pub fn load_config() -> Result { - let path = PathBuf::from("vespertide.json"); - if !path.exists() { - anyhow::bail!("vespertide.json not found. Run 'vespertide init' first."); - } - - let content = fs::read_to_string(&path).context("read vespertide.json")?; - let config: VespertideConfig = - serde_json::from_str(&content).context("parse vespertide.json")?; - Ok(config) -} - -/// Load all model definitions from the models directory (recursively). -pub fn load_models(config: &VespertideConfig) -> Result> { - let models_dir = config.models_dir(); - if !models_dir.exists() { - return Ok(Vec::new()); - } - - 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 before returning - if !normalized_tables.is_empty() { - validate_schema(&normalized_tables) - .map_err(|e| anyhow::anyhow!("schema validation failed: {}", e))?; - } - - Ok(normalized_tables) -} - -/// Recursively walk directory and load model files. -fn load_models_recursive(dir: &Path, tables: &mut Vec) -> Result<()> { - let entries = - fs::read_dir(dir).with_context(|| format!("read models directory: {}", dir.display()))?; - - for entry in entries { - let entry = entry.context("read directory entry")?; - let path = entry.path(); - - if path.is_dir() { - // Recursively process subdirectories - load_models_recursive(&path, tables)?; - continue; - } - - if path.is_file() { - let ext = path.extension().and_then(|s| s.to_str()); - if matches!(ext, Some("json") | Some("yaml") | Some("yml")) { - let content = fs::read_to_string(&path) - .with_context(|| format!("read model file: {}", path.display()))?; - - let table: TableDef = if ext == Some("json") { - serde_json::from_str(&content) - .with_context(|| format!("parse JSON model: {}", path.display()))? - } else { - serde_yaml::from_str(&content) - .with_context(|| format!("parse YAML model: {}", path.display()))? - }; - - tables.push(table); - } - } - } - - Ok(()) -} - -/// Load all migration plans from the migrations directory, sorted by version. -pub fn load_migrations(config: &VespertideConfig) -> Result> { - let migrations_dir = config.migrations_dir(); - if !migrations_dir.exists() { - return Ok(Vec::new()); - } - - let mut plans = Vec::new(); - let entries = fs::read_dir(migrations_dir).context("read migrations directory")?; - - for entry in entries { - let entry = entry.context("read directory entry")?; - let path = entry.path(); - if path.is_file() { - let ext = path.extension().and_then(|s| s.to_str()); - if ext == Some("json") || ext == Some("yaml") || ext == Some("yml") { - let content = fs::read_to_string(&path) - .with_context(|| format!("read migration file: {}", path.display()))?; - - let plan: MigrationPlan = if ext == Some("json") { - serde_json::from_str(&content) - .with_context(|| format!("parse migration: {}", path.display()))? - } else { - serde_yaml::from_str(&content) - .with_context(|| format!("parse migration: {}", path.display()))? - }; - - // Validate the migration plan - validate_migration_plan(&plan) - .with_context(|| format!("validate migration: {}", path.display()))?; - - plans.push(plan); - } - } - } - - // Sort by version number - plans.sort_by_key(|p| p.version); - Ok(plans) -} +// Re-export loader functions for convenience +pub use vespertide_loader::{load_config, load_migrations, load_models}; /// Generate a migration filename from version and optional comment with format and pattern. pub fn migration_filename_with_format_and_pattern( @@ -228,9 +105,12 @@ mod tests { use rstest::rstest; use serial_test::serial; use std::fs; + use std::path::PathBuf; use tempfile::tempdir; + use vespertide_config::VespertideConfig; use vespertide_core::{ - ColumnDef, ColumnType, SimpleColumnType, schema::foreign_key::ForeignKeySyntax, + ColumnDef, ColumnType, MigrationPlan, SimpleColumnType, TableConstraint, TableDef, + schema::foreign_key::ForeignKeySyntax, }; struct CwdGuard { @@ -287,7 +167,10 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], indexes: vec![], }; fs::write("models/users.yaml", serde_yaml::to_string(&table).unwrap()).unwrap(); @@ -320,7 +203,10 @@ mod tests { index: None, foreign_key: None, }], - constraints: vec![], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], indexes: vec![], }; let content = serde_json::to_string_pretty(&table).unwrap(); diff --git a/crates/vespertide-cli/tests/integration_test.rs b/crates/vespertide-cli/tests/integration_test.rs index 3e50cd3..562d3d4 100644 --- a/crates/vespertide-cli/tests/integration_test.rs +++ b/crates/vespertide-cli/tests/integration_test.rs @@ -39,6 +39,20 @@ fn test_main_with_sql_command() { let _ = cmd.assert(); } +#[test] +fn test_main_with_sql_command_mysql() { + let mut cmd = vespertide(); + cmd.args(["sql", "--backend", "mysql"]); + let _ = cmd.assert(); +} + +#[test] +fn test_main_with_sql_command_sqlite() { + let mut cmd = vespertide(); + cmd.args(["sql", "--backend", "sqlite"]); + let _ = cmd.assert(); +} + #[test] fn test_main_with_log_command() { let mut cmd = vespertide(); @@ -46,6 +60,20 @@ fn test_main_with_log_command() { let _ = cmd.assert(); } +#[test] +fn test_main_with_log_command_mysql() { + let mut cmd = vespertide(); + cmd.args(["log", "--backend", "mysql"]); + let _ = cmd.assert(); +} + +#[test] +fn test_main_with_log_command_sqlite() { + let mut cmd = vespertide(); + cmd.args(["log", "--backend", "sqlite"]); + let _ = cmd.assert(); +} + #[test] fn test_main_with_status_command() { let mut cmd = vespertide(); diff --git a/crates/vespertide-core/src/action.rs b/crates/vespertide-core/src/action.rs index 3b52953..6c3b0b7 100644 --- a/crates/vespertide-core/src/action.rs +++ b/crates/vespertide-core/src/action.rs @@ -3,6 +3,7 @@ use crate::schema::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::fmt; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -69,3 +70,354 @@ pub enum MigrationAction { sql: String, }, } + +impl fmt::Display for MigrationAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MigrationAction::CreateTable { table, .. } => { + write!(f, "CreateTable: {}", table) + } + MigrationAction::DeleteTable { table } => { + write!(f, "DeleteTable: {}", table) + } + MigrationAction::AddColumn { table, column, .. } => { + write!(f, "AddColumn: {}.{}", table, column.name) + } + MigrationAction::RenameColumn { table, from, to } => { + write!(f, "RenameColumn: {}.{} -> {}", table, from, to) + } + MigrationAction::DeleteColumn { table, column } => { + write!(f, "DeleteColumn: {}.{}", table, column) + } + 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", + TableConstraint::Unique { name, .. } => { + if let Some(n) = name { + return write!(f, "AddConstraint: {}.{} (UNIQUE)", table, n); + } + "UNIQUE" + } + TableConstraint::ForeignKey { name, .. } => { + if let Some(n) = name { + return write!(f, "AddConstraint: {}.{} (FOREIGN KEY)", table, n); + } + "FOREIGN KEY" + } + TableConstraint::Check { name, .. } => { + return write!(f, "AddConstraint: {}.{} (CHECK)", table, name); + } + }; + write!(f, "AddConstraint: {}.{}", table, constraint_name) + } + MigrationAction::RemoveConstraint { table, constraint } => { + let constraint_name = match constraint { + TableConstraint::PrimaryKey { .. } => "PRIMARY KEY", + TableConstraint::Unique { name, .. } => { + if let Some(n) = name { + return write!(f, "RemoveConstraint: {}.{} (UNIQUE)", table, n); + } + "UNIQUE" + } + TableConstraint::ForeignKey { name, .. } => { + if let Some(n) = name { + return write!(f, "RemoveConstraint: {}.{} (FOREIGN KEY)", table, n); + } + "FOREIGN KEY" + } + TableConstraint::Check { name, .. } => { + return write!(f, "RemoveConstraint: {}.{} (CHECK)", table, name); + } + }; + write!(f, "RemoveConstraint: {}.{}", table, constraint_name) + } + MigrationAction::RenameTable { from, to } => { + write!(f, "RenameTable: {} -> {}", from, to) + } + MigrationAction::RawSql { sql } => { + // Truncate SQL if too long for display + let display_sql = if sql.len() > 50 { + format!("{}...", &sql[..47]) + } else { + sql.clone() + }; + write!(f, "RawSql: {}", display_sql) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::{IndexDef, ReferenceAction, SimpleColumnType}; + use rstest::rstest; + + fn default_column() -> ColumnDef { + 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, + } + } + + #[rstest] + #[case::create_table( + MigrationAction::CreateTable { + table: "users".into(), + columns: vec![], + constraints: vec![], + }, + "CreateTable: users" + )] + #[case::delete_table( + MigrationAction::DeleteTable { + table: "users".into(), + }, + "DeleteTable: users" + )] + #[case::add_column( + MigrationAction::AddColumn { + table: "users".into(), + column: default_column(), + fill_with: None, + }, + "AddColumn: users.email" + )] + #[case::rename_column( + MigrationAction::RenameColumn { + table: "users".into(), + from: "old_name".into(), + to: "new_name".into(), + }, + "RenameColumn: users.old_name -> new_name" + )] + #[case::delete_column( + MigrationAction::DeleteColumn { + table: "users".into(), + column: "email".into(), + }, + "DeleteColumn: users.email" + )] + #[case::modify_column_type( + MigrationAction::ModifyColumnType { + table: "users".into(), + column: "age".into(), + new_type: ColumnType::Simple(SimpleColumnType::Integer), + }, + "ModifyColumnType: users.age" + )] + #[case::add_index( + MigrationAction::AddIndex { + table: "users".into(), + index: IndexDef { + name: "idx_email".into(), + columns: vec!["email".into()], + unique: false, + }, + }, + "AddIndex: users.idx_email" + )] + #[case::remove_index( + MigrationAction::RemoveIndex { + table: "users".into(), + name: "idx_email".into(), + }, + "RemoveIndex: idx_email" + )] + #[case::rename_table( + MigrationAction::RenameTable { + from: "old_table".into(), + to: "new_table".into(), + }, + "RenameTable: old_table -> new_table" + )] + fn test_display_basic_actions(#[case] action: MigrationAction, #[case] expected: &str) { + assert_eq!(action.to_string(), expected); + } + + #[rstest] + #[case::add_constraint_primary_key( + MigrationAction::AddConstraint { + table: "users".into(), + constraint: TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }, + }, + "AddConstraint: users.PRIMARY KEY" + )] + #[case::add_constraint_unique_with_name( + MigrationAction::AddConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }, + }, + "AddConstraint: users.uq_email (UNIQUE)" + )] + #[case::add_constraint_unique_without_name( + MigrationAction::AddConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: None, + columns: vec!["email".into()], + }, + }, + "AddConstraint: users.UNIQUE" + )] + #[case::add_constraint_foreign_key_with_name( + MigrationAction::AddConstraint { + table: "posts".into(), + constraint: TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: Some(ReferenceAction::Cascade), + on_update: None, + }, + }, + "AddConstraint: posts.fk_user (FOREIGN KEY)" + )] + #[case::add_constraint_foreign_key_without_name( + MigrationAction::AddConstraint { + table: "posts".into(), + constraint: TableConstraint::ForeignKey { + name: None, + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }, + }, + "AddConstraint: posts.FOREIGN KEY" + )] + #[case::add_constraint_check( + MigrationAction::AddConstraint { + table: "users".into(), + constraint: TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }, + }, + "AddConstraint: users.chk_age (CHECK)" + )] + fn test_display_add_constraint(#[case] action: MigrationAction, #[case] expected: &str) { + assert_eq!(action.to_string(), expected); + } + + #[rstest] + #[case::remove_constraint_primary_key( + MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }, + }, + "RemoveConstraint: users.PRIMARY KEY" + )] + #[case::remove_constraint_unique_with_name( + MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }, + }, + "RemoveConstraint: users.uq_email (UNIQUE)" + )] + #[case::remove_constraint_unique_without_name( + MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: None, + columns: vec!["email".into()], + }, + }, + "RemoveConstraint: users.UNIQUE" + )] + #[case::remove_constraint_foreign_key_with_name( + MigrationAction::RemoveConstraint { + table: "posts".into(), + constraint: TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }, + }, + "RemoveConstraint: posts.fk_user (FOREIGN KEY)" + )] + #[case::remove_constraint_foreign_key_without_name( + MigrationAction::RemoveConstraint { + table: "posts".into(), + constraint: TableConstraint::ForeignKey { + name: None, + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }, + }, + "RemoveConstraint: posts.FOREIGN KEY" + )] + #[case::remove_constraint_check( + MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }, + }, + "RemoveConstraint: users.chk_age (CHECK)" + )] + fn test_display_remove_constraint(#[case] action: MigrationAction, #[case] expected: &str) { + assert_eq!(action.to_string(), expected); + } + + #[rstest] + #[case::raw_sql_short( + MigrationAction::RawSql { + sql: "SELECT 1".into(), + }, + "RawSql: SELECT 1" + )] + fn test_display_raw_sql_short(#[case] action: MigrationAction, #[case] expected: &str) { + assert_eq!(action.to_string(), expected); + } + + #[test] + fn test_display_raw_sql_long() { + let action = MigrationAction::RawSql { + sql: + "SELECT * FROM users WHERE id = 1 AND name = 'test' AND email = 'test@example.com'" + .into(), + }; + let result = action.to_string(); + assert!(result.starts_with("RawSql: ")); + assert!(result.ends_with("...")); + assert!(result.len() > 10); + } +} diff --git a/crates/vespertide-core/src/schema/table.rs b/crates/vespertide-core/src/schema/table.rs index 2756bcb..e5cda51 100644 --- a/crates/vespertide-core/src/schema/table.rs +++ b/crates/vespertide-core/src/schema/table.rs @@ -745,6 +745,26 @@ mod tests { 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 diff --git a/crates/vespertide-loader/Cargo.toml b/crates/vespertide-loader/Cargo.toml new file mode 100644 index 0000000..3e7d323 --- /dev/null +++ b/crates/vespertide-loader/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "vespertide-loader" +version = "0.1.9" +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +description = "Loads migrations and models from filesystem" + +[dependencies] +vespertide-core = { workspace = true } +vespertide-config = { workspace = true } +vespertide-planner = { workspace = true } +anyhow = "1" +serde_json = "1.0" +serde_yaml = "0.9" + +[dev-dependencies] +tempfile = "3" +rstest = "0.26" +serial_test = "1.0" diff --git a/crates/vespertide-loader/src/config.rs b/crates/vespertide-loader/src/config.rs new file mode 100644 index 0000000..62f9a91 --- /dev/null +++ b/crates/vespertide-loader/src/config.rs @@ -0,0 +1,168 @@ +use std::fs; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use vespertide_config::VespertideConfig; + +/// Load vespertide.json config from current directory. +pub fn load_config() -> Result { + let path = PathBuf::from("vespertide.json"); + if !path.exists() { + anyhow::bail!("vespertide.json not found. Run 'vespertide init' first."); + } + + let content = fs::read_to_string(&path).context("read vespertide.json")?; + let config: VespertideConfig = + serde_json::from_str(&content).context("parse vespertide.json")?; + Ok(config) +} + +/// Load config from a specific path. +pub fn load_config_from_path(path: PathBuf) -> Result { + if !path.exists() { + anyhow::bail!("vespertide.json not found at: {}", path.display()); + } + + let content = fs::read_to_string(&path).context("read vespertide.json")?; + let config: VespertideConfig = + serde_json::from_str(&content).context("parse vespertide.json")?; + Ok(config) +} + +/// Load config from project root, with fallback to defaults. +pub fn load_config_or_default(project_root: Option) -> Result { + let config_path = if let Some(root) = project_root { + root.join("vespertide.json") + } else { + PathBuf::from("vespertide.json") + }; + + if config_path.exists() { + load_config_from_path(config_path) + } else { + Ok(VespertideConfig::default()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serial_test::serial; + use std::fs; + use tempfile::tempdir; + + struct CwdGuard { + original: PathBuf, + } + + impl CwdGuard { + fn new(dir: &PathBuf) -> Self { + let original = std::env::current_dir().unwrap(); + std::env::set_current_dir(dir).unwrap(); + Self { original } + } + } + + impl Drop for CwdGuard { + fn drop(&mut self) { + let _ = std::env::set_current_dir(&self.original); + } + } + + fn write_config(path: &PathBuf) { + let cfg = VespertideConfig::default(); + let text = serde_json::to_string_pretty(&cfg).unwrap(); + fs::write(path, text).unwrap(); + } + + #[test] + #[serial] + fn test_load_config_from_path_success() { + let tmp = tempdir().unwrap(); + let config_path = tmp.path().join("vespertide.json"); + write_config(&config_path); + + let result = load_config_from_path(config_path); + assert!(result.is_ok()); + let config = result.unwrap(); + assert_eq!(config.models_dir, PathBuf::from("models")); + } + + #[test] + #[serial] + fn test_load_config_from_path_not_found() { + let tmp = tempdir().unwrap(); + let config_path = tmp.path().join("nonexistent.json"); + + let result = load_config_from_path(config_path.clone()); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("vespertide.json not found at:")); + assert!(err_msg.contains(&config_path.display().to_string())); + } + + #[test] + #[serial] + fn test_load_config_or_default_with_root() { + let tmp = tempdir().unwrap(); + let config_path = tmp.path().join("vespertide.json"); + write_config(&config_path); + + let result = load_config_or_default(Some(tmp.path().to_path_buf())); + assert!(result.is_ok()); + let config = result.unwrap(); + assert_eq!(config.models_dir, PathBuf::from("models")); + } + + #[test] + #[serial] + fn test_load_config_or_default_without_root() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + let config_path = PathBuf::from("vespertide.json"); + write_config(&config_path); + + let result = load_config_or_default(None); + assert!(result.is_ok()); + let config = result.unwrap(); + assert_eq!(config.models_dir, PathBuf::from("models")); + } + + #[test] + #[serial] + fn test_load_config_or_default_fallback_to_default() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let result = load_config_or_default(None); + assert!(result.is_ok()); + let config = result.unwrap(); + assert_eq!(config.models_dir, PathBuf::from("models")); + } + + #[test] + #[serial] + fn test_load_config_success() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + let config_path = PathBuf::from("vespertide.json"); + write_config(&config_path); + + let result = load_config(); + assert!(result.is_ok()); + let config = result.unwrap(); + assert_eq!(config.models_dir, PathBuf::from("models")); + } + + #[test] + #[serial] + fn test_load_config_not_found() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + + let result = load_config(); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("vespertide.json not found")); + } +} diff --git a/crates/vespertide-loader/src/lib.rs b/crates/vespertide-loader/src/lib.rs new file mode 100644 index 0000000..b99ffaa --- /dev/null +++ b/crates/vespertide-loader/src/lib.rs @@ -0,0 +1,7 @@ +pub mod config; +pub mod migrations; +pub mod models; + +pub use config::{load_config, load_config_from_path, load_config_or_default}; +pub use migrations::{load_migrations, load_migrations_at_compile_time, load_migrations_from_dir}; +pub use models::{load_models, load_models_at_compile_time, load_models_from_dir}; diff --git a/crates/vespertide-loader/src/migrations.rs b/crates/vespertide-loader/src/migrations.rs new file mode 100644 index 0000000..9cab2bc --- /dev/null +++ b/crates/vespertide-loader/src/migrations.rs @@ -0,0 +1,343 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use vespertide_config::VespertideConfig; +use vespertide_core::MigrationPlan; +use vespertide_planner::validate_migration_plan; + +/// Load all migration plans from the migrations directory, sorted by version. +pub fn load_migrations(config: &VespertideConfig) -> Result> { + let migrations_dir = config.migrations_dir(); + if !migrations_dir.exists() { + return Ok(Vec::new()); + } + + let mut plans = Vec::new(); + let entries = fs::read_dir(migrations_dir).context("read migrations directory")?; + + for entry in entries { + let entry = entry.context("read directory entry")?; + let path = entry.path(); + if path.is_file() { + let ext = path.extension().and_then(|s| s.to_str()); + if ext == Some("json") || ext == Some("yaml") || ext == Some("yml") { + let content = fs::read_to_string(&path) + .with_context(|| format!("read migration file: {}", path.display()))?; + + let plan: MigrationPlan = if ext == Some("json") { + serde_json::from_str(&content) + .with_context(|| format!("parse migration: {}", path.display()))? + } else { + serde_yaml::from_str(&content) + .with_context(|| format!("parse migration: {}", path.display()))? + }; + + // Validate the migration plan + validate_migration_plan(&plan) + .with_context(|| format!("validate migration: {}", path.display()))?; + + plans.push(plan); + } + } + } + + // Sort by version number + plans.sort_by_key(|p| p.version); + Ok(plans) +} + +/// Load migrations from a specific directory (for compile-time use in macros). +pub fn load_migrations_from_dir( + project_root: Option, +) -> Result, Box> { + // Locate project root from CARGO_MANIFEST_DIR or use provided path + let project_root = if let Some(root) = project_root { + root + } else { + let manifest_dir = env::var("CARGO_MANIFEST_DIR") + .map_err(|_| "CARGO_MANIFEST_DIR environment variable not set")?; + PathBuf::from(manifest_dir) + }; + + // Read vespertide.json or use defaults + let config = crate::config::load_config_or_default(Some(project_root.clone())) + .map_err(|e| format!("Failed to load config: {}", e))?; + + // Read migrations directory + let migrations_dir = project_root.join(config.migrations_dir()); + if !migrations_dir.exists() { + return Ok(Vec::new()); + } + + let mut plans = Vec::new(); + let entries = fs::read_dir(&migrations_dir) + .map_err(|e| format!("Failed to read migrations directory: {}", e))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; + let path = entry.path(); + if path.is_file() { + let ext = path.extension().and_then(|s| s.to_str()); + if ext == Some("json") || ext == Some("yaml") || ext == Some("yml") { + let content = fs::read_to_string(&path) + .context(format!("Failed to read migration file {}", path.display()))?; + + let plan: MigrationPlan = if ext == Some("json") { + serde_json::from_str(&content).map_err(|e| { + format!("Failed to parse JSON migration {}: {}", path.display(), e) + })? + } else { + serde_yaml::from_str(&content).map_err(|e| { + format!("Failed to parse YAML migration {}: {}", path.display(), e) + })? + }; + + plans.push(plan); + } + } + } + + // Sort by version + plans.sort_by_key(|p| p.version); + Ok(plans) +} + +/// Load migrations at compile time (for macro use). +pub fn load_migrations_at_compile_time() -> Result, Box> { + load_migrations_from_dir(None) +} + +#[cfg(test)] +mod tests { + use super::*; + use serial_test::serial; + use std::fs; + use tempfile::TempDir; + + #[test] + fn test_load_migrations_from_dir_with_no_migrations_dir() { + let temp_dir = TempDir::new().unwrap(); + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 0); + } + + #[test] + fn test_load_migrations_from_dir_with_empty_migrations_dir() { + let temp_dir = TempDir::new().unwrap(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir_all(&migrations_dir).unwrap(); + + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 0); + } + + #[test] + fn test_load_migrations_from_dir_with_json_migration() { + let temp_dir = TempDir::new().unwrap(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir_all(&migrations_dir).unwrap(); + + let migration_content = r#"{ + "version": 1, + "actions": [ + { + "type": "create_table", + "table": "users", + "columns": [ + { + "name": "id", + "type": "integer", + "nullable": false + } + ], + "constraints": [] + } + ] + }"#; + + fs::write(migrations_dir.join("0001_test.json"), migration_content).unwrap(); + + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let plans = result.unwrap(); + assert_eq!(plans.len(), 1); + assert_eq!(plans[0].version, 1); + } + + #[test] + fn test_load_migrations_from_dir_sorts_by_version() { + let temp_dir = TempDir::new().unwrap(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir_all(&migrations_dir).unwrap(); + + let migration1 = r#"{"version": 2, "actions": []}"#; + let migration2 = r#"{"version": 1, "actions": []}"#; + let migration3 = r#"{"version": 3, "actions": []}"#; + + fs::write(migrations_dir.join("0002_second.json"), migration1).unwrap(); + fs::write(migrations_dir.join("0001_first.json"), migration2).unwrap(); + fs::write(migrations_dir.join("0003_third.json"), migration3).unwrap(); + + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let plans = result.unwrap(); + assert_eq!(plans.len(), 3); + assert_eq!(plans[0].version, 1); + assert_eq!(plans[1].version, 2); + assert_eq!(plans[2].version, 3); + } + + #[test] + fn test_load_migrations_from_dir_with_yaml_migration() { + let temp_dir = TempDir::new().unwrap(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir_all(&migrations_dir).unwrap(); + + let migration_content = r#"--- +version: 1 +actions: + - type: create_table + table: users + columns: + - name: id + type: integer + nullable: false + constraints: [] +"#; + + fs::write(migrations_dir.join("0001_test.yaml"), migration_content).unwrap(); + + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let plans = result.unwrap(); + assert_eq!(plans.len(), 1); + assert_eq!(plans[0].version, 1); + } + + #[test] + fn test_load_migrations_from_dir_with_yml_migration() { + let temp_dir = TempDir::new().unwrap(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir_all(&migrations_dir).unwrap(); + + let migration_content = r#"--- +version: 1 +actions: + - type: create_table + table: users + columns: + - name: id + type: integer + nullable: false + constraints: [] +"#; + + fs::write(migrations_dir.join("0001_test.yml"), migration_content).unwrap(); + + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let plans = result.unwrap(); + assert_eq!(plans.len(), 1); + assert_eq!(plans[0].version, 1); + } + + #[test] + fn test_load_migrations_from_dir_with_invalid_json() { + let temp_dir = TempDir::new().unwrap(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir_all(&migrations_dir).unwrap(); + + let invalid_json = r#"{"version": 1, "actions": [invalid]}"#; + fs::write(migrations_dir.join("0001_invalid.json"), invalid_json).unwrap(); + + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Failed to parse JSON migration")); + } + + #[test] + fn test_load_migrations_from_dir_with_invalid_yaml() { + let temp_dir = TempDir::new().unwrap(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir_all(&migrations_dir).unwrap(); + + let invalid_yaml = r#"--- +version: 1 +actions: + - invalid: [syntax +"#; + fs::write(migrations_dir.join("0001_invalid.yaml"), invalid_yaml).unwrap(); + + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Failed to parse YAML migration")); + } + + #[test] + fn test_load_migrations_from_dir_with_unreadable_file() { + // Note: Testing file read errors (line 85) is extremely difficult in unit tests + // because it requires actual I/O errors like: + // - Disk failures + // - Permission issues + // - File locks from other processes + // - Network filesystem issues + // + // The error handling code path at line 85 exists and will be executed + // in real-world scenarios when file read errors occur. + // The format! macro and error message construction are tested through + // other error paths (invalid JSON/YAML parsing). + // + // For now, we verify the function works correctly with valid files. + let temp_dir = TempDir::new().unwrap(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir_all(&migrations_dir).unwrap(); + + let file_path = migrations_dir.join("0001_test.json"); + fs::write(&file_path, r#"{"version": 1, "actions": []}"#).unwrap(); + + let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + } + + #[test] + #[serial] + fn test_load_migrations_from_dir_without_project_root() { + // Save the original value + let original = env::var("CARGO_MANIFEST_DIR").ok(); + + // Remove CARGO_MANIFEST_DIR to test the error path + // Note: remove_var is unsafe in multi-threaded environments, + // but serial_test ensures tests run sequentially + unsafe { + env::remove_var("CARGO_MANIFEST_DIR"); + } + + let result = load_migrations_from_dir(None); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("CARGO_MANIFEST_DIR environment variable not set")); + + // Restore the original value if it existed + // Note: In a test environment, we don't restore to avoid affecting other tests + // The serial_test ensures tests run sequentially + drop(original); + } + + #[test] + fn test_load_migrations_at_compile_time() { + // This function just calls load_migrations_from_dir(None) + // We can't easily test it without CARGO_MANIFEST_DIR, but we can verify + // it doesn't panic + let result = load_migrations_at_compile_time(); + // This might succeed if CARGO_MANIFEST_DIR is set (like in cargo test) + // or fail if it's not set + // Either way, we're testing the code path + let _ = result; + } +} diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs new file mode 100644 index 0000000..8d1b73d --- /dev/null +++ b/crates/vespertide-loader/src/models.rs @@ -0,0 +1,592 @@ +use std::fs; +use std::path::Path; + +use anyhow::{Context, Result}; +use vespertide_config::VespertideConfig; +use vespertide_core::TableDef; +use vespertide_planner::validate_schema; + +/// Load all model definitions from the models directory (recursively). +pub fn load_models(config: &VespertideConfig) -> Result> { + let models_dir = config.models_dir(); + if !models_dir.exists() { + return Ok(Vec::new()); + } + + 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 before returning + if !normalized_tables.is_empty() { + validate_schema(&normalized_tables) + .map_err(|e| anyhow::anyhow!("schema validation failed: {}", e))?; + } + + Ok(normalized_tables) +} + +/// Recursively walk directory and load model files. +fn load_models_recursive(dir: &Path, tables: &mut Vec) -> Result<()> { + let entries = + fs::read_dir(dir).with_context(|| format!("read models directory: {}", dir.display()))?; + + for entry in entries { + let entry = entry.context("read directory entry")?; + let path = entry.path(); + + if path.is_dir() { + // Recursively process subdirectories + load_models_recursive(&path, tables)?; + continue; + } + + if path.is_file() { + let ext = path.extension().and_then(|s| s.to_str()); + if matches!(ext, Some("json") | Some("yaml") | Some("yml")) { + let content = fs::read_to_string(&path) + .with_context(|| format!("read model file: {}", path.display()))?; + + let table: TableDef = if ext == Some("json") { + serde_json::from_str(&content) + .with_context(|| format!("parse JSON model: {}", path.display()))? + } else { + serde_yaml::from_str(&content) + .with_context(|| format!("parse YAML model: {}", path.display()))? + }; + + tables.push(table); + } + } + } + + Ok(()) +} + +/// Load models from a specific directory (for compile-time use in macros). +pub fn load_models_from_dir( + project_root: Option, +) -> Result, Box> { + use std::env; + + // Locate project root from CARGO_MANIFEST_DIR or use provided path + let project_root = if let Some(root) = project_root { + root + } else { + std::path::PathBuf::from( + env::var("CARGO_MANIFEST_DIR") + .context("CARGO_MANIFEST_DIR environment variable not set")?, + ) + }; + + // Read vespertide.json or use defaults + let config = crate::config::load_config_or_default(Some(project_root.clone())) + .map_err(|e| format!("Failed to load config: {}", e))?; + + // Read models directory + let models_dir = project_root.join(config.models_dir()); + if !models_dir.exists() { + return Ok(Vec::new()); + } + + let mut tables = Vec::new(); + load_models_recursive_internal(&models_dir, &mut tables) + .map_err(|e| format!("Failed to load models: {}", e))?; + + // Normalize tables + let normalized_tables: Vec = tables + .into_iter() + .map(|t| { + t.normalize() + .map_err(|e| format!("Failed to normalize table '{}': {}", t.name, e)) + }) + .collect::, _>>() + .map_err(|e| e.to_string())?; + + Ok(normalized_tables) +} + +/// Internal recursive function for loading models (used by both runtime and compile-time). +fn load_models_recursive_internal( + dir: &Path, + tables: &mut Vec, +) -> Result<(), Box> { + use std::fs; + + let entries = fs::read_dir(dir) + .map_err(|e| format!("Failed to read models directory {}: {}", dir.display(), e))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; + let path = entry.path(); + + if path.is_dir() { + // Recursively process subdirectories + load_models_recursive_internal(&path, tables)?; + continue; + } + + if path.is_file() { + let ext = path.extension().and_then(|s| s.to_str()); + if matches!(ext, Some("json") | Some("yaml") | Some("yml")) { + let content = fs::read_to_string(&path) + .map_err(|e| format!("Failed to read model file {}: {}", path.display(), e))?; + + let table: TableDef = if ext == Some("json") { + serde_json::from_str(&content).map_err(|e| { + format!("Failed to parse JSON model {}: {}", path.display(), e) + })? + } else { + serde_yaml::from_str(&content).map_err(|e| { + format!("Failed to parse YAML model {}: {}", path.display(), e) + })? + }; + + tables.push(table); + } + } + } + + Ok(()) +} + +/// Load models at compile time (for macro use). +pub fn load_models_at_compile_time() -> Result, Box> { + load_models_from_dir(None) +} + +#[cfg(test)] +mod tests { + use super::*; + use serial_test::serial; + use std::fs; + use tempfile::tempdir; + use vespertide_core::{ + ColumnDef, ColumnType, SimpleColumnType, TableConstraint, + schema::foreign_key::ForeignKeySyntax, + }; + + struct CwdGuard { + original: std::path::PathBuf, + } + + impl CwdGuard { + fn new(dir: &std::path::PathBuf) -> Self { + let original = std::env::current_dir().unwrap(); + std::env::set_current_dir(dir).unwrap(); + Self { original } + } + } + + impl Drop for CwdGuard { + fn drop(&mut self) { + let _ = std::env::set_current_dir(&self.original); + } + } + + fn write_config() { + let cfg = VespertideConfig::default(); + let text = serde_json::to_string_pretty(&cfg).unwrap(); + fs::write("vespertide.json", text).unwrap(); + } + + #[test] + #[serial] + fn load_models_returns_empty_when_no_models_dir() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + write_config(); + + // Don't create models directory + let models = load_models(&VespertideConfig::default()).unwrap(); + assert_eq!(models.len(), 0); + } + + #[test] + #[serial] + fn load_models_reads_yaml_and_validates() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + write_config(); + + fs::create_dir_all("models").unwrap(); + let table = 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![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + indexes: vec![], + }; + fs::write("models/users.yaml", serde_yaml::to_string(&table).unwrap()).unwrap(); + + let models = load_models(&VespertideConfig::default()).unwrap(); + assert_eq!(models.len(), 1); + assert_eq!(models[0].name, "users"); + } + + #[test] + #[serial] + fn load_models_recursive_processes_subdirectories() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + write_config(); + + fs::create_dir_all("models/subdir").unwrap(); + + // Create model in subdirectory + let table = TableDef { + name: "subtable".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![TableConstraint::PrimaryKey { + 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(); + + let models = load_models(&VespertideConfig::default()).unwrap(); + assert_eq!(models.len(), 1); + assert_eq!(models[0].name, "subtable"); + } + + #[test] + #[serial] + fn load_models_fails_on_invalid_fk_format() { + let tmp = tempdir().unwrap(); + let _guard = CwdGuard::new(&tmp.path().to_path_buf()); + write_config(); + + fs::create_dir_all("models").unwrap(); + + // Create a model with invalid FK string format (missing dot separator) + let table = TableDef { + name: "orders".into(), + columns: vec![ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + // Invalid FK format: should be "table.column" but missing the dot + foreign_key: Some(ForeignKeySyntax::String("invalid_format".into())), + }], + constraints: vec![], + indexes: vec![], + }; + fs::write( + "models/orders.json", + serde_json::to_string_pretty(&table).unwrap(), + ) + .unwrap(); + + let result = load_models(&VespertideConfig::default()); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Failed to normalize table 'orders'")); + } + + #[test] + #[serial] + fn test_load_models_from_dir_with_root() { + let temp_dir = tempdir().unwrap(); + let models_dir = temp_dir.path().join("models"); + fs::create_dir_all(&models_dir).unwrap(); + + let table = 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![], + }; + fs::write( + models_dir.join("users.json"), + serde_json::to_string_pretty(&table).unwrap(), + ) + .unwrap(); + + let result = load_models_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 1); + assert_eq!(models[0].name, "users"); + } + + #[test] + #[serial] + fn test_load_models_from_dir_without_root() { + use std::env; + + // Save the original value + let original = env::var("CARGO_MANIFEST_DIR").ok(); + + // Remove CARGO_MANIFEST_DIR to test the error path + unsafe { + env::remove_var("CARGO_MANIFEST_DIR"); + } + + let result = load_models_from_dir(None); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("CARGO_MANIFEST_DIR environment variable not set")); + + drop(original); + } + + #[test] + #[serial] + fn test_load_models_from_dir_no_models_dir() { + let temp_dir = tempdir().unwrap(); + // Don't create models directory + + let result = load_models_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 0); + } + + #[test] + #[serial] + fn test_load_models_from_dir_with_yaml() { + let temp_dir = tempdir().unwrap(); + let models_dir = temp_dir.path().join("models"); + fs::create_dir_all(&models_dir).unwrap(); + + let table = 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![], + }; + fs::write( + models_dir.join("users.yaml"), + serde_yaml::to_string(&table).unwrap(), + ) + .unwrap(); + + let result = load_models_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 1); + assert_eq!(models[0].name, "users"); + } + + #[test] + #[serial] + fn test_load_models_from_dir_with_yml() { + let temp_dir = tempdir().unwrap(); + let models_dir = temp_dir.path().join("models"); + fs::create_dir_all(&models_dir).unwrap(); + + let table = 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![], + }; + fs::write( + models_dir.join("users.yml"), + serde_yaml::to_string(&table).unwrap(), + ) + .unwrap(); + + let result = load_models_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 1); + assert_eq!(models[0].name, "users"); + } + + #[test] + #[serial] + fn test_load_models_from_dir_recursive() { + let temp_dir = tempdir().unwrap(); + let models_dir = temp_dir.path().join("models"); + let subdir = models_dir.join("subdir"); + fs::create_dir_all(&subdir).unwrap(); + + let table = TableDef { + name: "subtable".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![], + }; + fs::write( + subdir.join("subtable.json"), + serde_json::to_string_pretty(&table).unwrap(), + ) + .unwrap(); + + let result = load_models_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_ok()); + let models = result.unwrap(); + assert_eq!(models.len(), 1); + assert_eq!(models[0].name, "subtable"); + } + + #[test] + #[serial] + fn test_load_models_from_dir_with_invalid_json() { + let temp_dir = tempdir().unwrap(); + let models_dir = temp_dir.path().join("models"); + fs::create_dir_all(&models_dir).unwrap(); + + fs::write(models_dir.join("invalid.json"), r#"{"invalid": json}"#).unwrap(); + + let result = load_models_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Failed to parse JSON model")); + } + + #[test] + #[serial] + fn test_load_models_from_dir_with_invalid_yaml() { + let temp_dir = tempdir().unwrap(); + let models_dir = temp_dir.path().join("models"); + fs::create_dir_all(&models_dir).unwrap(); + + fs::write(models_dir.join("invalid.yaml"), r#"invalid: [yaml"#).unwrap(); + + let result = load_models_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Failed to parse YAML model")); + } + + #[test] + #[serial] + fn test_load_models_from_dir_normalization_error() { + let temp_dir = tempdir().unwrap(); + let models_dir = temp_dir.path().join("models"); + fs::create_dir_all(&models_dir).unwrap(); + + // Create a model with invalid FK format + let table = TableDef { + name: "orders".into(), + columns: vec![ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: Some(ForeignKeySyntax::String("invalid_format".into())), + }], + constraints: vec![], + indexes: vec![], + }; + fs::write( + models_dir.join("orders.json"), + serde_json::to_string_pretty(&table).unwrap(), + ) + .unwrap(); + + let result = load_models_from_dir(Some(temp_dir.path().to_path_buf())); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Failed to normalize table 'orders'")); + } + + #[test] + #[serial] + fn test_load_models_from_dir_with_cargo_manifest_dir() { + // Test the path where CARGO_MANIFEST_DIR is set (line 87) + // In cargo test environment, CARGO_MANIFEST_DIR is usually set + let result = load_models_from_dir(None); + // This might succeed if CARGO_MANIFEST_DIR is set (like in cargo test) + // or fail if it's not set + // Either way, we're testing the code path including line 87 + let _ = result; + } + + #[test] + #[serial] + fn test_load_models_at_compile_time() { + // This function just calls load_models_from_dir(None) + // We can't easily test it without CARGO_MANIFEST_DIR, but we can verify + // it doesn't panic + let result = load_models_at_compile_time(); + // This might succeed if CARGO_MANIFEST_DIR is set (like in cargo test) + // or fail if it's not set + // Either way, we're testing the code path + let _ = result; + } +} diff --git a/crates/vespertide-macro/Cargo.toml b/crates/vespertide-macro/Cargo.toml index 6001412..1697f53 100644 --- a/crates/vespertide-macro/Cargo.toml +++ b/crates/vespertide-macro/Cargo.toml @@ -14,13 +14,12 @@ proc-macro = true [dependencies] vespertide-core = { workspace = true } vespertide-config = { workspace = true } +vespertide-loader = { workspace = true } vespertide-query = { workspace = true } thiserror = "2" syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" -serde_json = "1.0" -serde_yaml = "0.9" [dev-dependencies] tempfile = "3" diff --git a/crates/vespertide-macro/src/lib.rs b/crates/vespertide-macro/src/lib.rs index f6d9003..83ef878 100644 --- a/crates/vespertide-macro/src/lib.rs +++ b/crates/vespertide-macro/src/lib.rs @@ -1,11 +1,10 @@ // MigrationOptions and MigrationError are now in vespertide-core -mod loader; - use proc_macro::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::{Expr, Ident, Token, parse_macro_input}; +use vespertide_loader::{load_migrations_at_compile_time, load_models_at_compile_time}; use vespertide_query::{DatabaseBackend, build_plan_queries}; struct MacroInput { @@ -54,7 +53,7 @@ pub fn vespertide_migration(input: TokenStream) -> TokenStream { .unwrap_or_else(|| "vespertide_version".to_string()); // Load migration files and build SQL at compile time - let migrations = match loader::load_migrations_at_compile_time() { + let migrations = match load_migrations_at_compile_time() { Ok(migrations) => migrations, Err(e) => { return syn::Error::new( @@ -65,12 +64,23 @@ pub fn vespertide_migration(input: TokenStream) -> TokenStream { .into(); } }; + let models = match load_models_at_compile_time() { + Ok(models) => models, + Err(e) => { + return syn::Error::new( + proc_macro2::Span::call_site(), + format!("Failed to load models at compile time: {}", e), + ) + .to_compile_error() + .into(); + } + }; // Build SQL for each migration let mut migration_blocks = Vec::new(); for migration in &migrations { let version = migration.version; - let queries = match build_plan_queries(migration) { + let queries = match build_plan_queries(migration, &models) { Ok(queries) => queries, Err(e) => { return syn::Error::new( @@ -89,9 +99,25 @@ pub fn vespertide_migration(input: TokenStream) -> TokenStream { let sql_statements: Vec<_> = queries .iter() .map(|q| { - let pg_sql = q.build(DatabaseBackend::Postgres); - let mysql_sql = q.build(DatabaseBackend::MySql); - let sqlite_sql = q.build(DatabaseBackend::Sqlite); + let pg_sql = q + .postgres + .iter() + .map(|q| q.build(DatabaseBackend::Postgres)) + .collect::>() + .join(";\n"); + let mysql_sql = q + .mysql + .iter() + .map(|q| q.build(DatabaseBackend::MySql)) + .collect::>() + .join(";\n"); + let sqlite_sql = q + .sqlite + .iter() + .map(|q| q.build(DatabaseBackend::Sqlite)) + .collect::>() + .join(";\n"); + quote! { match backend { sea_orm::DatabaseBackend::Postgres => #pg_sql, diff --git a/crates/vespertide-macro/src/loader.rs b/crates/vespertide-macro/src/loader.rs deleted file mode 100644 index 70de3de..0000000 --- a/crates/vespertide-macro/src/loader.rs +++ /dev/null @@ -1,255 +0,0 @@ -use std::env; -use std::fs; -use std::path::PathBuf; -use vespertide_config::VespertideConfig; -use vespertide_core::MigrationPlan; - -pub fn load_migrations_at_compile_time() -> Result, Box> { - load_migrations_from_dir(None) -} - -pub fn load_migrations_from_dir( - project_root: Option, -) -> Result, Box> { - // Locate project root from CARGO_MANIFEST_DIR or use provided path - let project_root = if let Some(root) = project_root { - root - } else { - let manifest_dir = env::var("CARGO_MANIFEST_DIR") - .map_err(|_| "CARGO_MANIFEST_DIR environment variable not set")?; - PathBuf::from(manifest_dir) - }; - - // Read vespertide.json - let config_path = project_root.join("vespertide.json"); - let config: VespertideConfig = if config_path.exists() { - let content = fs::read_to_string(&config_path)?; - serde_json::from_str(&content)? - } else { - // Fall back to defaults if config is missing - VespertideConfig::default() - }; - - // Read migrations directory - let migrations_dir = project_root.join(config.migrations_dir()); - if !migrations_dir.exists() { - return Ok(Vec::new()); - } - - let mut plans = Vec::new(); - let entries = fs::read_dir(&migrations_dir)?; - - for entry in entries { - let entry = entry?; - let path = entry.path(); - if path.is_file() { - let ext = path.extension().and_then(|s| s.to_str()); - if ext == Some("json") || ext == Some("yaml") || ext == Some("yml") { - let content = fs::read_to_string(&path)?; - - let plan: MigrationPlan = if ext == Some("json") { - serde_json::from_str(&content)? - } else { - serde_yaml::from_str(&content)? - }; - - plans.push(plan); - } - } - } - - // Sort by version - plans.sort_by_key(|p| p.version); - Ok(plans) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{env, fs}; - use tempfile::TempDir; - - #[test] - fn test_load_migrations_from_dir_with_no_migrations_dir() { - let temp_dir = TempDir::new().unwrap(); - let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); - assert!(result.is_ok()); - assert_eq!(result.unwrap().len(), 0); - } - - #[test] - fn test_load_migrations_from_dir_with_empty_migrations_dir() { - let temp_dir = TempDir::new().unwrap(); - let migrations_dir = temp_dir.path().join("migrations"); - fs::create_dir_all(&migrations_dir).unwrap(); - - let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); - assert!(result.is_ok()); - assert_eq!(result.unwrap().len(), 0); - } - - #[test] - fn test_load_migrations_from_dir_with_json_migration() { - let temp_dir = TempDir::new().unwrap(); - let migrations_dir = temp_dir.path().join("migrations"); - fs::create_dir_all(&migrations_dir).unwrap(); - - let migration_content = r#"{ - "version": 1, - "actions": [ - { - "type": "create_table", - "table": "users", - "columns": [ - { - "name": "id", - "type": "integer", - "nullable": false - } - ], - "constraints": [] - } - ] - }"#; - - fs::write(migrations_dir.join("0001_test.json"), migration_content).unwrap(); - - let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); - assert!(result.is_ok()); - let plans = result.unwrap(); - assert_eq!(plans.len(), 1); - assert_eq!(plans[0].version, 1); - } - - #[test] - fn test_load_migrations_from_dir_with_yaml_migration() { - let temp_dir = TempDir::new().unwrap(); - let migrations_dir = temp_dir.path().join("migrations"); - fs::create_dir_all(&migrations_dir).unwrap(); - - let migration_content = r#"version: 1 -actions: - - type: create_table - table: users - columns: - - name: id - type: integer - nullable: false - constraints: [] -"#; - - fs::write(migrations_dir.join("0001_test.yaml"), migration_content).unwrap(); - - let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); - assert!(result.is_ok()); - let plans = result.unwrap(); - assert_eq!(plans.len(), 1); - assert_eq!(plans[0].version, 1); - } - - #[test] - fn test_load_migrations_from_dir_sorts_by_version() { - let temp_dir = TempDir::new().unwrap(); - let migrations_dir = temp_dir.path().join("migrations"); - fs::create_dir_all(&migrations_dir).unwrap(); - - let migration1 = r#"{"version": 2, "actions": []}"#; - let migration2 = r#"{"version": 1, "actions": []}"#; - let migration3 = r#"{"version": 3, "actions": []}"#; - - fs::write(migrations_dir.join("0002_second.json"), migration1).unwrap(); - fs::write(migrations_dir.join("0001_first.json"), migration2).unwrap(); - fs::write(migrations_dir.join("0003_third.json"), migration3).unwrap(); - - let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); - assert!(result.is_ok()); - let plans = result.unwrap(); - assert_eq!(plans.len(), 3); - assert_eq!(plans[0].version, 1); - assert_eq!(plans[1].version, 2); - assert_eq!(plans[2].version, 3); - } - - #[test] - fn test_load_migrations_from_dir_with_config_file() { - let temp_dir = TempDir::new().unwrap(); - let migrations_dir = temp_dir.path().join("custom_migrations"); - fs::create_dir_all(&migrations_dir).unwrap(); - - let config_content = r#"{ - "modelsDir": "models", - "migrationsDir": "custom_migrations", - "tableNamingCase": "snake", - "columnNamingCase": "snake" - }"#; - fs::write(temp_dir.path().join("vespertide.json"), config_content).unwrap(); - - let migration_content = r#"{"version": 1, "actions": []}"#; - fs::write(migrations_dir.join("0001_test.json"), migration_content).unwrap(); - - let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); - match result { - Ok(plans) => { - assert_eq!(plans.len(), 1); - assert_eq!(plans[0].version, 1); - } - Err(e) => panic!("Failed to load migrations: {}", e), - } - } - - #[test] - fn test_load_migrations_from_dir_ignores_non_migration_files() { - let temp_dir = TempDir::new().unwrap(); - let migrations_dir = temp_dir.path().join("migrations"); - fs::create_dir_all(&migrations_dir).unwrap(); - - let migration_content = r#"{"version": 1, "actions": []}"#; - fs::write(migrations_dir.join("0001_test.json"), migration_content).unwrap(); - fs::write(migrations_dir.join("README.txt"), "not a migration").unwrap(); - fs::write( - migrations_dir.join("0002_test.xml"), - "", - ) - .unwrap(); - - let result = load_migrations_from_dir(Some(temp_dir.path().to_path_buf())); - assert!(result.is_ok()); - let plans = result.unwrap(); - assert_eq!(plans.len(), 1); - } - - #[test] - fn test_load_migrations_at_compile_time_reads_manifest_dir() { - let temp_dir = TempDir::new().unwrap(); - let root = temp_dir.path(); - - let config_content = r#"{ - "modelsDir": "models", - "migrationsDir": "migrations", - "tableNamingCase": "snake", - "columnNamingCase": "snake" - }"#; - fs::write(root.join("vespertide.json"), config_content).unwrap(); - - let migrations_dir = root.join("migrations"); - fs::create_dir_all(&migrations_dir).unwrap(); - fs::write( - migrations_dir.join("0001_test.json"), - r#"{"version":1,"actions":[]}"#, - ) - .unwrap(); - - unsafe { - env::set_var("CARGO_MANIFEST_DIR", root); - } - let result = load_migrations_at_compile_time(); - unsafe { - env::remove_var("CARGO_MANIFEST_DIR"); - } - - assert!(result.is_ok()); - let plans = result.unwrap(); - assert_eq!(plans.len(), 1); - assert_eq!(plans[0].version, 1); - } -} diff --git a/crates/vespertide-planner/src/error.rs b/crates/vespertide-planner/src/error.rs index 896a52a..907af19 100644 --- a/crates/vespertide-planner/src/error.rs +++ b/crates/vespertide-planner/src/error.rs @@ -28,4 +28,6 @@ pub enum PlannerError { MissingFillWith(String, String), #[error("table validation error: {0}")] TableValidation(String), + #[error("table '{0}' must have a primary key")] + MissingPrimaryKey(String), } diff --git a/crates/vespertide-planner/src/validate.rs b/crates/vespertide-planner/src/validate.rs index 6c428d6..2844f62 100644 --- a/crates/vespertide-planner/src/validate.rs +++ b/crates/vespertide-planner/src/validate.rs @@ -44,6 +44,20 @@ fn validate_table( ) -> Result<(), PlannerError> { let table_columns: HashSet<_> = table.columns.iter().map(|c| c.name.as_str()).collect(); + // Check that the table has a primary key + // Primary key can be defined either: + // 1. As a table-level constraint (TableConstraint::PrimaryKey) + // 2. As an inline column definition (column.primary_key = Some(...)) + let has_table_pk = table + .constraints + .iter() + .any(|c| matches!(c, TableConstraint::PrimaryKey { .. })); + let has_inline_pk = table.columns.iter().any(|c| c.primary_key.is_some()); + + if !has_table_pk && !has_inline_pk { + return Err(PlannerError::MissingPrimaryKey(table.name.clone())); + } + // Validate constraints for constraint in &table.constraints { validate_constraint(constraint, &table.name, &table_columns, table_map)?; @@ -277,6 +291,17 @@ mod tests { matches!(err, PlannerError::EmptyConstraintColumns(_, _)) } + fn is_missing_pk(err: &PlannerError) -> bool { + matches!(err, PlannerError::MissingPrimaryKey(_)) + } + + fn pk(columns: Vec<&str>) -> TableConstraint { + TableConstraint::PrimaryKey { + auto_increment: false, + columns: columns.into_iter().map(|s| s.to_string()).collect(), + } + } + #[rstest] #[case::valid_schema( vec![table( @@ -298,7 +323,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::ForeignKey { + vec![pk(vec!["id"]), TableConstraint::ForeignKey { name: None, columns: vec!["id".into()], ref_table: "nonexistent".into(), @@ -312,11 +337,11 @@ mod tests { )] #[case::fk_missing_column( vec![ - table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![]), + table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])], vec![]), table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::ForeignKey { + vec![pk(vec!["id"]), TableConstraint::ForeignKey { name: None, columns: vec!["id".into()], ref_table: "posts".into(), @@ -331,11 +356,11 @@ mod tests { )] #[case::fk_local_missing_column( vec![ - table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![], vec![]), + table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])], vec![]), table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::ForeignKey { + vec![pk(vec!["id"]), TableConstraint::ForeignKey { name: None, columns: vec!["missing".into()], ref_table: "posts".into(), @@ -353,13 +378,13 @@ mod tests { table( "posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }], + vec![pk(vec!["id"])], vec![], ), table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("post_id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::ForeignKey { + vec![pk(vec!["id"]), TableConstraint::ForeignKey { name: None, columns: vec!["post_id".into()], ref_table: "posts".into(), @@ -376,7 +401,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], + vec![pk(vec!["id"])], vec![IndexDef { name: "idx_name".into(), columns: vec!["nonexistent".into()], @@ -398,7 +423,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::Unique { + vec![pk(vec!["id"]), TableConstraint::Unique { name: Some("u".into()), columns: vec![], }], @@ -410,7 +435,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::Unique { + vec![pk(vec!["id"]), TableConstraint::Unique { name: None, columns: vec!["missing".into()], }], @@ -432,13 +457,13 @@ mod tests { table( "posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], + vec![pk(vec!["id"])], vec![], ), table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("post_id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::ForeignKey { + vec![pk(vec!["id"]), TableConstraint::ForeignKey { name: None, columns: vec!["id".into(), "post_id".into()], ref_table: "posts".into(), @@ -455,7 +480,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::ForeignKey { + vec![pk(vec!["id"]), TableConstraint::ForeignKey { name: None, columns: vec![], ref_table: "posts".into(), @@ -472,13 +497,13 @@ mod tests { table( "posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], + vec![pk(vec!["id"])], vec![], ), table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::ForeignKey { + vec![pk(vec!["id"]), TableConstraint::ForeignKey { name: None, columns: vec!["id".into()], ref_table: "posts".into(), @@ -495,7 +520,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![], + vec![pk(vec!["id"])], vec![IndexDef { name: "idx".into(), columns: vec![], @@ -508,7 +533,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text))], - vec![], + vec![pk(vec!["id"])], vec![IndexDef { name: "idx_name".into(), columns: vec!["name".into()], @@ -521,7 +546,7 @@ mod tests { vec![table( "users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - vec![TableConstraint::Check { + vec![pk(vec!["id"]), TableConstraint::Check { name: "ck".into(), expr: "id > 0".into(), }], @@ -529,6 +554,15 @@ mod tests { )], None )] + #[case::missing_primary_key( + vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![], + vec![], + )], + Some(is_missing_pk as fn(&PlannerError) -> bool) + )] fn validate_schema_cases( #[case] schema: Vec, #[case] expected_err: Option bool>, diff --git a/crates/vespertide-query/src/builder.rs b/crates/vespertide-query/src/builder.rs index 0aa4a4c..4c468ff 100644 --- a/crates/vespertide-query/src/builder.rs +++ b/crates/vespertide-query/src/builder.rs @@ -1,15 +1,33 @@ -use vespertide_core::MigrationPlan; +use vespertide_core::{MigrationAction, MigrationPlan, TableDef}; +use crate::DatabaseBackend; use crate::error::QueryError; use crate::sql::{BuiltQuery, build_action_queries}; -#[cfg(test)] -use crate::sql::DatabaseBackend; +pub struct PlanQueries { + pub action: MigrationAction, + pub postgres: Vec, + pub mysql: Vec, + pub sqlite: Vec, +} -pub fn build_plan_queries(plan: &MigrationPlan) -> Result, QueryError> { - let mut queries: Vec = Vec::new(); +pub fn build_plan_queries( + plan: &MigrationPlan, + current_schema: &[TableDef], +) -> Result, QueryError> { + let mut queries: Vec = Vec::new(); for action in &plan.actions { - queries.extend(build_action_queries(action)?); + let postgres_queries = + build_action_queries(&DatabaseBackend::Postgres, action, current_schema)?; + let mysql_queries = build_action_queries(&DatabaseBackend::MySql, action, current_schema)?; + let sqlite_queries = + build_action_queries(&DatabaseBackend::Sqlite, action, current_schema)?; + queries.push(PlanQueries { + action: action.clone(), + postgres: postgres_queries, + mysql: mysql_queries, + sqlite: sqlite_queries, + }); } Ok(queries) } @@ -17,6 +35,7 @@ pub fn build_plan_queries(plan: &MigrationPlan) -> Result, Query #[cfg(test)] mod tests { use super::*; + use crate::sql::DatabaseBackend; use rstest::rstest; use vespertide_core::{ ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, @@ -76,7 +95,7 @@ mod tests { 2 )] fn test_build_plan_queries(#[case] plan: MigrationPlan, #[case] expected_count: usize) { - let result = build_plan_queries(&plan).unwrap(); + let result = build_plan_queries(&plan, &[]).unwrap(); assert_eq!( result.len(), expected_count, @@ -104,24 +123,44 @@ mod tests { ], }; - let result = build_plan_queries(&plan).unwrap(); + let result = build_plan_queries(&plan, &[]).unwrap(); assert_eq!(result.len(), 2); // Test PostgreSQL output - let sql1 = result[0].build(DatabaseBackend::Postgres); + let sql1 = result[0] + .postgres + .iter() + .map(|q| q.build(DatabaseBackend::Postgres)) + .collect::>() + .join(";\n"); assert!(sql1.contains("CREATE TABLE")); assert!(sql1.contains("\"users\"")); assert!(sql1.contains("\"id\"")); - let sql2 = result[1].build(DatabaseBackend::Postgres); + let sql2 = result[1] + .postgres + .iter() + .map(|q| q.build(DatabaseBackend::Postgres)) + .collect::>() + .join(";\n"); assert!(sql2.contains("DROP TABLE")); assert!(sql2.contains("\"posts\"")); // Test MySQL output - let sql1_mysql = result[0].build(DatabaseBackend::MySql); + let sql1_mysql = result[0] + .mysql + .iter() + .map(|q| q.build(DatabaseBackend::MySql)) + .collect::>() + .join(";\n"); assert!(sql1_mysql.contains("`users`")); - let sql2_mysql = result[1].build(DatabaseBackend::MySql); + let sql2_mysql = result[1] + .mysql + .iter() + .map(|q| q.build(DatabaseBackend::MySql)) + .collect::>() + .join(";\n"); assert!(sql2_mysql.contains("`posts`")); } } diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_mysql.snap deleted file mode 100644 index 68b928a..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_mysql.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE `users` ADD COLUMN `age` int -UPDATE "users" SET "age" = 0 -ALTER TABLE "users" ALTER COLUMN "age" SET NOT NULL diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_postgres.snap deleted file mode 100644 index 202b660..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_postgres.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD COLUMN "age" integer -UPDATE "users" SET "age" = 0 -ALTER TABLE "users" ALTER COLUMN "age" SET NOT NULL diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_sqlite.snap deleted file mode 100644 index 202b660..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_with_backfill_sqlite.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD COLUMN "age" integer -UPDATE "users" SET "age" = 0 -ALTER TABLE "users" ALTER COLUMN "age" SET NOT NULL diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_postgres.snap deleted file mode 100644 index 03f180e..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD CONSTRAINT "chk_age" CHECK (age > 0) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_sqlite.snap deleted file mode 100644 index 03f180e..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD CONSTRAINT "chk_age" CHECK (age > 0) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_postgres.snap deleted file mode 100644 index 5ca5652..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD CHECK (age > 0) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_sqlite.snap deleted file mode 100644 index 5ca5652..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD CHECK (age > 0) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_postgres.snap deleted file mode 100644 index 4c3d98c..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_sqlite.snap deleted file mode 100644 index 4c3d98c..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_mysql.snap deleted file mode 100644 index f50b943..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_postgres.snap deleted file mode 100644 index f50b943..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_sqlite.snap deleted file mode 100644 index f50b943..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_unnamed_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_postgres.snap deleted file mode 100644 index 7e617d0..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD PRIMARY KEY ("id") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_sqlite.snap deleted file mode 100644 index 7e617d0..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD PRIMARY KEY ("id") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_mysql.snap deleted file mode 100644 index a34a67b..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD CONSTRAINT "uq_email" UNIQUE ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_postgres.snap deleted file mode 100644 index a34a67b..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD CONSTRAINT "uq_email" UNIQUE ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_sqlite.snap deleted file mode 100644 index a34a67b..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_named_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD CONSTRAINT "uq_email" UNIQUE ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_mysql.snap deleted file mode 100644 index b191d9c..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD UNIQUE ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_postgres.snap deleted file mode 100644 index b191d9c..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD UNIQUE ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_sqlite.snap deleted file mode 100644 index b191d9c..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_unique_unnamed_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ADD UNIQUE ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_mysql.snap deleted file mode 100644 index 076a40e..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE INDEX `idx_email` ON `users` (`email`) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_postgres.snap deleted file mode 100644 index bd73250..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_sqlite.snap deleted file mode 100644 index bd73250..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_index_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_mysql.snap deleted file mode 100644 index e1aed79..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE UNIQUE INDEX `idx_email` ON `users` (`email`) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_postgres.snap deleted file mode 100644 index 8b65029..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE UNIQUE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_sqlite.snap deleted file mode 100644 index 8b65029..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_unique_index_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE UNIQUE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_mysql.snap deleted file mode 100644 index a823b84..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE `users` ( `id` int ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_postgres.snap deleted file mode 100644 index 23c5c14..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE "users" ( "id" integer, "name" text ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_sqlite.snap deleted file mode 100644 index bdc3b71..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE "users" ( "id" integer ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_mysql.snap deleted file mode 100644 index 577057f..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE `users` ( `status` text DEFAULT 'active' ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_mysql.snap deleted file mode 100644 index 551e669..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE `posts` ( `id` int, `user_id` int, CONSTRAINT `fk_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_sqlite.snap deleted file mode 100644 index 78bbcab..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE "posts" ( "id" integer, "user_id" integer, FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_mysql.snap deleted file mode 100644 index 253dca2..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE "users" ( "id" integer NOT NULL, "email" text UNIQUE, PRIMARY KEY ("id") ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_postgres.snap deleted file mode 100644 index 253dca2..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE "users" ( "id" integer NOT NULL, "email" text UNIQUE, PRIMARY KEY ("id") ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_sqlite.snap deleted file mode 100644 index 253dca2..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_constraints_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE "users" ( "id" integer NOT NULL, "email" text UNIQUE, PRIMARY KEY ("id") ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_mysql.snap deleted file mode 100644 index d34ae95..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -CREATE TABLE `users` ( `id` int NOT NULL PRIMARY KEY ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_mysql.snap deleted file mode 100644 index 53a7e85..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE `users` DROP COLUMN `email` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_postgres.snap deleted file mode 100644 index 3b50025..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP COLUMN "email" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_sqlite.snap deleted file mode 100644 index 3b50025..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_column_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP COLUMN "email" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_mysql.snap deleted file mode 100644 index abba7fa..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -DROP TABLE `users` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_postgres.snap deleted file mode 100644 index 8623a72..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -DROP TABLE "users" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_sqlite.snap deleted file mode 100644 index 8623a72..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_delete_table_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -DROP TABLE "users" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_modify_column_type_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_modify_column_type_mysql.snap deleted file mode 100644 index 5838d76..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_modify_column_type_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE `users` MODIFY COLUMN `age` varchar(50) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_modify_column_type_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_modify_column_type_postgres.snap deleted file mode 100644 index a23c0da..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_modify_column_type_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" ALTER COLUMN "age" TYPE varchar(50) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_mysql.snap deleted file mode 100644 index 6e2c585..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -SELECT 1 diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_postgres.snap deleted file mode 100644 index 6e2c585..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -SELECT 1 diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_sqlite.snap deleted file mode 100644 index 6e2c585..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_raw_sql_action_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -SELECT 1 diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_postgres.snap deleted file mode 100644 index 3dcfbdf..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "chk_age" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_sqlite.snap deleted file mode 100644 index 3dcfbdf..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "chk_age" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_mysql.snap deleted file mode 100644 index f1479c1..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" DROP CONSTRAINT "fk_user" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_postgres.snap deleted file mode 100644 index f1479c1..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" DROP CONSTRAINT "fk_user" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_sqlite.snap deleted file mode 100644 index f1479c1..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_named_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" DROP CONSTRAINT "fk_user" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_mysql.snap deleted file mode 100644 index 7d76242..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" DROP CONSTRAINT "posts_user_id_fkey" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_postgres.snap deleted file mode 100644 index 7d76242..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" DROP CONSTRAINT "posts_user_id_fkey" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_sqlite.snap deleted file mode 100644 index 7d76242..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_foreign_key_unnamed_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "posts" DROP CONSTRAINT "posts_user_id_fkey" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_postgres.snap deleted file mode 100644 index c6d2a4f..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "users_pkey" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_sqlite.snap deleted file mode 100644 index c6d2a4f..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "users_pkey" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_named_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_named_postgres.snap deleted file mode 100644 index 39ad3fa..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_named_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "uq_email" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_named_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_named_sqlite.snap deleted file mode 100644 index 39ad3fa..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_named_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "uq_email" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_mysql.snap deleted file mode 100644 index e6f3dad..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "users_email_key" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_postgres.snap deleted file mode 100644 index e6f3dad..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "users_email_key" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_sqlite.snap deleted file mode 100644 index e6f3dad..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_unnamed_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" DROP CONSTRAINT "users_email_key" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_mysql.snap deleted file mode 100644 index 8336c55..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -DROP INDEX `idx_email` ON diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_postgres.snap deleted file mode 100644 index c54c79e..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -DROP INDEX "idx_email" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_sqlite.snap deleted file mode 100644 index c54c79e..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_index_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -DROP INDEX "idx_email" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_mysql.snap deleted file mode 100644 index 927d27a..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE `users` RENAME COLUMN `email` TO `contact_email` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_postgres.snap deleted file mode 100644 index f63b6ff..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" RENAME COLUMN "email" TO "contact_email" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_sqlite.snap deleted file mode 100644 index f63b6ff..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_column_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" RENAME COLUMN "email" TO "contact_email" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_mysql.snap deleted file mode 100644 index 32bf701..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -RENAME TABLE `users` TO `accounts` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_postgres.snap deleted file mode 100644 index 1a2e25c..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" RENAME TO "accounts" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_sqlite.snap deleted file mode 100644 index 1a2e25c..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_rename_table_action_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" ---- -ALTER TABLE "users" RENAME TO "accounts" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_mysql.snap deleted file mode 100644 index 33d6f4f..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE `t` ADD COLUMN `c` int diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_postgres.snap deleted file mode 100644 index c945c0e..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE "t" ADD COLUMN "c" integer diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_sqlite.snap deleted file mode 100644 index c945c0e..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_alter_table_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE "t" ADD COLUMN "c" integer diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_foreign_key_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_foreign_key_mysql.snap deleted file mode 100644 index 475864a..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_foreign_key_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE `a` ADD CONSTRAINT `fk` FOREIGN KEY (`c`) REFERENCES `b` (`id`) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_foreign_key_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_foreign_key_postgres.snap deleted file mode 100644 index f1b4adf..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_foreign_key_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE "a" ADD CONSTRAINT "fk" FOREIGN KEY ("c") REFERENCES "b" ("id") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_mysql.snap deleted file mode 100644 index 351cc89..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -CREATE INDEX `idx` ON `t` (`c`) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_postgres.snap deleted file mode 100644 index da47560..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -CREATE INDEX "idx" ON "t" ("c") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_sqlite.snap deleted file mode 100644 index da47560..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_index_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -CREATE INDEX "idx" ON "t" ("c") diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_mysql.snap deleted file mode 100644 index d0c931c..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -CREATE TABLE `t` ( `id` int ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_postgres.snap deleted file mode 100644 index 3f554ff..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -CREATE TABLE "t" ( "id" integer ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_sqlite.snap deleted file mode 100644 index 3f554ff..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_create_table_query_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -CREATE TABLE "t" ( "id" integer ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_foreign_key_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_foreign_key_mysql.snap deleted file mode 100644 index 73f640e..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_foreign_key_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE `a` DROP FOREIGN KEY `fk` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_foreign_key_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_foreign_key_postgres.snap deleted file mode 100644 index ad765ac..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_foreign_key_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE "a" DROP CONSTRAINT "fk" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_mysql.snap deleted file mode 100644 index 99f2f28..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -DROP INDEX `idx` ON `t` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_postgres.snap deleted file mode 100644 index 0724d04..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -DROP INDEX "idx" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_sqlite.snap deleted file mode 100644 index 0724d04..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_index_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -DROP INDEX "idx" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_mysql.snap deleted file mode 100644 index 7815af8..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -DROP TABLE `t` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_postgres.snap deleted file mode 100644 index 984540a..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -DROP TABLE "t" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_sqlite.snap deleted file mode 100644 index 984540a..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_drop_table_query_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -DROP TABLE "t" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_mysql.snap deleted file mode 100644 index bede88d..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -SELECT 1 diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_postgres.snap deleted file mode 100644 index bede88d..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -SELECT 1 diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_sqlite.snap deleted file mode 100644 index bede88d..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_raw_query_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -SELECT 1 diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_mysql.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_mysql.snap deleted file mode 100644 index 68c0a63..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_mysql.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -RENAME TABLE `a` TO `b` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_postgres.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_postgres.snap deleted file mode 100644 index b442933..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_postgres.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE "a" RENAME TO "b" diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_sqlite.snap b/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_sqlite.snap deleted file mode 100644 index b442933..0000000 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_query@build_query_rename_table_sqlite.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/vespertide-query/src/sql.rs -expression: sql ---- -ALTER TABLE "a" RENAME TO "b" diff --git a/crates/vespertide-query/src/sql.rs b/crates/vespertide-query/src/sql.rs deleted file mode 100644 index 8a13198..0000000 --- a/crates/vespertide-query/src/sql.rs +++ /dev/null @@ -1,2071 +0,0 @@ -use sea_query::{ - Alias, ColumnDef as SeaColumnDef, ForeignKey, ForeignKeyAction, ForeignKeyCreateStatement, - ForeignKeyDropStatement, Index, IndexCreateStatement, IndexDropStatement, MysqlQueryBuilder, - PostgresQueryBuilder, SqliteQueryBuilder, Table, TableAlterStatement, TableCreateStatement, - TableDropStatement, TableRenameStatement, -}; -use vespertide_core::{ - ColumnDef, ColumnType, ComplexColumnType, MigrationAction, ReferenceAction, SimpleColumnType, - TableConstraint, -}; - -use crate::error::QueryError; - -/// Database backend for SQL generation -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DatabaseBackend { - Postgres, - MySql, - Sqlite, -} - -/// Represents a built query that can be converted to SQL for any database backend -#[derive(Debug, Clone)] -pub enum BuiltQuery { - CreateTable(Box), - DropTable(Box), - AlterTable(Box), - CreateIndex(Box), - DropIndex(Box), - RenameTable(Box), - CreateForeignKey(Box), - DropForeignKey(Box), - Raw(String), -} - -impl BuiltQuery { - /// Build SQL string for the specified database backend - pub fn build(&self, backend: DatabaseBackend) -> String { - match self { - BuiltQuery::CreateTable(stmt) => match backend { - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - BuiltQuery::DropTable(stmt) => match backend { - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - BuiltQuery::AlterTable(stmt) => match backend { - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - BuiltQuery::CreateIndex(stmt) => match backend { - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - BuiltQuery::DropIndex(stmt) => match backend { - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - BuiltQuery::RenameTable(stmt) => match backend { - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - BuiltQuery::CreateForeignKey(stmt) => match backend { - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - BuiltQuery::DropForeignKey(stmt) => match backend { - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - BuiltQuery::Raw(sql) => sql.clone(), - } - } - - /// Backward compatibility: binds are now empty (DDL doesn't use bind parameters) - pub fn binds(&self) -> Vec { - Vec::new() - } -} - -/// Apply vespertide ColumnType to sea_query ColumnDef -fn apply_column_type(col: &mut SeaColumnDef, ty: &ColumnType) { - match ty { - ColumnType::Simple(simple) => match simple { - SimpleColumnType::SmallInt => { - col.small_integer(); - } - SimpleColumnType::Integer => { - col.integer(); - } - SimpleColumnType::BigInt => { - col.big_integer(); - } - SimpleColumnType::Real => { - col.float(); - } - SimpleColumnType::DoublePrecision => { - col.double(); - } - SimpleColumnType::Text => { - col.text(); - } - SimpleColumnType::Boolean => { - col.boolean(); - } - SimpleColumnType::Date => { - col.date(); - } - SimpleColumnType::Time => { - col.time(); - } - SimpleColumnType::Timestamp => { - col.timestamp(); - } - SimpleColumnType::Timestamptz => { - col.timestamp_with_time_zone(); - } - SimpleColumnType::Interval => { - col.interval(None, None); - } - SimpleColumnType::Bytea => { - col.binary(); - } - SimpleColumnType::Uuid => { - col.uuid(); - } - SimpleColumnType::Json => { - col.json(); - } - SimpleColumnType::Jsonb => { - col.json_binary(); - } - SimpleColumnType::Inet => { - col.custom(Alias::new("INET")); - } - SimpleColumnType::Cidr => { - col.custom(Alias::new("CIDR")); - } - SimpleColumnType::Macaddr => { - col.custom(Alias::new("MACADDR")); - } - SimpleColumnType::Xml => { - col.custom(Alias::new("XML")); - } - }, - ColumnType::Complex(complex) => match complex { - ComplexColumnType::Varchar { length } => { - col.string_len(*length); - } - ComplexColumnType::Numeric { precision, scale } => { - col.decimal_len(*precision, *scale); - } - ComplexColumnType::Char { length } => { - col.char_len(*length); - } - ComplexColumnType::Custom { custom_type } => { - col.custom(Alias::new(custom_type)); - } - }, - } -} - -/// Convert vespertide ReferenceAction to sea_query ForeignKeyAction -fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction { - match action { - ReferenceAction::Cascade => ForeignKeyAction::Cascade, - ReferenceAction::Restrict => ForeignKeyAction::Restrict, - ReferenceAction::SetNull => ForeignKeyAction::SetNull, - ReferenceAction::SetDefault => ForeignKeyAction::SetDefault, - ReferenceAction::NoAction => ForeignKeyAction::NoAction, - } -} - -/// Convert vespertide ReferenceAction to SQL string -fn reference_action_sql(action: &ReferenceAction) -> &'static str { - match action { - ReferenceAction::Cascade => "CASCADE", - ReferenceAction::Restrict => "RESTRICT", - ReferenceAction::SetNull => "SET NULL", - ReferenceAction::SetDefault => "SET DEFAULT", - ReferenceAction::NoAction => "NO ACTION", - } -} - -/// Build sea_query ColumnDef from vespertide ColumnDef -fn build_sea_column_def(column: &ColumnDef) -> SeaColumnDef { - let mut col = SeaColumnDef::new(Alias::new(&column.name)); - apply_column_type(&mut col, &column.r#type); - - if !column.nullable { - col.not_null(); - } - - if let Some(default) = &column.default { - col.default(sea_query::Expr::cust(default)); - } - - col -} - -pub fn build_action_queries(action: &MigrationAction) -> Result, QueryError> { - match action { - MigrationAction::CreateTable { - table, - columns, - constraints, - } => Ok(vec![build_create_table(table, columns, constraints)?]), - - MigrationAction::DeleteTable { table } => { - let stmt = Table::drop().table(Alias::new(table)).to_owned(); - Ok(vec![BuiltQuery::DropTable(Box::new(stmt))]) - } - - MigrationAction::AddColumn { - table, - column, - fill_with, - } => build_add_column(table, column, fill_with.as_deref()), - - MigrationAction::RenameColumn { table, from, to } => { - let stmt = Table::alter() - .table(Alias::new(table)) - .rename_column(Alias::new(from), Alias::new(to)) - .to_owned(); - Ok(vec![BuiltQuery::AlterTable(Box::new(stmt))]) - } - - MigrationAction::DeleteColumn { table, column } => { - let stmt = Table::alter() - .table(Alias::new(table)) - .drop_column(Alias::new(column)) - .to_owned(); - Ok(vec![BuiltQuery::AlterTable(Box::new(stmt))]) - } - - MigrationAction::ModifyColumnType { - table, - column, - new_type, - } => { - let mut col = SeaColumnDef::new(Alias::new(column)); - apply_column_type(&mut col, new_type); - - let stmt = Table::alter() - .table(Alias::new(table)) - .modify_column(col) - .to_owned(); - Ok(vec![BuiltQuery::AlterTable(Box::new(stmt))]) - } - - MigrationAction::AddIndex { table, index } => { - 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(); - } - - Ok(vec![BuiltQuery::CreateIndex(Box::new(stmt))]) - } - - MigrationAction::RemoveIndex { name, .. } => { - let stmt = Index::drop().name(name).to_owned(); - Ok(vec![BuiltQuery::DropIndex(Box::new(stmt))]) - } - - MigrationAction::RenameTable { from, to } => { - let stmt = Table::rename() - .table(Alias::new(from), Alias::new(to)) - .to_owned(); - Ok(vec![BuiltQuery::RenameTable(Box::new(stmt))]) - } - - MigrationAction::RawSql { sql } => Ok(vec![BuiltQuery::Raw(sql.clone())]), - - MigrationAction::AddConstraint { table, constraint } => { - build_add_constraint(table, constraint) - } - - MigrationAction::RemoveConstraint { table, constraint } => { - build_remove_constraint(table, constraint) - } - } -} - -fn build_create_table( - table: &str, - columns: &[ColumnDef], - constraints: &[TableConstraint], -) -> Result { - let mut stmt = Table::create().table(Alias::new(table)).to_owned(); - - let has_table_primary_key = constraints - .iter() - .any(|c| matches!(c, TableConstraint::PrimaryKey { .. })); - - // Add columns - for column in columns { - let mut col = build_sea_column_def(column); - - // Check for inline primary key - if column.primary_key.is_some() && !has_table_primary_key { - col.primary_key(); - } - - // Check for inline unique constraint - if column.unique.is_some() { - col.unique_key(); - } - - stmt = stmt.col(col).to_owned(); - } - - // Add table-level constraints - for constraint in constraints { - match constraint { - TableConstraint::PrimaryKey { - columns: pk_cols, - auto_increment: _, - } => { - // Build primary key index - let mut pk_idx = Index::create(); - for c in pk_cols { - pk_idx = pk_idx.col(Alias::new(c)).to_owned(); - } - stmt = stmt.primary_key(&mut pk_idx).to_owned(); - } - TableConstraint::Unique { - name, - columns: unique_cols, - } => { - let mut idx = Index::create(); - if let Some(n) = name { - idx = idx.name(n).to_owned(); - } - for col in unique_cols { - idx = idx.col(Alias::new(col)).to_owned(); - } - // Note: sea-query doesn't have a direct way to add named unique constraints - // We'll handle this as a separate index if needed - } - TableConstraint::ForeignKey { - name, - columns: fk_cols, - ref_table, - ref_columns, - on_delete, - on_update, - } => { - let mut fk = ForeignKey::create(); - if let Some(n) = name { - fk = fk.name(n).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(); - } - fk = fk.to_tbl(Alias::new(ref_table)).to_owned(); - for col in ref_columns { - fk = fk.to_col(Alias::new(col)).to_owned(); - } - if let Some(action) = on_delete { - fk = fk.on_delete(to_sea_fk_action(action)).to_owned(); - } - if let Some(action) = on_update { - fk = fk.on_update(to_sea_fk_action(action)).to_owned(); - } - stmt = stmt.foreign_key(&mut fk).to_owned(); - } - TableConstraint::Check { name, expr } => { - // sea-query doesn't have direct CHECK constraint support in TableCreateStatement - // This would need to be handled as raw SQL or post-creation ALTER - let _ = (name, expr); - } - } - } - - Ok(BuiltQuery::CreateTable(Box::new(stmt))) -} - -fn build_add_column( - table: &str, - column: &ColumnDef, - fill_with: Option<&str>, -) -> Result, QueryError> { - let mut stmts: Vec = Vec::new(); - - // If adding NOT NULL without default, we need special handling - let needs_backfill = !column.nullable && column.default.is_none() && fill_with.is_some(); - - if needs_backfill { - // Add as nullable first - let mut temp_col = column.clone(); - temp_col.nullable = true; - let col_def = build_sea_column_def(&temp_col); - - let stmt = Table::alter() - .table(Alias::new(table)) - .add_column(col_def) - .to_owned(); - stmts.push(BuiltQuery::AlterTable(Box::new(stmt))); - - // Backfill with provided value - if let Some(fill) = fill_with { - let sql = format!("UPDATE \"{}\" SET \"{}\" = {}", table, column.name, fill); - stmts.push(BuiltQuery::Raw(sql)); - } - - // Set NOT NULL - let sql = format!( - "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" SET NOT NULL", - table, column.name - ); - stmts.push(BuiltQuery::Raw(sql)); - } else { - let col_def = build_sea_column_def(column); - let stmt = Table::alter() - .table(Alias::new(table)) - .add_column(col_def) - .to_owned(); - stmts.push(BuiltQuery::AlterTable(Box::new(stmt))); - } - - Ok(stmts) -} - -fn build_add_constraint( - table: &str, - constraint: &TableConstraint, -) -> Result, QueryError> { - match constraint { - TableConstraint::PrimaryKey { columns, .. } => { - // Use raw SQL for adding primary key constraint - let cols = columns - .iter() - .map(|c| format!("\"{}\"", c)) - .collect::>() - .join(", "); - let sql = format!("ALTER TABLE \"{}\" ADD PRIMARY KEY ({})", table, cols); - Ok(vec![BuiltQuery::Raw(sql)]) - } - TableConstraint::Unique { name, columns } => { - let cols = columns - .iter() - .map(|c| format!("\"{}\"", c)) - .collect::>() - .join(", "); - let sql = if let Some(n) = name { - format!( - "ALTER TABLE \"{}\" ADD CONSTRAINT \"{}\" UNIQUE ({})", - table, n, cols - ) - } else { - format!("ALTER TABLE \"{}\" ADD UNIQUE ({})", table, cols) - }; - Ok(vec![BuiltQuery::Raw(sql)]) - } - TableConstraint::ForeignKey { - name, - columns, - ref_table, - ref_columns, - on_delete, - on_update, - } => { - // Use Raw SQL for FK creation to avoid SQLite panic from sea-query - // SQLite doesn't support ALTER TABLE ADD CONSTRAINT for FK, but we generate - // the SQL anyway - runtime will need to handle SQLite FK differently (table recreation) - let cols = columns - .iter() - .map(|c| format!("\"{}\"", c)) - .collect::>() - .join(", "); - let ref_cols = ref_columns - .iter() - .map(|c| format!("\"{}\"", c)) - .collect::>() - .join(", "); - - let mut sql = if let Some(n) = name { - format!( - "ALTER TABLE \"{}\" ADD CONSTRAINT \"{}\" FOREIGN KEY ({}) REFERENCES \"{}\" ({})", - table, n, cols, ref_table, ref_cols - ) - } else { - format!( - "ALTER TABLE \"{}\" ADD FOREIGN KEY ({}) REFERENCES \"{}\" ({})", - table, cols, ref_table, ref_cols - ) - }; - - if let Some(action) = on_delete { - sql.push_str(&format!(" ON DELETE {}", reference_action_sql(action))); - } - if let Some(action) = on_update { - sql.push_str(&format!(" ON UPDATE {}", reference_action_sql(action))); - } - - Ok(vec![BuiltQuery::Raw(sql)]) - } - TableConstraint::Check { name, expr } => { - let sql = format!( - "ALTER TABLE \"{}\" ADD CONSTRAINT \"{}\" CHECK ({})", - table, name, expr - ); - Ok(vec![BuiltQuery::Raw(sql)]) - } - } -} - -fn build_remove_constraint( - table: &str, - constraint: &TableConstraint, -) -> Result, QueryError> { - match constraint { - TableConstraint::PrimaryKey { .. } => { - let sql = format!( - "ALTER TABLE \"{}\" DROP CONSTRAINT \"{}_pkey\"", - table, table - ); - Ok(vec![BuiltQuery::Raw(sql)]) - } - TableConstraint::Unique { name, columns } => { - let constraint_name = if let Some(n) = name { - n.clone() - } else { - format!("{}_{}_key", table, columns.join("_")) - }; - let sql = format!( - "ALTER TABLE \"{}\" DROP CONSTRAINT \"{}\"", - table, constraint_name - ); - Ok(vec![BuiltQuery::Raw(sql)]) - } - TableConstraint::ForeignKey { name, columns, .. } => { - // Use Raw SQL to avoid SQLite panic from sea-query - // SQLite doesn't support ALTER TABLE DROP CONSTRAINT for FK - let constraint_name = if let Some(n) = name { - n.clone() - } else { - format!("{}_{}_fkey", table, columns.join("_")) - }; - let sql = format!( - "ALTER TABLE \"{}\" DROP CONSTRAINT \"{}\"", - table, constraint_name - ); - Ok(vec![BuiltQuery::Raw(sql)]) - } - TableConstraint::Check { name, .. } => { - let sql = format!("ALTER TABLE \"{}\" DROP CONSTRAINT \"{}\"", table, name); - Ok(vec![BuiltQuery::Raw(sql)]) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use insta::{assert_snapshot, with_settings}; - use rstest::rstest; - use vespertide_core::schema::primary_key::PrimaryKeySyntax; - use vespertide_core::{ - ColumnDef, ColumnType, ComplexColumnType, IndexDef, SimpleColumnType, 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, - } - } - - #[rstest] - #[case(ColumnType::Simple(SimpleColumnType::Integer))] - #[case(ColumnType::Simple(SimpleColumnType::BigInt))] - #[case(ColumnType::Simple(SimpleColumnType::Text))] - #[case(ColumnType::Simple(SimpleColumnType::Boolean))] - #[case(ColumnType::Simple(SimpleColumnType::Timestamp))] - #[case(ColumnType::Simple(SimpleColumnType::Uuid))] - #[case(ColumnType::Complex(ComplexColumnType::Varchar { length: 255 }))] - #[case(ColumnType::Complex(ComplexColumnType::Numeric { precision: 10, scale: 2 }))] - fn test_column_type_conversion(#[case] ty: ColumnType) { - // Just ensure no panic - test by creating a column with this type - let mut col = SeaColumnDef::new(Alias::new("test")); - apply_column_type(&mut col, &ty); - } - - #[rstest] - #[case(SimpleColumnType::SmallInt)] - #[case(SimpleColumnType::Integer)] - #[case(SimpleColumnType::BigInt)] - #[case(SimpleColumnType::Real)] - #[case(SimpleColumnType::DoublePrecision)] - #[case(SimpleColumnType::Text)] - #[case(SimpleColumnType::Boolean)] - #[case(SimpleColumnType::Date)] - #[case(SimpleColumnType::Time)] - #[case(SimpleColumnType::Timestamp)] - #[case(SimpleColumnType::Timestamptz)] - #[case(SimpleColumnType::Interval)] - #[case(SimpleColumnType::Bytea)] - #[case(SimpleColumnType::Uuid)] - #[case(SimpleColumnType::Json)] - #[case(SimpleColumnType::Jsonb)] - #[case(SimpleColumnType::Inet)] - #[case(SimpleColumnType::Cidr)] - #[case(SimpleColumnType::Macaddr)] - #[case(SimpleColumnType::Xml)] - fn test_all_simple_types_cover_branches(#[case] ty: SimpleColumnType) { - let mut col = SeaColumnDef::new(Alias::new("t")); - apply_column_type(&mut col, &ColumnType::Simple(ty)); - } - - #[rstest] - #[case(ComplexColumnType::Varchar { length: 42 })] - #[case(ComplexColumnType::Numeric { precision: 8, scale: 3 })] - #[case(ComplexColumnType::Char { length: 3 })] - #[case(ComplexColumnType::Custom { custom_type: "GEOGRAPHY".into() })] - fn test_all_complex_types_cover_branches(#[case] ty: ComplexColumnType) { - let mut col = SeaColumnDef::new(Alias::new("t")); - apply_column_type(&mut col, &ColumnType::Complex(ty)); - } - - #[rstest] - #[case::cascade(ReferenceAction::Cascade, ForeignKeyAction::Cascade)] - #[case::restrict(ReferenceAction::Restrict, ForeignKeyAction::Restrict)] - #[case::set_null(ReferenceAction::SetNull, ForeignKeyAction::SetNull)] - #[case::set_default(ReferenceAction::SetDefault, ForeignKeyAction::SetDefault)] - #[case::no_action(ReferenceAction::NoAction, ForeignKeyAction::NoAction)] - fn test_reference_action_conversion( - #[case] action: ReferenceAction, - #[case] expected: ForeignKeyAction, - ) { - // Just ensure the function doesn't panic and returns valid ForeignKeyAction - let result = to_sea_fk_action(&action); - assert!( - matches!(result, _expected), - "Expected {:?}, got {:?}", - expected, - result - ); - } - - #[rstest] - #[case(ReferenceAction::Cascade, "CASCADE")] - #[case(ReferenceAction::Restrict, "RESTRICT")] - #[case(ReferenceAction::SetNull, "SET NULL")] - #[case(ReferenceAction::SetDefault, "SET DEFAULT")] - #[case(ReferenceAction::NoAction, "NO ACTION")] - fn test_reference_action_sql_all_variants( - #[case] action: ReferenceAction, - #[case] expected: &str, - ) { - assert_eq!(reference_action_sql(&action), expected); - } - - #[test] - #[should_panic] - fn test_sqlite_create_foreign_key_build_panics() { - let fk = ForeignKey::create() - .name("fk") - .from_tbl(Alias::new("a")) - .from_col(Alias::new("c")) - .to_tbl(Alias::new("b")) - .to_col(Alias::new("id")) - .to_owned(); - let q = BuiltQuery::CreateForeignKey(Box::new(fk)); - // sea-query panics when building FK SQL for SQLite; exercise that branch - let _ = q.build(DatabaseBackend::Sqlite); - } - - #[test] - #[should_panic] - fn test_sqlite_drop_foreign_key_build_panics() { - let fk = ForeignKey::drop() - .name("fk") - .table(Alias::new("a")) - .to_owned(); - let q = BuiltQuery::DropForeignKey(Box::new(fk)); - // sea-query panics when building FK SQL for SQLite; exercise that branch - let _ = q.build(DatabaseBackend::Sqlite); - } - - #[test] - fn test_backend_specific_quoting() { - let action = MigrationAction::CreateTable { - table: "users".into(), - columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - constraints: vec![], - }; - let result = build_action_queries(&action).unwrap(); - - // PostgreSQL uses double quotes - let pg_sql = result[0].build(DatabaseBackend::Postgres); - assert!(pg_sql.contains("\"users\"")); - - // MySQL uses backticks - let mysql_sql = result[0].build(DatabaseBackend::MySql); - assert!(mysql_sql.contains("`users`")); - - // SQLite uses double quotes - let sqlite_sql = result[0].build(DatabaseBackend::Sqlite); - assert!(sqlite_sql.contains("\"users\"")); - } - - #[rstest] - #[case::create_table_postgres( - "create_table_postgres", - MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col("name", ColumnType::Simple(SimpleColumnType::Text)), - ], - constraints: vec![], - }, - DatabaseBackend::Postgres, - &["CREATE TABLE \"users\" ( \"id\" integer, \"name\" text )"] - )] - #[case::create_table_mysql( - "create_table_mysql", - MigrationAction::CreateTable { - table: "users".into(), - columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - constraints: vec![], - }, - DatabaseBackend::MySql, - &["CREATE TABLE `users` ( `id` int )"] - )] - #[case::create_table_sqlite( - "create_table_sqlite", - MigrationAction::CreateTable { - table: "users".into(), - columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], - constraints: vec![], - }, - DatabaseBackend::Sqlite, - &["CREATE TABLE \"users\" ( \"id\" integer )"] - )] - #[case::create_table_with_default_postgres( - "create_table_with_default_postgres", - MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "status".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: Some("'active'".into()), - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![], - }, - DatabaseBackend::Postgres, - &["DEFAULT", "'active'"] - )] - #[case::create_table_with_default_mysql( - "create_table_with_default_mysql", - MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "status".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: Some("'active'".into()), - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![], - }, - DatabaseBackend::MySql, - &["DEFAULT 'active'"] - )] - #[case::create_table_with_default_sqlite( - "create_table_with_default_sqlite", - MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "status".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: Some("'active'".into()), - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![], - }, - DatabaseBackend::Sqlite, - &["DEFAULT 'active'"] - )] - #[case::create_table_with_inline_primary_key_postgres( - "create_table_with_inline_primary_key_postgres", - 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![], - }, - DatabaseBackend::Postgres, - &["PRIMARY KEY"] - )] - #[case::create_table_with_inline_primary_key_mysql( - "create_table_with_inline_primary_key_mysql", - 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![], - }, - DatabaseBackend::MySql, - &["PRIMARY KEY"] - )] - #[case::create_table_with_inline_primary_key_sqlite( - "create_table_with_inline_primary_key_sqlite", - 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![], - }, - DatabaseBackend::Sqlite, - &["PRIMARY KEY"] - )] - #[case::create_table_with_inline_constraints_postgres( - "create_table_with_inline_constraints_postgres", - 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, - }, - ColumnDef { - name: "email".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: Some(StrOrBoolOrArray::Bool(true)), - index: None, - foreign_key: None, - }, - ], - constraints: vec![ - TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }, - TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - TableConstraint::Check { - name: "chk_always_true".into(), - expr: "1 = 1".into(), - }, - ], - }, - DatabaseBackend::Postgres, - &["PRIMARY KEY", "UNIQUE"] - )] - #[case::create_table_with_inline_constraints_mysql( - "create_table_with_inline_constraints_mysql", - 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, - }, - ColumnDef { - name: "email".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: Some(StrOrBoolOrArray::Bool(true)), - index: None, - foreign_key: None, - }, - ], - constraints: vec![ - TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }, - TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - TableConstraint::Check { - name: "chk_always_true".into(), - expr: "1 = 1".into(), - }, - ], - }, - DatabaseBackend::Postgres, - &["PRIMARY KEY", "UNIQUE"] - )] - #[case::create_table_with_inline_constraints_sqlite( - "create_table_with_inline_constraints_sqlite", - 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, - }, - ColumnDef { - name: "email".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: Some(StrOrBoolOrArray::Bool(true)), - index: None, - foreign_key: None, - }, - ], - constraints: vec![ - TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }, - TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - TableConstraint::Check { - name: "chk_always_true".into(), - expr: "1 = 1".into(), - }, - ], - }, - DatabaseBackend::Postgres, - &["PRIMARY KEY", "UNIQUE"] - )] - #[case::create_table_with_fk_postgres( - "create_table_with_fk_postgres", - MigrationAction::CreateTable { - table: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), - ], - constraints: vec![TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: Some(ReferenceAction::Cascade), - on_update: Some(ReferenceAction::Restrict), - }], - }, - DatabaseBackend::Postgres, - &["REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] - )] - #[case::create_table_with_fk_mysql( - "create_table_with_fk_mysql", - MigrationAction::CreateTable { - table: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), - ], - constraints: vec![TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: Some(ReferenceAction::Cascade), - on_update: Some(ReferenceAction::Restrict), - }], - }, - DatabaseBackend::MySql, - &["REFERENCES `users` (`id`)", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] - )] - #[case::create_table_with_fk_sqlite( - "create_table_with_fk_sqlite", - MigrationAction::CreateTable { - table: "posts".into(), - columns: vec![ - col("id", ColumnType::Simple(SimpleColumnType::Integer)), - col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), - ], - constraints: vec![TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: Some(ReferenceAction::Cascade), - on_update: Some(ReferenceAction::Restrict), - }], - }, - DatabaseBackend::Sqlite, - &["REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] - )] - #[case::delete_table_postgres( - "delete_table_postgres", - MigrationAction::DeleteTable { table: "users".into() }, - DatabaseBackend::Postgres, - &["DROP TABLE \"users\""] - )] - #[case::delete_table_mysql( - "delete_table_mysql", - MigrationAction::DeleteTable { table: "users".into() }, - DatabaseBackend::MySql, - &["DROP TABLE `users`"] - )] - #[case::delete_table_sqlite( - "delete_table_sqlite", - MigrationAction::DeleteTable { table: "users".into() }, - DatabaseBackend::Sqlite, - &["DROP TABLE \"users\""] - )] - #[case::rename_column_postgres( - "rename_column_postgres", - MigrationAction::RenameColumn { - table: "users".into(), - from: "email".into(), - to: "contact_email".into(), - }, - DatabaseBackend::Postgres, - &["ALTER TABLE \"users\" RENAME COLUMN \"email\" TO \"contact_email\""] - )] - #[case::rename_column_mysql( - "rename_column_mysql", - MigrationAction::RenameColumn { - table: "users".into(), - from: "email".into(), - to: "contact_email".into(), - }, - DatabaseBackend::MySql, - &["ALTER TABLE `users` RENAME COLUMN `email` TO `contact_email`"] - )] - #[case::rename_column_sqlite( - "rename_column_sqlite", - MigrationAction::RenameColumn { - table: "users".into(), - from: "email".into(), - to: "contact_email".into(), - }, - DatabaseBackend::Sqlite, - &["ALTER TABLE \"users\" RENAME COLUMN \"email\" TO \"contact_email\""] - )] - #[case::add_column_with_backfill_postgres( - "add_column_with_backfill_postgres", - MigrationAction::AddColumn { - table: "users".into(), - column: ColumnDef { - name: "age".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }, - fill_with: Some("0".into()), - }, - DatabaseBackend::Postgres, - &["ALTER TABLE \"users\" ADD COLUMN \"age\""] - )] - #[case::add_column_with_backfill_mysql( - "add_column_with_backfill_mysql", - MigrationAction::AddColumn { - table: "users".into(), - column: ColumnDef { - name: "age".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }, - fill_with: Some("0".into()), - }, - DatabaseBackend::MySql, - &["ALTER TABLE `users` ADD COLUMN `age` int"] - )] - #[case::add_column_with_backfill_sqlite( - "add_column_with_backfill_sqlite", - MigrationAction::AddColumn { - table: "users".into(), - column: ColumnDef { - name: "age".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }, - fill_with: Some("0".into()), - }, - DatabaseBackend::Sqlite, - &["ALTER TABLE \"users\" ADD COLUMN \"age\""] - )] - #[case::add_column_simple_postgres( - "add_column_simple_postgres", - MigrationAction::AddColumn { - table: "users".into(), - column: ColumnDef { - name: "nickname".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }, - fill_with: None, - }, - DatabaseBackend::Postgres, - &["ALTER TABLE \"users\" ADD COLUMN \"nickname\""] - )] - #[case::add_column_simple_mysql( - "add_column_simple_mysql", - MigrationAction::AddColumn { - table: "users".into(), - column: ColumnDef { - name: "nickname".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }, - fill_with: None, - }, - DatabaseBackend::MySql, - &["ALTER TABLE `users` ADD COLUMN `nickname` text"] - )] - #[case::add_column_simple_sqlite( - "add_column_simple_sqlite", - MigrationAction::AddColumn { - table: "users".into(), - column: ColumnDef { - name: "nickname".into(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }, - fill_with: None, - }, - DatabaseBackend::Sqlite, - &["ALTER TABLE \"users\" ADD COLUMN \"nickname\""] - )] - #[case::modify_column_type_postgres( - "modify_column_type_postgres", - MigrationAction::ModifyColumnType { - table: "users".into(), - column: "age".into(), - new_type: ColumnType::Complex(ComplexColumnType::Varchar { length: 50 }), - }, - DatabaseBackend::Postgres, - &["ALTER TABLE \"users\"", "\"age\""] - )] - #[case::modify_column_type_mysql( - "modify_column_type_mysql", - MigrationAction::ModifyColumnType { - table: "users".into(), - column: "age".into(), - new_type: ColumnType::Complex(ComplexColumnType::Varchar { length: 50 }), - }, - DatabaseBackend::MySql, - &["ALTER TABLE `users` MODIFY COLUMN `age` varchar(50)"] - )] - // NOTE: SQLite does not support MODIFY COLUMN; see sea-query sqlite backend. - // #[case::modify_column_type_sqlite( - // "modify_column_type_sqlite", - // MigrationAction::ModifyColumnType { - // table: "users".into(), - // column: "age".into(), - // new_type: ColumnType::Complex(ComplexColumnType::Varchar { length: 50 }), - // }, - // DatabaseBackend::Sqlite, - // &["ALTER TABLE \"users\" MODIFY COLUMN \"age\" VARCHAR(50)"] - // )] - #[case::rename_table_action_postgres( - "rename_table_action_postgres", - MigrationAction::RenameTable { - from: "users".into(), - to: "accounts".into(), - }, - DatabaseBackend::Postgres, - &["ALTER TABLE \"users\" RENAME TO \"accounts\""] - )] - #[case::rename_table_action_mysql( - "rename_table_action_mysql", - MigrationAction::RenameTable { - from: "users".into(), - to: "accounts".into(), - }, - DatabaseBackend::MySql, - &["RENAME TABLE `users` TO `accounts`"] - )] - #[case::rename_table_action_sqlite( - "rename_table_action_sqlite", - MigrationAction::RenameTable { - from: "users".into(), - to: "accounts".into(), - }, - DatabaseBackend::Sqlite, - &["ALTER TABLE \"users\" RENAME TO \"accounts\""] - )] - #[case::raw_sql_action_postgres( - "raw_sql_action_postgres", - MigrationAction::RawSql { - sql: "SELECT 1".into(), - }, - DatabaseBackend::Postgres, - &["SELECT 1"] - )] - #[case::raw_sql_action_mysql( - "raw_sql_action_mysql", - MigrationAction::RawSql { - sql: "SELECT 1".into(), - }, - DatabaseBackend::MySql, - &["SELECT 1"] - )] - #[case::raw_sql_action_sqlite( - "raw_sql_action_sqlite", - MigrationAction::RawSql { - sql: "SELECT 1".into(), - }, - DatabaseBackend::Sqlite, - &["SELECT 1"] - )] - #[case::delete_column_postgres( - "delete_column_postgres", - MigrationAction::DeleteColumn { - table: "users".into(), - column: "email".into(), - }, - DatabaseBackend::Postgres, - &["ALTER TABLE \"users\" DROP COLUMN \"email\""] - )] - #[case::delete_column_mysql( - "delete_column_mysql", - MigrationAction::DeleteColumn { - table: "users".into(), - column: "email".into(), - }, - DatabaseBackend::MySql, - &["ALTER TABLE `users` DROP COLUMN `email`"] - )] - #[case::delete_column_sqlite( - "delete_column_sqlite", - MigrationAction::DeleteColumn { - table: "users".into(), - column: "email".into(), - }, - DatabaseBackend::Sqlite, - &["ALTER TABLE \"users\" DROP COLUMN \"email\""] - )] - #[case::add_index_postgres( - "add_index_postgres", - MigrationAction::AddIndex { - table: "users".into(), - index: IndexDef { name: "idx_email".into(), columns: vec!["email".into()], unique: false }, - }, - DatabaseBackend::Postgres, - &["CREATE INDEX \"idx_email\" ON \"users\" (\"email\")"] - )] - #[case::add_index_mysql( - "add_index_mysql", - MigrationAction::AddIndex { - table: "users".into(), - index: IndexDef { name: "idx_email".into(), columns: vec!["email".into()], unique: false }, - }, - DatabaseBackend::MySql, - &["CREATE INDEX `idx_email` ON `users` (`email`)"] - )] - #[case::add_index_sqlite( - "add_index_sqlite", - MigrationAction::AddIndex { - table: "users".into(), - index: IndexDef { name: "idx_email".into(), columns: vec!["email".into()], unique: false }, - }, - DatabaseBackend::Sqlite, - &["CREATE INDEX \"idx_email\" ON \"users\" (\"email\")"] - )] - #[case::add_unique_index_postgres( - "add_unique_index_postgres", - MigrationAction::AddIndex { - table: "users".into(), - index: IndexDef { name: "idx_email".into(), columns: vec!["email".into()], unique: true }, - }, - DatabaseBackend::Postgres, - &["CREATE UNIQUE INDEX \"idx_email\" ON \"users\" (\"email\")"] - )] - #[case::add_unique_index_mysql( - "add_unique_index_mysql", - MigrationAction::AddIndex { - table: "users".into(), - index: IndexDef { name: "idx_email".into(), columns: vec!["email".into()], unique: true }, - }, - DatabaseBackend::MySql, - &["CREATE UNIQUE INDEX `idx_email` ON `users` (`email`)"] - )] - #[case::add_unique_index_sqlite( - "add_unique_index_sqlite", - MigrationAction::AddIndex { - table: "users".into(), - index: IndexDef { name: "idx_email".into(), columns: vec!["email".into()], unique: true }, - }, - DatabaseBackend::Sqlite, - &["CREATE UNIQUE INDEX \"idx_email\" ON \"users\" (\"email\")"] - )] - #[case::add_constraint_primary_key_postgres( - "add_constraint_primary_key_postgres", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::PrimaryKey { - columns: vec!["id".into()], - auto_increment: false, - }, - }, - DatabaseBackend::Postgres, - &["ALTER TABLE \"users\" ADD PRIMARY KEY (\"id\")"] - )] - #[case::add_constraint_primary_key_mysql( - "add_constraint_primary_key_mysql", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::PrimaryKey { - columns: vec!["id".into()], - auto_increment: false, - }, - }, - DatabaseBackend::MySql, - &["ALTER TABLE \"users\" ADD PRIMARY KEY (\"id\")"] - )] - #[case::add_constraint_primary_key_sqlite( - "add_constraint_primary_key_sqlite", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::PrimaryKey { - columns: vec!["id".into()], - auto_increment: false, - }, - }, - DatabaseBackend::Sqlite, - &["ALTER TABLE \"users\" ADD PRIMARY KEY (\"id\")"] - )] - #[case::add_constraint_unique_named_postgres( - "add_constraint_unique_named_postgres", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - }, - DatabaseBackend::Postgres, - &["ADD CONSTRAINT \"uq_email\" UNIQUE (\"email\")"] - )] - #[case::add_constraint_unique_named_mysql( - "add_constraint_unique_named_mysql", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - }, - DatabaseBackend::MySql, - &["ADD CONSTRAINT \"uq_email\" UNIQUE (\"email\")"] - )] - #[case::add_constraint_unique_named_sqlite( - "add_constraint_unique_named_sqlite", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - }, - DatabaseBackend::Sqlite, - &["ADD CONSTRAINT \"uq_email\" UNIQUE (\"email\")"] - )] - #[case::add_constraint_unique_unnamed_postgres( - "add_constraint_unique_unnamed_postgres", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: None, - columns: vec!["email".into()], - }, - }, - DatabaseBackend::Postgres, - &["ADD UNIQUE (\"email\")"] - )] - #[case::add_constraint_unique_unnamed_mysql( - "add_constraint_unique_unnamed_mysql", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: None, - columns: vec!["email".into()], - }, - }, - DatabaseBackend::MySql, - &["ADD UNIQUE (\"email\")"] - )] - #[case::add_constraint_unique_unnamed_sqlite( - "add_constraint_unique_unnamed_sqlite", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: None, - columns: vec!["email".into()], - }, - }, - DatabaseBackend::Sqlite, - &["ADD UNIQUE (\"email\")"] - )] - #[case::add_constraint_foreign_key_postgres( - "add_constraint_foreign_key_postgres", - MigrationAction::AddConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: Some(ReferenceAction::Cascade), - on_update: Some(ReferenceAction::Restrict), - }, - }, - DatabaseBackend::Postgres, - &["FOREIGN KEY (\"user_id\")", "REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] - )] - #[case::add_constraint_foreign_key_mysql( - "add_constraint_foreign_key_mysql", - MigrationAction::AddConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: Some(ReferenceAction::Cascade), - on_update: Some(ReferenceAction::Restrict), - }, - }, - DatabaseBackend::MySql, - &["FOREIGN KEY (\"user_id\")", "REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] - )] - #[case::add_constraint_foreign_key_unnamed_postgres( - "add_constraint_foreign_key_unnamed_postgres", - MigrationAction::AddConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: None, - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::Postgres, - &["ADD FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\")"] - )] - #[case::add_constraint_foreign_key_unnamed_mysql( - "add_constraint_foreign_key_unnamed_mysql", - MigrationAction::AddConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: None, - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::MySql, - &["ADD FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\")"] - )] - #[case::add_constraint_foreign_key_unnamed_sqlite( - "add_constraint_foreign_key_unnamed_sqlite", - MigrationAction::AddConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: None, - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::Sqlite, - &["ADD FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\")"] - )] - #[case::add_constraint_foreign_key_sqlite( - "add_constraint_foreign_key_sqlite", - MigrationAction::AddConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: Some(ReferenceAction::Cascade), - on_update: Some(ReferenceAction::Restrict), - }, - }, - DatabaseBackend::Sqlite, - &["FOREIGN KEY (\"user_id\")", "REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] - )] - #[case::add_constraint_check_named_postgres( - "add_constraint_check_named_postgres", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Check { - name: "chk_age".into(), - expr: "age > 0".into(), - }, - }, - DatabaseBackend::Postgres, - &["ADD CONSTRAINT \"chk_age\" CHECK (age > 0)"] - )] - #[case::add_constraint_check_named_mysql( - "add_constraint_check_named_mysql", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Check { - name: "chk_age".into(), - expr: "age > 0".into(), - }, - }, - DatabaseBackend::MySql, - &["ADD CONSTRAINT \"chk_age\" CHECK (age > 0)"] - )] - #[case::add_constraint_check_named_sqlite( - "add_constraint_check_named_sqlite", - MigrationAction::AddConstraint { - table: "users".into(), - constraint: TableConstraint::Check { - name: "chk_age".into(), - expr: "age > 0".into(), - }, - }, - DatabaseBackend::Sqlite, - &["ADD CONSTRAINT \"chk_age\" CHECK (age > 0)"] - )] - #[case::remove_constraint_primary_key_postgres( - "remove_constraint_primary_key_postgres", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::PrimaryKey { - columns: vec!["id".into()], - auto_increment: false, - }, - }, - DatabaseBackend::Postgres, - &["DROP CONSTRAINT \"users_pkey\""] - )] - #[case::remove_constraint_primary_key_mysql( - "remove_constraint_primary_key_mysql", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::PrimaryKey { - columns: vec!["id".into()], - auto_increment: false, - }, - }, - DatabaseBackend::MySql, - &["DROP CONSTRAINT \"users_pkey\""] - )] - #[case::remove_constraint_primary_key_sqlite( - "remove_constraint_primary_key_sqlite", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::PrimaryKey { - columns: vec!["id".into()], - auto_increment: false, - }, - }, - DatabaseBackend::Sqlite, - &["DROP CONSTRAINT \"users_pkey\""] - )] - #[case::remove_constraint_unique_named_postgres( - "remove_constraint_unique_named_postgres", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - }, - DatabaseBackend::Postgres, - &["DROP CONSTRAINT \"uq_email\""] - )] - #[case::remove_constraint_unique_named_mysql( - "remove_constraint_unique_named_mysql", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - }, - DatabaseBackend::MySql, - &["DROP CONSTRAINT \"uq_email\""] - )] - #[case::remove_constraint_unique_named_sqlite( - "remove_constraint_unique_named_sqlite", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: Some("uq_email".into()), - columns: vec!["email".into()], - }, - }, - DatabaseBackend::Sqlite, - &["DROP CONSTRAINT \"uq_email\""] - )] - #[case::remove_constraint_unique_unnamed_postgres( - "remove_constraint_unique_unnamed_postgres", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: None, - columns: vec!["email".into()], - }, - }, - DatabaseBackend::Postgres, - &["DROP CONSTRAINT \"users_email_key\""] - )] - #[case::remove_constraint_unique_unnamed_mysql( - "remove_constraint_unique_unnamed_mysql", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: None, - columns: vec!["email".into()], - }, - }, - DatabaseBackend::MySql, - &["DROP CONSTRAINT \"users_email_key\""] - )] - #[case::remove_constraint_unique_unnamed_sqlite( - "remove_constraint_unique_unnamed_sqlite", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Unique { - name: None, - columns: vec!["email".into()], - }, - }, - DatabaseBackend::Sqlite, - &["DROP CONSTRAINT \"users_email_key\""] - )] - #[case::remove_constraint_foreign_key_named_postgres( - "remove_constraint_foreign_key_named_postgres", - MigrationAction::RemoveConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::Postgres, - &["DROP CONSTRAINT \"fk_user\""] - )] - #[case::remove_constraint_foreign_key_named_mysql( - "remove_constraint_foreign_key_named_mysql", - MigrationAction::RemoveConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::MySql, - &["DROP CONSTRAINT \"fk_user\""] - )] - #[case::remove_constraint_foreign_key_named_sqlite( - "remove_constraint_foreign_key_named_sqlite", - MigrationAction::RemoveConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: Some("fk_user".into()), - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::Sqlite, - &["DROP CONSTRAINT \"fk_user\""] - )] - #[case::remove_constraint_foreign_key_unnamed_postgres( - "remove_constraint_foreign_key_unnamed_postgres", - MigrationAction::RemoveConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: None, - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::Postgres, - &["DROP CONSTRAINT \"posts_user_id_fkey\""] - )] - #[case::remove_constraint_foreign_key_unnamed_mysql( - "remove_constraint_foreign_key_unnamed_mysql", - MigrationAction::RemoveConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: None, - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::MySql, - &["DROP CONSTRAINT \"posts_user_id_fkey\""] - )] - #[case::remove_constraint_foreign_key_unnamed_sqlite( - "remove_constraint_foreign_key_unnamed_sqlite", - MigrationAction::RemoveConstraint { - table: "posts".into(), - constraint: TableConstraint::ForeignKey { - name: None, - columns: vec!["user_id".into()], - ref_table: "users".into(), - ref_columns: vec!["id".into()], - on_delete: None, - on_update: None, - }, - }, - DatabaseBackend::Sqlite, - &["DROP CONSTRAINT \"posts_user_id_fkey\""] - )] - #[case::remove_constraint_check_named_postgres( - "remove_constraint_check_named_postgres", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Check { - name: "chk_age".into(), - expr: "age > 0".into(), - }, - }, - DatabaseBackend::Postgres, - &["DROP CONSTRAINT \"chk_age\""] - )] - #[case::remove_constraint_check_named_mysql( - "remove_constraint_check_named_mysql", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Check { - name: "chk_age".into(), - expr: "age > 0".into(), - }, - }, - DatabaseBackend::MySql, - &["DROP CONSTRAINT \"chk_age\""] - )] - #[case::remove_constraint_check_named_sqlite( - "remove_constraint_check_named_sqlite", - MigrationAction::RemoveConstraint { - table: "users".into(), - constraint: TableConstraint::Check { - name: "chk_age".into(), - expr: "age > 0".into(), - }, - }, - DatabaseBackend::Sqlite, - &["DROP CONSTRAINT \"chk_age\""] - )] - #[case::remove_index_postgres( - "remove_index_postgres", - MigrationAction::RemoveIndex { - table: "users".into(), - name: "idx_email".into(), - }, - DatabaseBackend::Postgres, - &["DROP INDEX \"idx_email\""] - )] - #[case::remove_index_mysql( - "remove_index_mysql", - MigrationAction::RemoveIndex { - table: "users".into(), - name: "idx_email".into(), - }, - DatabaseBackend::MySql, - &["DROP INDEX `idx_email`"] - )] - #[case::remove_index_sqlite( - "remove_index_sqlite", - MigrationAction::RemoveIndex { - table: "users".into(), - name: "idx_email".into(), - }, - DatabaseBackend::Sqlite, - &["DROP INDEX \"idx_email\""] - )] - fn test_build_migration_action( - #[case] title: &str, - #[case] action: MigrationAction, - #[case] backend: DatabaseBackend, - #[case] expected: &[&str], - ) { - let result = build_action_queries(&action).unwrap(); - let sql = result[0].build(backend); - for exp in expected { - assert!( - sql.contains(exp), - "Expected SQL to contain '{}', got: {}", - exp, - sql - ); - } - - with_settings!({ snapshot_suffix => format!("build_migration_action_{}", title) }, { - assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::>().join("\n")); - }); - } - - #[rstest] - #[case::create_table_query_postgres( - "create_table_query_postgres", - BuiltQuery::CreateTable(Box::new( - Table::create() - .table(Alias::new("t")) - .col(SeaColumnDef::new(Alias::new("id")).integer().to_owned()) - .to_owned(), - )), - DatabaseBackend::Postgres, - &["CREATE TABLE \"t\""] - )] - #[case::create_table_query_mysql("create_table_query_mysql", BuiltQuery::CreateTable(Box::new(Table::create() - .table(Alias::new("t")) - .col(SeaColumnDef::new(Alias::new("id")).integer().to_owned()) - .to_owned())),DatabaseBackend::MySql, &["CREATE TABLE `t`"])] - #[case::create_table_query_sqlite("create_table_query_sqlite", BuiltQuery::CreateTable(Box::new(Table::create() - .table(Alias::new("t")) - .col(SeaColumnDef::new(Alias::new("id")).integer().to_owned()) - .to_owned())),DatabaseBackend::Sqlite, &["CREATE TABLE \"t\""])] - #[case::drop_table_query_postgres( - "drop_table_query_postgres", - BuiltQuery::DropTable(Box::new( - Table::drop().table(Alias::new("t")).to_owned(), - )), - DatabaseBackend::Postgres, - &["DROP TABLE \"t\""] - )] - #[case::drop_table_query_mysql("drop_table_query_mysql", BuiltQuery::DropTable(Box::new(Table::drop().table(Alias::new("t")).to_owned())), DatabaseBackend::MySql, &["DROP TABLE `t`"])] - #[case::drop_table_query_sqlite("drop_table_query_sqlite", BuiltQuery::DropTable(Box::new(Table::drop().table(Alias::new("t")).to_owned())), DatabaseBackend::Sqlite, &["DROP TABLE \"t\""])] - #[case::raw_query_postgres( - "raw_query_postgres", - BuiltQuery::Raw("SELECT 1".into()), - DatabaseBackend::Postgres, - &["SELECT 1"] - )] - #[case::raw_query_mysql("raw_query_mysql", BuiltQuery::Raw("SELECT 1".into()), DatabaseBackend::MySql, &["SELECT 1"])] - #[case::raw_query_sqlite("raw_query_sqlite", BuiltQuery::Raw("SELECT 1".into()), DatabaseBackend::Sqlite, &["SELECT 1"])] - #[case::alter_table_postgres("alter_table_postgres", BuiltQuery::AlterTable(Box::new(Table::alter() - .table(Alias::new("t")) - .add_column(SeaColumnDef::new(Alias::new("c")).integer().to_owned()) - .to_owned())),DatabaseBackend::Postgres, &["ALTER TABLE \"t\" ADD COLUMN \"c\" integer"])] - #[case::alter_table_mysql("alter_table_mysql", BuiltQuery::AlterTable(Box::new(Table::alter() - .table(Alias::new("t")) - .add_column(SeaColumnDef::new(Alias::new("c")).integer().to_owned()) - .to_owned())),DatabaseBackend::MySql, &["ALTER TABLE `t` ADD COLUMN `c` int"])] - #[case::alter_table_sqlite("alter_table_sqlite", BuiltQuery::AlterTable(Box::new(Table::alter() - .table(Alias::new("t")) - .add_column(SeaColumnDef::new(Alias::new("c")).integer().to_owned()) - .to_owned())),DatabaseBackend::Sqlite, &["ALTER TABLE \"t\" ADD COLUMN \"c\" integer"])] - #[case::create_index_postgres("create_index_postgres", BuiltQuery::CreateIndex(Box::new(Index::create() - .name("idx") - .table(Alias::new("t")) - .col(Alias::new("c")) - .to_owned())),DatabaseBackend::Postgres, &["CREATE INDEX \"idx\" ON \"t\" (\"c\")"])] - #[case::create_index_mysql("create_index_mysql", BuiltQuery::CreateIndex(Box::new(Index::create() - .name("idx") - .table(Alias::new("t")) - .col(Alias::new("c")) - .to_owned())),DatabaseBackend::MySql, &["CREATE INDEX `idx` ON `t` (`c`"])] - #[case::create_index_sqlite("create_index_sqlite", BuiltQuery::CreateIndex(Box::new(Index::create() - .name("idx") - .table(Alias::new("t")) - .col(Alias::new("c")) - .to_owned())),DatabaseBackend::Sqlite, &["CREATE INDEX \"idx\" ON \"t\" (\"c\")"])] - #[case::drop_index_postgres("drop_index_postgres", BuiltQuery::DropIndex(Box::new(Index::drop().name("idx").table(Alias::new("t")).to_owned())),DatabaseBackend::Postgres, &["DROP INDEX \"idx\""])] - #[case::drop_index_mysql("drop_index_mysql", BuiltQuery::DropIndex(Box::new(Index::drop().name("idx").table(Alias::new("t")).to_owned())),DatabaseBackend::MySql, &["`idx`"])] - #[case::drop_index_sqlite("drop_index_sqlite", BuiltQuery::DropIndex(Box::new(Index::drop().name("idx").table(Alias::new("t")).to_owned())),DatabaseBackend::Sqlite, &["\"idx\""])] - #[case::rename_table_postgres("rename_table_postgres", BuiltQuery::RenameTable(Box::new(Table::rename() - .table(Alias::new("a"), Alias::new("b")) - .to_owned())),DatabaseBackend::Postgres, &["ALTER TABLE \"a\" RENAME TO \"b\""])] - #[case::rename_table_mysql("rename_table_mysql", BuiltQuery::RenameTable(Box::new(Table::rename() - .table(Alias::new("a"), Alias::new("b")) - .to_owned())),DatabaseBackend::MySql, &["RENAME TABLE `a` TO `b`"])] - #[case::rename_table_sqlite("rename_table_sqlite", BuiltQuery::RenameTable(Box::new(Table::rename() - .table(Alias::new("a"), Alias::new("b")) - .to_owned())),DatabaseBackend::Sqlite, &["ALTER TABLE \"a\" RENAME TO \"b\""])] - #[case::create_foreign_key_postgres("create_foreign_key_postgres", BuiltQuery::CreateForeignKey(Box::new(ForeignKey::create() - .name("fk") - .from_tbl(Alias::new("a")) - .from_col(Alias::new("c")) - .to_tbl(Alias::new("b")) - .to_col(Alias::new("id")) - .to_owned())),DatabaseBackend::Postgres, &["ALTER TABLE \"a\" ADD CONSTRAINT \"fk\" FOREIGN KEY (\"c\") REFERENCES \"b\" (\"id\")"])] - #[case::create_foreign_key_mysql("create_foreign_key_mysql", BuiltQuery::CreateForeignKey(Box::new(ForeignKey::create() - .name("fk") - .from_tbl(Alias::new("a")) - .from_col(Alias::new("c")) - .to_tbl(Alias::new("b")) - .to_col(Alias::new("id")) - .to_owned())),DatabaseBackend::MySql, &["ALTER TABLE `a` ADD CONSTRAINT `fk` FOREIGN KEY (`c`) REFERENCES `b` (`id`)"])] - // NOTE: SQLite does not support modifying FK constraints on existing tables; sea-query panics for these. - // #[case::create_foreign_key_sqlite("create_foreign_key_sqlite", BuiltQuery::CreateForeignKey(Box::new(ForeignKey::create() - // .name("fk") - // .from_tbl(Alias::new("a")) - // .from_col(Alias::new("c")) - // .to_tbl(Alias::new("b")) - // .to_col(Alias::new("id")) - // .to_owned())),DatabaseBackend::Sqlite, &["ALTER TABLE \"a\" ADD CONSTRAINT \"fk\" FOREIGN KEY (\"c\") REFERENCES \"b\" (\"id\")"])] - #[case::drop_foreign_key_postgres("drop_foreign_key_postgres", BuiltQuery::DropForeignKey(Box::new(ForeignKey::drop() - .name("fk") - .table(Alias::new("a")) - .to_owned())),DatabaseBackend::Postgres, &["ALTER TABLE \"a\" DROP CONSTRAINT \"fk\""])] - #[case::drop_foreign_key_mysql("drop_foreign_key_mysql", BuiltQuery::DropForeignKey(Box::new(ForeignKey::drop() - .name("fk") - .table(Alias::new("a")) - .to_owned())),DatabaseBackend::MySql, &["ALTER TABLE `a` DROP FOREIGN KEY `fk`"])] - // #[case::drop_foreign_key_sqlite("drop_foreign_key_sqlite", BuiltQuery::DropForeignKey(Box::new(ForeignKey::drop() - // .name("fk") - // .table(Alias::new("a")) - // .to_owned())),DatabaseBackend::Sqlite, &["ALTER TABLE \"a\" DROP CONSTRAINT \"fk\""])] - fn test_build_query( - #[case] title: &str, - #[case] q: BuiltQuery, - #[case] backend: DatabaseBackend, - #[case] expected: &[&str], - ) { - let sql = q.build(backend); - for exp in expected { - assert!( - sql.contains(exp), - "Expected SQL to contain '{}', got: {}", - exp, - sql - ); - } - - with_settings!({ snapshot_suffix => format!("build_query_{}", title) }, { - assert_snapshot!(sql); - }); - } -} diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs new file mode 100644 index 0000000..0ac0be8 --- /dev/null +++ b/crates/vespertide-query/src/sql/add_column.rs @@ -0,0 +1,490 @@ +use sea_query::{Alias, Expr, Query, Table, TableAlterStatement}; + +use vespertide_core::{ColumnDef, TableDef}; + +use super::create_table::build_create_table_for_backend; +use super::helpers::build_sea_column_def; +use super::rename_table::build_rename_table; +use super::types::{BuiltQuery, DatabaseBackend}; +use crate::error::QueryError; + +fn build_add_column_alter_for_backend( + backend: &DatabaseBackend, + table: &str, + column: &ColumnDef, +) -> TableAlterStatement { + let col_def = build_sea_column_def(backend, column); + Table::alter() + .table(Alias::new(table)) + .add_column(col_def) + .to_owned() +} + +pub fn build_add_column( + backend: &DatabaseBackend, + table: &str, + column: &ColumnDef, + fill_with: Option<&str>, + current_schema: &[TableDef], +) -> Result, QueryError> { + // SQLite: only NOT NULL additions require table recreation + if *backend == DatabaseBackend::Sqlite && !column.nullable { + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to add columns.", + table + )))?; + + let mut new_columns = table_def.columns.clone(); + new_columns.push(column.clone()); + + let temp_table = format!("{}_temp", table); + let create_temp = build_create_table_for_backend( + backend, + &temp_table, + &new_columns, + &table_def.constraints, + ); + let create_query = BuiltQuery::CreateTable(Box::new(create_temp)); + + // Copy existing data, filling new column + let mut select_query = Query::select(); + for col in &table_def.columns { + select_query = select_query.column(Alias::new(&col.name)).to_owned(); + } + let fill_expr = if let Some(fill) = fill_with { + Expr::cust(fill) + } else if let Some(def) = &column.default { + Expr::cust(def) + } else { + Expr::cust("NULL") + }; + select_query = select_query + .expr_as(fill_expr, Alias::new(&column.name)) + .from(Alias::new(table)) + .to_owned(); + + let mut columns_alias: Vec = table_def + .columns + .iter() + .map(|c| Alias::new(&c.name)) + .collect(); + columns_alias.push(Alias::new(&column.name)); + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(columns_alias) + .select_from(select_query) + .unwrap() + .to_owned(); + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + let drop_query = + BuiltQuery::DropTable(Box::new(Table::drop().table(Alias::new(table)).to_owned())); + let rename_query = build_rename_table(&temp_table, table); + + 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(); + } + 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]; + stmts.extend(index_queries); + return Ok(stmts); + } + + let mut stmts: Vec = Vec::new(); + + // If adding NOT NULL without default, we need special handling + let needs_backfill = !column.nullable && column.default.is_none() && fill_with.is_some(); + + if needs_backfill { + // Add as nullable first + let mut temp_col = column.clone(); + temp_col.nullable = true; + + stmts.push(BuiltQuery::AlterTable(Box::new( + build_add_column_alter_for_backend(backend, table, &temp_col), + ))); + + // Backfill with provided value + if let Some(fill) = fill_with { + let update_stmt = Query::update() + .table(Alias::new(table)) + .value(Alias::new(&column.name), Expr::cust(fill)) + .to_owned(); + stmts.push(BuiltQuery::Update(Box::new(update_stmt))); + } + + // Set NOT NULL + let not_null_col = build_sea_column_def(backend, column); + let alter_not_null = Table::alter() + .table(Alias::new(table)) + .modify_column(not_null_col) + .to_owned(); + stmts.push(BuiltQuery::AlterTable(Box::new(alter_not_null))); + } else { + stmts.push(BuiltQuery::AlterTable(Box::new( + build_add_column_alter_for_backend(backend, table, column), + ))); + } + + Ok(stmts) +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::{ColumnType, SimpleColumnType, TableDef}; + + #[rstest] + #[case::add_column_with_backfill_postgres( + "add_column_with_backfill_postgres", + DatabaseBackend::Postgres, + &["ALTER TABLE \"users\" ADD COLUMN \"nickname\" text"] + )] + #[case::add_column_with_backfill_mysql( + "add_column_with_backfill_mysql", + DatabaseBackend::MySql, + &["ALTER TABLE `users` ADD COLUMN `nickname` text"] + )] + #[case::add_column_with_backfill_sqlite( + "add_column_with_backfill_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users_temp\""] + )] + #[case::add_column_simple_postgres( + "add_column_simple_postgres", + DatabaseBackend::Postgres, + &["ALTER TABLE \"users\" ADD COLUMN \"nickname\""] + )] + #[case::add_column_simple_mysql( + "add_column_simple_mysql", + DatabaseBackend::MySql, + &["ALTER TABLE `users` ADD COLUMN `nickname` text"] + )] + #[case::add_column_simple_sqlite( + "add_column_simple_sqlite", + DatabaseBackend::Sqlite, + &["ALTER TABLE \"users\" ADD COLUMN \"nickname\""] + )] + #[case::add_column_nullable_postgres( + "add_column_nullable_postgres", + DatabaseBackend::Postgres, + &["ALTER TABLE \"users\" ADD COLUMN \"email\" text"] + )] + #[case::add_column_nullable_mysql( + "add_column_nullable_mysql", + DatabaseBackend::MySql, + &["ALTER TABLE `users` ADD COLUMN `email` text"] + )] + #[case::add_column_nullable_sqlite( + "add_column_nullable_sqlite", + DatabaseBackend::Sqlite, + &["ALTER TABLE \"users\" ADD COLUMN \"email\" text"] + )] + fn test_add_column( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let column = ColumnDef { + name: if title.contains("age") { + "age" + } else if title.contains("nullable") { + "email" + } else { + "nickname" + } + .into(), + r#type: if title.contains("age") { + ColumnType::Simple(SimpleColumnType::Integer) + } else { + ColumnType::Simple(SimpleColumnType::Text) + }, + nullable: !title.contains("backfill"), + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }; + let fill_with = if title.contains("backfill") { + Some("0") + } else { + 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![], + }]; + let result = + build_add_column(&backend, "users", &column, fill_with, ¤t_schema).unwrap(); + let sql = result[0].build(backend); + for exp in expected { + assert!( + sql.contains(exp), + "Expected SQL to contain '{}', got: {}", + exp, + sql + ); + } + + with_settings!({ snapshot_suffix => format!("add_column_{}", title) }, { + assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::>().join("\n")); + }); + } + + #[test] + fn test_add_column_sqlite_table_not_found() { + 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![]; // Empty schema - table not found + let result = build_add_column( + &DatabaseBackend::Sqlite, + "users", + &column, + None, + ¤t_schema, + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'users' not found in current schema")); + } + + #[test] + fn test_add_column_sqlite_with_default() { + let column = ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: Some("18".into()), + 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![], + }]; + 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 use default value (18) for fill + assert!(sql.contains("18")); + } + + #[test] + fn test_add_column_sqlite_without_fill_or_default() { + let column = ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + 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![], + }]; + 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 use NULL for fill + assert!(sql.contains("NULL")); + } + + #[test] + fn test_add_column_sqlite_with_indexes() { + 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_id".into(), + columns: vec!["id".into()], + unique: false, + }], + }]; + 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 index + assert!(sql.contains("CREATE INDEX")); + 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")); + } +} diff --git a/crates/vespertide-query/src/sql/add_constraint.rs b/crates/vespertide-query/src/sql/add_constraint.rs new file mode 100644 index 0000000..02b42bb --- /dev/null +++ b/crates/vespertide-query/src/sql/add_constraint.rs @@ -0,0 +1,1206 @@ +use sea_query::{Alias, ForeignKey, Index, Query, Table}; + +use vespertide_core::{TableConstraint, TableDef}; + +use super::create_table::build_create_table_for_backend; +use super::helpers::{build_schema_statement, to_sea_fk_action}; +use super::rename_table::build_rename_table; +use super::types::{BuiltQuery, DatabaseBackend}; +use crate::error::QueryError; +use crate::sql::RawSql; + +/// Extract CHECK constraint clauses from a list of constraints +fn extract_check_clauses(constraints: &[TableConstraint]) -> Vec { + constraints + .iter() + .filter_map(|c| { + if let TableConstraint::Check { name, expr } = c { + Some(format!("CONSTRAINT \"{}\" CHECK ({})", name, expr)) + } else { + None + } + }) + .collect() +} + +/// Build CREATE TABLE query with CHECK constraints properly embedded +fn build_create_with_checks( + backend: &DatabaseBackend, + create_stmt: &sea_query::TableCreateStatement, + check_clauses: &[String], +) -> BuiltQuery { + if check_clauses.is_empty() { + BuiltQuery::CreateTable(Box::new(create_stmt.clone())) + } else { + let base_sql = build_schema_statement(create_stmt, *backend); + let mut modified_sql = base_sql; + if let Some(pos) = modified_sql.rfind(')') { + let check_sql = check_clauses.join(", "); + modified_sql.insert_str(pos, &format!(", {}", check_sql)); + } + BuiltQuery::Raw(RawSql::per_backend( + modified_sql.clone(), + modified_sql.clone(), + modified_sql, + )) + } +} + +pub fn build_add_constraint( + backend: &DatabaseBackend, + table: &str, + constraint: &TableConstraint, + current_schema: &[TableDef], +) -> Result, QueryError> { + match constraint { + TableConstraint::PrimaryKey { columns, .. } => { + if *backend == DatabaseBackend::Sqlite { + // SQLite does not support ALTER TABLE ... ADD PRIMARY KEY + // Use temporary table approach + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", + table + )))?; + + // Create new constraints with the added primary key constraint + let mut new_constraints = table_def.constraints.clone(); + new_constraints.push(constraint.clone()); + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table with new constraints + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &table_def.columns, + &new_constraints, + ); + + // Handle CHECK constraints (sea-query doesn't support them natively) + let check_clauses = extract_check_clauses(&new_constraints); + let create_query = + build_create_with_checks(backend, &create_temp_table, &check_clauses); + + // 2. Copy data + let column_aliases: Vec = table_def + .columns + .iter() + .map(|c| Alias::new(&c.name)) + .collect(); + let mut select_query = Query::select(); + for col_alias in &column_aliases { + select_query = select_query.column(col_alias.clone()).to_owned(); + } + select_query = select_query.from(Alias::new(table)).to_owned(); + + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(column_aliases.clone()) + .select_from(select_query) + .unwrap() + .to_owned(); + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + let drop_query = BuiltQuery::DropTable(Box::new(drop_table)); + + // 4. Rename temporary table + let rename_query = build_rename_table(&temp_table, table); + + // 5. Recreate indexes + 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(); + } + 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]; + queries.extend(index_queries); + Ok(queries) + } else { + // sea_query lacks ALTER TABLE ADD PRIMARY KEY; emit backend SQL + let pg_cols = columns + .iter() + .map(|c| format!("\"{}\"", c)) + .collect::>() + .join(", "); + let mysql_cols = columns + .iter() + .map(|c| format!("`{}`", c)) + .collect::>() + .join(", "); + let pg_sql = format!("ALTER TABLE \"{}\" ADD PRIMARY KEY ({})", table, pg_cols); + let mysql_sql = format!("ALTER TABLE `{}` ADD PRIMARY KEY ({})", table, mysql_cols); + Ok(vec![BuiltQuery::Raw(RawSql::per_backend( + pg_sql.clone(), + mysql_sql, + pg_sql, + ))]) + } + } + 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(); + } + for col in columns { + idx = idx.col(Alias::new(col)).to_owned(); + } + Ok(vec![BuiltQuery::CreateIndex(Box::new(idx))]) + } + TableConstraint::ForeignKey { + name, + columns, + ref_table, + ref_columns, + on_delete, + on_update, + } => { + // SQLite does not support ALTER TABLE ... ADD CONSTRAINT FOREIGN KEY + if *backend == DatabaseBackend::Sqlite { + // Use temporary table approach for SQLite + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", + table + )))?; + + // Create new constraints with the added foreign key constraint + let mut new_constraints = table_def.constraints.clone(); + new_constraints.push(constraint.clone()); + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table with new constraints + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &table_def.columns, + &new_constraints, + ); + + // Handle CHECK constraints (sea-query doesn't support them natively) + let check_clauses = extract_check_clauses(&new_constraints); + let create_query = + build_create_with_checks(backend, &create_temp_table, &check_clauses); + + // 2. Copy data (all columns) + let column_aliases: Vec = table_def + .columns + .iter() + .map(|c| Alias::new(&c.name)) + .collect(); + let mut select_query = Query::select(); + for col_alias in &column_aliases { + select_query = select_query.column(col_alias.clone()).to_owned(); + } + select_query = select_query.from(Alias::new(table)).to_owned(); + + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(column_aliases.clone()) + .select_from(select_query) + .unwrap() + .to_owned(); + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + let drop_query = BuiltQuery::DropTable(Box::new(drop_table)); + + // 4. Rename temporary table to original name + let rename_query = build_rename_table(&temp_table, table); + + // 5. Recreate indexes (if any) + 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(); + } + 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]; + queries.extend(index_queries); + Ok(queries) + } else { + // Build foreign key using ForeignKey::create + let mut fk = ForeignKey::create(); + if let Some(n) = name { + fk = fk.name(n).to_owned(); + } + fk = fk.from_tbl(Alias::new(table)).to_owned(); + for col in columns { + fk = fk.from_col(Alias::new(col)).to_owned(); + } + fk = fk.to_tbl(Alias::new(ref_table)).to_owned(); + for col in ref_columns { + fk = fk.to_col(Alias::new(col)).to_owned(); + } + if let Some(action) = on_delete { + fk = fk.on_delete(to_sea_fk_action(action)).to_owned(); + } + if let Some(action) = on_update { + fk = fk.on_update(to_sea_fk_action(action)).to_owned(); + } + Ok(vec![BuiltQuery::CreateForeignKey(Box::new(fk))]) + } + } + TableConstraint::Check { name, expr } => { + // SQLite does not support ALTER TABLE ... ADD CONSTRAINT CHECK + if *backend == DatabaseBackend::Sqlite { + // Use temporary table approach for SQLite + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", + table + )))?; + + // Create new constraints with the added check constraint + let mut new_constraints = table_def.constraints.clone(); + new_constraints.push(constraint.clone()); + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table with new constraints + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &table_def.columns, + &new_constraints, + ); + + // Handle CHECK constraints (sea-query doesn't support them natively) + let check_clauses = extract_check_clauses(&new_constraints); + let create_query = + build_create_with_checks(backend, &create_temp_table, &check_clauses); + + // 2. Copy data (all columns) + let column_aliases: Vec = table_def + .columns + .iter() + .map(|c| Alias::new(&c.name)) + .collect(); + let mut select_query = Query::select(); + for col_alias in &column_aliases { + select_query = select_query.column(col_alias.clone()).to_owned(); + } + select_query = select_query.from(Alias::new(table)).to_owned(); + + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(column_aliases.clone()) + .select_from(select_query) + .unwrap() + .to_owned(); + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + let drop_query = BuiltQuery::DropTable(Box::new(drop_table)); + + // 4. Rename temporary table to original name + let rename_query = build_rename_table(&temp_table, table); + + // 5. Recreate indexes (if any) + 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(); + } + 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]; + queries.extend(index_queries); + Ok(queries) + } else { + let pg_sql = format!( + "ALTER TABLE \"{}\" ADD CONSTRAINT \"{}\" CHECK ({})", + table, name, expr + ); + let mysql_sql = format!( + "ALTER TABLE `{}` ADD CONSTRAINT `{}` CHECK ({})", + table, name, expr + ); + Ok(vec![BuiltQuery::Raw(RawSql::per_backend( + pg_sql.clone(), + mysql_sql, + pg_sql, + ))]) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sql::types::DatabaseBackend; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::{ + ColumnDef, ColumnType, ReferenceAction, SimpleColumnType, TableConstraint, TableDef, + }; + + #[rstest] + #[case::add_constraint_primary_key_postgres( + "add_constraint_primary_key_postgres", + DatabaseBackend::Postgres, + &["ALTER TABLE \"users\" ADD PRIMARY KEY (\"id\")"] + )] + #[case::add_constraint_primary_key_mysql( + "add_constraint_primary_key_mysql", + DatabaseBackend::MySql, + &["ALTER TABLE `users` ADD PRIMARY KEY (`id`)"] + )] + #[case::add_constraint_primary_key_sqlite( + "add_constraint_primary_key_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users_temp\""] + )] + #[case::add_constraint_unique_named_postgres( + "add_constraint_unique_named_postgres", + DatabaseBackend::Postgres, + &["CREATE UNIQUE INDEX \"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`)"] + )] + #[case::add_constraint_unique_named_sqlite( + "add_constraint_unique_named_sqlite", + DatabaseBackend::Sqlite, + &["CREATE UNIQUE INDEX \"uq_email\" ON \"users\" (\"email\")"] + )] + #[case::add_constraint_foreign_key_postgres( + "add_constraint_foreign_key_postgres", + DatabaseBackend::Postgres, + &["FOREIGN KEY (\"user_id\")", "REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] + )] + #[case::add_constraint_foreign_key_mysql( + "add_constraint_foreign_key_mysql", + DatabaseBackend::MySql, + &["FOREIGN KEY (`user_id`)", "REFERENCES `users` (`id`)", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] + )] + #[case::add_constraint_foreign_key_sqlite( + "add_constraint_foreign_key_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users_temp\""] + )] + #[case::add_constraint_check_named_postgres( + "add_constraint_check_named_postgres", + DatabaseBackend::Postgres, + &["ADD CONSTRAINT \"chk_age\" CHECK (age > 0)"] + )] + #[case::add_constraint_check_named_mysql( + "add_constraint_check_named_mysql", + DatabaseBackend::MySql, + &["ADD CONSTRAINT `chk_age` CHECK (age > 0)"] + )] + #[case::add_constraint_check_named_sqlite( + "add_constraint_check_named_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users_temp\""] + )] + fn test_add_constraint( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let constraint = if title.contains("primary_key") { + TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + } + } else if title.contains("unique") { + TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + } + } else if title.contains("foreign_key") { + TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: Some(ReferenceAction::Cascade), + on_update: Some(ReferenceAction::Restrict), + } + } else { + TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + } + }; + + // For SQLite, we need to provide current schema + let current_schema = vec![TableDef { + name: "users".into(), + columns: if title.contains("foreign_key") { + 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, + }, + ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ] + } else { + 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, + }, + ColumnDef { + name: if title.contains("check") { + "age".into() + } else { + "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![], + indexes: vec![], + }]; + + let result = build_add_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result[0].build(backend); + for exp in expected { + assert!( + sql.contains(exp), + "Expected SQL to contain '{}', got: {}", + exp, + sql + ); + } + + with_settings!({ snapshot_suffix => format!("add_constraint_{}", title) }, { + assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::>().join("\n")); + }); + } + + #[test] + fn test_add_constraint_primary_key_sqlite_table_not_found() { + let constraint = TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }; + let current_schema = vec![]; // Empty schema - table not found + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤t_schema, + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'users' not found in current schema")); + } + + #[test] + fn test_add_constraint_primary_key_sqlite_with_check_constraints() { + let constraint = TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }; + 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![TableConstraint::Check { + name: "chk_id".into(), + expr: "id > 0".into(), + }], + indexes: vec![], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤t_schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(DatabaseBackend::Sqlite)) + .collect::>() + .join("\n"); + // Should include CHECK constraint in CREATE TABLE + assert!(sql.contains("CONSTRAINT \"chk_id\" CHECK")); + } + + #[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, + }; + 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_id".into(), + columns: vec!["id".into()], + unique: false, + }], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤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 index + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_id")); + } + + #[test] + fn test_add_constraint_primary_key_sqlite_with_unique_index() { + use vespertide_core::IndexDef; + + let constraint = TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }; + 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_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤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")); + } + + #[test] + fn test_add_constraint_foreign_key_sqlite_table_not_found() { + let constraint = TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let current_schema = vec![]; // Empty schema - table not found + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "posts", + &constraint, + ¤t_schema, + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'posts' not found in current schema")); + } + + #[test] + fn test_add_constraint_foreign_key_sqlite_with_check_constraints() { + let constraint = TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let current_schema = vec![TableDef { + name: "posts".into(), + columns: vec![ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::Check { + name: "chk_user_id".into(), + expr: "user_id > 0".into(), + }], + indexes: vec![], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "posts", + &constraint, + ¤t_schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(DatabaseBackend::Sqlite)) + .collect::>() + .join("\n"); + // Should include CHECK constraint in CREATE TABLE + assert!(sql.contains("CONSTRAINT \"chk_user_id\" CHECK")); + } + + #[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()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let current_schema = vec![TableDef { + name: "posts".into(), + columns: vec![ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + indexes: vec![IndexDef { + name: "idx_user_id".into(), + columns: vec!["user_id".into()], + unique: false, + }], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "posts", + &constraint, + ¤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 index + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_user_id")); + } + + #[test] + fn test_add_constraint_foreign_key_sqlite_with_unique_index() { + use vespertide_core::IndexDef; + + let constraint = TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let current_schema = vec![TableDef { + name: "posts".into(), + columns: vec![ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + indexes: vec![IndexDef { + name: "idx_user_id".into(), + columns: vec!["user_id".into()], + unique: true, + }], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "posts", + &constraint, + ¤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_user_id")); + } + + #[test] + fn test_add_constraint_check_sqlite_table_not_found() { + let constraint = TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }; + let current_schema = vec![]; // Empty schema - table not found + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤t_schema, + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'users' not found in current schema")); + } + + #[test] + fn test_add_constraint_check_sqlite_without_existing_check() { + // Test when there are no existing CHECK constraints (line 376) + let constraint = TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }; + let current_schema = vec![TableDef { + name: "users".into(), + columns: vec![ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], // No existing CHECK constraints + indexes: vec![], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤t_schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(DatabaseBackend::Sqlite)) + .collect::>() + .join("\n"); + // Should create table with CHECK constraint + assert!(sql.contains("CREATE TABLE")); + assert!(sql.contains("CONSTRAINT \"chk_age\" CHECK")); + } + + #[test] + fn test_add_constraint_primary_key_sqlite_without_existing_check() { + // Test PrimaryKey addition when there are no existing CHECK constraints (line 84) + // This should hit the else branch: BuiltQuery::CreateTable(Box::new(create_temp_table)) + let constraint = TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }; + let current_schema = vec![TableDef { + name: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], // No existing CHECK constraints + indexes: vec![], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤t_schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(DatabaseBackend::Sqlite)) + .collect::>() + .join("\n"); + // Should create table without CHECK constraints (using BuiltQuery::CreateTable) + assert!(sql.contains("CREATE TABLE")); + assert!(sql.contains("PRIMARY KEY")); + } + + #[test] + fn test_add_constraint_foreign_key_sqlite_without_existing_check() { + // Test ForeignKey addition when there are no existing CHECK constraints (line 238) + // This should hit the else branch: BuiltQuery::CreateTable(Box::new(create_temp_table)) + let constraint = TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let current_schema = vec![TableDef { + name: "posts".into(), + columns: vec![ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], // No existing CHECK constraints + indexes: vec![], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "posts", + &constraint, + ¤t_schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(DatabaseBackend::Sqlite)) + .collect::>() + .join("\n"); + // Should create table without CHECK constraints (using BuiltQuery::CreateTable) + assert!(sql.contains("CREATE TABLE")); + assert!(sql.contains("FOREIGN KEY")); + } + + #[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(), + }; + let current_schema = vec![TableDef { + name: "users".into(), + columns: vec![ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + indexes: vec![IndexDef { + name: "idx_age".into(), + columns: vec!["age".into()], + unique: false, + }], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤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 index + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_age")); + } + + #[test] + fn test_add_constraint_check_sqlite_with_unique_index() { + use vespertide_core::IndexDef; + + let constraint = TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }; + let current_schema = vec![TableDef { + name: "users".into(), + columns: vec![ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + indexes: vec![IndexDef { + name: "idx_age".into(), + columns: vec!["age".into()], + unique: true, + }], + }]; + let result = build_add_constraint( + &DatabaseBackend::Sqlite, + "users", + &constraint, + ¤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_age")); + } + + #[test] + fn test_extract_check_clauses_with_mixed_constraints() { + // Test that extract_check_clauses filters out non-Check constraints + let constraints = vec![ + TableConstraint::Check { + name: "chk1".into(), + expr: "a > 0".into(), + }, + TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }, + TableConstraint::Check { + name: "chk2".into(), + expr: "b < 100".into(), + }, + TableConstraint::Unique { + name: Some("uq".into()), + columns: vec!["email".into()], + }, + ]; + let clauses = extract_check_clauses(&constraints); + assert_eq!(clauses.len(), 2); + assert!(clauses[0].contains("chk1")); + assert!(clauses[1].contains("chk2")); + } + + #[test] + fn test_extract_check_clauses_with_no_check_constraints() { + let constraints = vec![ + TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }, + TableConstraint::Unique { + name: None, + columns: vec!["email".into()], + }, + ]; + let clauses = extract_check_clauses(&constraints); + assert!(clauses.is_empty()); + } + + #[test] + fn test_build_create_with_checks_empty_clauses() { + use super::build_create_table_for_backend; + + let create_stmt = build_create_table_for_backend( + &DatabaseBackend::Sqlite, + "test_table", + &[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, + }], + &[], + ); + + // Empty check_clauses should return CreateTable variant + let result = build_create_with_checks(&DatabaseBackend::Sqlite, &create_stmt, &[]); + let sql = result.build(DatabaseBackend::Sqlite); + assert!(sql.contains("CREATE TABLE")); + } + + #[test] + fn test_build_create_with_checks_with_clauses() { + use super::build_create_table_for_backend; + + let create_stmt = build_create_table_for_backend( + &DatabaseBackend::Sqlite, + "test_table", + &[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, + }], + &[], + ); + + // Non-empty check_clauses should return Raw variant with embedded checks + let check_clauses = vec!["CONSTRAINT \"chk1\" CHECK (id > 0)".to_string()]; + let result = + build_create_with_checks(&DatabaseBackend::Sqlite, &create_stmt, &check_clauses); + let sql = result.build(DatabaseBackend::Sqlite); + assert!(sql.contains("CREATE TABLE")); + assert!(sql.contains("CONSTRAINT \"chk1\" CHECK (id > 0)")); + } +} diff --git a/crates/vespertide-query/src/sql/add_index.rs b/crates/vespertide-query/src/sql/add_index.rs new file mode 100644 index 0000000..ebb6c9e --- /dev/null +++ b/crates/vespertide-query/src/sql/add_index.rs @@ -0,0 +1,88 @@ +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 new file mode 100644 index 0000000..e5bb4aa --- /dev/null +++ b/crates/vespertide-query/src/sql/create_table.rs @@ -0,0 +1,356 @@ +use sea_query::{Alias, ForeignKey, Index, Table, TableCreateStatement}; + +use vespertide_core::{ColumnDef, TableConstraint}; + +use super::helpers::{build_sea_column_def, to_sea_fk_action}; +use super::types::{BuiltQuery, DatabaseBackend}; +use crate::error::QueryError; + +pub(crate) fn build_create_table_for_backend( + backend: &DatabaseBackend, + table: &str, + columns: &[ColumnDef], + constraints: &[TableConstraint], +) -> TableCreateStatement { + let mut stmt = Table::create().table(Alias::new(table)).to_owned(); + + let has_table_primary_key = constraints + .iter() + .any(|c| matches!(c, TableConstraint::PrimaryKey { .. })); + + // Add columns + for column in columns { + let mut col = build_sea_column_def(backend, column); + + // Check for inline primary key + if column.primary_key.is_some() && !has_table_primary_key { + col.primary_key(); + } + + // Check for inline unique constraint + if column.unique.is_some() { + col.unique_key(); + } + + stmt = stmt.col(col).to_owned(); + } + + // Add table-level constraints + for constraint in constraints { + match constraint { + TableConstraint::PrimaryKey { + columns: pk_cols, + auto_increment: _, + } => { + // Build primary key index + let mut pk_idx = Index::create(); + for c in pk_cols { + pk_idx = pk_idx.col(Alias::new(c)).to_owned(); + } + stmt = stmt.primary_key(&mut pk_idx).to_owned(); + } + TableConstraint::Unique { + name, + columns: unique_cols, + } => { + // 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(); + } + for col in unique_cols { + idx = idx.col(Alias::new(col)).to_owned(); + } + stmt = stmt.index(&mut idx).to_owned(); + } + // For Postgres and SQLite, unique constraints will be handled in build_create_table + // as separate CREATE UNIQUE INDEX statements + } + TableConstraint::ForeignKey { + name, + columns: fk_cols, + ref_table, + ref_columns, + on_delete, + on_update, + } => { + let mut fk = ForeignKey::create(); + if let Some(n) = name { + fk = fk.name(n).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(); + } + fk = fk.to_tbl(Alias::new(ref_table)).to_owned(); + for col in ref_columns { + fk = fk.to_col(Alias::new(col)).to_owned(); + } + if let Some(action) = on_delete { + fk = fk.on_delete(to_sea_fk_action(action)).to_owned(); + } + if let Some(action) = on_update { + fk = fk.on_update(to_sea_fk_action(action)).to_owned(); + } + stmt = stmt.foreign_key(&mut fk).to_owned(); + } + TableConstraint::Check { name, expr } => { + // sea-query doesn't have direct CHECK constraint support in TableCreateStatement + // This would need to be handled as raw SQL or post-creation ALTER + let _ = (name, expr); + } + } + } + + stmt +} + +pub fn build_create_table( + backend: &DatabaseBackend, + table: &str, + columns: &[ColumnDef], + constraints: &[TableConstraint], +) -> Result, QueryError> { + let mut queries = Vec::new(); + + // Separate unique constraints for Postgres and SQLite (they need separate CREATE INDEX statements) + // For MySQL, unique constraints are added directly in CREATE TABLE via build_create_table_for_backend + let (table_constraints, unique_constraints): (Vec<&TableConstraint>, Vec<&TableConstraint>) = + constraints + .iter() + .partition(|c| !matches!(c, TableConstraint::Unique { .. })); + + // Build CREATE TABLE + // For MySQL, include unique constraints in CREATE TABLE + // For Postgres and SQLite, exclude them (will be added as separate CREATE INDEX statements) + let create_table_stmt = if matches!(backend, DatabaseBackend::MySql) { + build_create_table_for_backend(backend, table, columns, constraints) + } else { + // Convert references to owned values for build_create_table_for_backend + let table_constraints_owned: Vec = + table_constraints.iter().cloned().cloned().collect(); + build_create_table_for_backend(backend, table, columns, &table_constraints_owned) + }; + queries.push(BuiltQuery::CreateTable(Box::new(create_table_stmt))); + + // For Postgres and SQLite, add unique constraints as separate CREATE UNIQUE INDEX statements + if matches!(backend, DatabaseBackend::Postgres | DatabaseBackend::Sqlite) { + for constraint in unique_constraints { + if let TableConstraint::Unique { + name, + 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(); + } + for col in unique_cols { + idx = idx.col(Alias::new(col)).to_owned(); + } + queries.push(BuiltQuery::CreateIndex(Box::new(idx))); + } + } + } + + Ok(queries) +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::{ColumnType, SimpleColumnType}; + + 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, + } + } + + #[rstest] + #[case::create_table_postgres( + "create_table_postgres", + DatabaseBackend::Postgres, + &["CREATE TABLE \"users\" ( \"id\" integer )"] + )] + #[case::create_table_mysql( + "create_table_mysql", + DatabaseBackend::MySql, + &["CREATE TABLE `users` ( `id` int )"] + )] + #[case::create_table_sqlite( + "create_table_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users\" ( \"id\" integer )"] + )] + fn test_create_table( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let result = build_create_table( + &backend, + "users", + &[col("id", ColumnType::Simple(SimpleColumnType::Integer))], + &[], + ) + .unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + for exp in expected { + assert!( + sql.contains(exp), + "Expected SQL to contain '{}', got: {}", + exp, + sql + ); + } + + with_settings!({ snapshot_suffix => format!("create_table_{}", title) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::inline_unique_postgres(DatabaseBackend::Postgres)] + #[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) + use vespertide_core::schema::str_or_bool::StrOrBoolOrArray; + + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text)); + email_col.unique = Some(StrOrBoolOrArray::Bool(true)); + + let result = build_create_table( + &backend, + "users", + &[ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + email_col, + ], + &[], + ) + .unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + assert!(sql.contains("UNIQUE")); + with_settings!({ snapshot_suffix => format!("create_table_with_inline_unique_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::table_level_unique_postgres(DatabaseBackend::Postgres)] + #[case::table_level_unique_mysql(DatabaseBackend::MySql)] + #[case::table_level_unique_sqlite(DatabaseBackend::Sqlite)] + fn test_create_table_with_table_level_unique(#[case] backend: DatabaseBackend) { + // Test table-level unique constraint (lines 53-54, 56-58, 60-61) + let result = build_create_table( + &backend, + "users", + &[ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("email", ColumnType::Simple(SimpleColumnType::Text)), + ], + &[TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }], + ) + .unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + assert!(sql.contains("CREATE TABLE")); + // Verify unique constraint is present + match backend { + DatabaseBackend::MySql => { + assert!( + sql.contains("UNIQUE"), + "MySQL should have UNIQUE in CREATE TABLE: {}", + sql + ); + } + _ => { + // For Postgres and SQLite, unique constraint should be in a separate CREATE UNIQUE INDEX statement + assert!( + sql.contains("CREATE UNIQUE INDEX"), + "Postgres/SQLite should have CREATE UNIQUE INDEX: {}", + sql + ); + } + } + with_settings!({ snapshot_suffix => format!("create_table_with_table_level_unique_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::table_level_unique_no_name_postgres(DatabaseBackend::Postgres)] + #[case::table_level_unique_no_name_mysql(DatabaseBackend::MySql)] + #[case::table_level_unique_no_name_sqlite(DatabaseBackend::Sqlite)] + fn test_create_table_with_table_level_unique_no_name(#[case] backend: DatabaseBackend) { + // Test table-level unique constraint without name (lines 53-54, 56-58, 60-61) + let result = build_create_table( + &backend, + "users", + &[ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("email", ColumnType::Simple(SimpleColumnType::Text)), + ], + &[TableConstraint::Unique { + name: None, + columns: vec!["email".into()], + }], + ) + .unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + assert!(sql.contains("CREATE TABLE")); + // Verify unique constraint is present + match backend { + DatabaseBackend::MySql => { + assert!( + sql.contains("UNIQUE"), + "MySQL should have UNIQUE in CREATE TABLE: {}", + sql + ); + } + _ => { + // For Postgres and SQLite, unique constraint should be in a separate CREATE UNIQUE INDEX statement + assert!( + sql.contains("CREATE UNIQUE INDEX"), + "Postgres/SQLite should have CREATE UNIQUE INDEX: {}", + sql + ); + } + } + with_settings!({ snapshot_suffix => format!("create_table_with_table_level_unique_no_name_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/delete_column.rs b/crates/vespertide-query/src/sql/delete_column.rs new file mode 100644 index 0000000..1492778 --- /dev/null +++ b/crates/vespertide-query/src/sql/delete_column.rs @@ -0,0 +1,56 @@ +use sea_query::{Alias, Table}; + +use super::types::BuiltQuery; + +pub fn build_delete_column(table: &str, column: &str) -> BuiltQuery { + let stmt = Table::alter() + .table(Alias::new(table)) + .drop_column(Alias::new(column)) + .to_owned(); + BuiltQuery::AlterTable(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::delete_column_postgres( + "delete_column_postgres", + DatabaseBackend::Postgres, + &["ALTER TABLE \"users\" DROP COLUMN \"email\""] + )] + #[case::delete_column_mysql( + "delete_column_mysql", + DatabaseBackend::MySql, + &["ALTER TABLE `users` DROP COLUMN `email`"] + )] + #[case::delete_column_sqlite( + "delete_column_sqlite", + DatabaseBackend::Sqlite, + &["ALTER TABLE \"users\" DROP COLUMN \"email\""] + )] + fn test_delete_column( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let result = build_delete_column("users", "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!("delete_column_{}", title) }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/delete_table.rs b/crates/vespertide-query/src/sql/delete_table.rs new file mode 100644 index 0000000..158ffb8 --- /dev/null +++ b/crates/vespertide-query/src/sql/delete_table.rs @@ -0,0 +1,53 @@ +use sea_query::{Alias, Table}; + +use super::types::BuiltQuery; + +pub fn build_delete_table(table: &str) -> BuiltQuery { + let stmt = Table::drop().table(Alias::new(table)).to_owned(); + BuiltQuery::DropTable(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::delete_table_postgres( + "delete_table_postgres", + DatabaseBackend::Postgres, + &["DROP TABLE \"users\""] + )] + #[case::delete_table_mysql( + "delete_table_mysql", + DatabaseBackend::MySql, + &["DROP TABLE `users`"] + )] + #[case::delete_table_sqlite( + "delete_table_sqlite", + DatabaseBackend::Sqlite, + &["DROP TABLE \"users\""] + )] + fn test_delete_table( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let result = build_delete_table("users"); + 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!("delete_table_{}", title) }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs new file mode 100644 index 0000000..07c0c96 --- /dev/null +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -0,0 +1,317 @@ +use sea_query::{ + Alias, ColumnDef as SeaColumnDef, ForeignKeyAction, MysqlQueryBuilder, PostgresQueryBuilder, + QueryStatementWriter, SchemaStatementBuilder, SimpleExpr, SqliteQueryBuilder, +}; + +use vespertide_core::{ + ColumnDef, ColumnType, ComplexColumnType, ReferenceAction, SimpleColumnType, +}; + +use super::types::DatabaseBackend; + +/// Helper function to convert a schema statement to SQL for a specific backend +pub fn build_schema_statement( + stmt: &T, + backend: DatabaseBackend, +) -> String { + match backend { + DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), + DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), + DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), + } +} + +/// Helper function to convert a query statement (INSERT, SELECT, etc.) to SQL for a specific backend +pub fn build_query_statement( + stmt: &T, + backend: DatabaseBackend, +) -> String { + match backend { + DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), + DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), + DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), + } +} + +/// Apply vespertide ColumnType to sea_query ColumnDef +pub fn apply_column_type(col: &mut SeaColumnDef, ty: &ColumnType) { + match ty { + ColumnType::Simple(simple) => match simple { + SimpleColumnType::SmallInt => { + col.small_integer(); + } + SimpleColumnType::Integer => { + col.integer(); + } + SimpleColumnType::BigInt => { + col.big_integer(); + } + SimpleColumnType::Real => { + col.float(); + } + SimpleColumnType::DoublePrecision => { + col.double(); + } + SimpleColumnType::Text => { + col.text(); + } + SimpleColumnType::Boolean => { + col.boolean(); + } + SimpleColumnType::Date => { + col.date(); + } + SimpleColumnType::Time => { + col.time(); + } + SimpleColumnType::Timestamp => { + col.timestamp(); + } + SimpleColumnType::Timestamptz => { + col.timestamp_with_time_zone(); + } + SimpleColumnType::Interval => { + col.interval(None, None); + } + SimpleColumnType::Bytea => { + col.binary(); + } + SimpleColumnType::Uuid => { + col.uuid(); + } + SimpleColumnType::Json => { + col.json(); + } + SimpleColumnType::Jsonb => { + col.json_binary(); + } + SimpleColumnType::Inet => { + col.custom(Alias::new("INET")); + } + SimpleColumnType::Cidr => { + col.custom(Alias::new("CIDR")); + } + SimpleColumnType::Macaddr => { + col.custom(Alias::new("MACADDR")); + } + SimpleColumnType::Xml => { + col.custom(Alias::new("XML")); + } + }, + ColumnType::Complex(complex) => match complex { + ComplexColumnType::Varchar { length } => { + col.string_len(*length); + } + ComplexColumnType::Numeric { precision, scale } => { + col.decimal_len(*precision, *scale); + } + ComplexColumnType::Char { length } => { + col.char_len(*length); + } + ComplexColumnType::Custom { custom_type } => { + col.custom(Alias::new(custom_type)); + } + }, + } +} + +/// Convert vespertide ReferenceAction to sea_query ForeignKeyAction +pub fn to_sea_fk_action(action: &ReferenceAction) -> ForeignKeyAction { + match action { + ReferenceAction::Cascade => ForeignKeyAction::Cascade, + ReferenceAction::Restrict => ForeignKeyAction::Restrict, + ReferenceAction::SetNull => ForeignKeyAction::SetNull, + ReferenceAction::SetDefault => ForeignKeyAction::SetDefault, + ReferenceAction::NoAction => ForeignKeyAction::NoAction, + } +} + +/// Convert vespertide ReferenceAction to SQL string +pub fn reference_action_sql(action: &ReferenceAction) -> &'static str { + match action { + ReferenceAction::Cascade => "CASCADE", + ReferenceAction::Restrict => "RESTRICT", + ReferenceAction::SetNull => "SET NULL", + ReferenceAction::SetDefault => "SET DEFAULT", + ReferenceAction::NoAction => "NO ACTION", + } +} + +/// Convert a default value string to the appropriate backend-specific expression +pub fn convert_default_for_backend(default: &str, backend: &DatabaseBackend) -> String { + match default { + "gen_random_uuid()" => match backend { + DatabaseBackend::Postgres => "gen_random_uuid()".to_string(), + DatabaseBackend::MySql => "(UUID())".to_string(), + DatabaseBackend::Sqlite => "(lower(hex(randomblob(16))))".to_string(), + }, + "current_timestamp()" | "now()" | "CURRENT_TIMESTAMP" => match backend { + DatabaseBackend::Postgres => "CURRENT_TIMESTAMP".to_string(), + DatabaseBackend::MySql => "CURRENT_TIMESTAMP".to_string(), + DatabaseBackend::Sqlite => "CURRENT_TIMESTAMP".to_string(), + }, + other => other.to_string(), + } +} + +/// Build sea_query ColumnDef from vespertide ColumnDef for a specific backend +pub fn build_sea_column_def(backend: &DatabaseBackend, column: &ColumnDef) -> SeaColumnDef { + let mut col = SeaColumnDef::new(Alias::new(&column.name)); + apply_column_type(&mut col, &column.r#type); + + if !column.nullable { + col.not_null(); + } + + if let Some(default) = &column.default { + let converted = convert_default_for_backend(default, backend); + col.default(Into::::into(sea_query::Expr::cust(converted))); + } + + col +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + use sea_query::{Alias, ColumnDef as SeaColumnDef, ForeignKeyAction}; + + #[rstest] + #[case(ColumnType::Simple(SimpleColumnType::Integer))] + #[case(ColumnType::Simple(SimpleColumnType::BigInt))] + #[case(ColumnType::Simple(SimpleColumnType::Text))] + #[case(ColumnType::Simple(SimpleColumnType::Boolean))] + #[case(ColumnType::Simple(SimpleColumnType::Timestamp))] + #[case(ColumnType::Simple(SimpleColumnType::Uuid))] + #[case(ColumnType::Complex(ComplexColumnType::Varchar { length: 255 }))] + #[case(ColumnType::Complex(ComplexColumnType::Numeric { precision: 10, scale: 2 }))] + fn test_column_type_conversion(#[case] ty: ColumnType) { + // Just ensure no panic - test by creating a column with this type + let mut col = SeaColumnDef::new(Alias::new("test")); + apply_column_type(&mut col, &ty); + } + + #[rstest] + #[case(SimpleColumnType::SmallInt)] + #[case(SimpleColumnType::Integer)] + #[case(SimpleColumnType::BigInt)] + #[case(SimpleColumnType::Real)] + #[case(SimpleColumnType::DoublePrecision)] + #[case(SimpleColumnType::Text)] + #[case(SimpleColumnType::Boolean)] + #[case(SimpleColumnType::Date)] + #[case(SimpleColumnType::Time)] + #[case(SimpleColumnType::Timestamp)] + #[case(SimpleColumnType::Timestamptz)] + #[case(SimpleColumnType::Interval)] + #[case(SimpleColumnType::Bytea)] + #[case(SimpleColumnType::Uuid)] + #[case(SimpleColumnType::Json)] + #[case(SimpleColumnType::Jsonb)] + #[case(SimpleColumnType::Inet)] + #[case(SimpleColumnType::Cidr)] + #[case(SimpleColumnType::Macaddr)] + #[case(SimpleColumnType::Xml)] + fn test_all_simple_types_cover_branches(#[case] ty: SimpleColumnType) { + let mut col = SeaColumnDef::new(Alias::new("t")); + apply_column_type(&mut col, &ColumnType::Simple(ty)); + } + + #[rstest] + #[case(ComplexColumnType::Varchar { length: 42 })] + #[case(ComplexColumnType::Numeric { precision: 8, scale: 3 })] + #[case(ComplexColumnType::Char { length: 3 })] + #[case(ComplexColumnType::Custom { custom_type: "GEOGRAPHY".into() })] + fn test_all_complex_types_cover_branches(#[case] ty: ComplexColumnType) { + let mut col = SeaColumnDef::new(Alias::new("t")); + apply_column_type(&mut col, &ColumnType::Complex(ty)); + } + + #[rstest] + #[case::cascade(ReferenceAction::Cascade, ForeignKeyAction::Cascade)] + #[case::restrict(ReferenceAction::Restrict, ForeignKeyAction::Restrict)] + #[case::set_null(ReferenceAction::SetNull, ForeignKeyAction::SetNull)] + #[case::set_default(ReferenceAction::SetDefault, ForeignKeyAction::SetDefault)] + #[case::no_action(ReferenceAction::NoAction, ForeignKeyAction::NoAction)] + fn test_reference_action_conversion( + #[case] action: ReferenceAction, + #[case] expected: ForeignKeyAction, + ) { + // Just ensure the function doesn't panic and returns valid ForeignKeyAction + let result = to_sea_fk_action(&action); + assert!( + matches!(result, _expected), + "Expected {:?}, got {:?}", + expected, + result + ); + } + + #[rstest] + #[case(ReferenceAction::Cascade, "CASCADE")] + #[case(ReferenceAction::Restrict, "RESTRICT")] + #[case(ReferenceAction::SetNull, "SET NULL")] + #[case(ReferenceAction::SetDefault, "SET DEFAULT")] + #[case(ReferenceAction::NoAction, "NO ACTION")] + fn test_reference_action_sql_all_variants( + #[case] action: ReferenceAction, + #[case] expected: &str, + ) { + assert_eq!(reference_action_sql(&action), expected); + } + + #[rstest] + #[case::gen_random_uuid_postgres( + "gen_random_uuid()", + DatabaseBackend::Postgres, + "gen_random_uuid()" + )] + #[case::gen_random_uuid_mysql("gen_random_uuid()", DatabaseBackend::MySql, "(UUID())")] + #[case::gen_random_uuid_sqlite( + "gen_random_uuid()", + DatabaseBackend::Sqlite, + "(lower(hex(randomblob(16))))" + )] + #[case::current_timestamp_postgres( + "current_timestamp()", + DatabaseBackend::Postgres, + "CURRENT_TIMESTAMP" + )] + #[case::current_timestamp_mysql( + "current_timestamp()", + DatabaseBackend::MySql, + "CURRENT_TIMESTAMP" + )] + #[case::current_timestamp_sqlite( + "current_timestamp()", + DatabaseBackend::Sqlite, + "CURRENT_TIMESTAMP" + )] + #[case::now_postgres("now()", DatabaseBackend::Postgres, "CURRENT_TIMESTAMP")] + #[case::now_mysql("now()", DatabaseBackend::MySql, "CURRENT_TIMESTAMP")] + #[case::now_sqlite("now()", DatabaseBackend::Sqlite, "CURRENT_TIMESTAMP")] + #[case::current_timestamp_upper_postgres( + "CURRENT_TIMESTAMP", + DatabaseBackend::Postgres, + "CURRENT_TIMESTAMP" + )] + #[case::current_timestamp_upper_mysql( + "CURRENT_TIMESTAMP", + DatabaseBackend::MySql, + "CURRENT_TIMESTAMP" + )] + #[case::current_timestamp_upper_sqlite( + "CURRENT_TIMESTAMP", + DatabaseBackend::Sqlite, + "CURRENT_TIMESTAMP" + )] + fn test_convert_default_for_backend( + #[case] default: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &str, + ) { + let result = convert_default_for_backend(default, &backend); + assert_eq!(result, expected); + } +} diff --git a/crates/vespertide-query/src/sql/mod.rs b/crates/vespertide-query/src/sql/mod.rs new file mode 100644 index 0000000..9d30fbe --- /dev/null +++ b/crates/vespertide-query/src/sql/mod.rs @@ -0,0 +1,690 @@ +pub mod add_column; +pub mod add_constraint; +pub mod add_index; +pub mod create_table; +pub mod delete_column; +pub mod delete_table; +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; + +pub use helpers::*; +pub use types::{BuiltQuery, DatabaseBackend, RawSql}; + +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, + 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, +}; + +pub fn build_action_queries( + backend: &DatabaseBackend, + action: &MigrationAction, + current_schema: &[TableDef], +) -> Result, QueryError> { + match action { + MigrationAction::CreateTable { + table, + columns, + constraints, + } => build_create_table(backend, table, columns, constraints), + + MigrationAction::DeleteTable { table } => Ok(vec![build_delete_table(table)]), + + MigrationAction::AddColumn { + table, + column, + fill_with, + } => build_add_column(backend, table, column, fill_with.as_deref(), current_schema), + + MigrationAction::RenameColumn { table, from, to } => { + Ok(vec![build_rename_column(table, from, to)]) + } + + MigrationAction::DeleteColumn { table, column } => { + Ok(vec![build_delete_column(table, column)]) + } + + MigrationAction::ModifyColumnType { + table, + column, + 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())]), + + MigrationAction::AddConstraint { table, constraint } => { + build_add_constraint(backend, table, constraint, current_schema) + } + + MigrationAction::RemoveConstraint { table, constraint } => { + build_remove_constraint(backend, table, constraint, current_schema) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::schema::primary_key::PrimaryKeySyntax; + use vespertide_core::{ + ColumnDef, ColumnType, MigrationAction, ReferenceAction, 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, + } + } + + #[test] + fn test_backend_specific_quoting() { + let action = MigrationAction::CreateTable { + table: "users".into(), + columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + constraints: vec![], + }; + let result = build_action_queries(&DatabaseBackend::Postgres, &action, &[]).unwrap(); + + // PostgreSQL uses double quotes + let pg_sql = result[0].build(DatabaseBackend::Postgres); + assert!(pg_sql.contains("\"users\"")); + + // MySQL uses backticks + let mysql_sql = result[0].build(DatabaseBackend::MySql); + assert!(mysql_sql.contains("`users`")); + + // SQLite uses double quotes + let sqlite_sql = result[0].build(DatabaseBackend::Sqlite); + assert!(sqlite_sql.contains("\"users\"")); + } + + #[rstest] + #[case::create_table_with_default_postgres( + "create_table_with_default_postgres", + MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "status".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: Some("'active'".into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }, + DatabaseBackend::Postgres, + &["DEFAULT", "'active'"] + )] + #[case::create_table_with_default_mysql( + "create_table_with_default_mysql", + MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "status".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: Some("'active'".into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }, + DatabaseBackend::Postgres, + &["DEFAULT", "'active'"] + )] + #[case::create_table_with_default_sqlite( + "create_table_with_default_sqlite", + MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "status".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: Some("'active'".into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }, + DatabaseBackend::Postgres, + &["DEFAULT", "'active'"] + )] + #[case::create_table_with_inline_primary_key_postgres( + "create_table_with_inline_primary_key_postgres", + 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![], + }, + DatabaseBackend::Postgres, + &["PRIMARY KEY"] + )] + #[case::create_table_with_inline_primary_key_mysql( + "create_table_with_inline_primary_key_mysql", + 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![], + }, + DatabaseBackend::Postgres, + &["PRIMARY KEY"] + )] + #[case::create_table_with_inline_primary_key_sqlite( + "create_table_with_inline_primary_key_sqlite", + 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![], + }, + DatabaseBackend::Postgres, + &["PRIMARY KEY"] + )] + #[case::create_table_with_fk_postgres( + "create_table_with_fk_postgres", + MigrationAction::CreateTable { + table: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + constraints: vec![TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: Some(ReferenceAction::Cascade), + on_update: Some(ReferenceAction::Restrict), + }], + }, + DatabaseBackend::Postgres, + &["REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] + )] + #[case::create_table_with_fk_mysql( + "create_table_with_fk_mysql", + MigrationAction::CreateTable { + table: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + constraints: vec![TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: Some(ReferenceAction::Cascade), + on_update: Some(ReferenceAction::Restrict), + }], + }, + DatabaseBackend::Postgres, + &["REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] + )] + #[case::create_table_with_fk_sqlite( + "create_table_with_fk_sqlite", + MigrationAction::CreateTable { + table: "posts".into(), + columns: vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("user_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + constraints: vec![TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: Some(ReferenceAction::Cascade), + on_update: Some(ReferenceAction::Restrict), + }], + }, + DatabaseBackend::Postgres, + &["REFERENCES \"users\" (\"id\")", "ON DELETE CASCADE", "ON UPDATE RESTRICT"] + )] + fn test_build_migration_action( + #[case] title: &str, + #[case] action: MigrationAction, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let result = build_action_queries(&backend, &action, &[]).unwrap(); + let sql = result[0].build(backend); + for exp in expected { + assert!( + sql.contains(exp), + "Expected SQL to contain '{}', got: {}", + exp, + sql + ); + } + + with_settings!({ snapshot_suffix => format!("build_migration_action_{}", title) }, { + assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::>().join("\n")); + }); + } + + #[rstest] + #[case::rename_column_postgres(DatabaseBackend::Postgres)] + #[case::rename_column_mysql(DatabaseBackend::MySql)] + #[case::rename_column_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_rename_column( + #[case] backend: DatabaseBackend, + ) { + // Test MigrationAction::RenameColumn (lines 51-52) + let action = MigrationAction::RenameColumn { + table: "users".into(), + from: "old_name".into(), + to: "new_name".into(), + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + assert_eq!(result.len(), 1); + let sql = result[0].build(backend); + assert!(sql.contains("RENAME")); + assert!(sql.contains("old_name")); + assert!(sql.contains("new_name")); + + with_settings!({ snapshot_suffix => format!("rename_column_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::delete_column_postgres(DatabaseBackend::Postgres)] + #[case::delete_column_mysql(DatabaseBackend::MySql)] + #[case::delete_column_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_delete_column( + #[case] backend: DatabaseBackend, + ) { + // Test MigrationAction::DeleteColumn (lines 55-56) + let action = MigrationAction::DeleteColumn { + table: "users".into(), + column: "email".into(), + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + assert_eq!(result.len(), 1); + let sql = result[0].build(backend); + assert!(sql.contains("DROP COLUMN")); + assert!(sql.contains("email")); + + with_settings!({ snapshot_suffix => format!("delete_column_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::modify_column_type_postgres(DatabaseBackend::Postgres)] + #[case::modify_column_type_mysql(DatabaseBackend::MySql)] + #[case::modify_column_type_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_modify_column_type( + #[case] backend: DatabaseBackend, + ) { + // Test MigrationAction::ModifyColumnType (lines 60-63) + let action = MigrationAction::ModifyColumnType { + table: "users".into(), + column: "age".into(), + new_type: ColumnType::Simple(SimpleColumnType::BigInt), + }; + let current_schema = vec![TableDef { + name: "users".into(), + columns: vec![ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + indexes: vec![], + }]; + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + assert!(!result.is_empty()); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + assert!(sql.contains("ALTER TABLE")); + + with_settings!({ snapshot_suffix => format!("modify_column_type_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[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 { + table: "users".into(), + name: "idx_email".into(), + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + assert_eq!(result.len(), 1); + let sql = result[0].build(backend); + assert!(sql.contains("DROP INDEX")); + assert!(sql.contains("idx_email")); + + with_settings!({ snapshot_suffix => format!("remove_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::rename_table_postgres(DatabaseBackend::Postgres)] + #[case::rename_table_mysql(DatabaseBackend::MySql)] + #[case::rename_table_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_rename_table( + #[case] backend: DatabaseBackend, + ) { + // Test MigrationAction::RenameTable (line 69) + let action = MigrationAction::RenameTable { + from: "old_table".into(), + to: "new_table".into(), + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + assert_eq!(result.len(), 1); + let sql = result[0].build(backend); + assert!(sql.contains("RENAME")); + assert!(sql.contains("old_table")); + assert!(sql.contains("new_table")); + + with_settings!({ snapshot_suffix => format!("rename_table_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::add_constraint_postgres(DatabaseBackend::Postgres)] + #[case::add_constraint_mysql(DatabaseBackend::MySql)] + #[case::add_constraint_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_add_constraint( + #[case] backend: DatabaseBackend, + ) { + // Test MigrationAction::AddConstraint (lines 73-74) + let action = MigrationAction::AddConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }, + }; + 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, + }, + 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![], + indexes: vec![], + }]; + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + assert!(!result.is_empty()); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + assert!(sql.contains("UNIQUE") || sql.contains("uq_email")); + + with_settings!({ snapshot_suffix => format!("add_constraint_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_constraint_postgres(DatabaseBackend::Postgres)] + #[case::remove_constraint_mysql(DatabaseBackend::MySql)] + #[case::remove_constraint_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_remove_constraint( + #[case] backend: DatabaseBackend, + ) { + // Test MigrationAction::RemoveConstraint (lines 77-78) + let action = MigrationAction::RemoveConstraint { + table: "users".into(), + constraint: TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }, + }; + 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, + }, + 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![TableConstraint::Unique { + 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()); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + assert!(sql.contains("DROP") || sql.contains("CONSTRAINT")); + + with_settings!({ snapshot_suffix => format!("remove_constraint_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::add_column_postgres(DatabaseBackend::Postgres)] + #[case::add_column_mysql(DatabaseBackend::MySql)] + #[case::add_column_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_add_column( + #[case] backend: DatabaseBackend, + ) { + // Test MigrationAction::AddColumn (lines 46-49) + let action = MigrationAction::AddColumn { + table: "users".into(), + column: 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, + }, + fill_with: 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![], + }]; + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + assert!(!result.is_empty()); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + assert!(sql.contains("ALTER TABLE")); + assert!(sql.contains("ADD COLUMN") || sql.contains("ADD")); + + with_settings!({ snapshot_suffix => format!("add_column_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[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 { + table: "users".into(), + index: vespertide_core::IndexDef { + name: "idx_email".into(), + columns: vec!["email".into()], + unique: false, + }, + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + assert_eq!(result.len(), 1); + let sql = result[0].build(backend); + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_email")); + + with_settings!({ snapshot_suffix => format!("add_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::raw_sql_postgres(DatabaseBackend::Postgres)] + #[case::raw_sql_mysql(DatabaseBackend::MySql)] + #[case::raw_sql_sqlite(DatabaseBackend::Sqlite)] + fn test_build_action_queries_raw_sql( + #[case] backend: DatabaseBackend, + ) { + // Test MigrationAction::RawSql (line 71) + let action = MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }; + let result = build_action_queries(&backend, &action, &[]).unwrap(); + assert_eq!(result.len(), 1); + let sql = result[0].build(backend); + assert_eq!(sql, "SELECT 1;"); + + with_settings!({ snapshot_suffix => format!("raw_sql_{:?}", 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 new file mode 100644 index 0000000..c291b6a --- /dev/null +++ b/crates/vespertide-query/src/sql/modify_column_type.rs @@ -0,0 +1,409 @@ +use sea_query::{Alias, ColumnDef as SeaColumnDef, Query, Table}; + +use vespertide_core::{ColumnType, TableDef}; + +use super::create_table::build_create_table_for_backend; +use super::helpers::apply_column_type; +use super::rename_table::build_rename_table; +use super::types::{BuiltQuery, DatabaseBackend}; +use crate::error::QueryError; + +pub fn build_modify_column_type( + backend: &DatabaseBackend, + table: &str, + column: &str, + new_type: &ColumnType, + current_schema: &[TableDef], +) -> Result, QueryError> { + // SQLite does not support direct column type modification, so use temporary table approach + if *backend == DatabaseBackend::Sqlite { + // Current schema information is required + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to modify column types.", + table + )))?; + + // Create new column definitions with the modified column + let mut new_columns = table_def.columns.clone(); + let col_index = new_columns + .iter() + .position(|c| c.name == column) + .ok_or_else(|| { + QueryError::Other(format!( + "Column '{}' not found in table '{}'", + column, table + )) + })?; + + new_columns[col_index].r#type = new_type.clone(); + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table with new column types + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &new_columns, + &table_def.constraints, + ); + let create_query = BuiltQuery::CreateTable(Box::new(create_temp_table)); + + // 2. Copy data (all columns) - Use INSERT INTO ... SELECT + let column_aliases: Vec = new_columns.iter().map(|c| Alias::new(&c.name)).collect(); + + // Build SELECT query + let mut select_query = Query::select(); + for col_alias in &column_aliases { + select_query = select_query.column(col_alias.clone()).to_owned(); + } + select_query = select_query.from(Alias::new(table)).to_owned(); + + // Build INSERT query + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(column_aliases.clone()) + .select_from(select_query) + .unwrap() + .to_owned(); + + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + let drop_query = BuiltQuery::DropTable(Box::new(drop_table)); + + // 4. Rename temporary table to original name + let rename_query = build_rename_table(&temp_table, table); + + // 5. Recreate indexes (if any) + 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(); + } + 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]; + queries.extend(index_queries); + + Ok(queries) + } else { + // PostgreSQL, MySQL, etc. can use ALTER TABLE directly + let mut col = SeaColumnDef::new(Alias::new(column)); + apply_column_type(&mut col, new_type); + + let stmt = Table::alter() + .table(Alias::new(table)) + .modify_column(col) + .to_owned(); + Ok(vec![BuiltQuery::AlterTable(Box::new(stmt))]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::{ColumnDef, ColumnType, ComplexColumnType, SimpleColumnType, TableDef}; + + #[rstest] + #[case::modify_column_type_postgres( + "modify_column_type_postgres", + DatabaseBackend::Postgres, + &["ALTER TABLE \"users\"", "\"age\""] + )] + #[case::modify_column_type_mysql( + "modify_column_type_mysql", + DatabaseBackend::MySql, + &["ALTER TABLE `users` MODIFY COLUMN `age` varchar(50)"] + )] + #[case::modify_column_type_sqlite( + "modify_column_type_sqlite", + DatabaseBackend::Sqlite, + &[] + )] + fn test_modify_column_type( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + // For SQLite, we need to provide current schema + 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, + }, + ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![], + indexes: vec![], + }]; + + let result = build_modify_column_type( + &backend, + "users", + "age", + &ColumnType::Complex(ComplexColumnType::Varchar { length: 50 }), + ¤t_schema, + ); + + // SQLite may return multiple queries + let sql = result + .unwrap() + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join(";\n"); + + for exp in expected { + assert!( + sql.contains(exp), + "Expected SQL to contain '{}', got: {}", + exp, + sql + ); + } + println!("sql: {}", sql); + + with_settings!({ snapshot_suffix => format!("modify_column_type_{}", title) }, { + assert_snapshot!(sql); + }); + } + + #[test] + fn test_modify_column_type_sqlite_table_not_found() { + // Test error when table is not found in current schema (line 25) + let result = build_modify_column_type( + &DatabaseBackend::Sqlite, + "nonexistent_table", + "age", + &ColumnType::Simple(SimpleColumnType::BigInt), + &[], // Empty schema + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema")); + } + + #[test] + fn test_modify_column_type_sqlite_column_not_found() { + // Test error when column is not found in table (lines 35-37) + 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![], + }]; + let result = build_modify_column_type( + &DatabaseBackend::Sqlite, + "users", + "nonexistent_column", + &ColumnType::Simple(SimpleColumnType::BigInt), + ¤t_schema, + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Column 'nonexistent_column' not found in table 'users'")); + } + + #[rstest] + #[case::modify_column_type_with_index_postgres( + "modify_column_type_with_index_postgres", + DatabaseBackend::Postgres + )] + #[case::modify_column_type_with_index_mysql( + "modify_column_type_with_index_mysql", + DatabaseBackend::MySql + )] + #[case::modify_column_type_with_index_sqlite( + "modify_column_type_with_index_sqlite", + 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; + + 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, + }, + ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![], + indexes: vec![IndexDef { + name: "idx_age".into(), + columns: vec!["age".into()], + unique: false, + }], + }]; + + let result = build_modify_column_type( + &backend, + "users", + "age", + &ColumnType::Simple(SimpleColumnType::BigInt), + ¤t_schema, + ) + .unwrap(); + + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join(";\n"); + + // For SQLite, should recreate index + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_age")); + } + + with_settings!({ snapshot_suffix => format!("modify_column_type_with_index_{}", title) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::modify_column_type_with_unique_index_postgres( + "modify_column_type_with_unique_index_postgres", + DatabaseBackend::Postgres + )] + #[case::modify_column_type_with_unique_index_mysql( + "modify_column_type_with_unique_index_mysql", + DatabaseBackend::MySql + )] + #[case::modify_column_type_with_unique_index_sqlite( + "modify_column_type_with_unique_index_sqlite", + DatabaseBackend::Sqlite + )] + fn test_modify_column_type_with_unique_index( + #[case] title: &str, + #[case] backend: DatabaseBackend, + ) { + // Test modify column type with unique index (lines 85-88, 90-91, 93-94) + use vespertide_core::IndexDef; + + 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, + }, + 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![], + indexes: vec![IndexDef { + name: "idx_email".into(), + columns: vec!["email".into()], + unique: true, + }], + }]; + + let result = build_modify_column_type( + &backend, + "users", + "email", + &ColumnType::Complex(ComplexColumnType::Varchar { length: 255 }), + ¤t_schema, + ) + .unwrap(); + + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join(";\n"); + + // For SQLite, should recreate unique index + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE UNIQUE INDEX")); + assert!(sql.contains("idx_email")); + } + + with_settings!({ snapshot_suffix => format!("modify_column_type_with_unique_index_{}", title) }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/raw_sql.rs b/crates/vespertide-query/src/sql/raw_sql.rs new file mode 100644 index 0000000..01fbcad --- /dev/null +++ b/crates/vespertide-query/src/sql/raw_sql.rs @@ -0,0 +1,50 @@ +use super::types::{BuiltQuery, RawSql}; + +pub fn build_raw_sql(sql: String) -> BuiltQuery { + BuiltQuery::Raw(RawSql::uniform(sql)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sql::types::DatabaseBackend; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + + #[rstest] + #[case::raw_sql_action_postgres( + "raw_sql_action_postgres", + DatabaseBackend::Postgres, + &["SELECT 1"] + )] + #[case::raw_sql_action_mysql( + "raw_sql_action_mysql", + DatabaseBackend::MySql, + &["SELECT 1"] + )] + #[case::raw_sql_action_sqlite( + "raw_sql_action_sqlite", + DatabaseBackend::Sqlite, + &["SELECT 1"] + )] + fn test_raw_sql( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let result = build_raw_sql("SELECT 1".into()); + 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!("raw_sql_{}", title) }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/remove_constraint.rs b/crates/vespertide-query/src/sql/remove_constraint.rs new file mode 100644 index 0000000..bcf039a --- /dev/null +++ b/crates/vespertide-query/src/sql/remove_constraint.rs @@ -0,0 +1,1604 @@ +use sea_query::{Alias, ForeignKey, Query, Table}; + +use vespertide_core::{TableConstraint, TableDef}; + +use super::create_table::build_create_table_for_backend; +use super::rename_table::build_rename_table; +use super::types::{BuiltQuery, DatabaseBackend}; +use crate::error::QueryError; +use crate::sql::RawSql; + +pub fn build_remove_constraint( + backend: &DatabaseBackend, + table: &str, + constraint: &TableConstraint, + current_schema: &[TableDef], +) -> Result, QueryError> { + match constraint { + TableConstraint::PrimaryKey { .. } => { + if *backend == DatabaseBackend::Sqlite { + // SQLite does not support dropping primary key constraints, use temp table approach + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to remove constraints.", + table + )))?; + + // Remove the primary key constraint + let mut new_constraints = table_def.constraints.clone(); + new_constraints.retain(|c| !matches!(c, TableConstraint::PrimaryKey { .. })); + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table without primary key constraint + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &table_def.columns, + &new_constraints, + ); + let create_query = BuiltQuery::CreateTable(Box::new(create_temp_table)); + + // 2. Copy data (all columns) + let column_aliases: Vec = table_def + .columns + .iter() + .map(|c| Alias::new(&c.name)) + .collect(); + let mut select_query = Query::select(); + for col_alias in &column_aliases { + select_query = select_query.column(col_alias.clone()).to_owned(); + } + select_query = select_query.from(Alias::new(table)).to_owned(); + + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(column_aliases.clone()) + .select_from(select_query) + .unwrap() + .to_owned(); + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + let drop_query = BuiltQuery::DropTable(Box::new(drop_table)); + + // 4. Rename temporary table to original name + let rename_query = build_rename_table(&temp_table, table); + + // 5. Recreate indexes (if any) + 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(); + } + 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]; + queries.extend(index_queries); + Ok(queries) + } else { + // Other backends: use raw SQL + let pg_sql = format!( + "ALTER TABLE \"{}\" DROP CONSTRAINT \"{}_pkey\"", + table, table + ); + let mysql_sql = format!("ALTER TABLE `{}` DROP PRIMARY KEY", table); + Ok(vec![BuiltQuery::Raw(RawSql::per_backend( + pg_sql.clone(), + mysql_sql, + pg_sql, + ))]) + } + } + TableConstraint::Unique { name, columns } => { + // SQLite does not support ALTER TABLE ... DROP CONSTRAINT UNIQUE + if *backend == DatabaseBackend::Sqlite { + // Use temporary table approach for SQLite + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to remove constraints.", + table + )))?; + + // Create new constraints without the removed unique constraint + let mut new_constraints = table_def.constraints.clone(); + new_constraints.retain(|c| { + match (c, constraint) { + ( + TableConstraint::Unique { + name: c_name, + columns: c_cols, + }, + TableConstraint::Unique { + name: r_name, + columns: r_cols, + }, + ) => { + // Remove if names match, or if no name and columns match + if let (Some(cn), Some(rn)) = (c_name, r_name) { + cn != rn + } else { + c_cols != r_cols + } + } + _ => true, + } + }); + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table without the removed constraint + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &table_def.columns, + &new_constraints, + ); + let create_query = BuiltQuery::CreateTable(Box::new(create_temp_table)); + + // 2. Copy data (all columns) + let column_aliases: Vec = table_def + .columns + .iter() + .map(|c| Alias::new(&c.name)) + .collect(); + let mut select_query = Query::select(); + for col_alias in &column_aliases { + select_query = select_query.column(col_alias.clone()).to_owned(); + } + select_query = select_query.from(Alias::new(table)).to_owned(); + + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(column_aliases.clone()) + .select_from(select_query) + .unwrap() + .to_owned(); + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + let drop_query = BuiltQuery::DropTable(Box::new(drop_table)); + + // 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. + 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(); + } + 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]; + queries.extend(index_queries); + Ok(queries) + } else { + // For unique constraints, PostgreSQL uses DROP CONSTRAINT, MySQL uses DROP INDEX + // sea_query 0.32 doesn't support dropping unique constraint via Table::alter() directly + // We'll use Index::drop() which generates DROP INDEX for both backends + // 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("_")) + }; + // 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 + // We'll use raw SQL for PostgreSQL and Index::drop() for MySQL + let pg_sql = format!( + "ALTER TABLE \"{}\" DROP CONSTRAINT \"{}\"", + table, constraint_name + ); + let mysql_sql = format!("ALTER TABLE `{}` DROP INDEX `{}`", table, constraint_name); + Ok(vec![BuiltQuery::Raw(RawSql::per_backend( + pg_sql.clone(), + mysql_sql, + pg_sql, + ))]) + } + } + TableConstraint::ForeignKey { name, columns, .. } => { + // SQLite does not support ALTER TABLE ... DROP CONSTRAINT FOREIGN KEY + if *backend == DatabaseBackend::Sqlite { + // Use temporary table approach for SQLite + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to remove constraints.", + table + )))?; + + // Create new constraints without the removed foreign key constraint + let mut new_constraints = table_def.constraints.clone(); + new_constraints.retain(|c| { + match (c, constraint) { + ( + TableConstraint::ForeignKey { + name: c_name, + columns: c_cols, + .. + }, + TableConstraint::ForeignKey { + name: r_name, + columns: r_cols, + .. + }, + ) => { + // Remove if names match, or if no name and columns match + if let (Some(cn), Some(rn)) = (c_name, r_name) { + cn != rn + } else { + c_cols != r_cols + } + } + _ => true, + } + }); + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table without the removed constraint + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &table_def.columns, + &new_constraints, + ); + let create_query = BuiltQuery::CreateTable(Box::new(create_temp_table)); + + // 2. Copy data (all columns) + let column_aliases: Vec = table_def + .columns + .iter() + .map(|c| Alias::new(&c.name)) + .collect(); + let mut select_query = Query::select(); + for col_alias in &column_aliases { + select_query = select_query.column(col_alias.clone()).to_owned(); + } + select_query = select_query.from(Alias::new(table)).to_owned(); + + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(column_aliases.clone()) + .select_from(select_query) + .unwrap() + .to_owned(); + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + let drop_query = BuiltQuery::DropTable(Box::new(drop_table)); + + // 4. Rename temporary table to original name + let rename_query = build_rename_table(&temp_table, table); + + // 5. Recreate indexes (if any) + 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(); + } + 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]; + queries.extend(index_queries); + 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 fk_drop = ForeignKey::drop() + .name(&constraint_name) + .table(Alias::new(table)) + .to_owned(); + Ok(vec![BuiltQuery::DropForeignKey(Box::new(fk_drop))]) + } + } + TableConstraint::Check { name, .. } => { + // SQLite does not support ALTER TABLE ... DROP CONSTRAINT CHECK + if *backend == DatabaseBackend::Sqlite { + // Use temporary table approach for SQLite + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| QueryError::Other(format!( + "Table '{}' not found in current schema. SQLite requires current schema information to remove constraints.", + table + )))?; + + // Create new constraints without the removed check constraint + let mut new_constraints = table_def.constraints.clone(); + new_constraints.retain(|c| match (c, constraint) { + ( + TableConstraint::Check { name: c_name, .. }, + TableConstraint::Check { name: r_name, .. }, + ) => c_name != r_name, + _ => true, + }); + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table without the removed constraint + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &table_def.columns, + &new_constraints, + ); + let create_query = BuiltQuery::CreateTable(Box::new(create_temp_table)); + + // 2. Copy data (all columns) + let column_aliases: Vec = table_def + .columns + .iter() + .map(|c| Alias::new(&c.name)) + .collect(); + let mut select_query = Query::select(); + for col_alias in &column_aliases { + select_query = select_query.column(col_alias.clone()).to_owned(); + } + select_query = select_query.from(Alias::new(table)).to_owned(); + + let insert_stmt = Query::insert() + .into_table(Alias::new(&temp_table)) + .columns(column_aliases.clone()) + .select_from(select_query) + .unwrap() + .to_owned(); + let insert_query = BuiltQuery::Insert(Box::new(insert_stmt)); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + let drop_query = BuiltQuery::DropTable(Box::new(drop_table)); + + // 4. Rename temporary table to original name + let rename_query = build_rename_table(&temp_table, table); + + // 5. Recreate indexes (if any) + 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(); + } + 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]; + queries.extend(index_queries); + Ok(queries) + } else { + let pg_sql = format!("ALTER TABLE \"{}\" DROP CONSTRAINT \"{}\"", table, name); + let mysql_sql = format!("ALTER TABLE `{}` DROP CHECK `{}`", table, name); + Ok(vec![BuiltQuery::Raw(RawSql::per_backend( + pg_sql.clone(), + mysql_sql, + pg_sql, + ))]) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sql::types::DatabaseBackend; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint, TableDef}; + + #[rstest] + #[case::remove_constraint_primary_key_postgres( + "remove_constraint_primary_key_postgres", + DatabaseBackend::Postgres, + &["DROP CONSTRAINT \"users_pkey\""] + )] + #[case::remove_constraint_primary_key_mysql( + "remove_constraint_primary_key_mysql", + DatabaseBackend::MySql, + &["DROP PRIMARY KEY"] + )] + #[case::remove_constraint_primary_key_sqlite( + "remove_constraint_primary_key_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users_temp\""] + )] + #[case::remove_constraint_unique_named_postgres( + "remove_constraint_unique_named_postgres", + DatabaseBackend::Postgres, + &["DROP CONSTRAINT \"uq_email\""] + )] + #[case::remove_constraint_unique_named_mysql( + "remove_constraint_unique_named_mysql", + DatabaseBackend::MySql, + &["DROP INDEX `uq_email`"] + )] + #[case::remove_constraint_unique_named_sqlite( + "remove_constraint_unique_named_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users_temp\""] + )] + #[case::remove_constraint_foreign_key_named_postgres( + "remove_constraint_foreign_key_named_postgres", + DatabaseBackend::Postgres, + &["DROP CONSTRAINT \"fk_user\""] + )] + #[case::remove_constraint_foreign_key_named_mysql( + "remove_constraint_foreign_key_named_mysql", + DatabaseBackend::MySql, + &["DROP FOREIGN KEY `fk_user`"] + )] + #[case::remove_constraint_foreign_key_named_sqlite( + "remove_constraint_foreign_key_named_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users_temp\""] + )] + #[case::remove_constraint_check_named_postgres( + "remove_constraint_check_named_postgres", + DatabaseBackend::Postgres, + &["DROP CONSTRAINT \"chk_age\""] + )] + #[case::remove_constraint_check_named_mysql( + "remove_constraint_check_named_mysql", + DatabaseBackend::MySql, + &["DROP CHECK `chk_age`"] + )] + #[case::remove_constraint_check_named_sqlite( + "remove_constraint_check_named_sqlite", + DatabaseBackend::Sqlite, + &["CREATE TABLE \"users_temp\""] + )] + fn test_remove_constraint( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let constraint = if title.contains("primary_key") { + TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + } + } else if title.contains("unique") { + TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + } + } else if title.contains("foreign_key") { + TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + } + } else { + TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + } + }; + + // For SQLite, we need to provide current schema with the constraint to be removed + let current_schema = vec![TableDef { + name: "users".into(), + columns: if title.contains("check") { + 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, + }, + ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ] + } else if title.contains("foreign_key") { + 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, + }, + ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ] + } else { + // primary key / unique cases + 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![constraint.clone()], + indexes: vec![], + }]; + + let result = + build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result[0].build(backend); + for exp in expected { + assert!( + sql.contains(exp), + "Expected SQL to contain '{}', got: {}", + exp, + sql + ); + } + + with_settings!({ snapshot_suffix => format!("remove_constraint_{}", title) }, { + assert_snapshot!(result.iter().map(|q| q.build(backend)).collect::>().join("\n")); + }); + } + + #[test] + fn test_remove_constraint_primary_key_sqlite_table_not_found() { + // Test error when table is not found (line 25) + let constraint = TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }; + let result = build_remove_constraint( + &DatabaseBackend::Sqlite, + "nonexistent_table", + &constraint, + &[], // Empty schema + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema")); + } + + #[rstest] + #[case::remove_primary_key_with_index_postgres( + DatabaseBackend::Postgres + )] + #[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; + + let constraint = TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }; + 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![constraint.clone()], + indexes: vec![IndexDef { + name: "idx_id".into(), + columns: vec!["id".into()], + unique: false, + }], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_id")); + } + + with_settings!({ snapshot_suffix => format!("remove_primary_key_with_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[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; + + let constraint = TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }; + 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![constraint.clone()], + indexes: vec![IndexDef { + name: "idx_email".into(), + columns: vec!["email".into()], + unique: true, + }], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE UNIQUE INDEX")); + assert!(sql.contains("idx_email")); + } + + with_settings!({ snapshot_suffix => format!("remove_primary_key_with_unique_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[test] + fn test_remove_constraint_unique_sqlite_table_not_found() { + // Test error when table is not found (line 112) + let constraint = TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }; + let result = build_remove_constraint( + &DatabaseBackend::Sqlite, + "nonexistent_table", + &constraint, + &[], // Empty schema + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema")); + } + + #[rstest] + #[case::remove_unique_without_name_postgres( + DatabaseBackend::Postgres + )] + #[case::remove_unique_without_name_mysql( + DatabaseBackend::MySql + )] + #[case::remove_unique_without_name_sqlite( + DatabaseBackend::Sqlite + )] + fn test_remove_constraint_unique_without_name( + #[case] backend: DatabaseBackend, + ) { + // Test Unique removal without name (lines 134, 137, 210) + let constraint = TableConstraint::Unique { + name: None, + columns: vec!["email".into()], + }; + 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, + }, + 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()], + indexes: vec![], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should generate default constraint name + if !matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("users_email_key") || sql.contains("email")); + } + + with_settings!({ snapshot_suffix => format!("remove_unique_without_name_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_unique_with_index_postgres( + DatabaseBackend::Postgres + )] + #[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; + + let constraint = TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }; + 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, + }, + 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()], + indexes: vec![IndexDef { + name: "idx_id".into(), + columns: vec!["id".into()], + unique: false, + }], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_id")); + } + + with_settings!({ snapshot_suffix => format!("remove_unique_with_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[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; + + let constraint = TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }; + 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, + }, + 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()], + indexes: vec![IndexDef { + name: "idx_name".into(), + columns: vec!["name".into()], + unique: true, + }], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE UNIQUE INDEX")); + assert!(sql.contains("idx_name")); + } + + with_settings!({ snapshot_suffix => format!("remove_unique_with_unique_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[test] + fn test_remove_constraint_foreign_key_sqlite_table_not_found() { + // Test error when table is not found (line 236) + let constraint = TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let result = build_remove_constraint( + &DatabaseBackend::Sqlite, + "nonexistent_table", + &constraint, + &[], // Empty schema + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema")); + } + + #[rstest] + #[case::remove_foreign_key_without_name_postgres( + DatabaseBackend::Postgres + )] + #[case::remove_foreign_key_without_name_mysql( + DatabaseBackend::MySql + )] + #[case::remove_foreign_key_without_name_sqlite( + DatabaseBackend::Sqlite + )] + fn test_remove_constraint_foreign_key_without_name( + #[case] backend: DatabaseBackend, + ) { + // Test ForeignKey removal without name (lines 260, 263, 329) + let constraint = 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 current_schema = vec![TableDef { + name: "posts".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, + }, + ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![constraint.clone()], + indexes: vec![], + }]; + + let result = build_remove_constraint(&backend, "posts", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should generate default constraint name + if !matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("posts_user_id_fkey") || sql.contains("user_id")); + } + + with_settings!({ snapshot_suffix => format!("remove_foreign_key_without_name_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_foreign_key_with_index_postgres( + DatabaseBackend::Postgres + )] + #[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; + + let constraint = TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let current_schema = vec![TableDef { + name: "posts".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, + }, + ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![constraint.clone()], + indexes: vec![IndexDef { + name: "idx_user_id".into(), + columns: vec!["user_id".into()], + unique: false, + }], + }]; + + let result = build_remove_constraint(&backend, "posts", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_user_id")); + } + + with_settings!({ snapshot_suffix => format!("remove_foreign_key_with_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[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; + + let constraint = TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let current_schema = vec![TableDef { + name: "posts".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, + }, + ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![constraint.clone()], + indexes: vec![IndexDef { + name: "idx_user_id".into(), + columns: vec!["user_id".into()], + unique: true, + }], + }]; + + let result = build_remove_constraint(&backend, "posts", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE UNIQUE INDEX")); + assert!(sql.contains("idx_user_id")); + } + + with_settings!({ snapshot_suffix => format!("remove_foreign_key_with_unique_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[test] + fn test_remove_constraint_check_sqlite_table_not_found() { + // Test error when table is not found (line 346) + let constraint = TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }; + let result = build_remove_constraint( + &DatabaseBackend::Sqlite, + "nonexistent_table", + &constraint, + &[], // Empty schema + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'nonexistent_table' not found in current schema")); + } + + #[rstest] + #[case::remove_check_with_index_postgres( + DatabaseBackend::Postgres + )] + #[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; + + let constraint = TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }; + 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, + }, + ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![constraint.clone()], + indexes: vec![IndexDef { + name: "idx_age".into(), + columns: vec!["age".into()], + unique: false, + }], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_age")); + } + + with_settings!({ snapshot_suffix => format!("remove_check_with_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[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; + + let constraint = TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }; + 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, + }, + ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![constraint.clone()], + indexes: vec![IndexDef { + name: "idx_age".into(), + columns: vec!["age".into()], + unique: true, + }], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + if matches!(backend, DatabaseBackend::Sqlite) { + assert!(sql.contains("CREATE UNIQUE INDEX")); + assert!(sql.contains("idx_age")); + } + + with_settings!({ snapshot_suffix => format!("remove_check_with_unique_index_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_unique_with_other_constraints_postgres( + DatabaseBackend::Postgres + )] + #[case::remove_unique_with_other_constraints_mysql( + DatabaseBackend::MySql + )] + #[case::remove_unique_with_other_constraints_sqlite( + DatabaseBackend::Sqlite + )] + fn test_remove_constraint_unique_with_other_constraints( + #[case] backend: DatabaseBackend, + ) { + // Test Unique removal with other constraint types (line 137) + let constraint = TableConstraint::Unique { + name: Some("uq_email".into()), + columns: vec!["email".into()], + }; + 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, + }, + 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![ + TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }, + constraint.clone(), + TableConstraint::Check { + name: "chk_email".into(), + expr: "email IS NOT NULL".into(), + }, + ], + indexes: vec![], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should still work with other constraint types present + assert!(sql.contains("DROP") || sql.contains("CREATE TABLE")); + + with_settings!({ snapshot_suffix => format!("remove_unique_with_other_constraints_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_foreign_key_with_other_constraints_postgres( + DatabaseBackend::Postgres + )] + #[case::remove_foreign_key_with_other_constraints_mysql( + DatabaseBackend::MySql + )] + #[case::remove_foreign_key_with_other_constraints_sqlite( + DatabaseBackend::Sqlite + )] + fn test_remove_constraint_foreign_key_with_other_constraints( + #[case] backend: DatabaseBackend, + ) { + // Test ForeignKey removal with other constraint types (line 263) + let constraint = TableConstraint::ForeignKey { + name: Some("fk_user".into()), + columns: vec!["user_id".into()], + ref_table: "users".into(), + ref_columns: vec!["id".into()], + on_delete: None, + on_update: None, + }; + let current_schema = vec![TableDef { + name: "posts".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, + }, + ColumnDef { + name: "user_id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![ + TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }, + constraint.clone(), + TableConstraint::Unique { + name: Some("uq_user_id".into()), + columns: vec!["user_id".into()], + }, + TableConstraint::Check { + name: "chk_user_id".into(), + expr: "user_id > 0".into(), + }, + ], + indexes: vec![], + }]; + + let result = build_remove_constraint(&backend, "posts", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should still work with other constraint types present + assert!(sql.contains("DROP") || sql.contains("CREATE TABLE")); + + with_settings!({ snapshot_suffix => format!("remove_foreign_key_with_other_constraints_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + + #[rstest] + #[case::remove_check_with_other_constraints_postgres( + DatabaseBackend::Postgres + )] + #[case::remove_check_with_other_constraints_mysql( + DatabaseBackend::MySql + )] + #[case::remove_check_with_other_constraints_sqlite( + DatabaseBackend::Sqlite + )] + fn test_remove_constraint_check_with_other_constraints( + #[case] backend: DatabaseBackend, + ) { + // Test Check removal with other constraint types (line 357) + let constraint = TableConstraint::Check { + name: "chk_age".into(), + expr: "age > 0".into(), + }; + 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, + }, + ColumnDef { + name: "age".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }, + ], + constraints: vec![ + TableConstraint::PrimaryKey { + columns: vec!["id".into()], + auto_increment: false, + }, + TableConstraint::Unique { + name: Some("uq_age".into()), + columns: vec!["age".into()], + }, + constraint.clone(), + ], + indexes: vec![], + }]; + + let result = build_remove_constraint(&backend, "users", &constraint, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should still work with other constraint types present + assert!(sql.contains("DROP") || sql.contains("CREATE TABLE")); + + with_settings!({ snapshot_suffix => format!("remove_check_with_other_constraints_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/remove_index.rs b/crates/vespertide-query/src/sql/remove_index.rs new file mode 100644 index 0000000..a26a67a --- /dev/null +++ b/crates/vespertide-query/src/sql/remove_index.rs @@ -0,0 +1,57 @@ +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/rename_column.rs b/crates/vespertide-query/src/sql/rename_column.rs new file mode 100644 index 0000000..ea5700d --- /dev/null +++ b/crates/vespertide-query/src/sql/rename_column.rs @@ -0,0 +1,56 @@ +use sea_query::{Alias, Table}; + +use super::types::BuiltQuery; + +pub fn build_rename_column(table: &str, from: &str, to: &str) -> BuiltQuery { + let stmt = Table::alter() + .table(Alias::new(table)) + .rename_column(Alias::new(from), Alias::new(to)) + .to_owned(); + BuiltQuery::AlterTable(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::rename_column_postgres( + "rename_column_postgres", + DatabaseBackend::Postgres, + &["ALTER TABLE \"users\" RENAME COLUMN \"email\" TO \"contact_email\""] + )] + #[case::rename_column_mysql( + "rename_column_mysql", + DatabaseBackend::MySql, + &["ALTER TABLE `users` RENAME COLUMN `email` TO `contact_email`"] + )] + #[case::rename_column_sqlite( + "rename_column_sqlite", + DatabaseBackend::Sqlite, + &["ALTER TABLE \"users\" RENAME COLUMN \"email\" TO \"contact_email\""] + )] + fn test_rename_column( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let result = build_rename_column("users", "email", "contact_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!("rename_column_{}", title) }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/rename_table.rs b/crates/vespertide-query/src/sql/rename_table.rs new file mode 100644 index 0000000..7c3e3e8 --- /dev/null +++ b/crates/vespertide-query/src/sql/rename_table.rs @@ -0,0 +1,55 @@ +use sea_query::{Alias, Table}; + +use super::types::BuiltQuery; + +pub fn build_rename_table(from: &str, to: &str) -> BuiltQuery { + let stmt = Table::rename() + .table(Alias::new(from), Alias::new(to)) + .to_owned(); + BuiltQuery::RenameTable(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::rename_table_action_postgres( + "rename_table_action_postgres", + DatabaseBackend::Postgres, + &["ALTER TABLE \"users\" RENAME TO \"accounts\""] + )] + #[case::rename_table_action_mysql( + "rename_table_action_mysql", + DatabaseBackend::MySql, + &["RENAME TABLE `users` TO `accounts`"] + )] + #[case::rename_table_action_sqlite( + "rename_table_action_sqlite", + DatabaseBackend::Sqlite, + &["ALTER TABLE \"users\" RENAME TO \"accounts\""] + )] + fn test_rename_table( + #[case] title: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &[&str], + ) { + let result = build_rename_table("users", "accounts"); + 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!("rename_table_{}", title) }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_mysql.snap similarity index 50% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_mysql.snap index 5ca5652..50810fd 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_unnamed_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_mysql.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +source: crates/vespertide-query/src/sql/add_column.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -ALTER TABLE "users" ADD CHECK (age > 0) +ALTER TABLE `users` ADD COLUMN `email` text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_postgres.snap new file mode 100644 index 0000000..bc6668f --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/add_column.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +ALTER TABLE "users" ADD COLUMN "email" text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_sqlite.snap new file mode 100644 index 0000000..bc6668f --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_nullable_sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/add_column.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +ALTER TABLE "users" ADD COLUMN "email" text diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_mysql.snap similarity index 73% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_mysql.snap index 63bc66e..9c602d8 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_mysql.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +source: crates/vespertide-query/src/sql/add_column.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- ALTER TABLE `users` ADD COLUMN `nickname` text diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_postgres.snap similarity index 73% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_postgres.snap index 91a0019..a375b6a 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_postgres.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +source: crates/vespertide-query/src/sql/add_column.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- ALTER TABLE "users" ADD COLUMN "nickname" text diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_sqlite.snap similarity index 73% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_sqlite.snap index 91a0019..a375b6a 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_column_simple_sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_simple_sqlite.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +source: crates/vespertide-query/src/sql/add_column.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- ALTER TABLE "users" ADD COLUMN "nickname" text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_mysql.snap new file mode 100644 index 0000000..4187d8c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_mysql.snap @@ -0,0 +1,7 @@ +--- +source: crates/vespertide-query/src/sql/add_column.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +ALTER TABLE `users` ADD COLUMN `nickname` text +UPDATE `users` SET `nickname` = 0 +ALTER TABLE `users` MODIFY COLUMN `nickname` text NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_postgres.snap new file mode 100644 index 0000000..26f1b8b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_postgres.snap @@ -0,0 +1,7 @@ +--- +source: crates/vespertide-query/src/sql/add_column.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +ALTER TABLE "users" ADD COLUMN "nickname" text +UPDATE "users" SET "nickname" = 0 +ALTER TABLE "users" ALTER COLUMN "nickname" TYPE text, ALTER COLUMN "nickname" SET NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_sqlite.snap new file mode 100644 index 0000000..d67079a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column@add_column_add_column_with_backfill_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/add_column.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "nickname" text NOT NULL ) +INSERT INTO "users_temp" ("id", "nickname") SELECT "id", 0 AS "nickname" 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_check_named_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_check_named_mysql.snap new file mode 100644 index 0000000..cb26f7f --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_check_named_mysql.snap @@ -0,0 +1,5 @@ +--- +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 `chk_age` CHECK (age > 0) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_check_named_postgres.snap similarity index 73% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_check_named_postgres.snap index 03f180e..2e528c0 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_check_named_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_check_named_postgres.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +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 "chk_age" CHECK (age > 0) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_check_named_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_check_named_sqlite.snap new file mode 100644 index 0000000..2e4b30e --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_check_named_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/add_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "age" text , CONSTRAINT "chk_age" CHECK (age > 0)) +INSERT INTO "users_temp" ("id", "age") SELECT "id", "age" 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 new file mode 100644 index 0000000..8dfb6b7 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_mysql.snap @@ -0,0 +1,5 @@ +--- +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 diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_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_postgres.snap similarity index 58% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_foreign_key_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_postgres.snap index 4c3d98c..f955ca9 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_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_postgres.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +source: crates/vespertide-query/src/sql/add_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- -ALTER TABLE "posts" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT +ALTER TABLE "users" ADD CONSTRAINT "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_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_sqlite.snap new file mode 100644 index 0000000..4cae505 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_foreign_key_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/add_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "user_id" integer, FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE RESTRICT ) +INSERT INTO "users_temp" ("id", "user_id") SELECT "id", "user_id" 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_primary_key_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_primary_key_mysql.snap new file mode 100644 index 0000000..c7b9086 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_primary_key_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/add_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +ALTER TABLE `users` ADD PRIMARY KEY (`id`) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_primary_key_postgres.snap similarity index 71% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_primary_key_postgres.snap index 7e617d0..4e48f95 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_add_constraint_primary_key_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_primary_key_postgres.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +source: crates/vespertide-query/src/sql/add_constraint.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- ALTER TABLE "users" ADD PRIMARY KEY ("id") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_primary_key_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_primary_key_sqlite.snap new file mode 100644 index 0000000..9836f7d --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_primary_key_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/add_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text, PRIMARY KEY ("id") ) +INSERT INTO "users_temp" ("id", "email") SELECT "id", "email" 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_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 new file mode 100644 index 0000000..6203446 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_mysql.snap @@ -0,0 +1,5 @@ +--- +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`) 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 new file mode 100644 index 0000000..b3185dc --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_postgres.snap @@ -0,0 +1,5 @@ +--- +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") 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 new file mode 100644 index 0000000..b3185dc --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_constraint__tests__add_constraint@add_constraint_add_constraint_unique_named_sqlite.snap @@ -0,0 +1,5 @@ +--- +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") 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 new file mode 100644 index 0000000..bb16fa6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_mysql.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..b87bc63 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_postgres.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..b87bc63 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_index_sqlite.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..1b87bf4 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_mysql.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..5815d42 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_postgres.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..5815d42 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_index__tests__add_index@add_index_add_unique_index_sqlite.snap @@ -0,0 +1,5 @@ +--- +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@create_table_create_table_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table@create_table_create_table_mysql.snap new file mode 100644 index 0000000..09cba73 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table@create_table_create_table_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE `users` ( `id` int ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table@create_table_create_table_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table@create_table_create_table_postgres.snap new file mode 100644 index 0000000..c83f75a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table@create_table_create_table_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE "users" ( "id" integer ) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table@create_table_create_table_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table@create_table_create_table_sqlite.snap new file mode 100644 index 0000000..c83f75a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__create_table__tests__create_table@create_table_create_table_sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE "users" ( "id" integer ) 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 new file mode 100644 index 0000000..c3a51cf --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE `users` ( `id` int, `email` text UNIQUE ) 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 new file mode 100644 index 0000000..37564b5 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE "users" ( "id" integer, "email" text UNIQUE ) 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 new file mode 100644 index 0000000..37564b5 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE "users" ( "id" integer, "email" text UNIQUE ) 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 new file mode 100644 index 0000000..3bb00fe --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE `users` ( `id` int, `email` text, UNIQUE KEY `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 new file mode 100644 index 0000000..eb8f4da --- /dev/null +++ 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 @@ -0,0 +1,6 @@ +--- +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") 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 new file mode 100644 index 0000000..eb8f4da --- /dev/null +++ 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 @@ -0,0 +1,6 @@ +--- +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") 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 new file mode 100644 index 0000000..d0ed37f --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE `users` ( `id` int, `email` text, UNIQUE KEY (`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 new file mode 100644 index 0000000..8cca4cf --- /dev/null +++ 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 @@ -0,0 +1,6 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE "users" ( "id" integer, "email" text ) +CREATE UNIQUE INDEX 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 new file mode 100644 index 0000000..8cca4cf --- /dev/null +++ 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 @@ -0,0 +1,6 @@ +--- +source: crates/vespertide-query/src/sql/create_table.rs +expression: sql +--- +CREATE TABLE "users" ( "id" integer, "email" text ) +CREATE UNIQUE INDEX ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_mysql.snap new file mode 100644 index 0000000..ef4ee24 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/delete_column.rs +expression: sql +--- +ALTER TABLE `users` DROP COLUMN `email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_postgres.snap new file mode 100644 index 0000000..afe3bf0 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/delete_column.rs +expression: sql +--- +ALTER TABLE "users" DROP COLUMN "email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_sqlite.snap new file mode 100644 index 0000000..afe3bf0 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_column__tests__delete_column@delete_column_delete_column_sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/delete_column.rs +expression: sql +--- +ALTER TABLE "users" DROP COLUMN "email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_mysql.snap new file mode 100644 index 0000000..ae7887e --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/delete_table.rs +expression: sql +--- +DROP TABLE `users` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_postgres.snap new file mode 100644 index 0000000..f90f430 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/delete_table.rs +expression: sql +--- +DROP TABLE "users" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_sqlite.snap new file mode 100644 index 0000000..f90f430 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__delete_table__tests__delete_table@delete_table_delete_table_sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/delete_table.rs +expression: sql +--- +DROP TABLE "users" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_mysql.snap new file mode 100644 index 0000000..c85181c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `age` varchar(50) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_postgres.snap new file mode 100644 index 0000000..9646648 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "age" TYPE varchar(50) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_sqlite.snap new file mode 100644 index 0000000..a58a368 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_type__tests__modify_column_type@modify_column_type_modify_column_type_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "age" varchar(50) ); +INSERT INTO "users_temp" ("id", "age") SELECT "id", "age" FROM "users"; +DROP TABLE "users"; +ALTER TABLE "users_temp" RENAME TO "users" 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_mysql.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_mysql.snap new file mode 100644 index 0000000..b7fe40f --- /dev/null +++ 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_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `age` bigint 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_postgres.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_postgres.snap new file mode 100644 index 0000000..1c4613e --- /dev/null +++ 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_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "age" TYPE bigint 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 new file mode 100644 index 0000000..56591f0 --- /dev/null +++ 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 @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +expression: sql +--- +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") 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_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_mysql.snap new file mode 100644 index 0000000..aee9638 --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` varchar(255) 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_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_postgres.snap new file mode 100644 index 0000000..2893d0c --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" TYPE varchar(255) 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_index@modify_column_type_with_unique_index_modify_column_type_with_unique_index_sqlite.snap new file mode 100644 index 0000000..4ca6b18 --- /dev/null +++ b/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 @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_type.rs +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") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_mysql.snap new file mode 100644 index 0000000..2b8f226 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/raw_sql.rs +expression: sql +--- +SELECT 1 diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_postgres.snap new file mode 100644 index 0000000..2b8f226 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/raw_sql.rs +expression: sql +--- +SELECT 1 diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_sqlite.snap new file mode 100644 index 0000000..2b8f226 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__raw_sql__tests__raw_sql@raw_sql_raw_sql_action_sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/raw_sql.rs +expression: sql +--- +SELECT 1 diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_mysql.snap new file mode 100644 index 0000000..7480cd7 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +ALTER TABLE `users` DROP CHECK `chk_age` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_postgres.snap similarity index 70% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_postgres.snap index 3dcfbdf..2955d74 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_check_named_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_postgres.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +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 "chk_age" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_sqlite.snap new file mode 100644 index 0000000..a4ae525 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_check_named_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +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" 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 new file mode 100644 index 0000000..2c22e94 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_mysql.snap @@ -0,0 +1,5 @@ +--- +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` 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 new file mode 100644 index 0000000..bc8d252 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_postgres.snap @@ -0,0 +1,5 @@ +--- +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" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_sqlite.snap new file mode 100644 index 0000000..08791e1 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_foreign_key_named_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "user_id" integer ) +INSERT INTO "users_temp" ("id", "user_id") SELECT "id", "user_id" FROM "users" +DROP TABLE "users" +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_primary_key_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_primary_key_mysql.snap new file mode 100644 index 0000000..ae0b15a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_primary_key_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +ALTER TABLE `users` DROP PRIMARY KEY diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_primary_key_postgres.snap similarity index 71% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_primary_key_postgres.snap index c6d2a4f..5b80fb5 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_primary_key_mysql.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_primary_key_postgres.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +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 "users_pkey" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_primary_key_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_primary_key_sqlite.snap new file mode 100644 index 0000000..f2b818c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_primary_key_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +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" 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 new file mode 100644 index 0000000..07d23b1 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_mysql.snap @@ -0,0 +1,5 @@ +--- +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` diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_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_postgres.snap similarity index 70% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_remove_constraint_unique_named_mysql.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_postgres.snap index 39ad3fa..4157065 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_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_postgres.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +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" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_sqlite.snap new file mode 100644 index 0000000..f2b818c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint@remove_constraint_remove_constraint_unique_named_sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +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" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_MySql.snap new file mode 100644 index 0000000..0d111b6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP CHECK `chk_age` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_Postgres.snap new file mode 100644 index 0000000..8194132 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "chk_age" 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 new file mode 100644 index 0000000..9997adf --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_index@remove_check_with_index_Sqlite.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_MySql.snap new file mode 100644 index 0000000..0d111b6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP CHECK `chk_age` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_Postgres.snap new file mode 100644 index 0000000..8194132 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "chk_age" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_Sqlite.snap new file mode 100644 index 0000000..f3aefe9 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_check_with_other_constraints@remove_check_with_other_constraints_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "age" integer, PRIMARY KEY ("id") ) +INSERT INTO "users_temp" ("id", "age") SELECT "id", "age" FROM "users" +DROP TABLE "users" +ALTER TABLE "users_temp" RENAME TO "users" 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_index@remove_check_with_unique_index_MySql.snap new file mode 100644 index 0000000..0d111b6 --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP CHECK `chk_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_Postgres.snap b/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 new file mode 100644 index 0000000..8194132 --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "chk_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_Sqlite.snap b/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 new file mode 100644 index 0000000..f5e9dc0 --- /dev/null +++ b/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 @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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 new file mode 100644 index 0000000..83b2d94 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `posts` DROP FOREIGN KEY `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 new file mode 100644 index 0000000..2e42d24 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "posts" DROP CONSTRAINT "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 new file mode 100644 index 0000000..be1f539 --- /dev/null +++ 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 @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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") 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 new file mode 100644 index 0000000..83b2d94 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `posts` DROP FOREIGN KEY `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 new file mode 100644 index 0000000..2e42d24 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "posts" DROP CONSTRAINT "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_Sqlite.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_Sqlite.snap new file mode 100644 index 0000000..b50b776 --- /dev/null +++ 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_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +CREATE TABLE "posts_temp" ( "id" integer NOT NULL, "user_id" integer, PRIMARY KEY ("id") ) +INSERT INTO "posts_temp" ("id", "user_id") SELECT "id", "user_id" FROM "posts" +DROP TABLE "posts" +ALTER TABLE "posts_temp" RENAME TO "posts" 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_foreign_key_with_unique_index@remove_foreign_key_with_unique_index_MySql.snap new file mode 100644 index 0000000..83b2d94 --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `posts` DROP FOREIGN KEY `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_Postgres.snap b/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 new file mode 100644 index 0000000..2e42d24 --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "posts" DROP CONSTRAINT "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_index@remove_foreign_key_with_unique_index_Sqlite.snap new file mode 100644 index 0000000..c24749a --- /dev/null +++ b/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 @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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 new file mode 100644 index 0000000..f4e00cc --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `posts` DROP FOREIGN KEY `posts_user_id_fkey` 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 new file mode 100644 index 0000000..14e2fe9 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "posts" DROP CONSTRAINT "posts_user_id_fkey" 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_Sqlite.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_Sqlite.snap new file mode 100644 index 0000000..021f502 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_foreign_key_without_name@remove_foreign_key_without_name_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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" 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_MySql.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_MySql.snap new file mode 100644 index 0000000..31798a9 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_index@remove_primary_key_with_index_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP PRIMARY KEY 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_Postgres.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_Postgres.snap new file mode 100644 index 0000000..378fcee --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_primary_key_with_index@remove_primary_key_with_index_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "users_pkey" 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 new file mode 100644 index 0000000..d7f46fa --- /dev/null +++ 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 @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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") 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_index@remove_primary_key_with_unique_index_MySql.snap new file mode 100644 index 0000000..31798a9 --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP PRIMARY KEY 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_index@remove_primary_key_with_unique_index_Postgres.snap new file mode 100644 index 0000000..378fcee --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "users_pkey" 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_index@remove_primary_key_with_unique_index_Sqlite.snap new file mode 100644 index 0000000..a7a5af5 --- /dev/null +++ b/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 @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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 new file mode 100644 index 0000000..9e60d72 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP INDEX `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 new file mode 100644 index 0000000..affe793 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "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 new file mode 100644 index 0000000..438f2dc --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_index@remove_unique_with_index_Sqlite.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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") 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 new file mode 100644 index 0000000..9e60d72 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP INDEX `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 new file mode 100644 index 0000000..affe793 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "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_Sqlite.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_Sqlite.snap new file mode 100644 index 0000000..12998e0 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_with_other_constraints@remove_unique_with_other_constraints_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text, PRIMARY KEY ("id") ) +INSERT INTO "users_temp" ("id", "email") SELECT "id", "email" FROM "users" +DROP TABLE "users" +ALTER TABLE "users_temp" RENAME TO "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_unique_with_unique_index@remove_unique_with_unique_index_MySql.snap new file mode 100644 index 0000000..9e60d72 --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP INDEX `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_unique_index@remove_unique_with_unique_index_Postgres.snap new file mode 100644 index 0000000..affe793 --- /dev/null +++ b/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 @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "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_unique_index@remove_unique_with_unique_index_Sqlite.snap new file mode 100644 index 0000000..f14ae31 --- /dev/null +++ b/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 @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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 new file mode 100644 index 0000000..710074b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE `users` DROP INDEX `users_email_key` 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 new file mode 100644 index 0000000..cc202bb --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "users_email_key" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_Sqlite.snap new file mode 100644 index 0000000..673a40b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_constraint__tests__remove_constraint_unique_without_name@remove_unique_without_name_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/remove_constraint.rs +expression: sql +--- +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" 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 new file mode 100644 index 0000000..27b6995 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_mysql.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..539199b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_postgres.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..539199b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__remove_index__tests__remove_index@remove_index_remove_index_sqlite.snap @@ -0,0 +1,5 @@ +--- +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__rename_column__tests__rename_column@rename_column_rename_column_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_column__tests__rename_column@rename_column_rename_column_mysql.snap new file mode 100644 index 0000000..e32f1c7 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_column__tests__rename_column@rename_column_rename_column_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/rename_column.rs +expression: sql +--- +ALTER TABLE `users` RENAME COLUMN `email` TO `contact_email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_column__tests__rename_column@rename_column_rename_column_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_column__tests__rename_column@rename_column_rename_column_postgres.snap new file mode 100644 index 0000000..edec4ce --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_column__tests__rename_column@rename_column_rename_column_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/rename_column.rs +expression: sql +--- +ALTER TABLE "users" RENAME COLUMN "email" TO "contact_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_column__tests__rename_column@rename_column_rename_column_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_column__tests__rename_column@rename_column_rename_column_sqlite.snap new file mode 100644 index 0000000..edec4ce --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_column__tests__rename_column@rename_column_rename_column_sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/rename_column.rs +expression: sql +--- +ALTER TABLE "users" RENAME COLUMN "email" TO "contact_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_mysql.snap new file mode 100644 index 0000000..c50f509 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/rename_table.rs +expression: sql +--- +RENAME TABLE `users` TO `accounts` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_postgres.snap new file mode 100644 index 0000000..e85ab5e --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/rename_table.rs +expression: sql +--- +ALTER TABLE "users" RENAME TO "accounts" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_sqlite.snap new file mode 100644 index 0000000..e85ab5e --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__rename_table__tests__rename_table@rename_table_rename_table_action_sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/rename_table.rs +expression: sql +--- +ALTER TABLE "users" RENAME TO "accounts" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_MySql.snap new file mode 100644 index 0000000..45bdd76 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `users` ADD COLUMN `email` text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_Postgres.snap new file mode 100644 index 0000000..e0b7804 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" ADD COLUMN "email" text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_Sqlite.snap new file mode 100644 index 0000000..e0b7804 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_column@add_column_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" ADD COLUMN "email" text 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 new file mode 100644 index 0000000..60dfde9 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX `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 new file mode 100644 index 0000000..2b6e885 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX "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 new file mode 100644 index 0000000..2b6e885 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_constraint@add_constraint_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE UNIQUE INDEX "uq_email" ON "users" ("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_add_index@add_index_MySql.snap new file mode 100644 index 0000000..3c427d5 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX `idx_email` ON `users` (`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__build_action_queries_add_index@add_index_Postgres.snap new file mode 100644 index 0000000..1b1f0b5 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "idx_email" ON "users" ("email") 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__build_action_queries_add_index@add_index_Sqlite.snap new file mode 100644 index 0000000..1b1f0b5 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_add_index@add_index_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE INDEX "idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_MySql.snap new file mode 100644 index 0000000..cecb6a9 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `users` DROP COLUMN `email` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_Postgres.snap new file mode 100644 index 0000000..76640fd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" DROP COLUMN "email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_Sqlite.snap new file mode 100644 index 0000000..76640fd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_delete_column@delete_column_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" DROP COLUMN "email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_MySql.snap new file mode 100644 index 0000000..05d8e66 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `age` bigint diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_Postgres.snap new file mode 100644 index 0000000..92b2db1 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "age" TYPE bigint diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_Sqlite.snap new file mode 100644 index 0000000..687ddc1 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_type@modify_column_type_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "age" bigint ) +INSERT INTO "users_temp" ("age") SELECT "age" FROM "users" +DROP TABLE "users" +ALTER TABLE "users_temp" RENAME TO "users" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_MySql.snap new file mode 100644 index 0000000..9932f1b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +SELECT 1; diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_Postgres.snap new file mode 100644 index 0000000..9932f1b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +SELECT 1; diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_Sqlite.snap new file mode 100644 index 0000000..9932f1b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_raw_sql@raw_sql_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +SELECT 1; 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 new file mode 100644 index 0000000..7546253 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `users` DROP INDEX `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 new file mode 100644 index 0000000..d9a4473 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" DROP CONSTRAINT "uq_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_Sqlite.snap new file mode 100644 index 0000000..b9aebe3 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_constraint@remove_constraint_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +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" 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@remove_index_MySql.snap new file mode 100644 index 0000000..f053b40 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX `idx_email` ON `users` 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__build_action_queries_remove_index@remove_index_Postgres.snap new file mode 100644 index 0000000..bb04a9e --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX "idx_email" 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__build_action_queries_remove_index@remove_index_Sqlite.snap new file mode 100644 index 0000000..bb04a9e --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_remove_index@remove_index_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +DROP INDEX "idx_email" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_MySql.snap new file mode 100644 index 0000000..784e4d6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `users` RENAME COLUMN `old_name` TO `new_name` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_Postgres.snap new file mode 100644 index 0000000..7ffce2a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" RENAME COLUMN "old_name" TO "new_name" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_Sqlite.snap new file mode 100644 index 0000000..7ffce2a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_column@rename_column_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" RENAME COLUMN "old_name" TO "new_name" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_MySql.snap new file mode 100644 index 0000000..5ae314a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +RENAME TABLE `old_table` TO `new_table` diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_Postgres.snap new file mode 100644 index 0000000..4462882 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "old_table" RENAME TO "new_table" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_Sqlite.snap new file mode 100644 index 0000000..4462882 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_rename_table@rename_table_Sqlite.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "old_table" RENAME TO "new_table" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_mysql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_mysql.snap new file mode 100644 index 0000000..0451bfd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_mysql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" +--- +CREATE TABLE "users" ( "status" text DEFAULT 'active' ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_postgres.snap similarity index 76% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_postgres.snap index 1c816d2..0451bfd 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_postgres.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_postgres.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +source: crates/vespertide-query/src/sql/mod.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- CREATE TABLE "users" ( "status" text DEFAULT 'active' ) diff --git a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_sqlite.snap similarity index 76% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_sqlite.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_sqlite.snap index 1c816d2..0451bfd 100644 --- a/crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_sqlite.snap +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_default_sqlite.snap @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +source: crates/vespertide-query/src/sql/mod.rs expression: "result.iter().map(|q| q.build(backend)).collect::>().join(\"\\n\")" --- CREATE TABLE "users" ( "status" text DEFAULT 'active' ) 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 new file mode 100644 index 0000000..527e4a6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_mysql.snap @@ -0,0 +1,5 @@ +--- +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 ) diff --git a/crates/vespertide-query/src/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 similarity index 85% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_postgres.snap rename to crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_postgres.snap index 066b107..527e4a6 100644 --- a/crates/vespertide-query/src/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 @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +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 ) 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 new file mode 100644 index 0000000..527e4a6 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_fk_sqlite.snap @@ -0,0 +1,5 @@ +--- +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 ) 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 new file mode 100644 index 0000000..f9b6e45 --- /dev/null +++ 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 @@ -0,0 +1,5 @@ +--- +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 ) diff --git a/crates/vespertide-query/src/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 similarity index 77% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_postgres.snap rename to 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 53b85e0..f9b6e45 100644 --- a/crates/vespertide-query/src/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 @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +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 ) diff --git a/crates/vespertide-query/src/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 similarity index 77% rename from crates/vespertide-query/src/snapshots/vespertide_query__sql__tests__build_migration_action@build_migration_action_create_table_with_inline_primary_key_sqlite.snap rename to 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 53b85e0..f9b6e45 100644 --- a/crates/vespertide-query/src/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 @@ -1,5 +1,5 @@ --- -source: crates/vespertide-query/src/sql.rs +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 ) diff --git a/crates/vespertide-query/src/sql/types.rs b/crates/vespertide-query/src/sql/types.rs new file mode 100644 index 0000000..b44af3f --- /dev/null +++ b/crates/vespertide-query/src/sql/types.rs @@ -0,0 +1,94 @@ +/// Database backend for SQL generation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DatabaseBackend { + Postgres, + MySql, + Sqlite, +} + +/// Represents a built query that can be converted to SQL for any database backend +#[derive(Debug, Clone)] +pub enum BuiltQuery { + CreateTable(Box), + DropTable(Box), + AlterTable(Box), + CreateIndex(Box), + DropIndex(Box), + RenameTable(Box), + CreateForeignKey(Box), + DropForeignKey(Box), + Insert(Box), + Update(Box), + Raw(RawSql), +} + +/// Raw SQL that may have backend-specific variants +#[derive(Debug, Clone)] +pub struct RawSql { + pub postgres: String, + pub mysql: String, + pub sqlite: String, +} + +impl RawSql { + /// Create a RawSql with the same SQL for all backends + pub fn uniform(sql: String) -> Self { + Self { + postgres: sql.clone(), + mysql: sql.clone(), + sqlite: sql, + } + } + + /// Create a RawSql with different SQL for each backend + pub fn per_backend(postgres: String, mysql: String, sqlite: String) -> Self { + Self { + postgres, + mysql, + sqlite, + } + } +} + +impl BuiltQuery { + /// Build SQL string for the specified database backend + pub fn build(&self, backend: DatabaseBackend) -> String { + match self { + BuiltQuery::CreateTable(stmt) => { + crate::sql::helpers::build_schema_statement(stmt.as_ref(), backend) + } + BuiltQuery::DropTable(stmt) => { + crate::sql::helpers::build_schema_statement(stmt.as_ref(), backend) + } + BuiltQuery::AlterTable(stmt) => { + crate::sql::helpers::build_schema_statement(stmt.as_ref(), backend) + } + BuiltQuery::CreateIndex(stmt) => { + crate::sql::helpers::build_schema_statement(stmt.as_ref(), backend) + } + BuiltQuery::DropIndex(stmt) => { + crate::sql::helpers::build_schema_statement(stmt.as_ref(), backend) + } + BuiltQuery::RenameTable(stmt) => { + crate::sql::helpers::build_schema_statement(stmt.as_ref(), backend) + } + BuiltQuery::CreateForeignKey(stmt) => { + crate::sql::helpers::build_schema_statement(stmt.as_ref(), backend) + } + BuiltQuery::DropForeignKey(stmt) => { + crate::sql::helpers::build_schema_statement(stmt.as_ref(), backend) + } + BuiltQuery::Insert(stmt) => { + crate::sql::helpers::build_query_statement(stmt.as_ref(), backend) + } + BuiltQuery::Update(stmt) => { + crate::sql::helpers::build_query_statement(stmt.as_ref(), backend) + } + BuiltQuery::Raw(raw) => match backend { + DatabaseBackend::Postgres => raw.postgres.clone(), + DatabaseBackend::MySql => raw.mysql.clone(), + DatabaseBackend::Sqlite => raw.sqlite.clone(), + }, + } + } +} diff --git a/examples/app/migrations/0001_create_user.json b/examples/app/migrations/0001_create_user.json deleted file mode 100644 index 11f4322..0000000 --- a/examples/app/migrations/0001_create_user.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/migration.schema.json", - "actions": [ - { - "columns": [ - { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, - "name": "id", - "nullable": false, - "primary_key": true, - "type": "integer", - "unique": null - }, - { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, - "name": "name", - "nullable": false, - "primary_key": null, - "type": "text", - "unique": null - }, - { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, - "name": "email", - "nullable": false, - "primary_key": null, - "type": "text", - "unique": true - } - ], - "constraints": [ - { - "auto_increment": false, - "columns": [ - "id" - ], - "type": "primary_key" - }, - { - "columns": [ - "email" - ], - "name": null, - "type": "unique" - } - ], - "table": "user", - "type": "create_table" - } - ], - "comment": "Create user", - "created_at": "2025-12-15T06:19:36Z", - "version": 1 -} \ No newline at end of file diff --git a/examples/app/migrations/0001_init.json b/examples/app/migrations/0001_init.json new file mode 100644 index 0000000..8afd78e --- /dev/null +++ b/examples/app/migrations/0001_init.json @@ -0,0 +1,714 @@ +{ + "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/migration.schema.json", + "actions": [ + { + "columns": [ + { + "comment": null, + "default": "gen_random_uuid()", + "foreign_key": null, + "index": null, + "name": "id", + "nullable": false, + "primary_key": true, + "type": "uuid", + "unique": null + }, + { + "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 + }, + { + "comment": null, + "default": null, + "foreign_key": null, + "index": null, + "name": "summary", + "nullable": true, + "primary_key": null, + "type": "text", + "unique": null + }, + { + "comment": null, + "default": null, + "foreign_key": null, + "index": null, + "name": "thumbnail", + "nullable": true, + "primary_key": null, + "type": "text", + "unique": null + }, + { + "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 + }, + { + "comment": null, + "default": null, + "foreign_key": null, + "index": true, + "name": "published_at", + "nullable": true, + "primary_key": null, + "type": "timestamptz", + "unique": null + }, + { + "comment": null, + "default": "now()", + "foreign_key": null, + "index": null, + "name": "created_at", + "nullable": false, + "primary_key": null, + "type": "timestamptz", + "unique": null + }, + { + "comment": null, + "default": null, + "foreign_key": null, + "index": null, + "name": "updated_at", + "nullable": true, + "primary_key": null, + "type": "timestamptz", + "unique": null + } + ], + "constraints": [ + { + "expr": "status IN ('draft', 'review', 'published', 'archived')", + "name": "chk_article_status", + "type": "check" + }, + { + "auto_increment": false, + "columns": [ + "id" + ], + "type": "primary_key" + }, + { + "columns": [ + "media_id" + ], + "name": null, + "on_delete": "cascade", + "on_update": null, + "ref_columns": [ + "id" + ], + "ref_table": "media", + "type": "foreign_key" + } + ], + "table": "article", + "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, + "ref_columns": [ + "id" + ], + "ref_table": "article" + }, + "index": true, + "name": "article_id", + "nullable": false, + "primary_key": true, + "type": "uuid", + "unique": null + }, + { + "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": "1", + "foreign_key": null, + "index": null, + "name": "author_order", + "nullable": false, + "primary_key": null, + "type": "integer", + "unique": null + }, + { + "comment": null, + "default": "'contributor'", + "foreign_key": null, + "index": null, + "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": [ + "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" + } + ], + "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" + }, + { + "columns": [ + { + "comment": null, + "default": "gen_random_uuid()", + "foreign_key": null, + "index": null, + "name": "id", + "nullable": false, + "primary_key": true, + "type": "uuid", + "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": "description", + "nullable": true, + "primary_key": null, + "type": "text", + "unique": null + }, + { + "comment": null, + "default": null, + "foreign_key": null, + "index": null, + "name": "logo", + "nullable": true, + "primary_key": null, + "type": "text", + "unique": null + }, + { + "comment": null, + "default": null, + "foreign_key": { + "on_delete": "cascade", + "on_update": null, + "ref_columns": [ + "id" + ], + "ref_table": "user" + }, + "index": true, + "name": "owner_id", + "nullable": false, + "primary_key": null, + "type": "uuid", + "unique": null + }, + { + "comment": null, + "default": "now()", + "foreign_key": null, + "index": null, + "name": "created_at", + "nullable": false, + "primary_key": null, + "type": "timestamptz", + "unique": null + }, + { + "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" + } + ], + "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": "gen_random_uuid()", + "foreign_key": null, + "index": null, + "name": "id", + "nullable": false, + "primary_key": true, + "type": "uuid", + "unique": null + }, + { + "comment": null, + "default": null, + "foreign_key": null, + "index": true, + "name": "email", + "nullable": false, + "primary_key": null, + "type": { + "kind": "varchar", + "length": 255 + }, + "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 + }, + { + "comment": null, + "default": "now()", + "foreign_key": null, + "index": null, + "name": "created_at", + "nullable": false, + "primary_key": null, + "type": "timestamptz", + "unique": null + }, + { + "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" + } + ], + "table": "user", + "type": "create_table" + }, + { + "index": { + "columns": [ + "email" + ], + "name": "idx_user_email", + "unique": false + }, + "table": "user", + "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, + "ref_columns": [ + "id" + ], + "ref_table": "media" + }, + "index": true, + "name": "media_id", + "nullable": false, + "primary_key": true, + "type": "uuid", + "unique": null + }, + { + "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" + } + ], + "comment": "init", + "created_at": "2025-12-15T14:08:11Z", + "version": 1 +} \ No newline at end of file diff --git a/examples/app/migrations/0002_create_post.json b/examples/app/migrations/0002_create_post.json deleted file mode 100644 index 4b219fb..0000000 --- a/examples/app/migrations/0002_create_post.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/migration.schema.json", - "actions": [ - { - "columns": [ - { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, - "name": "id", - "nullable": false, - "primary_key": true, - "type": "integer", - "unique": null - }, - { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, - "name": "title", - "nullable": false, - "primary_key": null, - "type": "text", - "unique": null - }, - { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, - "name": "content", - "nullable": false, - "primary_key": null, - "type": "text", - "unique": null - }, - { - "comment": null, - "default": null, - "foreign_key": null, - "index": null, - "name": "created_at", - "nullable": false, - "primary_key": null, - "type": "timestamp", - "unique": null - }, - { - "comment": null, - "default": null, - "foreign_key": null, - "index": [ - "tuple", - "tuple2" - ], - "name": "updated_at", - "nullable": true, - "primary_key": null, - "type": "timestamp", - "unique": null - }, - { - "comment": null, - "default": null, - "foreign_key": { - "on_delete": null, - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "user" - }, - "index": [ - "tuple", - "tuple2" - ], - "name": "user_id", - "nullable": false, - "primary_key": null, - "type": "integer", - "unique": null - } - ], - "constraints": [ - { - "auto_increment": false, - "columns": [ - "id" - ], - "type": "primary_key" - }, - { - "columns": [ - "user_id" - ], - "name": null, - "on_delete": null, - "on_update": null, - "ref_columns": [ - "id" - ], - "ref_table": "user", - "type": "foreign_key" - } - ], - "table": "post", - "type": "create_table" - }, - { - "index": { - "columns": [ - "updated_at", - "user_id" - ], - "name": "tuple", - "unique": false - }, - "table": "post", - "type": "add_index" - }, - { - "index": { - "columns": [ - "updated_at", - "user_id" - ], - "name": "tuple2", - "unique": false - }, - "table": "post", - "type": "add_index" - } - ], - "comment": "Create post", - "created_at": "2025-12-15T06:36:34Z", - "version": 2 -} \ No newline at end of file diff --git a/examples/app/models/article.json b/examples/app/models/article.json new file mode 100644 index 0000000..2099518 --- /dev/null +++ b/examples/app/models/article.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json", + "name": "article", + "columns": [ + { + "name": "id", + "type": "uuid", + "nullable": false, + "default": "gen_random_uuid()", + "primary_key": true + }, + { + "name": "title", + "type": { "kind": "varchar", "length": 499 }, + "nullable": false + }, + { + "name": "content", + "type": "text", + "nullable": false + }, + { + "name": "summary", + "type": "text", + "nullable": true + }, + { + "name": "thumbnail", + "type": "text", + "nullable": true + }, + { + "name": "media_id", + "type": "uuid", + "nullable": false, + "foreign_key": { + "ref_table": "media", + "ref_columns": ["id"], + "on_delete": "cascade" + }, + "index": true + }, + { + "name": "status", + "type": { "kind": "varchar", "length": 19 }, + "nullable": false, + "default": "'draft'", + "index": true + }, + { + "name": "published_at", + "type": "timestamptz", + "nullable": true, + "index": true + }, + { + "name": "created_at", + "type": "timestamptz", + "nullable": false, + "default": "now()" + }, + { + "name": "updated_at", + "type": "timestamptz", + "nullable": true + } + ], + "constraints": [ + { + "type": "check", + "name": "chk_article_status", + "expr": "status IN ('draft', 'review', 'published', 'archived')" + } + ], + "indexes": [] +} diff --git a/examples/app/models/article_user.json b/examples/app/models/article_user.json new file mode 100644 index 0000000..7f92ab3 --- /dev/null +++ b/examples/app/models/article_user.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json", + "name": "article_user", + "columns": [ + { + "name": "article_id", + "type": "uuid", + "nullable": false, + "foreign_key": { + "ref_table": "article", + "ref_columns": ["id"], + "on_delete": "cascade" + }, + "index": true, + "primary_key": true + }, + { + "name": "user_id", + "type": "uuid", + "nullable": false, + "foreign_key": { + "ref_table": "user", + "ref_columns": ["id"], + "on_delete": "cascade" + }, + "index": true, + "primary_key": true + }, + { + "name": "author_order", + "type": "integer", + "nullable": false, + "default": "1" + }, + { + "name": "role", + "type": { "kind": "varchar", "length": 20 }, + "nullable": false, + "default": "'contributor'" + }, + { + "name": "created_at", + "type": "timestamptz", + "nullable": false, + "default": "now()" + } + ], + "constraints": [], + "indexes": [] +} diff --git a/examples/app/models/media.json b/examples/app/models/media.json new file mode 100644 index 0000000..43552b6 --- /dev/null +++ b/examples/app/models/media.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json", + "name": "media", + "columns": [ + { + "name": "id", + "type": "uuid", + "nullable": false, + "default": "gen_random_uuid()", + "primary_key": true + }, + { + "name": "name", + "type": { "kind": "varchar", "length": 100 }, + "nullable": false + }, + { + "name": "description", + "type": "text", + "nullable": true + }, + { + "name": "logo", + "type": "text", + "nullable": true + }, + { + "name": "owner_id", + "type": "uuid", + "nullable": false, + "foreign_key": { + "ref_table": "user", + "ref_columns": ["id"], + "on_delete": "cascade" + }, + "index": true + }, + { + "name": "created_at", + "type": "timestamptz", + "nullable": false, + "default": "now()" + }, + { + "name": "updated_at", + "type": "timestamptz", + "nullable": true + } + ], + "constraints": [], + "indexes": [] +} diff --git a/examples/app/models/post/post.json b/examples/app/models/post/post.json deleted file mode 100644 index 1986cf8..0000000 --- a/examples/app/models/post/post.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json", - "name": "post", - "columns": [{ - "name": "id", - "type": "integer", - "nullable": false, - "primary_key": true - }, { - "name": "title", - "type": "text", - "nullable": false - }, { - "name": "content", - "type": "text", - "nullable": false - }, { - "name": "created_at", - "type": "timestamp", - "nullable": false - }, { - "name": "updated_at", - "type": "timestamp", - "nullable": true, - "index": ["tuple","tuple2"] - }, { - "name": "user_id", - "type": "integer", - "nullable": false, - "foreign_key": { - "ref_table": "user", - "ref_columns": ["id"] - }, - "index": ["tuple","tuple2"] - }], - "constraints": [], - "indexes": [] -} \ No newline at end of file diff --git a/examples/app/models/user.json b/examples/app/models/user.json index 206cbab..7221fb1 100644 --- a/examples/app/models/user.json +++ b/examples/app/models/user.json @@ -1,21 +1,48 @@ { - "$schema": "../../../schemas/model.schema.json", + "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json", "name": "user", - "columns": [{ - "name": "id", - "type": "integer", - "nullable": false, - "primary_key": true - }, { - "name": "name", - "type": "text", - "nullable": false - }, { - "name": "email", - "type": "text", - "nullable": false, - "unique": true - }], + "columns": [ + { + "name": "id", + "type": "uuid", + "nullable": false, + "default": "gen_random_uuid()", + "primary_key": true + }, + { + "name": "email", + "type": { "kind": "varchar", "length": 255 }, + "nullable": false, + "unique": true, + "index": true + }, + { + "name": "password", + "type": { "kind": "varchar", "length": 255 }, + "nullable": false + }, + { + "name": "name", + "type": { "kind": "varchar", "length": 100 }, + "nullable": false + }, + { + "name": "profile_image", + "type": "text", + "nullable": true + }, + { + "name": "created_at", + "type": "timestamptz", + "nullable": false, + "default": "now()" + }, + { + "name": "updated_at", + "type": "timestamptz", + "nullable": true + } + ], "constraints": [], "indexes": [] -} \ No newline at end of file +} diff --git a/examples/app/models/user_media_role.json b/examples/app/models/user_media_role.json new file mode 100644 index 0000000..ccf2f4a --- /dev/null +++ b/examples/app/models/user_media_role.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json", + "name": "user_media_role", + "columns": [ + { + "name": "user_id", + "type": "uuid", + "nullable": false, + "foreign_key": { + "ref_table": "user", + "ref_columns": ["id"], + "on_delete": "cascade" + }, + "index": true, + "primary_key": true + }, + { + "name": "media_id", + "type": "uuid", + "nullable": false, + "foreign_key": { + "ref_table": "media", + "ref_columns": ["id"], + "on_delete": "cascade" + }, + "index": true, + "primary_key": true + }, + { + "name": "role", + "type": { "kind": "varchar", "length": 20 }, + "nullable": false, + "index": true + }, + { + "name": "created_at", + "type": "timestamptz", + "nullable": false, + "default": "now()" + } + ], + "constraints": [], + "indexes": [] +} diff --git a/examples/app/src/main.rs b/examples/app/src/main.rs index 449abe9..bc9619b 100644 --- a/examples/app/src/main.rs +++ b/examples/app/src/main.rs @@ -6,9 +6,9 @@ use std::time::Duration; async fn main() -> Result<()> { println!("Hello, world!"); - let mut opt = ConnectOptions::new("postgres://postgres:password@localhost:5432/postgres"); - // // Configure SQLite connection - // let mut opt = ConnectOptions::new("sqlite://./local.db?mode=rwc"); + // let mut opt = ConnectOptions::new("postgres://postgres:password@localhost:5432/postgres"); + // Configure SQLite connection + let mut opt = ConnectOptions::new("sqlite://./local.db"); opt.max_connections(100) .min_connections(5) .connect_timeout(Duration::from_secs(8)) diff --git a/examples/app/src/models/article.rs b/examples/app/src/models/article.rs new file mode 100644 index 0000000..aa57d26 --- /dev/null +++ b/examples/app/src/models/article.rs @@ -0,0 +1,27 @@ +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "article")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: Uuid, + pub title: String, + pub content: String, + pub summary: Option, + pub thumbnail: Option, + pub media_id: Uuid, + pub status: String, + pub published_at: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: Option, + #[sea_orm(belongs_to, from = "media_id", to = "id")] + pub media: HasOne, +} + + +// Index definitions (SeaORM uses Statement builders externally) +// idx_article_media_id on [media_id] unique=false +// idx_article_status on [status] unique=false +// idx_article_published_at on [published_at] unique=false +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/app/src/models/article_user.rs b/examples/app/src/models/article_user.rs new file mode 100644 index 0000000..28ebea8 --- /dev/null +++ b/examples/app/src/models/article_user.rs @@ -0,0 +1,24 @@ +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "article_user")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub article_id: Uuid, + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: Uuid, + pub author_order: i32, + pub role: String, + pub created_at: DateTimeWithTimeZone, + #[sea_orm(belongs_to, from = "article_id", to = "id")] + pub article: HasOne, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: HasOne, +} + + +// Index definitions (SeaORM uses Statement builders externally) +// idx_article_user_article_id on [article_id] unique=false +// idx_article_user_user_id on [user_id] unique=false +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/app/src/models/media.rs b/examples/app/src/models/media.rs new file mode 100644 index 0000000..acde69e --- /dev/null +++ b/examples/app/src/models/media.rs @@ -0,0 +1,22 @@ +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "media")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: Uuid, + pub name: String, + pub description: Option, + pub logo: Option, + pub owner_id: Uuid, + pub created_at: DateTimeWithTimeZone, + pub updated_at: Option, + #[sea_orm(belongs_to, from = "owner_id", to = "id")] + pub user: HasOne, +} + + +// Index definitions (SeaORM uses Statement builders externally) +// idx_media_owner_id on [owner_id] unique=false +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/app/src/models/mod.rs b/examples/app/src/models/mod.rs index 8d4c7f6..f40d2b1 100644 --- a/examples/app/src/models/mod.rs +++ b/examples/app/src/models/mod.rs @@ -1,3 +1,7 @@ pub mod post; pub mod user_copy; pub mod user; +pub mod article; +pub mod article_user; +pub mod media; +pub mod user_media_role; diff --git a/examples/app/src/models/user.rs b/examples/app/src/models/user.rs index 07b4fc3..e352fc0 100644 --- a/examples/app/src/models/user.rs +++ b/examples/app/src/models/user.rs @@ -5,9 +5,16 @@ use sea_orm::entity::prelude::*; #[sea_orm(table_name = "user")] pub struct Model { #[sea_orm(primary_key)] - pub id: i32, - pub name: String, + pub id: Uuid, pub email: String, + pub password: String, + pub name: String, + pub profile_image: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: Option, } + +// Index definitions (SeaORM uses Statement builders externally) +// idx_user_email on [email] unique=false impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/app/src/models/user_media_role.rs b/examples/app/src/models/user_media_role.rs new file mode 100644 index 0000000..d13c829 --- /dev/null +++ b/examples/app/src/models/user_media_role.rs @@ -0,0 +1,24 @@ +use sea_orm::entity::prelude::*; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "user_media_role")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: Uuid, + #[sea_orm(primary_key, auto_increment = false)] + pub media_id: Uuid, + pub role: String, + pub created_at: DateTimeWithTimeZone, + #[sea_orm(belongs_to, from = "user_id", to = "id")] + pub user: HasOne, + #[sea_orm(belongs_to, from = "media_id", to = "id")] + pub media: HasOne, +} + + +// Index definitions (SeaORM uses Statement builders externally) +// idx_user_media_role_user_id on [user_id] unique=false +// idx_user_media_role_media_id on [media_id] unique=false +// idx_user_media_role_role on [role] unique=false +impl ActiveModelBehavior for ActiveModel {} diff --git a/schemas/migration.schema.json b/schemas/migration.schema.json index e26f591..9c152d3 100644 --- a/schemas/migration.schema.json +++ b/schemas/migration.schema.json @@ -557,11 +557,11 @@ "ReferenceAction": { "type": "string", "enum": [ - "Cascade", - "Restrict", - "SetNull", - "SetDefault", - "NoAction" + "cascade", + "restrict", + "set_null", + "set_default", + "no_action" ] }, "SimpleColumnType": { @@ -718,10 +718,7 @@ "type": "string" }, "name": { - "type": [ - "string", - "null" - ] + "type": "string" }, "type": { "type": "string", @@ -730,6 +727,7 @@ }, "required": [ "type", + "name", "expr" ] } diff --git a/schemas/model.schema.json b/schemas/model.schema.json index ddf8a3d..ae47378 100644 --- a/schemas/model.schema.json +++ b/schemas/model.schema.json @@ -295,11 +295,11 @@ "ReferenceAction": { "type": "string", "enum": [ - "Cascade", - "Restrict", - "SetNull", - "SetDefault", - "NoAction" + "cascade", + "restrict", + "set_null", + "set_default", + "no_action" ] }, "SimpleColumnType": { @@ -456,10 +456,7 @@ "type": "string" }, "name": { - "type": [ - "string", - "null" - ] + "type": "string" }, "type": { "type": "string", @@ -468,6 +465,7 @@ }, "required": [ "type", + "name", "expr" ] }