fix(postgres): decode json arrays without binary prefixes
This commit is contained in:
parent
e48902fa06
commit
65bf06f12e
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(µseconds.to_be_bytes());
|
||||
|
|
|
|||
Loading…
Reference in New Issue