fix(postgres): support altering column identity in the structure editor

This commit is contained in:
lxk955
2026-10-11 15:39:15 +08:00
committed by GitHub
parent 6cd2dc1c9d
commit 9f41cdc3f5
2 changed files with 312 additions and 6 deletions
@@ -7,7 +7,7 @@ use super::column_format::{
use super::columns::{build_add_column_sql, build_drop_column_sql};
use super::comments::build_sqlserver_column_comment_sql_for_profile;
use super::dialect::{capabilities_for, database_label, StructureDialect};
use super::types::{EditableStructureColumn, SingleColumnAlterSqlOptions, TableStructureSqlResult};
use super::types::{ColumnIdentity, EditableStructureColumn, SingleColumnAlterSqlOptions, TableStructureSqlResult};
use super::util::{
clean, format_default_for_sql, is_protected_manticore_id_column, normalize_default, original_comment,
original_default, qualified_table, quote_ident, quote_string,
@@ -189,10 +189,32 @@ pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> Ta
TableStructureSqlResult { statements, warnings }
}
fn is_column_identity_empty(identity: &Option<ColumnIdentity>) -> bool {
let Some(id) = identity.as_ref() else { return true };
let generation_empty =
id.generation.as_deref().map(|g| g.trim().is_empty() || g.eq_ignore_ascii_case("none")).unwrap_or(true);
generation_empty && id.seed.is_none() && id.increment.is_none()
}
fn postgres_identity_from_column(column: &EditableStructureColumn) -> Option<&ColumnIdentity> {
let identity = column.extra.as_ref()?.identity.as_ref()?;
if let Some(generation) = &identity.generation {
let trimmed = generation.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
return None;
}
return Some(identity);
}
if identity.seed.is_some() || identity.increment.is_some() {
return Some(identity);
}
None
}
fn is_column_extra_empty(extra: &ColumnExtra) -> bool {
!extra.auto_increment.unwrap_or(false)
&& !extra.on_update_current_timestamp.unwrap_or(false)
&& extra.identity.is_none()
&& is_column_identity_empty(&extra.identity)
&& !extra.generated.as_ref().is_some_and(|generated| !generated.expression.trim().is_empty())
&& !extra.manticore_indexed.unwrap_or(false)
&& !extra.manticore_stored.unwrap_or(false)
@@ -271,9 +293,16 @@ fn postgres_identity_values(extra: &str) -> Option<(String, Option<i64>, Option<
Some((generation, seed, increment))
}
fn identity_matches_original(identity: &super::types::ColumnIdentity, original_extra: &str) -> bool {
fn identity_matches_original(identity: &ColumnIdentity, original_extra: &str) -> bool {
if let Some((generation, original_seed, original_increment)) = postgres_identity_values(original_extra) {
return identity.generation.as_deref().unwrap_or("BY DEFAULT").eq_ignore_ascii_case(&generation)
if is_column_identity_empty(&Some(identity.clone())) {
return false;
}
let curr_generation = match identity.generation.as_deref() {
Some(g) if g.eq_ignore_ascii_case("always") => "ALWAYS",
_ => "BY DEFAULT",
};
return curr_generation.eq_ignore_ascii_case(&generation)
&& identity.seed.unwrap_or(1) == original_seed.unwrap_or(1)
&& identity.increment.unwrap_or(1) == original_increment.unwrap_or(1);
}
@@ -389,9 +418,16 @@ pub(super) fn has_column_extra_change(dialect: StructureDialect, column: &Editab
let orig_has_ai = original_has_auto_increment(orig) || (orig_has_identity && curr_has_ai);
let curr_has_on_update = curr.on_update_current_timestamp.unwrap_or(false);
let orig_has_on_update = orig_lower.contains("on update");
let curr_identity_empty = is_column_identity_empty(&curr.identity);
let identity_changed = match (&curr.identity, orig_has_identity) {
(Some(identity), true) => !identity_matches_original(identity, orig),
(Some(_), false) => true,
(Some(identity), true) => {
if curr_identity_empty {
true
} else {
!identity_matches_original(identity, orig)
}
}
(Some(_), false) => !curr_identity_empty,
(None, true) => !curr_has_ai,
(None, false) => false,
};
@@ -555,6 +591,15 @@ fn build_postgres_like_existing_column_sql(
statements.push(format!("ALTER TABLE {table} {};", alterations.join(", ")));
}
}
let orig_identity =
if !use_xugu_type_syntax { original.extra.as_deref().and_then(postgres_identity_values) } else { None };
let curr_identity = if !use_xugu_type_syntax { postgres_identity_from_column(column) } else { None };
if orig_identity.is_some() && curr_identity.is_none() {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} DROP IDENTITY;",
quote_ident(StructureDialect::Postgres, current_name)
));
}
if column.is_nullable != original.is_nullable {
let action = if column.is_nullable { "DROP NOT NULL" } else { "SET NOT NULL" };
statements.push(format!(
@@ -579,6 +624,69 @@ fn build_postgres_like_existing_column_sql(
quote_ident(StructureDialect::Postgres, current_name)
));
}
if !use_xugu_type_syntax {
match (orig_identity, curr_identity) {
(None, Some(identity)) => {
if column.is_nullable {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} SET NOT NULL;",
quote_ident(StructureDialect::Postgres, current_name)
));
}
if !original_default(column).is_empty()
&& normalize_default(Some(&column.default_value)) == original_default(column)
{
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} DROP DEFAULT;",
quote_ident(StructureDialect::Postgres, current_name)
));
}
let generation = match identity.generation.as_deref() {
Some(g) if g.eq_ignore_ascii_case("always") => "ALWAYS",
_ => "BY DEFAULT",
};
let mut clause = format!("ADD GENERATED {generation} AS IDENTITY");
if identity.seed.is_some() || identity.increment.is_some() {
let start = identity.seed.unwrap_or(1);
let inc = identity.increment.unwrap_or(1);
clause.push_str(&format!(" (START WITH {start} INCREMENT BY {inc})"));
}
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} {clause};",
quote_ident(StructureDialect::Postgres, current_name)
));
}
(Some((orig_generation, orig_seed, orig_increment)), Some(identity)) => {
let curr_generation = match identity.generation.as_deref() {
Some(g) if g.eq_ignore_ascii_case("always") => "ALWAYS",
_ => "BY DEFAULT",
};
if !curr_generation.eq_ignore_ascii_case(&orig_generation) {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} SET GENERATED {curr_generation};",
quote_ident(StructureDialect::Postgres, current_name)
));
}
if identity.seed.unwrap_or(1) != orig_seed.unwrap_or(1) {
let new_seed = identity.seed.unwrap_or(1);
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} RESTART WITH {new_seed};",
quote_ident(StructureDialect::Postgres, current_name)
));
}
if identity.increment.unwrap_or(1) != orig_increment.unwrap_or(1) {
let new_increment = identity.increment.unwrap_or(1);
if new_increment != 0 {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} SET INCREMENT BY {new_increment};",
quote_ident(StructureDialect::Postgres, current_name)
));
}
}
}
_ => {}
}
}
if clean(&column.comment) != original_comment(column) {
let comment_value =
if clean(&column.comment).is_empty() { "NULL".to_string() } else { quote_string(&clean(&column.comment)) };
@@ -6033,6 +6033,204 @@ fn postgres_create_table_with_identity() {
assert!(result.statements[0].contains("GENERATED BY DEFAULT AS IDENTITY"));
}
#[test]
fn postgres_alter_column_identity_generation_always_to_by_default() {
let mut id = existing_pk_column("id", "integer", true, true);
id.extra = Some(ColumnExtra {
identity: Some(ColumnIdentity { generation: Some("BY DEFAULT".to_string()), seed: None, increment: None }),
..Default::default()
});
id.original.as_mut().unwrap().extra = Some(
"generated always as identity (start with 1 increment by 1 minvalue 1 maxvalue 2147483647 cache 1 no cycle)"
.to_string(),
);
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Postgres,
Some("public"),
"users",
vec![id],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec!["ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" SET GENERATED BY DEFAULT;"]
);
}
#[test]
fn postgres_alter_column_identity_generation_by_default_to_always() {
let mut id = existing_pk_column("id", "integer", true, true);
id.extra = Some(ColumnExtra {
identity: Some(ColumnIdentity { generation: Some("ALWAYS".to_string()), seed: None, increment: None }),
..Default::default()
});
id.original.as_mut().unwrap().extra = Some("generated by default as identity".to_string());
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Postgres,
Some("public"),
"users",
vec![id],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(result.statements, vec!["ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" SET GENERATED ALWAYS;"]);
}
#[test]
fn postgres_alter_column_identity_seed_and_increment() {
let mut id = existing_pk_column("id", "integer", true, true);
id.extra = Some(ColumnExtra {
identity: Some(ColumnIdentity {
generation: Some("BY DEFAULT".to_string()),
seed: Some(100),
increment: Some(5),
}),
..Default::default()
});
id.original.as_mut().unwrap().extra = Some("generated by default as identity (start with 1 increment by 1 minvalue 1 maxvalue 2147483647 cache 1 no cycle)".to_string());
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Postgres,
Some("public"),
"users",
vec![id],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec![
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" RESTART WITH 100;",
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" SET INCREMENT BY 5;",
]
);
}
#[test]
fn postgres_alter_column_identity_generation_and_sequence_options_together() {
let mut id = existing_pk_column("id", "integer", true, true);
id.extra = Some(ColumnExtra {
identity: Some(ColumnIdentity {
generation: Some("BY DEFAULT".to_string()),
seed: Some(100),
increment: Some(2),
}),
..Default::default()
});
id.original.as_mut().unwrap().extra = Some(
"generated always as identity (start with 1 increment by 1 minvalue 1 maxvalue 2147483647 cache 1 no cycle)"
.to_string(),
);
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Postgres,
Some("public"),
"users",
vec![id],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec![
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" SET GENERATED BY DEFAULT;",
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" RESTART WITH 100;",
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" SET INCREMENT BY 2;",
]
);
}
#[test]
fn postgres_enable_identity_on_existing_column() {
let mut id = column("id");
id.data_type = "integer".to_string();
id.is_nullable = true;
id.extra = Some(ColumnExtra {
identity: Some(ColumnIdentity { generation: Some("ALWAYS".to_string()), seed: Some(10), increment: Some(2) }),
..Default::default()
});
id.original = Some(ColumnInfo {
name: "id".to_string(),
data_type: "integer".to_string(),
is_nullable: true,
..Default::default()
});
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Postgres,
Some("public"),
"users",
vec![id.clone()],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(
result.statements,
vec![
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" SET NOT NULL;",
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" ADD GENERATED ALWAYS AS IDENTITY (START WITH 10 INCREMENT BY 2);",
]
);
let single = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
database_type: Some(DatabaseType::Postgres),
driver_profile: None,
schema: Some("public".to_string()),
table_name: "users".to_string(),
column: id,
});
assert_eq!(
single.statements,
vec![
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" SET NOT NULL;",
"ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" ADD GENERATED ALWAYS AS IDENTITY (START WITH 10 INCREMENT BY 2);",
]
);
}
#[test]
fn postgres_disable_identity_on_existing_column() {
let mut id = existing_pk_column("id", "integer", true, true);
id.extra = Some(ColumnExtra::default());
id.original.as_mut().unwrap().extra = Some("generated always as identity".to_string());
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Postgres,
Some("public"),
"users",
vec![id],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(result.statements, vec!["ALTER TABLE \"public\".\"users\" ALTER COLUMN \"id\" DROP IDENTITY;"]);
}
#[test]
fn postgres_unchanged_identity_emits_no_statements() {
let mut id = existing_pk_column("id", "integer", true, true);
id.extra = Some(ColumnExtra {
identity: Some(ColumnIdentity { generation: Some("ALWAYS".to_string()), seed: Some(1), increment: Some(1) }),
..Default::default()
});
id.original.as_mut().unwrap().extra = Some(
"generated always as identity (start with 1 increment by 1 minvalue 1 maxvalue 2147483647 cache 1 no cycle)"
.to_string(),
);
let result = build_table_structure_change_sql(structure_change_options(
DatabaseType::Postgres,
Some("public"),
"users",
vec![id],
));
assert_eq!(result.warnings, Vec::<String>::new());
assert_eq!(result.statements, Vec::<String>::new());
}
#[test]
fn dameng_create_table_with_identity() {
let mut col = column("ID");