diff --git a/crates/dbx-core/src/data/transfer.rs b/crates/dbx-core/src/data/transfer.rs index 36c90305c0..d8b43a1a1d 100644 --- a/crates/dbx-core/src/data/transfer.rs +++ b/crates/dbx-core/src/data/transfer.rs @@ -3354,13 +3354,22 @@ pub fn map_column_type(source_type: &str, source_db: &DatabaseType, target_db: & }, "decimal" | "numeric" | "number" => { if t.contains('(') { + let params = &t[t.find('(').unwrap()..]; match target_db { - DatabaseType::Mysql | DatabaseType::SqlServer | DatabaseType::Oracle | DatabaseType::H2 => { - format!("DECIMAL{}", &t[t.find('(').unwrap()..]) - } - target_db if is_postgres_transfer_dialect(target_db) => { - format!("DECIMAL{}", &t[t.find('(').unwrap()..]) - } + // Oracle-family targets accept DECIMAL(p, s) as a synonym for + // NUMBER(p, s). OceanBase's Oracle mode and Dameng used to fall into + // the bare-NUMERIC fallback below, which is NUMBER(38, 0): transferring + // Oracle NUMBER(14, 2) silently dropped every decimal (#9667). + DatabaseType::Mysql + | DatabaseType::SqlServer + | DatabaseType::Oracle + | DatabaseType::OceanbaseOracle + | DatabaseType::Dameng + | DatabaseType::H2 => format!("DECIMAL{params}"), + target_db if is_postgres_transfer_dialect(target_db) => format!("DECIMAL{params}"), + // Every other target keeps the historical bare spelling: its decimal + // semantics are not the Oracle NUMBER synonym, so passing (p, s) through + // needs per-engine verification and stays out of scope here. _ => "NUMERIC".into(), } } else { @@ -16333,6 +16342,55 @@ SELECT 1 FROM dual"# assert_eq!(map_column_type("NVARCHAR2(50 CHAR)", &DatabaseType::Oracle, &DatabaseType::Mysql), "TEXT"); } + #[test] + fn map_column_type_keeps_number_precision_for_oracle_family_targets() { + // Issue #9667: transferring an Oracle table to OceanBase(Oracle mode) generated a + // bare `NUMERIC` for every NUMBER(p, s) column. In Oracle-compatible semantics that + // is NUMBER(38, 0), so the target silently dropped every decimal (only the integer + // part survived). Oracle-family targets must keep precision and scale. + for target in [DatabaseType::OceanbaseOracle, DatabaseType::Dameng] { + assert_eq!(map_column_type("NUMBER(14,2)", &DatabaseType::Oracle, &target), "DECIMAL(14,2)"); + assert_eq!(map_column_type("NUMBER(14,10)", &DatabaseType::Oracle, &target), "DECIMAL(14,10)"); + assert_eq!(map_column_type("NUMBER(15,0)", &DatabaseType::Oracle, &target), "DECIMAL(15,0)"); + assert_eq!(map_column_type("NUMBER(12,4)", &DatabaseType::Oracle, &target), "DECIMAL(12,4)"); + assert_eq!(map_column_type("NUMBER(10,-2)", &DatabaseType::Oracle, &target), "DECIMAL(10,-2)"); + // Spacing inside the parameter list is preserved verbatim, exactly like the + // existing Mysql/H2/Oracle branches. + assert_eq!(map_column_type("NUMBER(14, 2)", &DatabaseType::Oracle, &target), "DECIMAL(14, 2)"); + } + + // A source type without parameters keeps the historical bare spelling: it carries + // no precision to lose. + assert_eq!(map_column_type("NUMBER", &DatabaseType::Oracle, &DatabaseType::OceanbaseOracle), "NUMERIC"); + assert_eq!(map_column_type("NUMBER", &DatabaseType::Oracle, &DatabaseType::Dameng), "NUMERIC"); + } + + #[test] + fn transfer_create_table_oracle_to_oceanbase_oracle_keeps_number_scale() { + let columns = vec![ + db::ColumnInfo { is_primary_key: true, ..test_column("ID", "NUMBER(10)") }, + test_column("SUMZEROTAXPREMIUM", "NUMBER(14,2)"), + test_column("XBCOMPANYRATE", "NUMBER(14,10)"), + test_column("PLAINNUMBER", "NUMBER"), + ]; + + let ddl = generate_create_table_ddl( + &columns, + "TXF_PREC", + "DBX_TEST", + "DBX_TEST", + &DatabaseType::OceanbaseOracle, + &DatabaseType::Oracle, + None, + None, + ); + + assert!(ddl.contains("\"SUMZEROTAXPREMIUM\" DECIMAL(14,2)"), "{ddl}"); + assert!(ddl.contains("\"XBCOMPANYRATE\" DECIMAL(14,10)"), "{ddl}"); + assert!(ddl.contains("\"ID\" DECIMAL(10)"), "{ddl}"); + assert!(ddl.contains("\"PLAINNUMBER\" NUMERIC"), "{ddl}"); + } + #[test] fn map_column_type_oracle_plain_varchar2_to_mysql() { // Plain VARCHAR2 without a length unit must keep working unchanged. diff --git a/docs/screenshots/issue-9667-mysql-target-data.png b/docs/screenshots/issue-9667-mysql-target-data.png new file mode 100644 index 0000000000..80cd072e37 Binary files /dev/null and b/docs/screenshots/issue-9667-mysql-target-data.png differ diff --git a/docs/screenshots/issue-9667-mysql-target-metadata.png b/docs/screenshots/issue-9667-mysql-target-metadata.png new file mode 100644 index 0000000000..cf4f2823b3 Binary files /dev/null and b/docs/screenshots/issue-9667-mysql-target-metadata.png differ diff --git a/docs/screenshots/issue-9667-oracle-bare-numeric-vs-decimal.png b/docs/screenshots/issue-9667-oracle-bare-numeric-vs-decimal.png new file mode 100644 index 0000000000..ab9b646a78 Binary files /dev/null and b/docs/screenshots/issue-9667-oracle-bare-numeric-vs-decimal.png differ diff --git a/docs/screenshots/issue-9667-oracle-source-metadata.png b/docs/screenshots/issue-9667-oracle-source-metadata.png new file mode 100644 index 0000000000..f7de01d7e2 Binary files /dev/null and b/docs/screenshots/issue-9667-oracle-source-metadata.png differ