diff --git a/.changepacks/changepack_log_nEUFzhXlesutfHyTo9oo1.json b/.changepacks/changepack_log_nEUFzhXlesutfHyTo9oo1.json new file mode 100644 index 0000000..312263a --- /dev/null +++ b/.changepacks/changepack_log_nEUFzhXlesutfHyTo9oo1.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch"},"note":"Add migration actions","date":"2025-12-24T08:09:20.483794300Z"} \ No newline at end of file diff --git a/crates/vespertide-cli/src/commands/diff.rs b/crates/vespertide-cli/src/commands/diff.rs index c4114d4..32f78e4 100644 --- a/crates/vespertide-cli/src/commands/diff.rs +++ b/crates/vespertide-cli/src/commands/diff.rs @@ -89,6 +89,57 @@ fn format_action(action: &MigrationAction) -> String { column.bright_cyan().bold() ) } + MigrationAction::ModifyColumnNullable { + table, + column, + nullable, + .. + } => { + let nullability = if *nullable { "NULL" } else { "NOT NULL" }; + format!( + "{} {}.{} {} {}", + "Modify column nullability:".bright_yellow(), + table.bright_cyan(), + column.bright_cyan().bold(), + "->".bright_white(), + nullability.bright_cyan().bold() + ) + } + MigrationAction::ModifyColumnDefault { + table, + column, + new_default, + } => { + let default_display = new_default.as_deref().unwrap_or("(none)"); + format!( + "{} {}.{} {} {}", + "Modify column default:".bright_yellow(), + table.bright_cyan(), + column.bright_cyan().bold(), + "->".bright_white(), + default_display.bright_cyan().bold() + ) + } + MigrationAction::ModifyColumnComment { + table, + column, + new_comment, + } => { + let comment_display = new_comment.as_deref().unwrap_or("(none)"); + let truncated = if comment_display.len() > 30 { + format!("{}...", &comment_display[..27]) + } else { + comment_display.to_string() + }; + format!( + "{} {}.{} {} '{}'", + "Modify column comment:".bright_yellow(), + table.bright_cyan(), + column.bright_cyan().bold(), + "->".bright_white(), + truncated.bright_cyan().bold() + ) + } MigrationAction::RenameTable { from, to } => { format!( "{} {} {} {}", @@ -393,6 +444,113 @@ mod tests { "users".bright_cyan() ) )] + #[case( + MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: false, + fill_with: None, + }, + format!( + "{} {}.{} {} {}", + "Modify column nullability:".bright_yellow(), + "users".bright_cyan(), + "email".bright_cyan().bold(), + "->".bright_white(), + "NOT NULL".bright_cyan().bold() + ) + )] + #[case( + MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: true, + fill_with: None, + }, + format!( + "{} {}.{} {} {}", + "Modify column nullability:".bright_yellow(), + "users".bright_cyan(), + "email".bright_cyan().bold(), + "->".bright_white(), + "NULL".bright_cyan().bold() + ) + )] + #[case( + MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: Some("'active'".into()), + }, + format!( + "{} {}.{} {} {}", + "Modify column default:".bright_yellow(), + "users".bright_cyan(), + "status".bright_cyan().bold(), + "->".bright_white(), + "'active'".bright_cyan().bold() + ) + )] + #[case( + MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: None, + }, + format!( + "{} {}.{} {} {}", + "Modify column default:".bright_yellow(), + "users".bright_cyan(), + "status".bright_cyan().bold(), + "->".bright_white(), + "(none)".bright_cyan().bold() + ) + )] + #[case( + MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: Some("User email address".into()), + }, + format!( + "{} {}.{} {} '{}'", + "Modify column comment:".bright_yellow(), + "users".bright_cyan(), + "email".bright_cyan().bold(), + "->".bright_white(), + "User email address".bright_cyan().bold() + ) + )] + #[case( + MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: None, + }, + format!( + "{} {}.{} {} '{}'", + "Modify column comment:".bright_yellow(), + "users".bright_cyan(), + "email".bright_cyan().bold(), + "->".bright_white(), + "(none)".bright_cyan().bold() + ) + )] + #[case( + MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: Some("This is a very long comment that exceeds thirty characters and should be truncated".into()), + }, + format!( + "{} {}.{} {} '{}'", + "Modify column comment:".bright_yellow(), + "users".bright_cyan(), + "email".bright_cyan().bold(), + "->".bright_white(), + "This is a very long comment...".bright_cyan().bold() + ) + )] #[serial] fn format_action_cases(#[case] action: MigrationAction, #[case] expected: String) { assert_eq!(format_action(&action), expected); diff --git a/crates/vespertide-core/src/action.rs b/crates/vespertide-core/src/action.rs index ff21146..34a8cfe 100644 --- a/crates/vespertide-core/src/action.rs +++ b/crates/vespertide-core/src/action.rs @@ -44,6 +44,25 @@ pub enum MigrationAction { column: ColumnName, new_type: ColumnType, }, + ModifyColumnNullable { + table: TableName, + column: ColumnName, + nullable: bool, + /// Required when changing from nullable to non-nullable to backfill existing NULL values. + fill_with: Option, + }, + ModifyColumnDefault { + table: TableName, + column: ColumnName, + /// The new default value, or None to remove the default. + new_default: Option, + }, + ModifyColumnComment { + table: TableName, + column: ColumnName, + /// The new comment, or None to remove the comment. + new_comment: Option, + }, AddConstraint { table: TableName, constraint: TableConstraint, @@ -82,6 +101,42 @@ impl fmt::Display for MigrationAction { MigrationAction::ModifyColumnType { table, column, .. } => { write!(f, "ModifyColumnType: {}.{}", table, column) } + MigrationAction::ModifyColumnNullable { + table, + column, + nullable, + .. + } => { + let nullability = if *nullable { "NULL" } else { "NOT NULL" }; + write!(f, "ModifyColumnNullable: {}.{} -> {}", table, column, nullability) + } + MigrationAction::ModifyColumnDefault { + table, + column, + new_default, + } => { + if let Some(default) = new_default { + write!(f, "ModifyColumnDefault: {}.{} -> {}", table, column, default) + } else { + write!(f, "ModifyColumnDefault: {}.{} -> (none)", table, column) + } + } + MigrationAction::ModifyColumnComment { + table, + column, + new_comment, + } => { + if let Some(comment) = new_comment { + let display = if comment.len() > 30 { + format!("{}...", &comment[..27]) + } else { + comment.clone() + }; + write!(f, "ModifyColumnComment: {}.{} -> '{}'", table, column, display) + } else { + write!(f, "ModifyColumnComment: {}.{} -> (none)", table, column) + } + } MigrationAction::AddConstraint { table, constraint } => { let constraint_name = match constraint { TableConstraint::PrimaryKey { .. } => "PRIMARY KEY", @@ -438,4 +493,95 @@ mod tests { assert!(result.ends_with("...")); assert!(result.len() > 10); } + + #[rstest] + #[case::modify_column_nullable_to_not_null( + MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: false, + fill_with: None, + }, + "ModifyColumnNullable: users.email -> NOT NULL" + )] + #[case::modify_column_nullable_to_null( + MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: true, + fill_with: None, + }, + "ModifyColumnNullable: users.email -> NULL" + )] + fn test_display_modify_column_nullable( + #[case] action: MigrationAction, + #[case] expected: &str, + ) { + assert_eq!(action.to_string(), expected); + } + + #[rstest] + #[case::modify_column_default_set( + MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: Some("'active'".into()), + }, + "ModifyColumnDefault: users.status -> 'active'" + )] + #[case::modify_column_default_drop( + MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: None, + }, + "ModifyColumnDefault: users.status -> (none)" + )] + fn test_display_modify_column_default( + #[case] action: MigrationAction, + #[case] expected: &str, + ) { + assert_eq!(action.to_string(), expected); + } + + #[rstest] + #[case::modify_column_comment_set( + MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: Some("User email address".into()), + }, + "ModifyColumnComment: users.email -> 'User email address'" + )] + #[case::modify_column_comment_drop( + MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: None, + }, + "ModifyColumnComment: users.email -> (none)" + )] + fn test_display_modify_column_comment( + #[case] action: MigrationAction, + #[case] expected: &str, + ) { + assert_eq!(action.to_string(), expected); + } + + #[test] + fn test_display_modify_column_comment_long() { + // Test truncation for long comments (> 30 chars) + let action = MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: Some( + "This is a very long comment that should be truncated in display".into(), + ), + }; + let result = action.to_string(); + assert!(result.contains("...")); + assert!(result.contains("This is a very long comment")); + // Should be truncated at 27 chars + "..." + assert!(!result.contains("truncated in display")); + } } diff --git a/crates/vespertide-planner/src/apply.rs b/crates/vespertide-planner/src/apply.rs index 095bdd6..9dd8b2d 100644 --- a/crates/vespertide-planner/src/apply.rs +++ b/crates/vespertide-planner/src/apply.rs @@ -96,6 +96,58 @@ pub fn apply_action( col.r#type = new_type.clone(); Ok(()) } + MigrationAction::ModifyColumnNullable { + table, + column, + nullable, + fill_with: _, + } => { + let tbl = schema + .iter_mut() + .find(|t| t.name == *table) + .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?; + let col = tbl + .columns + .iter_mut() + .find(|c| c.name == *column) + .ok_or_else(|| PlannerError::ColumnNotFound(table.clone(), column.clone()))?; + col.nullable = *nullable; + Ok(()) + } + MigrationAction::ModifyColumnDefault { + table, + column, + new_default, + } => { + let tbl = schema + .iter_mut() + .find(|t| t.name == *table) + .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?; + let col = tbl + .columns + .iter_mut() + .find(|c| c.name == *column) + .ok_or_else(|| PlannerError::ColumnNotFound(table.clone(), column.clone()))?; + col.default = new_default.as_ref().map(|s| s.as_str().into()); + Ok(()) + } + MigrationAction::ModifyColumnComment { + table, + column, + new_comment, + } => { + let tbl = schema + .iter_mut() + .find(|t| t.name == *table) + .ok_or_else(|| PlannerError::TableNotFound(table.clone()))?; + let col = tbl + .columns + .iter_mut() + .find(|c| c.name == *column) + .ok_or_else(|| PlannerError::ColumnNotFound(table.clone(), column.clone()))?; + col.comment = new_comment.clone(); + Ok(()) + } MigrationAction::RenameTable { from, to } => { if schema.iter().any(|t| t.name == *to) { Err(PlannerError::TableExists(to.clone())) @@ -1087,4 +1139,242 @@ mod tests { // Inline index cleared assert!(schema[0].columns[0].index.is_none()); } + + // Tests for ModifyColumnNullable + #[test] + fn apply_modify_column_nullable_success() { + let mut schema = vec![table( + "users", + vec![col("email", ColumnType::Simple(SimpleColumnType::Text))], + vec![], + )]; + + // Initially nullable: true (from col helper) + assert!(schema[0].columns[0].nullable); + + apply_action( + &mut schema, + &MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: false, + fill_with: None, + }, + ) + .unwrap(); + + assert!(!schema[0].columns[0].nullable); + } + + #[test] + fn apply_modify_column_nullable_table_not_found() { + let mut schema = vec![]; + + let err = apply_action( + &mut schema, + &MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: false, + fill_with: None, + }, + ) + .unwrap_err(); + + assert_err_kind(err, ErrKind::TableNotFound); + } + + #[test] + fn apply_modify_column_nullable_column_not_found() { + let mut schema = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![], + )]; + + let err = apply_action( + &mut schema, + &MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: false, + fill_with: None, + }, + ) + .unwrap_err(); + + assert_err_kind(err, ErrKind::ColumnNotFound); + } + + // Tests for ModifyColumnDefault + #[test] + fn apply_modify_column_default_set() { + let mut schema = vec![table( + "users", + vec![col("status", ColumnType::Simple(SimpleColumnType::Text))], + vec![], + )]; + + // Initially no default + assert!(schema[0].columns[0].default.is_none()); + + apply_action( + &mut schema, + &MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: Some("'active'".into()), + }, + ) + .unwrap(); + + assert_eq!( + schema[0].columns[0].default, + Some(vespertide_core::StringOrBool::String("'active'".into())) + ); + } + + #[test] + fn apply_modify_column_default_drop() { + let mut col_with_default = col("status", ColumnType::Simple(SimpleColumnType::Text)); + col_with_default.default = Some(vespertide_core::StringOrBool::String("'active'".into())); + + let mut schema = vec![table("users", vec![col_with_default], vec![])]; + + apply_action( + &mut schema, + &MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: None, + }, + ) + .unwrap(); + + assert!(schema[0].columns[0].default.is_none()); + } + + #[test] + fn apply_modify_column_default_table_not_found() { + let mut schema = vec![]; + + let err = apply_action( + &mut schema, + &MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: Some("'active'".into()), + }, + ) + .unwrap_err(); + + assert_err_kind(err, ErrKind::TableNotFound); + } + + #[test] + fn apply_modify_column_default_column_not_found() { + let mut schema = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![], + )]; + + let err = apply_action( + &mut schema, + &MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: Some("'active'".into()), + }, + ) + .unwrap_err(); + + assert_err_kind(err, ErrKind::ColumnNotFound); + } + + // Tests for ModifyColumnComment + #[test] + fn apply_modify_column_comment_set() { + let mut schema = vec![table( + "users", + vec![col("email", ColumnType::Simple(SimpleColumnType::Text))], + vec![], + )]; + + // Initially no comment + assert!(schema[0].columns[0].comment.is_none()); + + apply_action( + &mut schema, + &MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: Some("User email address".into()), + }, + ) + .unwrap(); + + assert_eq!( + schema[0].columns[0].comment, + Some("User email address".into()) + ); + } + + #[test] + fn apply_modify_column_comment_drop() { + let mut col_with_comment = col("email", ColumnType::Simple(SimpleColumnType::Text)); + col_with_comment.comment = Some("User email address".into()); + + let mut schema = vec![table("users", vec![col_with_comment], vec![])]; + + apply_action( + &mut schema, + &MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: None, + }, + ) + .unwrap(); + + assert!(schema[0].columns[0].comment.is_none()); + } + + #[test] + fn apply_modify_column_comment_table_not_found() { + let mut schema = vec![]; + + let err = apply_action( + &mut schema, + &MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: Some("User email".into()), + }, + ) + .unwrap_err(); + + assert_err_kind(err, ErrKind::TableNotFound); + } + + #[test] + fn apply_modify_column_comment_column_not_found() { + let mut schema = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![], + )]; + + let err = apply_action( + &mut schema, + &MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: Some("User email".into()), + }, + ) + .unwrap_err(); + + assert_err_kind(err, ErrKind::ColumnNotFound); + } } diff --git a/crates/vespertide-planner/src/diff.rs b/crates/vespertide-planner/src/diff.rs index 65edb2c..addd4bd 100644 --- a/crates/vespertide-planner/src/diff.rs +++ b/crates/vespertide-planner/src/diff.rs @@ -266,7 +266,7 @@ pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result Result Result Result) -> TableConstraint { + TableConstraint::PrimaryKey { + auto_increment: false, + columns: columns.into_iter().map(|s| s.to_string()).collect(), + } + } + #[test] - fn create_table_with_inline_index() { - let base = [table( + fn add_column_to_composite_pk() { + // Primary key: [id] -> [id, tenant_id] + let from = vec![table( "users", vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: Some(StrOrBoolOrArray::Bool(false)), - foreign_key: None, - }, - ColumnDef { - name: "name".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: Some(StrOrBoolOrArray::Bool(true)), - index: Some(StrOrBoolOrArray::Bool(true)), - foreign_key: None, - }, + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), ], - vec![], + vec![pk(vec!["id"])], )]; - let plan = diff_schemas(&[], &base).unwrap(); - assert_eq!(plan.actions.len(), 1); - assert_debug_snapshot!(plan.actions); + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(vec!["id", "tenant_id"])], + )]; - let plan = diff_schemas( - &base, - &[table( - "users", - vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: Some(StrOrBoolOrArray::Bool(false)), - foreign_key: None, - }, - ColumnDef { - name: "name".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Text), - nullable: true, - default: None, - comment: None, - primary_key: None, - unique: Some(StrOrBoolOrArray::Bool(true)), - index: Some(StrOrBoolOrArray::Bool(false)), - foreign_key: None, - }, - ], - vec![], - )], - ) - .unwrap(); + let plan = diff_schemas(&from, &to).unwrap(); - assert_eq!(plan.actions.len(), 1); - assert_debug_snapshot!(plan.actions); + // Should remove old PK and add new composite PK + assert_eq!(plan.actions.len(), 2); + + let has_remove = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::RemoveConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string()] + ) + }); + assert!(has_remove, "Should have RemoveConstraint for old PK"); + + let has_add = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::AddConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string(), "tenant_id".to_string()] + ) + }); + assert!(has_add, "Should have AddConstraint for new composite PK"); } - #[rstest] - #[case( - "add_index", - vec![table( + #[test] + fn remove_column_from_composite_pk() { + // Primary key: [id, tenant_id] -> [id] + let from = vec![table( "users", vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: None, - foreign_key: None, - }, + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), ], - vec![], - )], - vec![table( + vec![pk(vec!["id", "tenant_id"])], + )]; + + let to = vec![table( "users", vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: Some(StrOrBoolOrArray::Bool(true)), - foreign_key: None, - }, + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), ], - vec![], - )], - )] - #[case( - "remove_index", - vec![table( + vec![pk(vec!["id"])], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + // Should remove old composite PK and add new single-column PK + assert_eq!(plan.actions.len(), 2); + + let has_remove = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::RemoveConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string(), "tenant_id".to_string()] + ) + }); + assert!(has_remove, "Should have RemoveConstraint for old composite PK"); + + let has_add = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::AddConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string()] + ) + }); + assert!(has_add, "Should have AddConstraint for new single-column PK"); + } + + #[test] + fn change_pk_columns_entirely() { + // Primary key: [id] -> [uuid] + let from = vec![table( "users", vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: Some(StrOrBoolOrArray::Bool(true)), - foreign_key: None, - }, + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("uuid", ColumnType::Simple(SimpleColumnType::Text)), ], - vec![], - )], - vec![table( + vec![pk(vec!["id"])], + )]; + + let to = vec![table( "users", vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: Some(StrOrBoolOrArray::Bool(false)), - foreign_key: None, - }, + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("uuid", ColumnType::Simple(SimpleColumnType::Text)), ], - vec![], - )], - )] - #[case( - "add_named_index", - vec![table( + vec![pk(vec!["uuid"])], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 2); + + let has_remove = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::RemoveConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string()] + ) + }); + assert!(has_remove, "Should have RemoveConstraint for old PK"); + + let has_add = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::AddConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["uuid".to_string()] + ) + }); + assert!(has_add, "Should have AddConstraint for new PK"); + } + + #[test] + fn add_multiple_columns_to_composite_pk() { + // Primary key: [id] -> [id, tenant_id, region_id] + let from = vec![table( "users", vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: None, - foreign_key: None, - }, + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("region_id", ColumnType::Simple(SimpleColumnType::Integer)), ], - vec![], - )], - vec![table( + vec![pk(vec!["id"])], + )]; + + let to = vec![table( "users", vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: Some(StrOrBoolOrArray::Str("hello".to_string())), - foreign_key: None, - }, + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("region_id", ColumnType::Simple(SimpleColumnType::Integer)), ], - vec![], - )], - )] - #[case( - "remove_named_index", - vec![table( + vec![pk(vec!["id", "tenant_id", "region_id"])], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 2); + + let has_remove = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::RemoveConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string()] + ) + }); + assert!(has_remove, "Should have RemoveConstraint for old single-column PK"); + + let has_add = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::AddConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec![ + "id".to_string(), + "tenant_id".to_string(), + "region_id".to_string() + ] + ) + }); + assert!(has_add, "Should have AddConstraint for new 3-column composite PK"); + } + + #[test] + fn remove_multiple_columns_from_composite_pk() { + // Primary key: [id, tenant_id, region_id] -> [id] + let from = vec![table( "users", vec![ - ColumnDef { - name: "id".to_string(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: Some(PrimaryKeySyntax::Bool(true)), - unique: None, - index: Some(StrOrBoolOrArray::Str("hello".to_string())), - foreign_key: None, - }, + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("region_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(vec!["id", "tenant_id", "region_id"])], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("region_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(vec!["id"])], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 2); + + let has_remove = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::RemoveConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec![ + "id".to_string(), + "tenant_id".to_string(), + "region_id".to_string() + ] + ) + }); + assert!(has_remove, "Should have RemoveConstraint for old 3-column composite PK"); + + let has_add = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::AddConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string()] + ) + }); + assert!(has_add, "Should have AddConstraint for new single-column PK"); + } + + #[test] + fn change_composite_pk_columns_partially() { + // Primary key: [id, tenant_id] -> [id, region_id] + // One column kept, one removed, one added + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("region_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(vec!["id", "tenant_id"])], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)), + col("region_id", ColumnType::Simple(SimpleColumnType::Integer)), + ], + vec![pk(vec!["id", "region_id"])], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 2); + + let has_remove = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::RemoveConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string(), "tenant_id".to_string()] + ) + }); + assert!(has_remove, "Should have RemoveConstraint for old PK with tenant_id"); + + let has_add = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::AddConstraint { + table, + constraint: TableConstraint::PrimaryKey { columns, .. } + } if table == "users" && columns == &vec!["id".to_string(), "region_id".to_string()] + ) + }); + assert!(has_add, "Should have AddConstraint for new PK with region_id"); + } + } + + mod default_changes { + use super::*; + + fn col_with_default(name: &str, ty: ColumnType, default: Option<&str>) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable: true, + default: default.map(|s| s.into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + #[test] + fn add_default_value() { + // Column: no default -> has default + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default("status", ColumnType::Simple(SimpleColumnType::Text), None), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "status", + ColumnType::Simple(SimpleColumnType::Text), + Some("'active'"), + ), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::ModifyColumnDefault { + table, + column, + new_default: Some(default), + } if table == "users" && column == "status" && default == "'active'" + )); + } + + #[test] + fn remove_default_value() { + // Column: has default -> no default + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "status", + ColumnType::Simple(SimpleColumnType::Text), + Some("'active'"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default("status", ColumnType::Simple(SimpleColumnType::Text), None), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::ModifyColumnDefault { + table, + column, + new_default: None, + } if table == "users" && column == "status" + )); + } + + #[test] + fn change_default_value() { + // Column: 'active' -> 'pending' + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "status", + ColumnType::Simple(SimpleColumnType::Text), + Some("'active'"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "status", + ColumnType::Simple(SimpleColumnType::Text), + Some("'pending'"), + ), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::ModifyColumnDefault { + table, + column, + new_default: Some(default), + } if table == "users" && column == "status" && default == "'pending'" + )); + } + + #[test] + fn no_change_same_default() { + // Column: same default -> no action + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "status", + ColumnType::Simple(SimpleColumnType::Text), + Some("'active'"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "status", + ColumnType::Simple(SimpleColumnType::Text), + Some("'active'"), + ), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert!(plan.actions.is_empty()); + } + + #[test] + fn multiple_columns_default_changes() { + // Multiple columns with default changes + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default("status", ColumnType::Simple(SimpleColumnType::Text), None), + col_with_default( + "role", + ColumnType::Simple(SimpleColumnType::Text), + Some("'user'"), + ), + col_with_default( + "active", + ColumnType::Simple(SimpleColumnType::Boolean), + Some("true"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "status", + ColumnType::Simple(SimpleColumnType::Text), + Some("'pending'"), + ), // None -> 'pending' + col_with_default("role", ColumnType::Simple(SimpleColumnType::Text), None), // 'user' -> None + col_with_default( + "active", + ColumnType::Simple(SimpleColumnType::Boolean), + Some("true"), + ), // no change + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 2); + + let has_status_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnDefault { + table, + column, + new_default: Some(default), + } if table == "users" && column == "status" && default == "'pending'" + ) + }); + assert!(has_status_change, "Should detect status default added"); + + let has_role_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnDefault { + table, + column, + new_default: None, + } if table == "users" && column == "role" + ) + }); + assert!(has_role_change, "Should detect role default removed"); + } + + #[test] + fn default_change_with_type_change() { + // Column changing both type and default + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "count", + ColumnType::Simple(SimpleColumnType::Integer), + Some("0"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_default( + "count", + ColumnType::Simple(SimpleColumnType::Text), + Some("'0'"), + ), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + // Should generate both ModifyColumnType and ModifyColumnDefault + assert_eq!(plan.actions.len(), 2); + + let has_type_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnType { table, column, .. } + if table == "users" && column == "count" + ) + }); + assert!(has_type_change, "Should detect type change"); + + let has_default_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnDefault { + table, + column, + new_default: Some(default), + } if table == "users" && column == "count" && default == "'0'" + ) + }); + assert!(has_default_change, "Should detect default change"); + } + } + + mod comment_changes { + use super::*; + + fn col_with_comment(name: &str, ty: ColumnType, comment: Option<&str>) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable: true, + default: None, + comment: comment.map(|s| s.to_string()), + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + #[test] + fn add_comment() { + // Column: no comment -> has comment + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), None), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment( + "email", + ColumnType::Simple(SimpleColumnType::Text), + Some("User's email address"), + ), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::ModifyColumnComment { + table, + column, + new_comment: Some(comment), + } if table == "users" && column == "email" && comment == "User's email address" + )); + } + + #[test] + fn remove_comment() { + // Column: has comment -> no comment + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment( + "email", + ColumnType::Simple(SimpleColumnType::Text), + Some("User's email address"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), None), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::ModifyColumnComment { + table, + column, + new_comment: None, + } if table == "users" && column == "email" + )); + } + + #[test] + fn change_comment() { + // Column: 'old comment' -> 'new comment' + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment( + "email", + ColumnType::Simple(SimpleColumnType::Text), + Some("Old comment"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment( + "email", + ColumnType::Simple(SimpleColumnType::Text), + Some("New comment"), + ), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::ModifyColumnComment { + table, + column, + new_comment: Some(comment), + } if table == "users" && column == "email" && comment == "New comment" + )); + } + + #[test] + fn no_change_same_comment() { + // Column: same comment -> no action + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment( + "email", + ColumnType::Simple(SimpleColumnType::Text), + Some("Same comment"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment( + "email", + ColumnType::Simple(SimpleColumnType::Text), + Some("Same comment"), + ), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert!(plan.actions.is_empty()); + } + + #[test] + fn multiple_columns_comment_changes() { + // Multiple columns with comment changes + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), None), + col_with_comment( + "name", + ColumnType::Simple(SimpleColumnType::Text), + Some("User name"), + ), + col_with_comment( + "phone", + ColumnType::Simple(SimpleColumnType::Text), + Some("Phone number"), + ), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_with_comment( + "email", + ColumnType::Simple(SimpleColumnType::Text), + Some("Email address"), + ), // None -> "Email address" + col_with_comment("name", ColumnType::Simple(SimpleColumnType::Text), None), // "User name" -> None + col_with_comment( + "phone", + ColumnType::Simple(SimpleColumnType::Text), + Some("Phone number"), + ), // no change + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 2); + + let has_email_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnComment { + table, + column, + new_comment: Some(comment), + } if table == "users" && column == "email" && comment == "Email address" + ) + }); + assert!(has_email_change, "Should detect email comment added"); + + let has_name_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnComment { + table, + column, + new_comment: None, + } if table == "users" && column == "name" + ) + }); + assert!(has_name_change, "Should detect name comment removed"); + } + + #[test] + fn comment_change_with_nullable_change() { + // Column changing both nullable and comment + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + { + let mut c = + col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), None); + c.nullable = true; + c + }, + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + { + let mut c = col_with_comment( + "email", + ColumnType::Simple(SimpleColumnType::Text), + Some("Required email"), + ); + c.nullable = false; + c + }, + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + // Should generate both ModifyColumnNullable and ModifyColumnComment + assert_eq!(plan.actions.len(), 2); + + let has_nullable_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnNullable { + table, + column, + nullable: false, + .. + } if table == "users" && column == "email" + ) + }); + assert!(has_nullable_change, "Should detect nullable change"); + + let has_comment_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnComment { + table, + column, + new_comment: Some(comment), + } if table == "users" && column == "email" && comment == "Required email" + ) + }); + assert!(has_comment_change, "Should detect comment change"); + } + } + + mod nullable_changes { + use super::*; + + fn col_nullable(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + #[test] + fn column_nullable_to_non_nullable() { + // Column: nullable -> non-nullable + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), true), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), false), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::ModifyColumnNullable { + table, + column, + nullable: false, + fill_with: None, + } if table == "users" && column == "email" + )); + } + + #[test] + fn column_non_nullable_to_nullable() { + // Column: non-nullable -> nullable + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), false), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), true), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::ModifyColumnNullable { + table, + column, + nullable: true, + fill_with: None, + } if table == "users" && column == "email" + )); + } + + #[test] + fn multiple_columns_nullable_changes() { + // Multiple columns changing nullability at once + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), true), + col_nullable("name", ColumnType::Simple(SimpleColumnType::Text), false), + col_nullable("phone", ColumnType::Simple(SimpleColumnType::Text), true), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), false), // nullable -> non-nullable + col_nullable("name", ColumnType::Simple(SimpleColumnType::Text), true), // non-nullable -> nullable + col_nullable("phone", ColumnType::Simple(SimpleColumnType::Text), true), // no change + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + assert_eq!(plan.actions.len(), 2); + + let has_email_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnNullable { + table, + column, + nullable: false, + .. + } if table == "users" && column == "email" + ) + }); + assert!(has_email_change, "Should detect email nullable -> non-nullable"); + + let has_name_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnNullable { + table, + column, + nullable: true, + .. + } if table == "users" && column == "name" + ) + }); + assert!(has_name_change, "Should detect name non-nullable -> nullable"); + } + + #[test] + fn nullable_change_with_type_change() { + // Column changing both type and nullability + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_nullable("age", ColumnType::Simple(SimpleColumnType::Integer), true), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col_nullable("age", ColumnType::Simple(SimpleColumnType::Text), false), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + + // Should generate both ModifyColumnType and ModifyColumnNullable + assert_eq!(plan.actions.len(), 2); + + let has_type_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnType { table, column, .. } + if table == "users" && column == "age" + ) + }); + assert!(has_type_change, "Should detect type change"); + + let has_nullable_change = plan.actions.iter().any(|a| { + matches!( + a, + MigrationAction::ModifyColumnNullable { + table, + column, + nullable: false, + .. + } if table == "users" && column == "age" + ) + }); + assert!(has_nullable_change, "Should detect nullable change"); + } + } + + mod diff_tables { + use insta::assert_debug_snapshot; + + use super::*; + + #[test] + fn create_table_with_inline_index() { + let base = [table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(false)), + foreign_key: None, + }, + ColumnDef { + name: "name".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: Some(StrOrBoolOrArray::Bool(true)), + index: Some(StrOrBoolOrArray::Bool(true)), + foreign_key: None, + }, + ], + vec![], + )]; + let plan = diff_schemas(&[], &base).unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert_debug_snapshot!(plan.actions); + + let plan = diff_schemas( + &base, + &[table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(false)), + foreign_key: None, + }, + ColumnDef { + name: "name".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: Some(StrOrBoolOrArray::Bool(true)), + index: Some(StrOrBoolOrArray::Bool(false)), + foreign_key: None, + }, + ], + vec![], + )], + ) + .unwrap(); + + assert_eq!(plan.actions.len(), 1); + assert_debug_snapshot!(plan.actions); + } + + #[rstest] + #[case( + "add_index", + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: None, + foreign_key: None, + }, + ], + vec![], + )], + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(true)), + foreign_key: None, + }, + ], + vec![], + )], + )] + #[case( + "remove_index", + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(true)), + foreign_key: None, + }, + ], + vec![], + )], + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Bool(false)), + foreign_key: None, + }, + ], + vec![], + )], + )] + #[case( + "add_named_index", + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: None, + foreign_key: None, + }, + ], + vec![], + )], + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Str("hello".to_string())), + foreign_key: None, + }, + ], + vec![], + )], + )] + #[case( + "remove_named_index", + vec![table( + "users", + vec![ + ColumnDef { + name: "id".to_string(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: Some(PrimaryKeySyntax::Bool(true)), + unique: None, + index: Some(StrOrBoolOrArray::Str("hello".to_string())), + foreign_key: None, + }, ], vec![], )], @@ -1914,4 +3066,111 @@ mod tests { }); } } + + // Explicit coverage tests for lines that tarpaulin might miss in rstest + mod coverage_explicit { + use super::*; + + #[test] + fn delete_column_explicit() { + // Covers lines 292-294: DeleteColumn action inside modified table loop + let from = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("name", ColumnType::Simple(SimpleColumnType::Text)), + ], + vec![], + )]; + + let to = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::DeleteColumn { table, column } + if table == "users" && column == "name" + )); + } + + #[test] + fn add_column_explicit() { + // Covers lines 359-362: AddColumn action inside modified table loop + let from = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![], + )]; + + let to = vec![table( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer)), + col("email", ColumnType::Simple(SimpleColumnType::Text)), + ], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::AddColumn { table, column, .. } + if table == "users" && column.name == "email" + )); + } + + #[test] + fn remove_constraint_explicit() { + // Covers lines 370-372: RemoveConstraint action inside modified table loop + let from = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![idx("idx_users_id", vec!["id"])], + )]; + + let to = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::RemoveConstraint { table, constraint } + if table == "users" && matches!(constraint, TableConstraint::Index { name: Some(n), .. } if n == "idx_users_id") + )); + } + + #[test] + fn add_constraint_explicit() { + // Covers lines 378-380: AddConstraint action inside modified table loop + let from = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![], + )]; + + let to = vec![table( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], + vec![idx("idx_users_id", vec!["id"])], + )]; + + let plan = diff_schemas(&from, &to).unwrap(); + assert_eq!(plan.actions.len(), 1); + assert!(matches!( + &plan.actions[0], + MigrationAction::AddConstraint { table, constraint } + if table == "users" && matches!(constraint, TableConstraint::Index { name: Some(n), .. } if n == "idx_users_id") + )); + } + } } diff --git a/crates/vespertide-planner/src/validate.rs b/crates/vespertide-planner/src/validate.rs index b25d245..eed711b 100644 --- a/crates/vespertide-planner/src/validate.rs +++ b/crates/vespertide-planner/src/validate.rs @@ -259,21 +259,35 @@ fn validate_constraint( /// Validate a migration plan for correctness. /// Checks for: /// - AddColumn actions with NOT NULL columns without default must have fill_with +/// - ModifyColumnNullable actions changing from nullable to non-nullable must have fill_with pub fn validate_migration_plan(plan: &MigrationPlan) -> Result<(), PlannerError> { for action in &plan.actions { - if let MigrationAction::AddColumn { - table, - column, - fill_with, - } = action - { - // If column is NOT NULL and has no default, fill_with is required - if !column.nullable && column.default.is_none() && fill_with.is_none() { - return Err(PlannerError::MissingFillWith( - table.clone(), - column.name.clone(), - )); + match action { + MigrationAction::AddColumn { + table, + column, + fill_with, + } => { + // If column is NOT NULL and has no default, fill_with is required + if !column.nullable && column.default.is_none() && fill_with.is_none() { + return Err(PlannerError::MissingFillWith( + table.clone(), + column.name.clone(), + )); + } } + MigrationAction::ModifyColumnNullable { + table, + column, + nullable, + fill_with, + } => { + // If changing from nullable to non-nullable, fill_with is required + if !nullable && fill_with.is_none() { + return Err(PlannerError::MissingFillWith(table.clone(), column.clone())); + } + } + _ => {} } } Ok(()) @@ -897,4 +911,66 @@ mod tests { let result = validate_schema(&schema); assert!(result.is_ok()); } + + #[test] + fn validate_migration_plan_modify_nullable_to_non_nullable_missing_fill_with() { + let plan = MigrationPlan { + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: false, + fill_with: None, + }], + }; + + let result = validate_migration_plan(&plan); + assert!(result.is_err()); + match result.unwrap_err() { + PlannerError::MissingFillWith(table, column) => { + assert_eq!(table, "users"); + assert_eq!(column, "email"); + } + _ => panic!("expected MissingFillWith error"), + } + } + + #[test] + fn validate_migration_plan_modify_nullable_to_non_nullable_with_fill_with() { + let plan = MigrationPlan { + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: false, + fill_with: Some("'unknown'".into()), + }], + }; + + let result = validate_migration_plan(&plan); + assert!(result.is_ok()); + } + + #[test] + fn validate_migration_plan_modify_non_nullable_to_nullable() { + // Changing from non-nullable to nullable does NOT require fill_with + let plan = MigrationPlan { + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: true, + fill_with: None, + }], + }; + + let result = validate_migration_plan(&plan); + assert!(result.is_ok()); + } } diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 6d5f9b1..8ed9994 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -157,10 +157,10 @@ pub fn reference_action_sql(action: &ReferenceAction) -> &'static str { /// 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 { + "gen_random_uuid()" | "UUID()" | "lower(hex(randomblob(16)))" => match backend { DatabaseBackend::Postgres => "gen_random_uuid()".to_string(), DatabaseBackend::MySql => "(UUID())".to_string(), - DatabaseBackend::Sqlite => "(lower(hex(randomblob(16))))".to_string(), + DatabaseBackend::Sqlite => "lower(hex(randomblob(16)))".to_string(), }, "current_timestamp()" | "now()" | "CURRENT_TIMESTAMP" => match backend { DatabaseBackend::Postgres => "CURRENT_TIMESTAMP".to_string(), @@ -411,7 +411,7 @@ mod tests { #[case::gen_random_uuid_sqlite( "gen_random_uuid()", DatabaseBackend::Sqlite, - "(lower(hex(randomblob(16))))" + "lower(hex(randomblob(16)))" )] #[case::current_timestamp_postgres( "current_timestamp()", diff --git a/crates/vespertide-query/src/sql/mod.rs b/crates/vespertide-query/src/sql/mod.rs index a4396ef..d2326ca 100644 --- a/crates/vespertide-query/src/sql/mod.rs +++ b/crates/vespertide-query/src/sql/mod.rs @@ -4,6 +4,9 @@ pub mod create_table; pub mod delete_column; pub mod delete_table; pub mod helpers; +pub mod modify_column_comment; +pub mod modify_column_default; +pub mod modify_column_nullable; pub mod modify_column_type; pub mod raw_sql; pub mod remove_constraint; @@ -20,9 +23,12 @@ use vespertide_core::{MigrationAction, TableDef}; use self::{ add_column::build_add_column, add_constraint::build_add_constraint, create_table::build_create_table, delete_column::build_delete_column, - delete_table::build_delete_table, modify_column_type::build_modify_column_type, - raw_sql::build_raw_sql, remove_constraint::build_remove_constraint, - rename_column::build_rename_column, rename_table::build_rename_table, + delete_table::build_delete_table, modify_column_comment::build_modify_column_comment, + modify_column_default::build_modify_column_default, + modify_column_nullable::build_modify_column_nullable, + modify_column_type::build_modify_column_type, raw_sql::build_raw_sql, + remove_constraint::build_remove_constraint, rename_column::build_rename_column, + rename_table::build_rename_table, }; pub fn build_action_queries( @@ -65,6 +71,25 @@ pub fn build_action_queries( new_type, } => build_modify_column_type(backend, table, column, new_type, current_schema), + MigrationAction::ModifyColumnNullable { + table, + column, + nullable, + fill_with, + } => build_modify_column_nullable(backend, table, column, *nullable, fill_with.as_deref(), current_schema), + + MigrationAction::ModifyColumnDefault { + table, + column, + new_default, + } => build_modify_column_default(backend, table, column, new_default.as_deref(), current_schema), + + MigrationAction::ModifyColumnComment { + table, + column, + new_comment, + } => build_modify_column_comment(backend, table, column, new_comment.as_deref(), current_schema), + MigrationAction::RenameTable { from, to } => Ok(vec![build_rename_table(from, to)]), MigrationAction::RawSql { sql } => Ok(vec![build_raw_sql(sql.clone())]), @@ -1171,4 +1196,160 @@ mod tests { assert_snapshot!(sql); }); } + + /// Test build_action_queries for ModifyColumnNullable + #[rstest] + #[case::postgres_modify_nullable(DatabaseBackend::Postgres)] + #[case::mysql_modify_nullable(DatabaseBackend::MySql)] + #[case::sqlite_modify_nullable(DatabaseBackend::Sqlite)] + fn test_build_action_queries_modify_column_nullable(#[case] backend: DatabaseBackend) { + let action = MigrationAction::ModifyColumnNullable { + table: "users".into(), + column: "email".into(), + nullable: false, + fill_with: Some("'unknown'".into()), + }; + let current_schema = vec![TableDef { + name: "users".into(), + columns: vec![ColumnDef { + name: "email".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }]; + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + assert!(!result.is_empty()); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should contain UPDATE for fill_with and ALTER for nullable change + assert!(sql.contains("UPDATE")); + assert!(sql.contains("unknown")); + + let suffix = format!( + "{}_modify_nullable", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test build_action_queries for ModifyColumnDefault + #[rstest] + #[case::postgres_modify_default(DatabaseBackend::Postgres)] + #[case::mysql_modify_default(DatabaseBackend::MySql)] + #[case::sqlite_modify_default(DatabaseBackend::Sqlite)] + fn test_build_action_queries_modify_column_default(#[case] backend: DatabaseBackend) { + let action = MigrationAction::ModifyColumnDefault { + table: "users".into(), + column: "status".into(), + new_default: Some("'active'".into()), + }; + let current_schema = vec![TableDef { + name: "users".into(), + columns: vec![ColumnDef { + name: "status".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }]; + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + assert!(!result.is_empty()); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Should contain DEFAULT and 'active' + assert!(sql.contains("DEFAULT") || sql.contains("active")); + + let suffix = format!( + "{}_modify_default", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test build_action_queries for ModifyColumnComment + #[rstest] + #[case::postgres_modify_comment(DatabaseBackend::Postgres)] + #[case::mysql_modify_comment(DatabaseBackend::MySql)] + #[case::sqlite_modify_comment(DatabaseBackend::Sqlite)] + fn test_build_action_queries_modify_column_comment(#[case] backend: DatabaseBackend) { + let action = MigrationAction::ModifyColumnComment { + table: "users".into(), + column: "email".into(), + new_comment: Some("User email address".into()), + }; + let current_schema = vec![TableDef { + name: "users".into(), + columns: vec![ColumnDef { + name: "email".into(), + r#type: ColumnType::Simple(SimpleColumnType::Text), + nullable: true, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }]; + let result = build_action_queries(&backend, &action, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Postgres and MySQL should have comment, SQLite returns empty + if backend != DatabaseBackend::Sqlite { + assert!(sql.contains("COMMENT") || sql.contains("User email address")); + } + + let suffix = format!( + "{}_modify_comment", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } } diff --git a/crates/vespertide-query/src/sql/modify_column_comment.rs b/crates/vespertide-query/src/sql/modify_column_comment.rs new file mode 100644 index 0000000..2cfeba8 --- /dev/null +++ b/crates/vespertide-query/src/sql/modify_column_comment.rs @@ -0,0 +1,558 @@ +use sea_query::Alias; + +use vespertide_core::TableDef; + +use super::helpers::build_sea_column_def_with_table; +use super::types::{BuiltQuery, DatabaseBackend, RawSql}; +use crate::error::QueryError; + +/// Build SQL for changing column comment. +/// Note: SQLite does not support column comments natively. +pub fn build_modify_column_comment( + backend: &DatabaseBackend, + table: &str, + column: &str, + new_comment: Option<&str>, + current_schema: &[TableDef], +) -> Result, QueryError> { + let mut queries = Vec::new(); + + match backend { + DatabaseBackend::Postgres => { + let comment_sql = if let Some(comment) = new_comment { + // Escape single quotes in comment + let escaped = comment.replace('\'', "''"); + format!( + "COMMENT ON COLUMN \"{}\".\"{}\" IS '{}'", + table, column, escaped + ) + } else { + format!( + "COMMENT ON COLUMN \"{}\".\"{}\" IS NULL", + table, column + ) + }; + queries.push(BuiltQuery::Raw(RawSql::uniform(comment_sql))); + } + DatabaseBackend::MySql => { + // MySQL requires the full column definition in MODIFY COLUMN to change comment + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| { + QueryError::Other(format!( + "Table '{}' not found in current schema.", + table + )) + })?; + + let column_def = table_def + .columns + .iter() + .find(|c| c.name == column) + .ok_or_else(|| { + QueryError::Other(format!( + "Column '{}' not found in table '{}'.", + column, table + )) + })?; + + // Build the full column definition with updated comment + let modified_col_def = vespertide_core::ColumnDef { + comment: new_comment.map(|s| s.to_string()), + ..column_def.clone() + }; + + // Build base ALTER TABLE statement using sea-query for type/nullable/default + let sea_col = build_sea_column_def_with_table(backend, table, &modified_col_def); + + // Build the ALTER TABLE ... MODIFY COLUMN statement + let stmt = sea_query::Table::alter() + .table(Alias::new(table)) + .modify_column(sea_col) + .to_owned(); + + // Get the base SQL from sea-query + let base_sql = super::helpers::build_schema_statement(&stmt, *backend); + + // Add COMMENT clause if needed (sea-query doesn't support COMMENT) + let final_sql = if let Some(comment) = new_comment { + let escaped = comment.replace('\'', "''"); + format!("{} COMMENT '{}'", base_sql, escaped) + } else { + base_sql + }; + + queries.push(BuiltQuery::Raw(RawSql::uniform(final_sql))); + } + DatabaseBackend::Sqlite => { + // SQLite doesn't support column comments + // We could store the comment in a separate table or just ignore it + // For now, we'll skip this operation for SQLite since it doesn't affect the schema + // Just update the internal schema representation (handled by apply.rs) + } + } + + Ok(queries) +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint}; + + fn col(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + fn table_def(name: &str, columns: Vec, constraints: Vec) -> TableDef { + TableDef { + name: name.to_string(), + columns, + constraints, + } + } + + #[rstest] + #[case::postgres_set_comment(DatabaseBackend::Postgres, Some("User email address"))] + #[case::postgres_drop_comment(DatabaseBackend::Postgres, None)] + #[case::mysql_set_comment(DatabaseBackend::MySql, Some("User email address"))] + #[case::mysql_drop_comment(DatabaseBackend::MySql, None)] + #[case::sqlite_set_comment(DatabaseBackend::Sqlite, Some("User email address"))] + #[case::sqlite_drop_comment(DatabaseBackend::Sqlite, None)] + fn test_build_modify_column_comment( + #[case] backend: DatabaseBackend, + #[case] new_comment: Option<&str>, + ) { + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col("email", ColumnType::Simple(SimpleColumnType::Text), true), + ], + vec![], + )]; + + let result = build_modify_column_comment( + &backend, + "users", + "email", + new_comment, + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_{}_users", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + }, + if new_comment.is_some() { "set_comment" } else { "drop_comment" } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test comment with quotes escaping + #[rstest] + #[case::postgres_comment_with_quotes(DatabaseBackend::Postgres)] + #[case::mysql_comment_with_quotes(DatabaseBackend::MySql)] + #[case::sqlite_comment_with_quotes(DatabaseBackend::Sqlite)] + fn test_comment_with_quotes(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "users", + vec![col("email", ColumnType::Simple(SimpleColumnType::Text), true)], + vec![], + )]; + + let result = build_modify_column_comment( + &backend, + "users", + "email", + Some("User's email address"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // Postgres and MySQL should escape quotes, SQLite returns empty + if backend != DatabaseBackend::Sqlite { + assert!(sql.contains("User''s email address"), "Should escape single quotes"); + } + + let suffix = format!( + "{}_comment_with_quotes", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test table not found error + #[rstest] + #[case::postgres_table_not_found(DatabaseBackend::Postgres)] + #[case::mysql_table_not_found(DatabaseBackend::MySql)] + #[case::sqlite_table_not_found(DatabaseBackend::Sqlite)] + fn test_table_not_found(#[case] backend: DatabaseBackend) { + // Postgres and SQLite don't need schema lookup, so skip this test for them + if backend == DatabaseBackend::Postgres || backend == DatabaseBackend::Sqlite { + return; + } + + let result = build_modify_column_comment( + &backend, + "users", + "email", + Some("comment"), + &[], + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'users' not found")); + } + + /// Test column not found error + #[rstest] + #[case::postgres_column_not_found(DatabaseBackend::Postgres)] + #[case::mysql_column_not_found(DatabaseBackend::MySql)] + #[case::sqlite_column_not_found(DatabaseBackend::Sqlite)] + fn test_column_not_found(#[case] backend: DatabaseBackend) { + // Postgres and SQLite don't need schema lookup, so skip this test for them + if backend == DatabaseBackend::Postgres || backend == DatabaseBackend::Sqlite { + return; + } + + let schema = vec![table_def( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer), false)], + vec![], + )]; + + let result = build_modify_column_comment( + &backend, + "users", + "email", + Some("comment"), + &schema, + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Column 'email' not found")); + } + + /// Test with long comment + #[rstest] + #[case::postgres_long_comment(DatabaseBackend::Postgres)] + #[case::mysql_long_comment(DatabaseBackend::MySql)] + #[case::sqlite_long_comment(DatabaseBackend::Sqlite)] + fn test_long_comment(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "users", + vec![col("bio", ColumnType::Simple(SimpleColumnType::Text), true)], + vec![], + )]; + + let long_comment = "This is a very long comment that describes the bio field in great detail. It contains multiple sentences and provides thorough documentation for this column."; + + let result = build_modify_column_comment( + &backend, + "users", + "bio", + Some(long_comment), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_long_comment", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test preserves column properties when modifying comment + #[rstest] + #[case::postgres_preserves_properties(DatabaseBackend::Postgres)] + #[case::mysql_preserves_properties(DatabaseBackend::MySql)] + #[case::sqlite_preserves_properties(DatabaseBackend::Sqlite)] + fn test_preserves_column_properties(#[case] backend: DatabaseBackend) { + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text), true); + email_col.default = Some("'default@example.com'".into()); + + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + email_col, + ], + vec![], + )]; + + let result = build_modify_column_comment( + &backend, + "users", + "email", + Some("User email address"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // MySQL should preserve the default value in the MODIFY COLUMN statement + if backend == DatabaseBackend::MySql { + assert!(sql.contains("DEFAULT"), "Should preserve DEFAULT clause"); + } + + let suffix = format!( + "{}_preserves_properties", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test changing comment from one value to another + #[rstest] + #[case::postgres_change_comment(DatabaseBackend::Postgres)] + #[case::mysql_change_comment(DatabaseBackend::MySql)] + #[case::sqlite_change_comment(DatabaseBackend::Sqlite)] + fn test_change_comment(#[case] backend: DatabaseBackend) { + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text), true); + email_col.comment = Some("Old comment".into()); + + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + email_col, + ], + vec![], + )]; + + let result = build_modify_column_comment( + &backend, + "users", + "email", + Some("New comment"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_change_comment", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test dropping existing comment + #[rstest] + #[case::postgres_drop_existing_comment(DatabaseBackend::Postgres)] + #[case::mysql_drop_existing_comment(DatabaseBackend::MySql)] + #[case::sqlite_drop_existing_comment(DatabaseBackend::Sqlite)] + fn test_drop_existing_comment(#[case] backend: DatabaseBackend) { + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text), true); + email_col.comment = Some("Existing comment".into()); + + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + email_col, + ], + vec![], + )]; + + let result = build_modify_column_comment( + &backend, + "users", + "email", + None, // Drop comment + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_drop_existing_comment", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test with different column types + #[rstest] + #[case::postgres_integer_column(DatabaseBackend::Postgres, SimpleColumnType::Integer, "Auto-increment ID")] + #[case::mysql_integer_column(DatabaseBackend::MySql, SimpleColumnType::Integer, "Auto-increment ID")] + #[case::sqlite_integer_column(DatabaseBackend::Sqlite, SimpleColumnType::Integer, "Auto-increment ID")] + #[case::postgres_boolean_column(DatabaseBackend::Postgres, SimpleColumnType::Boolean, "Is user active")] + #[case::mysql_boolean_column(DatabaseBackend::MySql, SimpleColumnType::Boolean, "Is user active")] + #[case::sqlite_boolean_column(DatabaseBackend::Sqlite, SimpleColumnType::Boolean, "Is user active")] + #[case::postgres_timestamp_column(DatabaseBackend::Postgres, SimpleColumnType::Timestamp, "Creation timestamp")] + #[case::mysql_timestamp_column(DatabaseBackend::MySql, SimpleColumnType::Timestamp, "Creation timestamp")] + #[case::sqlite_timestamp_column(DatabaseBackend::Sqlite, SimpleColumnType::Timestamp, "Creation timestamp")] + fn test_comment_on_different_types( + #[case] backend: DatabaseBackend, + #[case] column_type: SimpleColumnType, + #[case] comment: &str, + ) { + let schema = vec![table_def( + "data", + vec![col("field", ColumnType::Simple(column_type.clone()), false)], + vec![], + )]; + + let result = build_modify_column_comment( + &backend, + "data", + "field", + Some(comment), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let type_name = format!("{:?}", column_type).to_lowercase(); + let suffix = format!( + "{}_{}_comment", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + }, + type_name + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test with NOT NULL column + #[rstest] + #[case::postgres_not_null_column(DatabaseBackend::Postgres)] + #[case::mysql_not_null_column(DatabaseBackend::MySql)] + #[case::sqlite_not_null_column(DatabaseBackend::Sqlite)] + fn test_comment_on_not_null_column(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "users", + vec![col("username", ColumnType::Simple(SimpleColumnType::Text), false)], + vec![], + )]; + + let result = build_modify_column_comment( + &backend, + "users", + "username", + Some("Required username"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_not_null_column", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/modify_column_default.rs b/crates/vespertide-query/src/sql/modify_column_default.rs new file mode 100644 index 0000000..fed2e6d --- /dev/null +++ b/crates/vespertide-query/src/sql/modify_column_default.rs @@ -0,0 +1,574 @@ +use sea_query::{Alias, Query, Table}; + +use vespertide_core::{ColumnDef, TableDef}; + +use super::create_table::build_create_table_for_backend; +use super::helpers::build_sea_column_def_with_table; +use super::rename_table::build_rename_table; +use super::types::{BuiltQuery, DatabaseBackend, RawSql}; +use crate::error::QueryError; + +/// Build SQL for changing column default value. +pub fn build_modify_column_default( + backend: &DatabaseBackend, + table: &str, + column: &str, + new_default: Option<&str>, + current_schema: &[TableDef], +) -> Result, QueryError> { + let mut queries = Vec::new(); + + match backend { + DatabaseBackend::Postgres => { + let alter_sql = if let Some(default_value) = new_default { + format!( + "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" SET DEFAULT {}", + table, column, default_value + ) + } else { + format!( + "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" DROP DEFAULT", + table, column + ) + }; + queries.push(BuiltQuery::Raw(RawSql::uniform(alter_sql))); + } + DatabaseBackend::MySql => { + // MySQL requires the full column definition in ALTER COLUMN + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| { + QueryError::Other(format!( + "Table '{}' not found in current schema.", + table + )) + })?; + + let column_def = table_def + .columns + .iter() + .find(|c| c.name == column) + .ok_or_else(|| { + QueryError::Other(format!( + "Column '{}' not found in table '{}'.", + column, table + )) + })?; + + // Create a modified column def with the new default + let modified_col_def = ColumnDef { + default: new_default.map(|s| s.into()), + ..column_def.clone() + }; + + let sea_col = build_sea_column_def_with_table(backend, table, &modified_col_def); + + let stmt = Table::alter() + .table(Alias::new(table)) + .modify_column(sea_col) + .to_owned(); + queries.push(BuiltQuery::AlterTable(Box::new(stmt))); + } + DatabaseBackend::Sqlite => { + // SQLite doesn't support ALTER COLUMN for default changes + // 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.", + table + )) + })?; + + // Create modified columns with the new default + let mut new_columns = table_def.columns.clone(); + if let Some(col) = new_columns.iter_mut().find(|c| c.name == column) { + col.default = new_default.map(|s| s.into()); + } + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table with modified column + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &new_columns, + &table_def.constraints, + ); + queries.push(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(); + queries.push(BuiltQuery::Insert(Box::new(insert_stmt))); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + queries.push(BuiltQuery::DropTable(Box::new(drop_table))); + + // 4. Rename temporary table to original name + queries.push(build_rename_table(&temp_table, table)); + + // 5. Recreate indexes from Index constraints + for constraint in &table_def.constraints { + if let vespertide_core::TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = constraint + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); + } + } + } + } + + Ok(queries) +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint}; + + fn col(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + fn table_def(name: &str, columns: Vec, constraints: Vec) -> TableDef { + TableDef { + name: name.to_string(), + columns, + constraints, + } + } + + #[rstest] + #[case::postgres_set_default(DatabaseBackend::Postgres, Some("'unknown'"))] + #[case::postgres_drop_default(DatabaseBackend::Postgres, None)] + #[case::mysql_set_default(DatabaseBackend::MySql, Some("'unknown'"))] + #[case::mysql_drop_default(DatabaseBackend::MySql, None)] + #[case::sqlite_set_default(DatabaseBackend::Sqlite, Some("'unknown'"))] + #[case::sqlite_drop_default(DatabaseBackend::Sqlite, None)] + fn test_build_modify_column_default( + #[case] backend: DatabaseBackend, + #[case] new_default: Option<&str>, + ) { + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col("email", ColumnType::Simple(SimpleColumnType::Text), true), + ], + vec![], + )]; + + let result = build_modify_column_default( + &backend, + "users", + "email", + new_default, + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_{}_users", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + }, + if new_default.is_some() { "set_default" } else { "drop_default" } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test table not found error + #[rstest] + #[case::postgres_table_not_found(DatabaseBackend::Postgres)] + #[case::mysql_table_not_found(DatabaseBackend::MySql)] + #[case::sqlite_table_not_found(DatabaseBackend::Sqlite)] + fn test_table_not_found(#[case] backend: DatabaseBackend) { + // Postgres doesn't need schema lookup for default changes + if backend == DatabaseBackend::Postgres { + return; + } + + let result = build_modify_column_default( + &backend, + "users", + "email", + Some("'default'"), + &[], + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'users' not found")); + } + + /// Test column not found error + #[rstest] + #[case::postgres_column_not_found(DatabaseBackend::Postgres)] + #[case::mysql_column_not_found(DatabaseBackend::MySql)] + #[case::sqlite_column_not_found(DatabaseBackend::Sqlite)] + fn test_column_not_found(#[case] backend: DatabaseBackend) { + // Postgres doesn't need schema lookup for default changes + // SQLite doesn't validate column existence in modify_column_default + if backend == DatabaseBackend::Postgres || backend == DatabaseBackend::Sqlite { + return; + } + + let schema = vec![table_def( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer), false)], + vec![], + )]; + + let result = build_modify_column_default( + &backend, + "users", + "email", + Some("'default'"), + &schema, + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Column 'email' not found")); + } + + /// Test with index - should recreate index after table rebuild (SQLite) + #[rstest] + #[case::postgres_with_index(DatabaseBackend::Postgres)] + #[case::mysql_with_index(DatabaseBackend::MySql)] + #[case::sqlite_with_index(DatabaseBackend::Sqlite)] + fn test_modify_default_with_index(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col("email", ColumnType::Simple(SimpleColumnType::Text), true), + ], + vec![TableConstraint::Index { + name: Some("idx_users_email".into()), + columns: vec!["email".into()], + }], + )]; + + let result = build_modify_column_default( + &backend, + "users", + "email", + Some("'default@example.com'"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // SQLite should recreate the index after table rebuild + if backend == DatabaseBackend::Sqlite { + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_users_email")); + } + + let suffix = format!( + "{}_with_index", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test changing default value from one to another + #[rstest] + #[case::postgres_change_default(DatabaseBackend::Postgres)] + #[case::mysql_change_default(DatabaseBackend::MySql)] + #[case::sqlite_change_default(DatabaseBackend::Sqlite)] + fn test_change_default_value(#[case] backend: DatabaseBackend) { + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text), true); + email_col.default = Some("'old@example.com'".into()); + + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + email_col, + ], + vec![], + )]; + + let result = build_modify_column_default( + &backend, + "users", + "email", + Some("'new@example.com'"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_change_default", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test with integer default value + #[rstest] + #[case::postgres_integer_default(DatabaseBackend::Postgres)] + #[case::mysql_integer_default(DatabaseBackend::MySql)] + #[case::sqlite_integer_default(DatabaseBackend::Sqlite)] + fn test_integer_default(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "products", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col("quantity", ColumnType::Simple(SimpleColumnType::Integer), false), + ], + vec![], + )]; + + let result = build_modify_column_default( + &backend, + "products", + "quantity", + Some("0"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_integer_default", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test with boolean default value + #[rstest] + #[case::postgres_boolean_default(DatabaseBackend::Postgres)] + #[case::mysql_boolean_default(DatabaseBackend::MySql)] + #[case::sqlite_boolean_default(DatabaseBackend::Sqlite)] + fn test_boolean_default(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col("is_active", ColumnType::Simple(SimpleColumnType::Boolean), false), + ], + vec![], + )]; + + let result = build_modify_column_default( + &backend, + "users", + "is_active", + Some("true"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_boolean_default", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test with function default (e.g., NOW(), CURRENT_TIMESTAMP) + #[rstest] + #[case::postgres_function_default(DatabaseBackend::Postgres)] + #[case::mysql_function_default(DatabaseBackend::MySql)] + #[case::sqlite_function_default(DatabaseBackend::Sqlite)] + fn test_function_default(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "events", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col("created_at", ColumnType::Simple(SimpleColumnType::Timestamp), false), + ], + vec![], + )]; + + let default_value = match backend { + DatabaseBackend::Postgres => "NOW()", + DatabaseBackend::MySql => "CURRENT_TIMESTAMP", + DatabaseBackend::Sqlite => "CURRENT_TIMESTAMP", + }; + + let result = build_modify_column_default( + &backend, + "events", + "created_at", + Some(default_value), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_function_default", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test dropping default from column that had one + #[rstest] + #[case::postgres_drop_existing_default(DatabaseBackend::Postgres)] + #[case::mysql_drop_existing_default(DatabaseBackend::MySql)] + #[case::sqlite_drop_existing_default(DatabaseBackend::Sqlite)] + fn test_drop_existing_default(#[case] backend: DatabaseBackend) { + let mut status_col = col("status", ColumnType::Simple(SimpleColumnType::Text), false); + status_col.default = Some("'pending'".into()); + + let schema = vec![table_def( + "orders", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + status_col, + ], + vec![], + )]; + + let result = build_modify_column_default( + &backend, + "orders", + "status", + None, // Drop default + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_drop_existing_default", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs new file mode 100644 index 0000000..8c2340d --- /dev/null +++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs @@ -0,0 +1,426 @@ +use sea_query::{Alias, Query, Table}; + +use vespertide_core::{ColumnDef, TableDef}; + +use super::create_table::build_create_table_for_backend; +use super::helpers::build_sea_column_def_with_table; +use super::rename_table::build_rename_table; +use super::types::{BuiltQuery, DatabaseBackend, RawSql}; +use crate::error::QueryError; + +/// Build SQL for changing column nullability. +/// For nullable -> non-nullable transitions, fill_with should be provided to update NULL values. +pub fn build_modify_column_nullable( + backend: &DatabaseBackend, + table: &str, + column: &str, + nullable: bool, + fill_with: Option<&str>, + current_schema: &[TableDef], +) -> Result, QueryError> { + let mut queries = Vec::new(); + + // If changing to NOT NULL, first update existing NULL values if fill_with is provided + if !nullable + && let Some(fill_value) = fill_with + { + let update_sql = match backend { + DatabaseBackend::Postgres | DatabaseBackend::Sqlite => format!( + "UPDATE \"{}\" SET \"{}\" = {} WHERE \"{}\" IS NULL", + table, column, fill_value, column + ), + DatabaseBackend::MySql => format!( + "UPDATE `{}` SET `{}` = {} WHERE `{}` IS NULL", + table, column, fill_value, column + ), + }; + queries.push(BuiltQuery::Raw(RawSql::uniform(update_sql))); + } + + // Generate ALTER TABLE statement based on backend + match backend { + DatabaseBackend::Postgres => { + let alter_sql = if nullable { + format!( + "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" DROP NOT NULL", + table, column + ) + } else { + format!( + "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" SET NOT NULL", + table, column + ) + }; + queries.push(BuiltQuery::Raw(RawSql::uniform(alter_sql))); + } + DatabaseBackend::MySql => { + // MySQL requires the full column definition in MODIFY COLUMN + // We need to get the column type from current schema + let table_def = current_schema + .iter() + .find(|t| t.name == table) + .ok_or_else(|| { + QueryError::Other(format!( + "Table '{}' not found in current schema. MySQL requires current schema information to modify column nullability.", + table + )) + })?; + + let column_def = table_def + .columns + .iter() + .find(|c| c.name == column) + .ok_or_else(|| { + QueryError::Other(format!( + "Column '{}' not found in table '{}'. MySQL requires column information to modify nullability.", + column, table + )) + })?; + + // Create a modified column def with the new nullability + let modified_col_def = ColumnDef { + nullable, + ..column_def.clone() + }; + + // Build sea-query ColumnDef with all properties (type, nullable, default) + let sea_col = build_sea_column_def_with_table(backend, table, &modified_col_def); + + let stmt = Table::alter() + .table(Alias::new(table)) + .modify_column(sea_col) + .to_owned(); + queries.push(BuiltQuery::AlterTable(Box::new(stmt))); + } + DatabaseBackend::Sqlite => { + // SQLite doesn't support ALTER COLUMN for nullability changes + // 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 modify column nullability.", + table + )) + })?; + + // Create modified columns with the new nullability + let mut new_columns = table_def.columns.clone(); + if let Some(col) = new_columns.iter_mut().find(|c| c.name == column) { + col.nullable = nullable; + } + + // Generate temporary table name + let temp_table = format!("{}_temp", table); + + // 1. Create temporary table with modified column + let create_temp_table = build_create_table_for_backend( + backend, + &temp_table, + &new_columns, + &table_def.constraints, + ); + queries.push(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(); + queries.push(BuiltQuery::Insert(Box::new(insert_stmt))); + + // 3. Drop original table + let drop_table = Table::drop().table(Alias::new(table)).to_owned(); + queries.push(BuiltQuery::DropTable(Box::new(drop_table))); + + // 4. Rename temporary table to original name + queries.push(build_rename_table(&temp_table, table)); + + // 5. Recreate indexes from Index constraints + for constraint in &table_def.constraints { + if let vespertide_core::TableConstraint::Index { + name: idx_name, + columns: idx_cols, + } = constraint + { + let index_name = vespertide_naming::build_index_name( + table, + idx_cols, + idx_name.as_deref(), + ); + let mut idx_stmt = sea_query::Index::create(); + idx_stmt = idx_stmt.name(&index_name).to_owned(); + for col_name in idx_cols { + idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned(); + } + idx_stmt = idx_stmt.table(Alias::new(table)).to_owned(); + queries.push(BuiltQuery::CreateIndex(Box::new(idx_stmt))); + } + } + } + } + + Ok(queries) +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::{assert_snapshot, with_settings}; + use rstest::rstest; + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType, TableConstraint}; + + fn col(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef { + ColumnDef { + name: name.to_string(), + r#type: ty, + nullable, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + } + } + + fn table_def(name: &str, columns: Vec, constraints: Vec) -> TableDef { + TableDef { + name: name.to_string(), + columns, + constraints, + } + } + + #[rstest] + #[case::postgres_set_not_null(DatabaseBackend::Postgres, false, None)] + #[case::postgres_drop_not_null(DatabaseBackend::Postgres, true, None)] + #[case::postgres_set_not_null_with_fill(DatabaseBackend::Postgres, false, Some("'unknown'"))] + #[case::mysql_set_not_null(DatabaseBackend::MySql, false, None)] + #[case::mysql_drop_not_null(DatabaseBackend::MySql, true, None)] + #[case::mysql_set_not_null_with_fill(DatabaseBackend::MySql, false, Some("'unknown'"))] + #[case::sqlite_set_not_null(DatabaseBackend::Sqlite, false, None)] + #[case::sqlite_drop_not_null(DatabaseBackend::Sqlite, true, None)] + #[case::sqlite_set_not_null_with_fill(DatabaseBackend::Sqlite, false, Some("'unknown'"))] + fn test_build_modify_column_nullable( + #[case] backend: DatabaseBackend, + #[case] nullable: bool, + #[case] fill_with: Option<&str>, + ) { + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col("email", ColumnType::Simple(SimpleColumnType::Text), !nullable), + ], + vec![], + )]; + + let result = build_modify_column_nullable( + &backend, + "users", + "email", + nullable, + fill_with, + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + let suffix = format!( + "{}_{}_users{}", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + }, + if nullable { "nullable" } else { "not_null" }, + if fill_with.is_some() { "_with_fill" } else { "" } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test table not found error + #[rstest] + #[case::postgres_table_not_found(DatabaseBackend::Postgres)] + #[case::mysql_table_not_found(DatabaseBackend::MySql)] + #[case::sqlite_table_not_found(DatabaseBackend::Sqlite)] + fn test_table_not_found(#[case] backend: DatabaseBackend) { + // Postgres doesn't need schema lookup for nullability changes + if backend == DatabaseBackend::Postgres { + return; + } + + let result = build_modify_column_nullable( + &backend, + "users", + "email", + false, + None, + &[], + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Table 'users' not found")); + } + + /// Test column not found error + #[rstest] + #[case::postgres_column_not_found(DatabaseBackend::Postgres)] + #[case::mysql_column_not_found(DatabaseBackend::MySql)] + #[case::sqlite_column_not_found(DatabaseBackend::Sqlite)] + fn test_column_not_found(#[case] backend: DatabaseBackend) { + // Postgres doesn't need schema lookup for nullability changes + // SQLite doesn't validate column existence in modify_column_nullable + if backend == DatabaseBackend::Postgres || backend == DatabaseBackend::Sqlite { + return; + } + + let schema = vec![table_def( + "users", + vec![col("id", ColumnType::Simple(SimpleColumnType::Integer), false)], + vec![], + )]; + + let result = build_modify_column_nullable( + &backend, + "users", + "email", + false, + None, + &schema, + ); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Column 'email' not found")); + } + + /// Test with index - should recreate index after table rebuild (SQLite) + #[rstest] + #[case::postgres_with_index(DatabaseBackend::Postgres)] + #[case::mysql_with_index(DatabaseBackend::MySql)] + #[case::sqlite_with_index(DatabaseBackend::Sqlite)] + fn test_modify_nullable_with_index(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col("email", ColumnType::Simple(SimpleColumnType::Text), true), + ], + vec![TableConstraint::Index { + name: Some("idx_email".into()), + columns: vec!["email".into()], + }], + )]; + + let result = build_modify_column_nullable( + &backend, + "users", + "email", + false, + None, + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // SQLite should recreate the index after table rebuild + if backend == DatabaseBackend::Sqlite { + assert!(sql.contains("CREATE INDEX")); + assert!(sql.contains("idx_email")); + } + + let suffix = format!( + "{}_with_index", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + + /// Test with default value - should preserve default in MODIFY COLUMN (MySQL) + #[rstest] + #[case::postgres_with_default(DatabaseBackend::Postgres)] + #[case::mysql_with_default(DatabaseBackend::MySql)] + #[case::sqlite_with_default(DatabaseBackend::Sqlite)] + fn test_with_default_value(#[case] backend: DatabaseBackend) { + let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text), true); + email_col.default = Some("'default@example.com'".into()); + + let schema = vec![table_def( + "users", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + email_col, + ], + vec![], + )]; + + let result = build_modify_column_nullable( + &backend, + "users", + "email", + false, + None, + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // MySQL and SQLite should include DEFAULT clause + if backend == DatabaseBackend::MySql || backend == DatabaseBackend::Sqlite { + assert!(sql.contains("DEFAULT")); + } + + let suffix = format!( + "{}_with_default", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } +} diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@mysql_drop_comment_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@mysql_drop_comment_users.snap new file mode 100644 index 0000000..86e8748 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@mysql_drop_comment_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@mysql_set_comment_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@mysql_set_comment_users.snap new file mode 100644 index 0000000..ac96a7c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@mysql_set_comment_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text COMMENT 'User email address' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@postgres_drop_comment_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@postgres_drop_comment_users.snap new file mode 100644 index 0000000..8169417 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@postgres_drop_comment_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "users"."email" IS NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@postgres_set_comment_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@postgres_set_comment_users.snap new file mode 100644 index 0000000..140b758 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@postgres_set_comment_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "users"."email" IS 'User email address' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@sqlite_drop_comment_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@sqlite_drop_comment_users.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@sqlite_drop_comment_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@sqlite_set_comment_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@sqlite_set_comment_users.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__build_modify_column_comment@sqlite_set_comment_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@mysql_change_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@mysql_change_comment.snap new file mode 100644 index 0000000..1c33350 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@mysql_change_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text COMMENT 'New comment' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@postgres_change_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@postgres_change_comment.snap new file mode 100644 index 0000000..074ce2f --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@postgres_change_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "users"."email" IS 'New comment' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@sqlite_change_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@sqlite_change_comment.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__change_comment@sqlite_change_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_boolean_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_boolean_comment.snap new file mode 100644 index 0000000..324f308 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_boolean_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `data` MODIFY COLUMN `field` bool NOT NULL COMMENT 'Is user active' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_integer_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_integer_comment.snap new file mode 100644 index 0000000..a9311ea --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_integer_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `data` MODIFY COLUMN `field` int NOT NULL COMMENT 'Auto-increment ID' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_timestamp_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_timestamp_comment.snap new file mode 100644 index 0000000..b01d749 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@mysql_timestamp_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `data` MODIFY COLUMN `field` timestamp NOT NULL COMMENT 'Creation timestamp' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_boolean_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_boolean_comment.snap new file mode 100644 index 0000000..7538567 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_boolean_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "data"."field" IS 'Is user active' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_integer_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_integer_comment.snap new file mode 100644 index 0000000..a094935 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_integer_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "data"."field" IS 'Auto-increment ID' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_timestamp_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_timestamp_comment.snap new file mode 100644 index 0000000..815aef5 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@postgres_timestamp_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "data"."field" IS 'Creation timestamp' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_boolean_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_boolean_comment.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_boolean_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_integer_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_integer_comment.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_integer_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_timestamp_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_timestamp_comment.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_different_types@sqlite_timestamp_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@mysql_not_null_column.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@mysql_not_null_column.snap new file mode 100644 index 0000000..ba0ab26 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@mysql_not_null_column.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `username` text NOT NULL COMMENT 'Required username' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@postgres_not_null_column.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@postgres_not_null_column.snap new file mode 100644 index 0000000..d5c261c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@postgres_not_null_column.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "users"."username" IS 'Required username' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@sqlite_not_null_column.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@sqlite_not_null_column.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_on_not_null_column@sqlite_not_null_column.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@mysql_comment_with_quotes.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@mysql_comment_with_quotes.snap new file mode 100644 index 0000000..9660f45 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@mysql_comment_with_quotes.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text COMMENT 'User''s email address' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@postgres_comment_with_quotes.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@postgres_comment_with_quotes.snap new file mode 100644 index 0000000..5f43da1 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@postgres_comment_with_quotes.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "users"."email" IS 'User''s email address' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@sqlite_comment_with_quotes.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@sqlite_comment_with_quotes.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__comment_with_quotes@sqlite_comment_with_quotes.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@mysql_drop_existing_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@mysql_drop_existing_comment.snap new file mode 100644 index 0000000..86e8748 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@mysql_drop_existing_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@postgres_drop_existing_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@postgres_drop_existing_comment.snap new file mode 100644 index 0000000..8169417 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@postgres_drop_existing_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "users"."email" IS NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@sqlite_drop_existing_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@sqlite_drop_existing_comment.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__drop_existing_comment@sqlite_drop_existing_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@mysql_long_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@mysql_long_comment.snap new file mode 100644 index 0000000..31fc15d --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@mysql_long_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `bio` text COMMENT 'This is a very long comment that describes the bio field in great detail. It contains multiple sentences and provides thorough documentation for this column.' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@postgres_long_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@postgres_long_comment.snap new file mode 100644 index 0000000..bd79ce2 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@postgres_long_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "users"."bio" IS 'This is a very long comment that describes the bio field in great detail. It contains multiple sentences and provides thorough documentation for this column.' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@sqlite_long_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@sqlite_long_comment.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__long_comment@sqlite_long_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@mysql_preserves_properties.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@mysql_preserves_properties.snap new file mode 100644 index 0000000..9de2a41 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@mysql_preserves_properties.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text DEFAULT 'default@example.com' COMMENT 'User email address' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@postgres_preserves_properties.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@postgres_preserves_properties.snap new file mode 100644 index 0000000..140b758 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@postgres_preserves_properties.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- +COMMENT ON COLUMN "users"."email" IS 'User email address' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@sqlite_preserves_properties.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@sqlite_preserves_properties.snap new file mode 100644 index 0000000..8cbcbdd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_comment__tests__preserves_column_properties@sqlite_preserves_properties.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_comment.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@mysql_boolean_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@mysql_boolean_default.snap new file mode 100644 index 0000000..cc34922 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@mysql_boolean_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `is_active` bool NOT NULL DEFAULT true diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@postgres_boolean_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@postgres_boolean_default.snap new file mode 100644 index 0000000..0563509 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@postgres_boolean_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "is_active" SET DEFAULT true diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@sqlite_boolean_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@sqlite_boolean_default.snap new file mode 100644 index 0000000..3e75c83 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__boolean_default@sqlite_boolean_default.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "is_active" boolean NOT NULL DEFAULT true ) +INSERT INTO "users_temp" ("id", "is_active") SELECT "id", "is_active" 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_default__tests__build_modify_column_default@mysql_drop_default_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@mysql_drop_default_users.snap new file mode 100644 index 0000000..237349a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@mysql_drop_default_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@mysql_set_default_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@mysql_set_default_users.snap new file mode 100644 index 0000000..bfff705 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@mysql_set_default_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text DEFAULT 'unknown' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@postgres_drop_default_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@postgres_drop_default_users.snap new file mode 100644 index 0000000..bcce260 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@postgres_drop_default_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" DROP DEFAULT diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@postgres_set_default_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@postgres_set_default_users.snap new file mode 100644 index 0000000..2dc0b25 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@postgres_set_default_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" SET DEFAULT 'unknown' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@sqlite_drop_default_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@sqlite_drop_default_users.snap new file mode 100644 index 0000000..031e883 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@sqlite_drop_default_users.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.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__modify_column_default__tests__build_modify_column_default@sqlite_set_default_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@sqlite_set_default_users.snap new file mode 100644 index 0000000..d6b0892 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__build_modify_column_default@sqlite_set_default_users.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text DEFAULT 'unknown' ) +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__modify_column_default__tests__change_default_value@mysql_change_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__change_default_value@mysql_change_default.snap new file mode 100644 index 0000000..ad9cdeb --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__change_default_value@mysql_change_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text DEFAULT 'new@example.com' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__change_default_value@postgres_change_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__change_default_value@postgres_change_default.snap new file mode 100644 index 0000000..51b04cd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__change_default_value@postgres_change_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" SET DEFAULT 'new@example.com' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__change_default_value@sqlite_change_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__change_default_value@sqlite_change_default.snap new file mode 100644 index 0000000..3074e6a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__change_default_value@sqlite_change_default.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text DEFAULT 'new@example.com' ) +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__modify_column_default__tests__drop_existing_default@mysql_drop_existing_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__drop_existing_default@mysql_drop_existing_default.snap new file mode 100644 index 0000000..9e8e9f0 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__drop_existing_default@mysql_drop_existing_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE `orders` MODIFY COLUMN `status` text NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__drop_existing_default@postgres_drop_existing_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__drop_existing_default@postgres_drop_existing_default.snap new file mode 100644 index 0000000..e37d15d --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__drop_existing_default@postgres_drop_existing_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE "orders" ALTER COLUMN "status" DROP DEFAULT diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__drop_existing_default@sqlite_drop_existing_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__drop_existing_default@sqlite_drop_existing_default.snap new file mode 100644 index 0000000..3802cea --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__drop_existing_default@sqlite_drop_existing_default.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +CREATE TABLE "orders_temp" ( "id" integer NOT NULL, "status" text NOT NULL ) +INSERT INTO "orders_temp" ("id", "status") SELECT "id", "status" FROM "orders" +DROP TABLE "orders" +ALTER TABLE "orders_temp" RENAME TO "orders" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@mysql_function_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@mysql_function_default.snap new file mode 100644 index 0000000..d070747 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@mysql_function_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE `events` MODIFY COLUMN `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@postgres_function_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@postgres_function_default.snap new file mode 100644 index 0000000..6d5e732 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@postgres_function_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE "events" ALTER COLUMN "created_at" SET DEFAULT NOW() diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@sqlite_function_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@sqlite_function_default.snap new file mode 100644 index 0000000..4e482b0 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__function_default@sqlite_function_default.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +CREATE TABLE "events_temp" ( "id" integer NOT NULL, "created_at" timestamp_text NOT NULL DEFAULT CURRENT_TIMESTAMP ) +INSERT INTO "events_temp" ("id", "created_at") SELECT "id", "created_at" FROM "events" +DROP TABLE "events" +ALTER TABLE "events_temp" RENAME TO "events" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@mysql_integer_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@mysql_integer_default.snap new file mode 100644 index 0000000..b7d4dde --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@mysql_integer_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE `products` MODIFY COLUMN `quantity` int NOT NULL DEFAULT 0 diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@postgres_integer_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@postgres_integer_default.snap new file mode 100644 index 0000000..ffbec28 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@postgres_integer_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE "products" ALTER COLUMN "quantity" SET DEFAULT 0 diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@sqlite_integer_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@sqlite_integer_default.snap new file mode 100644 index 0000000..7571f0b --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__integer_default@sqlite_integer_default.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +CREATE TABLE "products_temp" ( "id" integer NOT NULL, "quantity" integer NOT NULL DEFAULT 0 ) +INSERT INTO "products_temp" ("id", "quantity") SELECT "id", "quantity" FROM "products" +DROP TABLE "products" +ALTER TABLE "products_temp" RENAME TO "products" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@mysql_with_index.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@mysql_with_index.snap new file mode 100644 index 0000000..8e417e4 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@mysql_with_index.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text DEFAULT 'default@example.com' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@postgres_with_index.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@postgres_with_index.snap new file mode 100644 index 0000000..341d57f --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@postgres_with_index.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" SET DEFAULT 'default@example.com' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@sqlite_with_index.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@sqlite_with_index.snap new file mode 100644 index 0000000..35fd7a1 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_default__tests__modify_default_with_index@sqlite_with_index.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_default.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text DEFAULT 'default@example.com' ) +INSERT INTO "users_temp" ("id", "email") SELECT "id", "email" FROM "users" +DROP TABLE "users" +ALTER TABLE "users_temp" RENAME TO "users" +CREATE INDEX "ix_users__idx_users_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_not_null_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_not_null_users.snap new file mode 100644 index 0000000..d03f517 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_not_null_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_not_null_users_with_fill.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_not_null_users_with_fill.snap new file mode 100644 index 0000000..c5fe93a --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_not_null_users_with_fill.snap @@ -0,0 +1,6 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +UPDATE `users` SET `email` = 'unknown' WHERE `email` IS NULL +ALTER TABLE `users` MODIFY COLUMN `email` text NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_nullable_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_nullable_users.snap new file mode 100644 index 0000000..e1bcd0f --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@mysql_nullable_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_not_null_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_not_null_users.snap new file mode 100644 index 0000000..a458a59 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_not_null_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_not_null_users_with_fill.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_not_null_users_with_fill.snap new file mode 100644 index 0000000..2f724ea --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_not_null_users_with_fill.snap @@ -0,0 +1,6 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +UPDATE "users" SET "email" = 'unknown' WHERE "email" IS NULL +ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_nullable_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_nullable_users.snap new file mode 100644 index 0000000..4d59583 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@postgres_nullable_users.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" DROP NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@sqlite_not_null_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@sqlite_not_null_users.snap new file mode 100644 index 0000000..1553ecd --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@sqlite_not_null_users.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text NOT NULL ) +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__modify_column_nullable__tests__build_modify_column_nullable@sqlite_not_null_users_with_fill.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@sqlite_not_null_users_with_fill.snap new file mode 100644 index 0000000..67d6d89 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@sqlite_not_null_users_with_fill.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +UPDATE "users" SET "email" = 'unknown' WHERE "email" IS NULL +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text NOT NULL ) +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__modify_column_nullable__tests__build_modify_column_nullable@sqlite_nullable_users.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@sqlite_nullable_users.snap new file mode 100644 index 0000000..7f50d3f --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__build_modify_column_nullable@sqlite_nullable_users.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.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__modify_column_nullable__tests__modify_nullable_with_index@mysql_with_index.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__modify_nullable_with_index@mysql_with_index.snap new file mode 100644 index 0000000..d03f517 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__modify_nullable_with_index@mysql_with_index.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__modify_nullable_with_index@postgres_with_index.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__modify_nullable_with_index@postgres_with_index.snap new file mode 100644 index 0000000..a458a59 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__modify_nullable_with_index@postgres_with_index.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__modify_nullable_with_index@sqlite_with_index.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__modify_nullable_with_index@sqlite_with_index.snap new file mode 100644 index 0000000..5192d1c --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__modify_nullable_with_index@sqlite_with_index.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text NOT NULL ) +INSERT INTO "users_temp" ("id", "email") SELECT "id", "email" FROM "users" +DROP TABLE "users" +ALTER TABLE "users_temp" RENAME TO "users" +CREATE INDEX "ix_users__idx_email" ON "users" ("email") diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@mysql_with_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@mysql_with_default.snap new file mode 100644 index 0000000..8ad74ce --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@mysql_with_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text NOT NULL DEFAULT 'default@example.com' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@postgres_with_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@postgres_with_default.snap new file mode 100644 index 0000000..a458a59 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@postgres_with_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@sqlite_with_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@sqlite_with_default.snap new file mode 100644 index 0000000..51acb8d --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__with_default_value@sqlite_with_default.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "id" integer NOT NULL, "email" text NOT NULL DEFAULT 'default@example.com' ) +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_modify_column_comment@mysql_modify_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_comment@mysql_modify_comment.snap new file mode 100644 index 0000000..fb127d3 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_comment@mysql_modify_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `email` text COMMENT 'User email address' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_comment@postgres_modify_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_comment@postgres_modify_comment.snap new file mode 100644 index 0000000..9575a54 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_comment@postgres_modify_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +COMMENT ON COLUMN "users"."email" IS 'User email address' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_comment@sqlite_modify_comment.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_comment@sqlite_modify_comment.snap new file mode 100644 index 0000000..7a8c643 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_comment@sqlite_modify_comment.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- + diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@mysql_modify_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@mysql_modify_default.snap new file mode 100644 index 0000000..fb43892 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@mysql_modify_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE `users` MODIFY COLUMN `status` text DEFAULT 'active' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@postgres_modify_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@postgres_modify_default.snap new file mode 100644 index 0000000..b248620 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@postgres_modify_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +ALTER TABLE "users" ALTER COLUMN "status" SET DEFAULT 'active' diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@sqlite_modify_default.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@sqlite_modify_default.snap new file mode 100644 index 0000000..fb84296 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_default@sqlite_modify_default.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +CREATE TABLE "users_temp" ( "status" text DEFAULT 'active' ) +INSERT INTO "users_temp" ("status") SELECT "status" FROM "users" +DROP TABLE "users" +ALTER TABLE "users_temp" RENAME TO "users" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@mysql_modify_nullable.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@mysql_modify_nullable.snap new file mode 100644 index 0000000..d3bfb5e --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@mysql_modify_nullable.snap @@ -0,0 +1,6 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +UPDATE `users` SET `email` = 'unknown' WHERE `email` IS NULL +ALTER TABLE `users` MODIFY COLUMN `email` text NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@postgres_modify_nullable.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@postgres_modify_nullable.snap new file mode 100644 index 0000000..088b6a0 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@postgres_modify_nullable.snap @@ -0,0 +1,6 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +UPDATE "users" SET "email" = 'unknown' WHERE "email" IS NULL +ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@sqlite_modify_nullable.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@sqlite_modify_nullable.snap new file mode 100644 index 0000000..6a74200 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__tests__build_action_queries_modify_column_nullable@sqlite_modify_nullable.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/mod.rs +expression: sql +--- +UPDATE "users" SET "email" = 'unknown' WHERE "email" IS NULL +CREATE TABLE "users_temp" ( "email" text NOT NULL ) +INSERT INTO "users_temp" ("email") SELECT "email" FROM "users" +DROP TABLE "users" +ALTER TABLE "users_temp" RENAME TO "users"