From 3b457f780310b637df77fdcddc1c01dc30f02271 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sun, 7 Jun 2026 22:45:11 +0800 Subject: [PATCH] fix(core): use ClickHouse array syntax `[...]` instead of PostgreSQL `{...}` for array literals ClickHouse and Databend expect square-bracket array literals (unquoted), while PostgreSQL/DuckDB/etc. use curly-brace quoted format. Previously all array values were unconditionally rendered with `'{...}'` syntax, which produced wrong INSERT/UPDATE statements for ClickHouse columns of nested array types like `Array(Array(Point))`. --- crates/dbx-core/src/data_grid_sql.rs | 5 ++- crates/dbx-core/src/database_export.rs | 7 ++++- crates/dbx-core/src/transfer.rs | 42 +++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 2d4edf1e42..4f14630b2f 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -12,7 +12,7 @@ use data_grid_tdengine_sql::build_tdengine_data_grid_save_statements; use crate::models::connection::DatabaseType; use crate::sql_dialect::quote_table_identifier; -use crate::transfer::format_pg_array_sql_literal; +use crate::transfer::{format_ch_array_sql_literal, format_pg_array_sql_literal}; const DBX_ROWID_COLUMN: &str = "__DBX_ROWID"; pub(crate) const DBX_NEO4J_ELEMENT_ID_COLUMN: &str = "__DBX_ELEMENT_ID"; @@ -743,6 +743,9 @@ pub fn format_grid_sql_literal( return number.to_string(); } if let Some(arr) = value.as_array() { + if matches!(database_type, Some(DatabaseType::ClickHouse) | Some(DatabaseType::Databend)) { + return format_ch_array_sql_literal(arr); + } return format_pg_array_sql_literal(arr); } let text = value.as_str().map_or_else(|| value.to_string(), ToString::to_string); diff --git a/crates/dbx-core/src/database_export.rs b/crates/dbx-core/src/database_export.rs index b48c37d7db..c43732ef88 100644 --- a/crates/dbx-core/src/database_export.rs +++ b/crates/dbx-core/src/database_export.rs @@ -6,7 +6,7 @@ use tokio::sync::RwLock; use crate::models::connection::DatabaseType; use crate::sql_dialect::{qualified_table_name, quote_table_identifier}; -use crate::transfer::format_pg_array_sql_literal; +use crate::transfer::{format_ch_array_sql_literal, format_pg_array_sql_literal}; static EXPORT_CANCELLED: std::sync::LazyLock>> = std::sync::LazyLock::new(|| RwLock::new(HashSet::new())); @@ -146,6 +146,11 @@ fn format_export_sql_literal_typed( if matches!(database_type, Some(DatabaseType::Mysql)) && column_type.is_some_and(is_mysql_bit_type) { return format_mysql_bit_literal(value); } + if let Some(arr) = value.as_array() { + if matches!(database_type, Some(DatabaseType::ClickHouse) | Some(DatabaseType::Databend)) { + return format_ch_array_sql_literal(arr); + } + } format_export_sql_literal(value) } diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index 7099c6d526..0a23611e8d 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -485,7 +485,10 @@ pub fn escape_value_typed(val: &serde_json::Value, db_type: &DatabaseType, colum serde_json::Value::String(s) => { format!("'{}'", format_literal_string(s, db_type, column_type).replace('\\', "\\\\").replace('\'', "''")) } - serde_json::Value::Array(arr) => format_pg_array_sql_literal(arr), + serde_json::Value::Array(arr) => match db_type { + DatabaseType::ClickHouse | DatabaseType::Databend => format_ch_array_sql_literal(arr), + _ => format_pg_array_sql_literal(arr), + }, _ => { let s = val.to_string(); format!("'{}'", s.replace('\\', "\\\\").replace('\'', "''")) @@ -532,6 +535,43 @@ fn format_pg_array_element(val: &serde_json::Value) -> String { } } +pub fn format_ch_array_sql_literal(arr: &[serde_json::Value]) -> String { + if arr.is_empty() { + return "[]".to_string(); + } + let elements: Vec = arr.iter().map(format_ch_array_element).collect(); + format!("[{}]", elements.join(",")) +} + +fn format_ch_array_element(val: &serde_json::Value) -> String { + match val { + serde_json::Value::Null => "NULL".to_string(), + serde_json::Value::Array(arr) => { + if arr.is_empty() { + return "[]".to_string(); + } + let elements: Vec = arr.iter().map(format_ch_array_element).collect(); + format!("[{}]", elements.join(",")) + } + serde_json::Value::String(s) => { + let escaped = s.replace('\\', "\\\\").replace('\'', "''"); + format!("'{}'", escaped) + } + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::Bool(b) => { + if *b { + "true".to_string() + } else { + "false".to_string() + } + } + serde_json::Value::Object(o) => { + let json = serde_json::to_string(o).unwrap_or_default(); + format!("'{}'", json.replace('\\', "\\\\").replace('\'', "''")) + } + } +} + fn format_literal_string(value: &str, db_type: &DatabaseType, column_type: Option<&str>) -> String { if is_mysql_datetime_literal_database(db_type) && column_type.map(is_temporal_column_type).unwrap_or(true) { normalize_mysql_temporal_literal(value, column_type).unwrap_or_else(|| value.to_string())