From 4f2c67e34e9fd02691c3d41ab8df85dfa7fbce5d Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 8 Sep 2026 00:53:12 +0800 Subject: [PATCH] fix(import): route oracle jdbc imports through insert all Closes #8309 --- crates/dbx-core/src/table_import.rs | 63 +++++++++++++++++++++++++++++ crates/dbx-core/src/transfer.rs | 60 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/crates/dbx-core/src/table_import.rs b/crates/dbx-core/src/table_import.rs index 147116edb3..128435b602 100644 --- a/crates/dbx-core/src/table_import.rs +++ b/crates/dbx-core/src/table_import.rs @@ -11096,6 +11096,69 @@ mod tests { ); } + #[tokio::test] + async fn oracle_jdbc_import_maps_xls_rows_to_insert_all() { + let dir = tempfile::tempdir().unwrap(); + let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let state = AppState::new(storage); + let connection_id = "oracle-jdbc-import"; + let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ + "id": connection_id, + "name": "Oracle over JDBC", + "db_type": "jdbc", + "host": "", + "port": 0, + "username": "", + "password": "", + "connection_string": "jdbc:oracle:thin:@localhost:1521:ORCL", + "jdbc_driver_class": "oracle.jdbc.driver.OracleDriver" + })) + .unwrap(); + state.configs.write().await.insert(connection_id.to_string(), config); + + let db_type = crate::transfer::get_db_type(&state, connection_id).await.unwrap(); + assert_eq!(db_type, DatabaseType::Oracle); + + let mappings = vec![ + TableImportColumnMapping { + source_column: "id".to_string(), + target_column: "id".to_string(), + target_data_type: None, + }, + TableImportColumnMapping { + source_column: "name".to_string(), + target_column: "name".to_string(), + target_data_type: None, + }, + ]; + let data = ParsedImportFile { + columns: vec!["id".to_string(), "name".to_string()], + rows: vec![ + vec![serde_json::json!(1), serde_json::json!("Ada")], + vec![serde_json::json!(2), serde_json::json!("Grace")], + ], + total_rows: 2, + effective_encoding: None, + }; + + let batches = build_import_insert_batches( + &data, + &mappings, + &[("id".to_string(), "NUMBER".to_string()), ("name".to_string(), "VARCHAR2(64)".to_string())], + "events", + "APP", + &db_type, + 500, + ) + .unwrap(); + + assert_eq!(batches.len(), 1); + assert_eq!(batches[0].row_count, 2); + assert!(batches[0].sql.starts_with("INSERT ALL\nINTO ")); + assert!(batches[0].sql.ends_with("SELECT 1 FROM dual")); + assert!(!batches[0].sql.contains("),\n(")); + } + fn kingbase_date_import_sql(oracle_mode: bool) -> String { let mappings = vec![TableImportColumnMapping { source_column: "created_at".to_string(), diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index c3ea03e7f4..22bdf31a15 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -5349,6 +5349,16 @@ fn effective_transfer_database_type(config: &ConnectionConfig) -> DatabaseType { return DatabaseType::Jdbc; } + // Oracle behind a generic JDBC connection: Oracle rejects MySQL-style + // multi-row VALUES lists with ORA-00933, so reuse the native Oracle + // transfer dialect (INSERT ALL, TO_DATE literals, row-capped batches). + let is_oracle = + config.connection_string.as_deref().is_some_and(|url| url.to_ascii_lowercase().contains("jdbc:oracle:")) + || config.jdbc_driver_class.as_deref().is_some_and(|class| class.to_ascii_lowercase().contains("oracle")); + if is_oracle { + return DatabaseType::Oracle; + } + let jdbc_identity = [ config.driver_profile.as_deref().unwrap_or(""), config.connection_string.as_deref().unwrap_or(""), @@ -12896,6 +12906,56 @@ SELECT 1 FROM dual"# assert!(statements[0].ends_with("SELECT 1 FROM dual")); } + #[test] + fn oracle_jdbc_url_routes_multi_row_insert_through_insert_all() { + let config = + jdbc_transfer_config("jdbc:oracle:thin:@localhost:1521:ORCL", "oracle.jdbc.driver.OracleDriver", ""); + assert_eq!(effective_transfer_database_type(&config), DatabaseType::Oracle); + // Mixed-case URL and driver class alone must be recognized too. + assert_eq!( + effective_transfer_database_type(&jdbc_transfer_config("JDBC:Oracle:thin:@localhost:1521:ORCL", "", "")), + DatabaseType::Oracle + ); + assert_eq!( + effective_transfer_database_type(&jdbc_transfer_config("", "oracle.jdbc.OracleDriver", "")), + DatabaseType::Oracle + ); + + let sql = generate_insert_typed( + &[String::from("id"), String::from("name")], + &[Some(String::from("number")), Some(String::from("varchar2(64)"))], + &[vec![json!(1), json!("Ada")], vec![json!(2), json!("Grace")]], + "INSTR_CATEGORY", + "APP", + &effective_transfer_database_type(&config), + None, + ); + + assert!(sql.starts_with("INSERT ALL\nINTO ")); + assert!(sql.ends_with("SELECT 1 FROM dual")); + assert!(!sql.contains("),\n(")); + } + + #[test] + fn non_oracle_jdbc_url_keeps_multi_row_values_insert() { + let config = jdbc_transfer_config("jdbc:mysql://localhost:3306/dbx_test", "com.mysql.cj.jdbc.Driver", ""); + assert_eq!(effective_transfer_database_type(&config), DatabaseType::Jdbc); + + let sql = generate_insert_typed( + &[String::from("id"), String::from("name")], + &[Some(String::from("int")), Some(String::from("varchar(64)"))], + &[vec![json!(1), json!("Ada")], vec![json!(2), json!("Grace")]], + "instr_category", + "", + &effective_transfer_database_type(&config), + None, + ); + + assert!(sql.starts_with("INSERT INTO ")); + assert!(sql.contains("),\n(")); + assert!(!sql.contains("INSERT ALL")); + } + #[test] fn transfer_write_sql_batches_split_large_insert_statements() { let rows = (0..4).map(|index| vec![json!(index), json!("x".repeat(180 * 1024))]).collect::>();