From 9008169dd19d91d92fe4599b24b760e53afdb682 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 25 May 2026 10:55:16 +0800 Subject: [PATCH] fix: format PostgreSQL array values as {..} instead of JSON [..] in SQL export PostgreSQL array columns were serialized as JSON arrays (e.g. [1,2,3]) by serde_json::Value::Array Display, producing invalid SQL literals like '[1,2,3]' and '[]' instead of PostgreSQL's required '{1,2,3}' and '{}'. Add format_pg_array_sql_literal helper and apply it to all three SQL literal formatting paths: escape_value_typed (transfer), format_export_sql_literal (database_export), and format_grid_sql_literal (data_grid_sql). --- crates/dbx-core/src/data_grid_sql.rs | 4 +++ crates/dbx-core/src/database_export.rs | 4 +++ crates/dbx-core/src/transfer.rs | 36 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 81d7e7813..c9cf2e8c7 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -4,6 +4,7 @@ use serde_json::Value; use crate::models::connection::DatabaseType; use crate::sql_dialect::quote_table_identifier; +use crate::transfer::format_pg_array_sql_literal; const DBX_ROWID_COLUMN: &str = "__DBX_ROWID"; const DBX_NEO4J_ELEMENT_ID_COLUMN: &str = "__DBX_ELEMENT_ID"; @@ -936,6 +937,9 @@ pub fn format_grid_sql_literal( if let Some(number) = value.as_number() { return number.to_string(); } + if let Some(arr) = value.as_array() { + return format_pg_array_sql_literal(arr); + } let text = value.as_str().map_or_else(|| value.to_string(), ToString::to_string); if text.is_empty() { return if database_type == Some(DatabaseType::SqlServer) { "N''" } else { "''" }.to_string(); diff --git a/crates/dbx-core/src/database_export.rs b/crates/dbx-core/src/database_export.rs index 50ac86281..608e96330 100644 --- a/crates/dbx-core/src/database_export.rs +++ b/crates/dbx-core/src/database_export.rs @@ -6,6 +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; static EXPORT_CANCELLED: std::sync::LazyLock>> = std::sync::LazyLock::new(|| RwLock::new(HashSet::new())); @@ -125,6 +126,9 @@ pub fn format_export_sql_literal(value: &Value) -> String { if let Some(value) = value.as_bool() { return if value { "TRUE" } else { "FALSE" }.to_string(); } + if let Some(arr) = value.as_array() { + return format_pg_array_sql_literal(arr); + } let text = value.as_str().map_or_else(|| value.to_string(), ToString::to_string); format!("'{}'", text.replace('\\', "\\\\").replace('\'', "''")) } diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index 56b2c17aa..15439f1ca 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -108,6 +108,7 @@ 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), _ => { let s = val.to_string(); format!("'{}'", s.replace('\\', "\\\\").replace('\'', "''")) @@ -115,6 +116,41 @@ pub fn escape_value_typed(val: &serde_json::Value, db_type: &DatabaseType, colum } } +pub fn format_pg_array_sql_literal(arr: &[serde_json::Value]) -> String { + if arr.is_empty() { + return "'{}'".to_string(); + } + let elements: Vec = arr.iter().map(format_pg_array_element).collect(); + let inner = format!("{{{}}}", elements.join(",")); + format!("'{}'", inner.replace('\\', "\\\\").replace('\'', "''")) +} + +fn format_pg_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_pg_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(); + let escaped = json.replace('\\', "\\\\").replace('"', "\\\""); + format!("\"{}\"", escaped) + } + } +} + 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())