From 0023c03520d876759b88c9f01cf0a3f3bccaaab7 Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 22 Sep 2026 12:52:54 +0800 Subject: [PATCH] fix(transfer): map text types to CLOB for oracle-family targets --- crates/dbx-core/src/data/transfer.rs | 107 +++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/crates/dbx-core/src/data/transfer.rs b/crates/dbx-core/src/data/transfer.rs index d8b43a1a1d..3ebecdaf8d 100644 --- a/crates/dbx-core/src/data/transfer.rs +++ b/crates/dbx-core/src/data/transfer.rs @@ -3277,6 +3277,19 @@ fn oracle_char_length_params(params: &str) -> String { format!("({digits} CHAR)") } +/// Text-ish source types resolve here. Oracle-syntax-family targets (Oracle, +/// OceanBase Oracle mode, Dameng) have no `TEXT` data type — large character +/// values live in `CLOB` — so emitting `TEXT` there makes the generated +/// `CREATE TABLE` invalid (`ORA-00902: invalid datatype`, OceanBase +/// `OBE-00900`). `table_import`'s `text_data_type` already maps Oracle-family +/// targets to `CLOB` for file imports; the transfer path did not (#9886). +fn target_text_type(target_db: &DatabaseType) -> &'static str { + match target_db { + DatabaseType::Oracle | DatabaseType::OceanbaseOracle | DatabaseType::Dameng => "CLOB", + _ => "TEXT", + } +} + pub fn map_column_type(source_type: &str, source_db: &DatabaseType, target_db: &DatabaseType) -> String { if source_db == target_db { return source_type.to_string(); @@ -3399,13 +3412,13 @@ pub fn map_column_type(source_type: &str, source_db: &DatabaseType, target_db: & } "longtext" => match target_db { DatabaseType::Mysql => "LONGTEXT".into(), - _ => "TEXT".into(), + _ => target_text_type(target_db).into(), }, "mediumtext" => match target_db { DatabaseType::Mysql => "MEDIUMTEXT".into(), - _ => "TEXT".into(), + _ => target_text_type(target_db).into(), }, - "text" | "tinytext" | "clob" | "ntext" => "TEXT".into(), + "text" | "tinytext" | "clob" | "ntext" => target_text_type(target_db).into(), "bool" | "boolean" => match target_db { DatabaseType::Mysql => "TINYINT(1)".into(), DatabaseType::SqlServer => "BIT".into(), @@ -3450,7 +3463,7 @@ pub fn map_column_type(source_type: &str, source_db: &DatabaseType, target_db: & "json" | "jsonb" => match target_db { target_db if is_postgres_transfer_dialect(target_db) => "JSONB".into(), DatabaseType::Mysql => "JSON".into(), - _ => "TEXT".into(), + _ => target_text_type(target_db).into(), }, "uuid" => match target_db { target_db if is_postgres_transfer_dialect(target_db) => "UUID".into(), @@ -3460,7 +3473,7 @@ pub fn map_column_type(source_type: &str, source_db: &DatabaseType, target_db: & target_db if is_postgres_transfer_dialect(target_db) => "BOOLEAN".into(), _ => "BIT".into(), }, - _ => "TEXT".into(), + _ => target_text_type(target_db).into(), } } @@ -16391,6 +16404,90 @@ SELECT 1 FROM dual"# assert!(ddl.contains("\"PLAINNUMBER\" NUMERIC"), "{ddl}"); } + #[test] + fn map_column_type_uses_clob_for_oracle_family_text_types() { + // Issue #9886: an Oracle 19c table transferred to OceanBase 4.4(Oracle mode) failed + // with `OBE-00900` because every text-ish column was emitted as `TEXT`. Oracle syntax + // has no `TEXT` data type — the large-character type is `CLOB` — so Oracle-family + // targets must map text here exactly like `table_import`'s `text_data_type` does. + for target in [DatabaseType::OceanbaseOracle, DatabaseType::Dameng] { + // Oracle source: CLOB survives the hop to the Oracle-family target. + assert_eq!(map_column_type("CLOB", &DatabaseType::Oracle, &target), "CLOB"); + assert_eq!(map_column_type("clob", &DatabaseType::Oracle, &target), "CLOB"); + // Every text-ish source type funnels into the same target spelling. + assert_eq!(map_column_type("TEXT", &DatabaseType::Mysql, &target), "CLOB"); + assert_eq!(map_column_type("tinytext", &DatabaseType::Mysql, &target), "CLOB"); + assert_eq!(map_column_type("longtext", &DatabaseType::Mysql, &target), "CLOB"); + assert_eq!(map_column_type("mediumtext", &DatabaseType::Mysql, &target), "CLOB"); + assert_eq!(map_column_type("text", &DatabaseType::Postgres, &target), "CLOB"); + assert_eq!(map_column_type("ntext", &DatabaseType::SqlServer, &target), "CLOB"); + assert_eq!(map_column_type("json", &DatabaseType::Mysql, &target), "CLOB"); + // Unrecognised source types fall back to the large-character type instead of an + // `TEXT` spelling the target rejects outright. + assert_eq!(map_column_type("geometry", &DatabaseType::Mysql, &target), "CLOB"); + } + + // A plain Oracle target behaves identically. + assert_eq!(map_column_type("CLOB", &DatabaseType::OceanbaseOracle, &DatabaseType::Oracle), "CLOB"); + assert_eq!(map_column_type("TEXT", &DatabaseType::Mysql, &DatabaseType::Oracle), "CLOB"); + assert_eq!(map_column_type("longtext", &DatabaseType::Mysql, &DatabaseType::Oracle), "CLOB"); + + // Non-Oracle-family targets keep their historical `TEXT` spelling. + assert_eq!(map_column_type("TEXT", &DatabaseType::Mysql, &DatabaseType::Postgres), "TEXT"); + assert_eq!(map_column_type("CLOB", &DatabaseType::Oracle, &DatabaseType::Mysql), "TEXT"); + assert_eq!(map_column_type("ntext", &DatabaseType::SqlServer, &DatabaseType::Sqlite), "TEXT"); + assert_eq!(map_column_type("geometry", &DatabaseType::Mysql, &DatabaseType::Sqlite), "TEXT"); + } + + #[test] + fn transfer_create_table_oracle_to_oceanbase_oracle_uses_clob_for_text_columns() { + let columns = vec![ + db::ColumnInfo { is_primary_key: true, ..test_column("ID", "NUMBER(10)") }, + test_column("RESPONSEXML", "CLOB"), + test_column("REMARK", "VARCHAR2(100 BYTE)"), + ]; + + let ddl = generate_create_table_ddl( + &columns, + "T_CLOB_PROBE", + "DBX_TEST", + "DBX_TEST", + &DatabaseType::OceanbaseOracle, + &DatabaseType::Oracle, + None, + None, + ); + + assert!(ddl.contains("\"RESPONSEXML\" CLOB"), "{ddl}"); + assert!(!ddl.contains("TEXT"), "{ddl}"); + // The byte length unit still travels between Oracle-family targets. + assert!(ddl.contains("\"REMARK\" VARCHAR(100 byte)"), "{ddl}"); + } + + #[test] + fn transfer_create_table_mysql_to_oracle_uses_clob_for_text_columns() { + let columns = vec![ + db::ColumnInfo { is_primary_key: true, ..test_column("ID", "INT") }, + test_column("RESPONSE_XML", "TEXT"), + test_column("PAYLOAD", "LONGTEXT"), + ]; + + let ddl = generate_create_table_ddl( + &columns, + "SRC_CLOB_PROBE", + "dbx", + "DBX_TEST", + &DatabaseType::Oracle, + &DatabaseType::Mysql, + None, + None, + ); + + assert!(ddl.contains("\"RESPONSE_XML\" CLOB"), "{ddl}"); + assert!(ddl.contains("\"PAYLOAD\" CLOB"), "{ddl}"); + assert!(!ddl.contains("TEXT"), "{ddl}"); + } + #[test] fn map_column_type_oracle_plain_varchar2_to_mysql() { // Plain VARCHAR2 without a length unit must keep working unchanged.