From 175c79f2eeaf471cc2cd7c9f056f38a0bcb5bd86 Mon Sep 17 00:00:00 2001 From: ordinary being Date: Mon, 19 Oct 2026 11:58:45 +0800 Subject: [PATCH] fix(transfer): keep identity sources auto-increment on mysql targets --- crates/dbx-core/src/data/transfer.rs | 72 +++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/crates/dbx-core/src/data/transfer.rs b/crates/dbx-core/src/data/transfer.rs index c8de5bbe6b..e32f2eb85d 100644 --- a/crates/dbx-core/src/data/transfer.rs +++ b/crates/dbx-core/src/data/transfer.rs @@ -2762,6 +2762,15 @@ fn parse_mysql_extra_clauses(extra: Option<&str>) -> MysqlExtraClauses { result } +/// Whether a mapped target type is one MySQL accepts for `AUTO_INCREMENT`. +/// +/// MySQL allows the clause on integer columns only, so an identity source mapped to +/// `DECIMAL`/`NUMBER` must not receive it even when its metadata is identity-style. +fn mysql_auto_increment_type_supported(mapped_type: &str) -> bool { + let base = mapped_type.split(['(', ' ']).next().unwrap_or("").trim().to_ascii_lowercase(); + matches!(base.as_str(), "tinyint" | "smallint" | "mediumint" | "int" | "integer" | "bigint") +} + fn postgres_order_by_expression(columns: &[String], db_type: &DatabaseType) -> Option { postgres_order_by_expression_with_identifier_quote(columns, db_type, None) } @@ -4768,7 +4777,15 @@ fn generate_create_table_ddl_with_column_quoting( } if is_mysql_family { let extra_clauses = parse_mysql_extra_clauses(c.extra.as_deref()); - if extra_clauses.auto_increment { + // `AUTO_INCREMENT` must also follow identity-style sources (SQL + // Server `identity(1, 1)`, Dameng `identity`): the MySQL-spelling + // parser only recognises `auto_increment`. Only a primary key on + // an integer column qualifies — MySQL requires the auto-increment + // column to be a key and rejects the clause on other types. + let identity_source = c.is_primary_key + && column_is_auto_increment(c) + && mysql_auto_increment_type_supported(&mapped_type); + if extra_clauses.auto_increment || identity_source { line.push_str(" AUTO_INCREMENT"); } if let Some(on_update_expr) = extra_clauses.on_update { @@ -23337,6 +23354,59 @@ SELECT 1 FROM dual"# } } + #[test] + fn transfer_create_table_preserves_identity_sources_for_mysql() { + // `AUTO_INCREMENT` must also follow identity-style sources (SQL Server + // `identity(1, 1)`, Dameng `identity`); the MySQL-spelling parser alone only + // recognises `auto_increment`. + for (source, extra) in [(DatabaseType::SqlServer, "identity(1,1)"), (DatabaseType::Dameng, "identity")] { + 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", "shop", &DatabaseType::Mysql, &source, None, None); + assert!(ddl.contains("`id` INT NOT NULL AUTO_INCREMENT"), "{source:?}->Mysql: {ddl}"); + } + } + + #[test] + fn transfer_create_table_keeps_non_key_identity_sources_out_of_auto_increment() { + // MySQL requires the auto-increment column to be a key: an identity source on + // a non-key column stays a plain column instead of failing CREATE TABLE. + let cols = vec![db::ColumnInfo { + is_nullable: false, + extra: Some("identity".to_string()), + ..test_column("ticket", "int") + }]; + + let ddl = generate_create_table_ddl( + &cols, + "users", + "source", + "shop", + &DatabaseType::Mysql, + &DatabaseType::Dameng, + None, + None, + ); + + assert!(!ddl.contains("AUTO_INCREMENT"), "ddl: {ddl}"); + } + + #[test] + fn mysql_auto_increment_type_supported_accepts_only_mysql_integers() { + for supported in ["INT", "int(11)", "BIGINT", "TINYINT(1)", "SMALLINT UNSIGNED", "MEDIUMINT", "INTEGER"] { + assert!(mysql_auto_increment_type_supported(supported), "{supported}"); + } + for rejected in ["DECIMAL(10,0)", "NUMBER(10)", "VARCHAR(32)", "TEXT", "POINT", "DATETIME"] { + assert!(!mysql_auto_increment_type_supported(rejected), "{rejected}"); + } + } + #[test] fn postgres_create_table_default_clause_unchanged() { let cols = vec![db::ColumnInfo {