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))`.
This commit is contained in:
t8y2 2026-06-07 22:45:11 +08:00
parent 4a64240e8a
commit 3b457f7803
3 changed files with 51 additions and 3 deletions

View File

@ -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);

View File

@ -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<RwLock<HashSet<String>>> =
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)
}

View File

@ -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<String> = 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<String> = 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())