diff --git a/crates/dbx-core/src/table_structure_sql/column_alter.rs b/crates/dbx-core/src/table_structure_sql/column_alter.rs index b1a68e2d2e..6d6e320d9d 100644 --- a/crates/dbx-core/src/table_structure_sql/column_alter.rs +++ b/crates/dbx-core/src/table_structure_sql/column_alter.rs @@ -316,11 +316,31 @@ fn build_postgres_like_existing_column_sql( quote_ident(StructureDialect::Postgres, &column.name) )); } - if column.data_type.trim() != original.data_type.trim() { + let type_changed = column.data_type.trim() != original.data_type.trim(); + if type_changed { let column_name = quote_ident(StructureDialect::Postgres, current_name); let data_type = column_data_type(StructureDialect::Postgres, column); - let type_clause = if use_xugu_type_syntax { data_type } else { format!("TYPE {data_type}") }; - statements.push(format!("ALTER TABLE {table} ALTER COLUMN {column_name} {type_clause};")); + if use_xugu_type_syntax { + statements.push(format!("ALTER TABLE {table} ALTER COLUMN {column_name} {data_type};")); + } else { + let current_default = normalize_default(Some(&column.default_value)); + let mut alterations = Vec::new(); + if !original_default(column).is_empty() { + alterations.push(format!("ALTER COLUMN {column_name} DROP DEFAULT")); + } + // PostgreSQL assignment casts do not cover many valid explicit conversions. USING keeps + // type changes generic for built-ins, arrays, and domains while still rejecting bad data. + alterations.push(format!("ALTER COLUMN {column_name} TYPE {data_type} USING {column_name}::{data_type}")); + if !current_default.is_empty() { + alterations.push(format!( + "ALTER COLUMN {column_name} SET DEFAULT {}", + format_default_for_sql(StructureDialect::Postgres, &column.data_type, ¤t_default) + )); + } + // USING is not applied to defaults. Keeping all three actions in one ALTER TABLE makes + // an incompatible restored default fail atomically instead of leaving it dropped. + statements.push(format!("ALTER TABLE {table} {};", alterations.join(", "))); + } } if column.is_nullable != original.is_nullable { let action = if column.is_nullable { "DROP NOT NULL" } else { "SET NOT NULL" }; @@ -329,7 +349,9 @@ fn build_postgres_like_existing_column_sql( quote_ident(StructureDialect::Postgres, current_name) )); } - if normalize_default(Some(&column.default_value)) != original_default(column) { + if (!type_changed || use_xugu_type_syntax) + && normalize_default(Some(&column.default_value)) != original_default(column) + { let default_value = normalize_default(Some(&column.default_value)); let action = if default_value.is_empty() { "DROP DEFAULT".to_string() diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index c97bc8b561..d753c865de 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -193,7 +193,127 @@ fn builds_xugu_type_change_with_native_syntax() { assert_eq!( postgres_result.statements, - vec!["ALTER TABLE \"public\".\"info_x\" ALTER COLUMN \"code\" TYPE integer;"] + vec!["ALTER TABLE \"public\".\"info_x\" ALTER COLUMN \"code\" TYPE integer USING \"code\"::integer;"] + ); +} + +#[test] +fn builds_postgres_explicit_type_cast_for_renamed_column() { + let mut code = column("new code"); + code.data_type = "bigint".to_string(); + code.original = Some(ColumnInfo { + name: "old code".to_string(), + data_type: "character varying(20)".to_string(), + is_nullable: true, + column_default: None, + ..Default::default() + }); + + let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: Some("public".to_string()), + table_name: "items".to_string(), + column: code, + }); + + assert_eq!( + result.statements, + vec![ + "ALTER TABLE \"public\".\"items\" RENAME COLUMN \"old code\" TO \"new code\";", + "ALTER TABLE \"public\".\"items\" ALTER COLUMN \"new code\" TYPE bigint USING \"new code\"::bigint;", + ] + ); +} + +#[test] +fn builds_postgres_atomic_type_change_with_existing_default() { + let mut code = column("code"); + code.data_type = "varchar(20)".to_string(); + code.default_value = "7".to_string(); + code.original = Some(ColumnInfo { + name: "code".to_string(), + data_type: "integer".to_string(), + is_nullable: true, + column_default: Some("7".to_string()), + ..Default::default() + }); + + let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: Some("public".to_string()), + table_name: "items".to_string(), + column: code, + }); + + assert_eq!( + result.statements, + vec!["ALTER TABLE \"public\".\"items\" ALTER COLUMN \"code\" DROP DEFAULT, ALTER COLUMN \"code\" TYPE varchar(20) USING \"code\"::varchar(20), ALTER COLUMN \"code\" SET DEFAULT '7';"] + ); +} + +#[test] +fn builds_postgres_type_change_that_drops_default() { + let mut code = column("code"); + code.data_type = "bigint".to_string(); + code.original = Some(ColumnInfo { + name: "code".to_string(), + data_type: "character varying".to_string(), + is_nullable: true, + column_default: Some("'7'::character varying".to_string()), + ..Default::default() + }); + + let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: None, + table_name: "items".to_string(), + column: code, + }); + + assert_eq!( + result.statements, + vec!["ALTER TABLE \"items\" ALTER COLUMN \"code\" DROP DEFAULT, ALTER COLUMN \"code\" TYPE bigint USING \"code\"::bigint;"] + ); +} + +#[test] +fn builds_postgres_array_and_domain_type_casts_without_affecting_xugu() { + let mut tags = column("tags"); + tags.data_type = "text[]".to_string(); + tags.original = Some(ColumnInfo { + name: "tags".to_string(), + data_type: "varchar(20)[]".to_string(), + is_nullable: true, + ..Default::default() + }); + let postgres = build_single_column_alter_sql(SingleColumnAlterSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: Some("catalog".to_string()), + table_name: "items".to_string(), + column: tags, + }); + assert_eq!( + postgres.statements, + vec!["ALTER TABLE \"catalog\".\"items\" ALTER COLUMN \"tags\" TYPE text[] USING \"tags\"::text[];"] + ); + + let mut status = column("status"); + status.data_type = "catalog.status_domain".to_string(); + status.original = Some(ColumnInfo { + name: "status".to_string(), + data_type: "text".to_string(), + is_nullable: true, + ..Default::default() + }); + let postgres = build_single_column_alter_sql(SingleColumnAlterSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: Some("catalog".to_string()), + table_name: "items".to_string(), + column: status, + }); + assert_eq!( + postgres.statements, + vec!["ALTER TABLE \"catalog\".\"items\" ALTER COLUMN \"status\" TYPE catalog.status_domain USING \"status\"::catalog.status_domain;"] ); }