fix(postgres): cast values during column type changes

This commit is contained in:
t8y2
2026-07-19 11:47:38 +08:00
parent a0e38b3da9
commit feb1c4bd34
2 changed files with 147 additions and 5 deletions
@@ -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, &current_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()
@@ -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;"]
);
}