mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:10:49 +00:00
fix(transfer): keep explicit auto-increment as PostgreSQL identity
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user