fix(postgres): decode json arrays without binary prefixes

This commit is contained in:
t8y2 2026-07-31 15:10:48 +08:00
parent e48902fa06
commit 65bf06f12e
No known key found for this signature in database
2 changed files with 195 additions and 0 deletions

View File

@ -1774,6 +1774,9 @@ pub fn format_grid_sql_literal(
return number.to_string();
}
if let Some(arr) = value.as_array() {
if let Some(element_type) = postgres_json_array_element_type(database_type, column_info) {
return format_postgres_json_array_sql_literal(arr, element_type);
}
if matches!(database_type, Some(DatabaseType::ClickHouse) | Some(DatabaseType::Databend)) {
return format_ch_array_sql_literal(arr);
}
@ -1855,6 +1858,44 @@ pub fn format_grid_sql_literal(
escaped
}
fn postgres_json_array_element_type(
database_type: Option<DatabaseType>,
column_info: Option<&DataGridColumnInfo>,
) -> Option<&'static str> {
if database_type != Some(DatabaseType::Postgres) {
return None;
}
match column_info?.data_type.trim().to_ascii_lowercase().as_str() {
"json[]" | "_json" => Some("json"),
"jsonb[]" | "_jsonb" => Some("jsonb"),
_ => None,
}
}
fn format_postgres_json_array_sql_literal(arr: &[Value], element_type: &str) -> String {
if arr.is_empty() {
return format!("ARRAY[]::{element_type}[]");
}
let elements = arr
.iter()
.map(|value| {
if value.is_null() {
return "NULL".to_string();
}
let json = match value {
Value::String(text) => serde_json::from_str::<Value>(text)
.map(|value| value.to_string())
.unwrap_or_else(|_| Value::String(text.clone()).to_string()),
_ => value.to_string(),
};
let escaped = json.replace('\\', "\\\\").replace('\'', "''");
format!("E'{escaped}'::{element_type}")
})
.collect::<Vec<_>>()
.join(", ");
format!("ARRAY[{elements}]")
}
fn format_sqlserver_unicode_literal(text: &str) -> String {
let mut parts = Vec::new();
let mut segment = String::new();
@ -2959,6 +3000,78 @@ mod tests {
}
}
#[test]
fn postgres_keyless_update_preserves_jsonb_array_elements() {
let endpoints =
json!([r#"{"port":10031,"type":"admin_web"}"#, r#""quoted""#, r#"[1,true,{"nested":null}]"#, null]);
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: Some("public".to_string()),
table_name: "services".to_string(),
primary_keys: vec![],
columns: Some(vec![
column("id", "integer", false, None),
column("name", "text", false, None),
column("endpoints", "jsonb[]", true, None),
]),
},
columns: vec!["id".to_string(), "name".to_string(), "endpoints".to_string()],
source_columns: None,
rows: vec![vec![json!(1), json!("before"), endpoints]],
dirty_rows: vec![(0, vec![(1, json!("after"))])],
deleted_rows: vec![],
new_rows: vec![],
});
assert_eq!(result.validation_error, None);
assert_eq!(result.statements.len(), 1);
let statement = &result.statements[0];
assert!(statement.starts_with("UPDATE \"public\".\"services\" SET \"name\" = 'after' WHERE "));
assert!(statement.contains("\"endpoints\" = ARRAY["));
assert!(statement.contains("admin_web"));
assert!(statement.contains(r#"E'"quoted"'::jsonb"#), "{statement}");
assert!(statement.contains("NULL]"));
assert!(!statement.contains('\u{1}'));
}
#[test]
fn postgres_json_array_literals_preserve_json_documents_and_nulls() {
let value = json!([r#"{"object":true}"#, r#""text""#, "[1,2]", "plain text", "null", null]);
let json_column = column("payload", "json[]", true, None);
let jsonb_column = column("payload", "jsonb[]", true, None);
let text_column = column("payload", "text[]", true, None);
let integer_column = column("payload", "integer[]", true, None);
assert_eq!(
format_grid_sql_literal(&value, Some(DatabaseType::Postgres), Some(&json_column)),
r#"ARRAY[E'{"object":true}'::json, E'"text"'::json, E'[1,2]'::json, E'"plain text"'::json, E'null'::json, NULL]"#
);
assert_eq!(
format_grid_sql_literal(&value, Some(DatabaseType::Postgres), Some(&jsonb_column)),
r#"ARRAY[E'{"object":true}'::jsonb, E'"text"'::jsonb, E'[1,2]'::jsonb, E'"plain text"'::jsonb, E'null'::jsonb, NULL]"#
);
assert_eq!(
format_grid_sql_literal(&json!([]), Some(DatabaseType::Postgres), Some(&jsonb_column)),
"ARRAY[]::jsonb[]"
);
assert_eq!(
format_grid_sql_literal(
&json!(["first", null, "second"]),
Some(DatabaseType::Postgres),
Some(&text_column)
),
r#"'{"first",NULL,"second"}'"#
);
assert_eq!(
format_grid_sql_literal(&json!([1, null, 2]), Some(DatabaseType::Postgres), Some(&integer_column)),
"'{1,NULL,2}'"
);
}
#[test]
fn builds_copy_update_statements() {
let statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {

View File

@ -230,6 +230,10 @@ fn pg_optional_array_to_json<T>(
)
}
fn pg_json_array_values_to_json(values: Vec<Option<serde_json::Value>>) -> serde_json::Value {
pg_optional_array_to_json(values, |value| serde_json::Value::String(value.to_string()))
}
fn pg_float_number(v: f64) -> serde_json::Value {
serde_json::Number::from_f64(v).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null)
}
@ -347,6 +351,9 @@ fn pg_bit_string_array_to_json_value(row: &Row, idx: usize) -> Option<serde_json
}
fn pg_array_to_json_value(row: &Row, idx: usize) -> Option<serde_json::Value> {
if let Ok(values) = row.try_get::<_, Vec<Option<serde_json::Value>>>(idx) {
return Some(pg_json_array_values_to_json(values));
}
if let Ok(values) = row.try_get::<_, Vec<Option<String>>>(idx) {
return Some(pg_optional_array_to_json(values, serde_json::Value::String));
}
@ -4064,6 +4071,32 @@ mod tests {
use std::time::Instant;
use tokio_postgres::types::FromSql;
fn pg_array_binary(element_oid: u32, elements: &[Option<Vec<u8>>]) -> Vec<u8> {
let mut raw = Vec::new();
raw.extend_from_slice(&1_i32.to_be_bytes());
raw.extend_from_slice(&i32::from(elements.iter().any(Option::is_none)).to_be_bytes());
raw.extend_from_slice(&element_oid.to_be_bytes());
raw.extend_from_slice(&(elements.len() as i32).to_be_bytes());
raw.extend_from_slice(&1_i32.to_be_bytes());
for element in elements {
match element {
Some(bytes) => {
raw.extend_from_slice(&(bytes.len() as i32).to_be_bytes());
raw.extend_from_slice(bytes);
}
None => raw.extend_from_slice(&(-1_i32).to_be_bytes()),
}
}
raw
}
fn pg_jsonb_binary(value: &[u8]) -> Vec<u8> {
let mut raw = Vec::with_capacity(value.len() + 1);
raw.push(1);
raw.extend_from_slice(value);
raw
}
#[test]
fn gaussdb_compatibility_mode_selects_identifier_quote() {
for mode in ["M", "B", "mysql", " MYSQL "] {
@ -4076,6 +4109,55 @@ mod tests {
assert_eq!(gaussdb_identifier_quote_for_compatibility_mode(""), None);
}
#[test]
fn postgres_json_arrays_decode_elements_without_jsonb_version_bytes() {
let json_raw = pg_array_binary(
Type::JSON.oid(),
&[
Some(br#"{"kind":"json"}"#.to_vec()),
Some(br#""text""#.to_vec()),
Some(br#"[1,true,null]"#.to_vec()),
None,
],
);
let jsonb_raw = pg_array_binary(
Type::JSONB.oid(),
&[
Some(pg_jsonb_binary(br#"{"port":10031,"type":"admin_web"}"#)),
Some(pg_jsonb_binary(br#""quoted""#)),
Some(pg_jsonb_binary(br#"[2,false,{"nested":true}]"#)),
None,
],
);
let json_values = Vec::<Option<serde_json::Value>>::from_sql(&Type::JSON_ARRAY, &json_raw).unwrap();
let jsonb_values = Vec::<Option<serde_json::Value>>::from_sql(&Type::JSONB_ARRAY, &jsonb_raw).unwrap();
assert_eq!(
pg_json_array_values_to_json(json_values),
serde_json::json!([r#"{"kind":"json"}"#, r#""text""#, "[1,true,null]", null])
);
let decoded = pg_json_array_values_to_json(jsonb_values);
assert_eq!(
decoded,
serde_json::json!([
r#"{"port":10031,"type":"admin_web"}"#,
r#""quoted""#,
r#"[2,false,{"nested":true}]"#,
null
])
);
assert!(!decoded.to_string().contains('\u{1}'));
}
#[test]
fn postgres_json_array_decoder_is_limited_to_json_element_types() {
assert!(Vec::<Option<serde_json::Value>>::accepts(&Type::JSON_ARRAY));
assert!(Vec::<Option<serde_json::Value>>::accepts(&Type::JSONB_ARRAY));
assert!(!Vec::<Option<serde_json::Value>>::accepts(&Type::TEXT_ARRAY));
assert!(!Vec::<Option<serde_json::Value>>::accepts(&Type::INT4_ARRAY));
}
fn pg_interval_bytes(microseconds: i64, days: i32, months: i32) -> [u8; 16] {
let mut raw = [0_u8; 16];
raw[0..8].copy_from_slice(&microseconds.to_be_bytes());