From c8efe06ddc30be3d9f22c12df61c7126990416e0 Mon Sep 17 00:00:00 2001 From: LRcoding <38545827+LwClick@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:33:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20Oracle=20=E5=A4=8D=E5=88=B6=20INSE?= =?UTF-8?q?RT=20=E6=8C=89=E8=A1=8C=E7=94=9F=E6=88=90=E8=AF=AD=E5=8F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(core): emit oracle copy inserts per row * refactor(core): extract single-row insert dialect check into sql_dialect --------- Co-authored-by: zero Co-authored-by: t8y2 <1156263951@qq.com> --- crates/dbx-core/src/data_grid_sql.rs | 34 ++++++++++++++++++- crates/dbx-core/src/database_export.rs | 32 +++++++++++++++-- crates/dbx-core/src/sql_dialect.rs | 2 +- .../dbx-core/src/sql_dialect/capabilities.rs | 6 ++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 9ae4b83eb5..cf35fdcff4 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -12,7 +12,8 @@ use data_grid_tdengine_sql::build_tdengine_data_grid_save_statements; use crate::models::connection::DatabaseType; use crate::sql_dialect::{ - firebird_rows_clause, quote_table_identifier, table_pagination_strategy, TablePaginationStrategy, + firebird_rows_clause, quote_table_identifier, table_pagination_strategy, uses_single_row_insert_statements, + TablePaginationStrategy, }; use crate::transfer::{format_ch_array_sql_literal, format_pg_array_sql_literal}; @@ -358,6 +359,15 @@ pub fn build_data_grid_copy_insert_statement(options: DataGridCopyInsertStatemen ) }) .collect::>(); + if options.database_type.is_some_and(uses_single_row_insert_statements) { + return Some( + value_rows + .iter() + .map(|values| format!("INSERT INTO {table} ({columns}) VALUES {values};")) + .collect::>() + .join("\n"), + ); + } Some(format!( "INSERT INTO {table} ({columns}) VALUES{}{};", if value_rows.len() == 1 { " " } else { "\n" }, @@ -2064,6 +2074,28 @@ mod tests { ); } + #[test] + fn oracle_copy_insert_statement_uses_one_statement_per_row() { + let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions { + database_type: Some(DatabaseType::Oracle), + table_meta: Some(DataGridTableMeta { + schema: Some("APP".to_string()), + table_name: "USERS".to_string(), + primary_keys: vec!["ID".to_string()], + columns: None, + }), + columns: vec!["ID".to_string(), "NAME".to_string()], + source_columns: None, + rows: vec![vec![json!(1), json!("Ada")], vec![json!(2), json!("Linus")]], + exclude_primary_keys: false, + }); + + assert_eq!( + statement.as_deref(), + Some("INSERT INTO \"APP\".\"USERS\" (\"ID\", \"NAME\") VALUES (1, 'Ada');\nINSERT INTO \"APP\".\"USERS\" (\"ID\", \"NAME\") VALUES (2, 'Linus');") + ); + } + #[test] fn mysql_copy_statements_preserve_blob_hex_literals() { let table_meta = DataGridTableMeta { diff --git a/crates/dbx-core/src/database_export.rs b/crates/dbx-core/src/database_export.rs index f335667032..bc227c0e0d 100644 --- a/crates/dbx-core/src/database_export.rs +++ b/crates/dbx-core/src/database_export.rs @@ -5,7 +5,7 @@ use std::io::Write; use tokio::sync::RwLock; use crate::models::connection::DatabaseType; -use crate::sql_dialect::{qualified_table_name, quote_table_identifier}; +use crate::sql_dialect::{qualified_table_name, quote_table_identifier, uses_single_row_insert_statements}; use crate::transfer::{ format_ch_array_sql_literal, format_pg_array_sql_literal, is_identity_column_extra, quote_identifier, selected_columns_include_identity_extras, wrap_dameng_identity_insert_sql, @@ -388,7 +388,11 @@ pub fn build_export_insert_statements(options: BuildExportInsertStatementsOption if insert_columns.is_empty() { return Ok(Vec::new()); } - let batch_size = options.batch_size.unwrap_or(DATABASE_EXPORT_INSERT_BATCH_SIZE).max(1); + let batch_size = if options.database_type.is_some_and(uses_single_row_insert_statements) { + 1 + } else { + options.batch_size.unwrap_or(DATABASE_EXPORT_INSERT_BATCH_SIZE).max(1) + }; let columns = insert_columns .iter() .map(|(_, column)| quote_table_identifier(options.database_type, column)) @@ -1282,6 +1286,30 @@ mod tests { ); } + #[test] + fn oracle_export_inserts_use_one_statement_per_row() { + let statements = build_export_insert_statements(BuildExportInsertStatementsOptions { + database_type: Some(DatabaseType::Oracle), + schema: Some("APP".to_string()), + table_name: Some("USERS".to_string()), + qualified_table_name: None, + columns: vec!["ID".to_string(), "NAME".to_string()], + column_types: Vec::new(), + column_extras: Vec::new(), + rows: vec![vec![json!(1), json!("Ada")], vec![json!(2), json!("Linus")]], + batch_size: Some(100), + }) + .unwrap(); + + assert_eq!( + statements, + vec![ + "INSERT INTO \"APP\".\"USERS\" (\"ID\", \"NAME\") VALUES (1, 'Ada');", + "INSERT INTO \"APP\".\"USERS\" (\"ID\", \"NAME\") VALUES (2, 'Linus');", + ] + ); + } + #[test] fn mysql_bit_columns_export_without_quoted_string_values() { let statements = build_export_insert_statements(BuildExportInsertStatementsOptions { diff --git a/crates/dbx-core/src/sql_dialect.rs b/crates/dbx-core/src/sql_dialect.rs index f8763359c4..54f3f92969 100644 --- a/crates/dbx-core/src/sql_dialect.rs +++ b/crates/dbx-core/src/sql_dialect.rs @@ -8,7 +8,7 @@ mod tests; pub use capabilities::{ firebird_rows_clause, is_schema_aware, pagination_strategy, table_pagination_strategy, uses_fetch_first, - PaginationContext, TablePaginationStrategy, + uses_single_row_insert_statements, PaginationContext, TablePaginationStrategy, }; pub use identifiers::{normalize_where_input, qualified_table_name, quote_table_identifier}; pub(crate) use identifiers::{parse_sqlserver_linked_schema_ref, qualified_transfer_table, quote_transfer_identifier}; diff --git a/crates/dbx-core/src/sql_dialect/capabilities.rs b/crates/dbx-core/src/sql_dialect/capabilities.rs index 8775c01a99..228501bb1d 100644 --- a/crates/dbx-core/src/sql_dialect/capabilities.rs +++ b/crates/dbx-core/src/sql_dialect/capabilities.rs @@ -64,6 +64,12 @@ pub fn uses_fetch_first(database_type: DatabaseType) -> bool { matches!(database_type, DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::Db2) } +/// Oracle 系方言不支持 `INSERT ... VALUES (...), (...)` 多行语法, +/// 复制为 INSERT 与导出 INSERT 都需按行生成单条语句。 +pub fn uses_single_row_insert_statements(database_type: DatabaseType) -> bool { + matches!(database_type, DatabaseType::Oracle | DatabaseType::OceanbaseOracle) +} + pub fn pagination_strategy(database_type: Option, context: PaginationContext) -> TablePaginationStrategy { match database_type { Some(DatabaseType::Jdbc) => TablePaginationStrategy::AgentMaxRows,