fix(transfer): keep explicit auto-increment as PostgreSQL identity

This commit is contained in:
ordinary being
2026-10-18 12:02:55 +08:00
committed by GitHub
parent 6e04b6a665
commit 16755c7d6b
+97 -12
View File
@@ -1651,6 +1651,19 @@ fn is_postgres_compat_transfer(source_db: &DatabaseType, target_db: &DatabaseTyp
is_postgres_transfer_dialect(source_db) && is_postgres_transfer_dialect(target_db)
}
/// Postgres-family targets that receive an inline PostgreSQL identity column for an
/// explicit source auto-increment.
///
/// Only the PG transfer dialects with verified identity support qualify: openGauss
/// has no identity columns (the structure editor hides them there), and the other
/// PG-wire engines stay opt-in until their transfer behavior is verified.
/// PG-compatible sources keep expressing identity through the source column default
/// instead (see `postgres_default_clause`).
fn postgres_identity_target(source_db: &DatabaseType, target_db: &DatabaseType) -> bool {
matches!(target_db, DatabaseType::Postgres | DatabaseType::Kingbase)
&& !is_postgres_compat_transfer(source_db, target_db)
}
/// Oracle-family transfer dialects that return `DBMS_METADATA`-style owner-qualified
/// DDL. Dameng is deliberately excluded: its reuse path strips storage clauses as well,
/// so it keeps its own branch above.
@@ -2867,6 +2880,12 @@ fn generate_mysql_foreign_key_alter_statements(
statements
}
/// `setval` statements that move a PostgreSQL sequence or identity column past the
/// values already in the table.
///
/// Covers serial/`nextval` defaults and PostgreSQL identity extras, plus explicit
/// cross-engine auto-increment columns that the generated DDL turned into identity
/// columns (see `postgres_identity_target`).
fn generate_postgres_sequence_sync_sql(columns: &[db::ColumnInfo], table: &str, schema: &str) -> Vec<String> {
let full_table = qualified_table(table, schema, &DatabaseType::Postgres, None);
columns
@@ -2874,6 +2893,7 @@ fn generate_postgres_sequence_sync_sql(columns: &[db::ColumnInfo], table: &str,
.filter(|column| {
is_postgres_sequence_default(column.column_default.as_deref())
|| is_postgres_identity_extra(column.extra.as_deref())
|| (column.is_primary_key && column_is_auto_increment(column))
})
.map(|column| {
let quoted_column = quote_identifier(&column.name, &DatabaseType::Postgres);
@@ -4448,8 +4468,8 @@ fn generate_create_table_ddl_with_column_quoting(
// Auto-increment beyond the MySQL family goes through the shared dialect
// profile (the same facility the structure-diff generator uses), so the target
// spells it natively: Dameng and SQL Server `IDENTITY(1,1)`, Oracle
// `GENERATED BY DEFAULT AS IDENTITY`, and so on.
// spells it natively: Dameng and SQL Server `IDENTITY(1,1)`, Oracle and
// Postgres/Kingbase `GENERATED BY DEFAULT AS IDENTITY`, and so on.
//
// Only an explicit source auto-increment qualifies. A transfer must not invent
// identity the source never declared, and both MySQL (one AUTO_INCREMENT
@@ -4461,26 +4481,26 @@ fn generate_create_table_ddl_with_column_quoting(
// plain path:
// - The profile-level MySQL family beyond the transfer's own list (Databend,
// Gbase, ManticoreSearch): they reject the `AUTO_INCREMENT` spelling.
// - Postgres family (`PostgresSequence`): the target needs a created sequence
// plus `ALTER TABLE ... SET DEFAULT`, which this generator does not emit;
// those transfers keep their own sequence handling.
// - PG-wire engines without verified identity or transfer support (openGauss,
// Gaussdb, ...): see `postgres_identity_target`.
// - SQLite family: `AUTOINCREMENT` is only valid as part of an inline
// `INTEGER PRIMARY KEY`, and primary keys here are a table-level clause —
// SQLite's rowid alias already generates ids for `INTEGER PRIMARY KEY`.
let target_profile = profile_for(*target_db);
let postgres_identity = postgres_identity_target(source_db, target_db);
let inline_identity = !is_mysql_family
&& !matches!(
target_db,
DatabaseType::Sqlite | DatabaseType::Rqlite | DatabaseType::Turso | DatabaseType::CloudflareD1
)
&& match target_profile.auto_inc {
&& (match target_profile.auto_inc {
// `profile_for` puts Databend/Gbase/ManticoreSearch in the MySQL family
// too, but their grammars reject MySQL's `AUTO_INCREMENT` spelling; only
// the transfer's own MySQL-family targets above keep it.
AutoIncSyntax::Suffix(suffix) => suffix != " AUTO_INCREMENT",
AutoIncSyntax::ReplaceTypeWith(_) => true,
_ => false,
};
} || postgres_identity);
let mut col_lines = Vec::with_capacity(columns.len());
for c in columns {
@@ -4505,8 +4525,17 @@ fn generate_create_table_ddl_with_column_quoting(
build => {
let mut line = format!(" {column_name} {mapped_type}");
let skip_default = match build {
AutoIncColumnBuild::AppendSuffix { suffix, skip_default, .. } => {
line.push_str(suffix);
AutoIncColumnBuild::AppendSuffix { suffix, skip_default, postgres_sequence } => {
if postgres_sequence {
// A `PostgresSequence` profile carries no inline keyword;
// emit PostgreSQL's identity syntax — the same text
// `postgres_default_clause` already produces for
// PG-compatible sources — and keep the source's own
// sequence default out of the DDL via `skip_default`.
line.push_str(" GENERATED BY DEFAULT AS IDENTITY");
} else {
line.push_str(suffix);
}
skip_default
}
_ => false,
@@ -12750,7 +12779,10 @@ where
.await?;
}
if pg_compat_transfer {
let syncs_postgres_identity = !pg_compat_transfer
&& postgres_identity_target(source_db_type, target_db_type)
&& columns.iter().any(|column| column.is_primary_key && column_is_auto_increment(column));
if pg_compat_transfer || syncs_postgres_identity {
for statement in generate_postgres_sequence_sync_sql(&columns, &target_table, &request.target_schema) {
execute_on_pool(state, target_pool_key, &statement)
.await
@@ -20060,6 +20092,38 @@ PARTITION p_old VALUES LESS THAN (TO_DAYS('2026-01-01')))";
);
}
#[test]
fn postgres_sequence_sync_covers_cross_engine_auto_increment_columns() {
let sql = generate_postgres_sequence_sync_sql(
&[
db::ColumnInfo {
name: "id".to_string(),
data_type: "int".to_string(),
is_primary_key: true,
extra: Some("auto_increment".to_string()),
..test_column("id", "int")
},
// Not a key on the source: the generated DDL kept it a plain column,
// so it must not get a sync statement either.
db::ColumnInfo {
name: "ticket".to_string(),
data_type: "int".to_string(),
extra: Some("auto_increment".to_string()),
..test_column("ticket", "int")
},
],
"users",
"public",
);
assert_eq!(
sql,
vec![
"SELECT setval(pg_get_serial_sequence('\"public\".\"users\"', 'id')::regclass, GREATEST(COALESCE(MAX(\"id\"::bigint), 0), 1), MAX(\"id\"::bigint) IS NOT NULL) FROM \"public\".\"users\"".to_string()
]
);
}
#[test]
fn postgres_sequence_sync_sql_casts_text_and_numeric_columns_to_bigint() {
let sql = generate_postgres_sequence_sync_sql(
@@ -22635,7 +22699,26 @@ SELECT 1 FROM dual"#
}
#[test]
fn transfer_create_table_keeps_postgres_and_sqlite_auto_increment_outs() {
fn transfer_create_table_preserves_explicit_auto_increment_for_postgres_and_kingbase() {
for (source, extra) in [(DatabaseType::Mysql, "auto_increment"), (DatabaseType::SqlServer, "identity(1,1)")] {
for target in [DatabaseType::Postgres, DatabaseType::Kingbase] {
let cols = vec![db::ColumnInfo {
is_primary_key: true,
is_nullable: false,
extra: Some(extra.to_string()),
..test_column("id", "int")
}];
let ddl = generate_create_table_ddl(&cols, "users", "source", "public", &target, &source, None, None);
assert!(ddl.contains("GENERATED BY DEFAULT AS IDENTITY"), "{source:?}->{target:?}: {ddl}");
assert!(!ddl.contains("nextval"), "{source:?}->{target:?}: {ddl}");
}
}
}
#[test]
fn transfer_create_table_keeps_unverified_postgres_wire_and_sqlite_outs() {
let cols = vec![db::ColumnInfo {
is_primary_key: true,
is_nullable: false,
@@ -22643,7 +22726,9 @@ SELECT 1 FROM dual"#
..test_column("id", "int")
}];
for target in [DatabaseType::Postgres, DatabaseType::Kingbase] {
// openGauss has no identity columns, and the other PG-wire engines stay
// opt-in until their transfer behavior is verified.
for target in [DatabaseType::OpenGauss, DatabaseType::Gaussdb, DatabaseType::Vastbase] {
let ddl = generate_create_table_ddl(
&cols,
"users",