Line data Source code
1 : use postgres_client::Row;
2 : use postgres_client::types::{Kind, Type};
3 : use serde_json::{Map, Value};
4 :
5 : //
6 : // Convert json non-string types to strings, so that they can be passed to Postgres
7 : // as parameters.
8 : //
9 10 : pub(crate) fn json_to_pg_text(json: Vec<Value>) -> Vec<Option<String>> {
10 10 : json.iter().map(json_value_to_pg_text).collect()
11 10 : }
12 :
13 11 : fn json_value_to_pg_text(value: &Value) -> Option<String> {
14 11 : match value {
15 : // special care for nulls
16 1 : Value::Null => None,
17 :
18 : // convert to text with escaping
19 3 : v @ (Value::Bool(_) | Value::Number(_) | Value::Object(_)) => Some(v.to_string()),
20 :
21 : // avoid escaping here, as we pass this as a parameter
22 4 : Value::String(s) => Some(s.to_string()),
23 :
24 : // special care for arrays
25 3 : Value::Array(_) => json_array_to_pg_array(value),
26 : }
27 11 : }
28 :
29 : //
30 : // Serialize a JSON array to a Postgres array. Contrary to the strings in the params
31 : // in the array we need to escape the strings. Postgres is okay with arrays of form
32 : // '{1,"2",3}'::int[], so we don't check that array holds values of the same type, leaving
33 : // it for Postgres to check.
34 : //
35 : // Example of the same escaping in node-postgres: packages/pg/lib/utils.js
36 : //
37 23 : fn json_array_to_pg_array(value: &Value) -> Option<String> {
38 23 : match value {
39 : // special care for nulls
40 2 : Value::Null => None,
41 :
42 : // convert to text with escaping
43 : // here string needs to be escaped, as it is part of the array
44 13 : v @ (Value::Bool(_) | Value::Number(_) | Value::String(_)) => Some(v.to_string()),
45 2 : v @ Value::Object(_) => json_array_to_pg_array(&Value::String(v.to_string())),
46 :
47 : // recurse into array
48 6 : Value::Array(arr) => {
49 6 : let vals = arr
50 6 : .iter()
51 6 : .map(json_array_to_pg_array)
52 18 : .map(|v| v.unwrap_or_else(|| "NULL".to_string()))
53 6 : .collect::<Vec<_>>()
54 6 : .join(",");
55 6 :
56 6 : Some(format!("{{{vals}}}"))
57 : }
58 : }
59 23 : }
60 :
61 : #[derive(Debug, thiserror::Error)]
62 : pub(crate) enum JsonConversionError {
63 : #[error("internal error compute returned invalid data: {0}")]
64 : AsTextError(postgres_client::Error),
65 : #[error("parse int error: {0}")]
66 : ParseIntError(#[from] std::num::ParseIntError),
67 : #[error("parse float error: {0}")]
68 : ParseFloatError(#[from] std::num::ParseFloatError),
69 : #[error("parse json error: {0}")]
70 : ParseJsonError(#[from] serde_json::Error),
71 : #[error("unbalanced array")]
72 : UnbalancedArray,
73 : }
74 :
75 : //
76 : // Convert postgres row with text-encoded values to JSON object
77 : //
78 0 : pub(crate) fn pg_text_row_to_json(
79 0 : row: &Row,
80 0 : columns: &[Type],
81 0 : raw_output: bool,
82 0 : array_mode: bool,
83 0 : ) -> Result<Value, JsonConversionError> {
84 0 : let iter = row
85 0 : .columns()
86 0 : .iter()
87 0 : .zip(columns)
88 0 : .enumerate()
89 0 : .map(|(i, (column, typ))| {
90 0 : let name = column.name();
91 0 : let pg_value = row.as_text(i).map_err(JsonConversionError::AsTextError)?;
92 0 : let json_value = if raw_output {
93 0 : match pg_value {
94 0 : Some(v) => Value::String(v.to_string()),
95 0 : None => Value::Null,
96 : }
97 : } else {
98 0 : pg_text_to_json(pg_value, typ)?
99 : };
100 0 : Ok((name.to_string(), json_value))
101 0 : });
102 0 :
103 0 : if array_mode {
104 : // drop keys and aggregate into array
105 0 : let arr = iter
106 0 : .map(|r| r.map(|(_key, val)| val))
107 0 : .collect::<Result<Vec<Value>, JsonConversionError>>()?;
108 0 : Ok(Value::Array(arr))
109 : } else {
110 0 : let obj = iter.collect::<Result<Map<String, Value>, JsonConversionError>>()?;
111 0 : Ok(Value::Object(obj))
112 : }
113 0 : }
114 :
115 : //
116 : // Convert postgres text-encoded value to JSON value
117 : //
118 84 : fn pg_text_to_json(pg_value: Option<&str>, pg_type: &Type) -> Result<Value, JsonConversionError> {
119 84 : if let Some(val) = pg_value {
120 76 : if let Kind::Array(elem_type) = pg_type.kind() {
121 0 : return pg_array_parse(val, elem_type);
122 76 : }
123 76 :
124 76 : match *pg_type {
125 12 : Type::BOOL => Ok(Value::Bool(val == "t")),
126 : Type::INT2 | Type::INT4 => {
127 14 : let val = val.parse::<i32>()?;
128 14 : Ok(Value::Number(serde_json::Number::from(val)))
129 : }
130 : Type::FLOAT4 | Type::FLOAT8 => {
131 23 : let fval = val.parse::<f64>()?;
132 23 : let num = serde_json::Number::from_f64(fval);
133 23 : if let Some(num) = num {
134 14 : Ok(Value::Number(num))
135 : } else {
136 : // Pass Nan, Inf, -Inf as strings
137 : // JS JSON.stringify() does converts them to null, but we
138 : // want to preserve them, so we pass them as strings
139 9 : Ok(Value::String(val.to_string()))
140 : }
141 : }
142 7 : Type::JSON | Type::JSONB => Ok(serde_json::from_str(val)?),
143 20 : _ => Ok(Value::String(val.to_string())),
144 : }
145 : } else {
146 8 : Ok(Value::Null)
147 : }
148 84 : }
149 :
150 : //
151 : // Parse postgres array into JSON array.
152 : //
153 : // This is a bit involved because we need to handle nested arrays and quoted
154 : // values. Unlike postgres we don't check that all nested arrays have the same
155 : // dimensions, we just return them as is.
156 : //
157 22 : fn pg_array_parse(pg_array: &str, elem_type: &Type) -> Result<Value, JsonConversionError> {
158 22 : pg_array_parse_inner(pg_array, elem_type, false).map(|(v, _)| v)
159 22 : }
160 :
161 39 : fn pg_array_parse_inner(
162 39 : pg_array: &str,
163 39 : elem_type: &Type,
164 39 : nested: bool,
165 39 : ) -> Result<(Value, usize), JsonConversionError> {
166 39 : let mut pg_array_chr = pg_array.char_indices();
167 39 : let mut level = 0;
168 39 : let mut quote = false;
169 39 : let mut entries: Vec<Value> = Vec::new();
170 39 : let mut entry = String::new();
171 39 :
172 39 : // skip bounds decoration
173 39 : if let Some('[') = pg_array.chars().next() {
174 18 : for (_, c) in pg_array_chr.by_ref() {
175 18 : if c == '=' {
176 1 : break;
177 17 : }
178 : }
179 38 : }
180 :
181 90 : fn push_checked(
182 90 : entry: &mut String,
183 90 : entries: &mut Vec<Value>,
184 90 : elem_type: &Type,
185 90 : ) -> Result<(), JsonConversionError> {
186 90 : if !entry.is_empty() {
187 : // While in usual postgres response we get nulls as None and everything else
188 : // as Some(&str), in arrays we get NULL as unquoted 'NULL' string (while
189 : // string with value 'NULL' will be represented by '"NULL"'). So catch NULLs
190 : // here while we have quotation info and convert them to None.
191 58 : if entry == "NULL" {
192 7 : entries.push(pg_text_to_json(None, elem_type)?);
193 : } else {
194 51 : entries.push(pg_text_to_json(Some(entry), elem_type)?);
195 : }
196 58 : entry.clear();
197 32 : }
198 :
199 90 : Ok(())
200 90 : }
201 :
202 437 : while let Some((mut i, mut c)) = pg_array_chr.next() {
203 415 : let mut escaped = false;
204 415 :
205 415 : if c == '\\' {
206 15 : escaped = true;
207 15 : let Some(x) = pg_array_chr.next() else {
208 0 : return Err(JsonConversionError::UnbalancedArray);
209 : };
210 15 : (i, c) = x;
211 400 : }
212 :
213 62 : match c {
214 56 : '{' if !quote => {
215 56 : level += 1;
216 56 : if level > 1 {
217 17 : let (res, off) = pg_array_parse_inner(&pg_array[i..], elem_type, true)?;
218 17 : entries.push(res);
219 195 : for _ in 0..off - 1 {
220 195 : pg_array_chr.next();
221 195 : }
222 39 : }
223 : }
224 56 : '}' if !quote => {
225 56 : level -= 1;
226 56 : if level == 0 {
227 39 : push_checked(&mut entry, &mut entries, elem_type)?;
228 39 : if nested {
229 17 : return Ok((Value::Array(entries), i));
230 22 : }
231 17 : }
232 : }
233 30 : '"' if !escaped => {
234 30 : if quote {
235 : // end of quoted string, so push it manually without any checks
236 : // for emptiness or nulls
237 15 : entries.push(pg_text_to_json(Some(&entry), elem_type)?);
238 15 : entry.clear();
239 15 : }
240 30 : quote = !quote;
241 : }
242 51 : ',' if !quote => {
243 51 : push_checked(&mut entry, &mut entries, elem_type)?;
244 : }
245 222 : _ => {
246 222 : entry.push(c);
247 222 : }
248 : }
249 : }
250 :
251 22 : if level != 0 {
252 0 : return Err(JsonConversionError::UnbalancedArray);
253 22 : }
254 22 :
255 22 : Ok((Value::Array(entries), 0))
256 39 : }
257 :
258 : #[cfg(test)]
259 : #[expect(clippy::unwrap_used)]
260 : mod tests {
261 : use serde_json::json;
262 :
263 : use super::*;
264 :
265 : #[test]
266 1 : fn test_atomic_types_to_pg_params() {
267 1 : let json = vec![Value::Bool(true), Value::Bool(false)];
268 1 : let pg_params = json_to_pg_text(json);
269 1 : assert_eq!(
270 1 : pg_params,
271 1 : vec![Some("true".to_owned()), Some("false".to_owned())]
272 1 : );
273 :
274 1 : let json = vec![Value::Number(serde_json::Number::from(42))];
275 1 : let pg_params = json_to_pg_text(json);
276 1 : assert_eq!(pg_params, vec![Some("42".to_owned())]);
277 :
278 1 : let json = vec![Value::String("foo\"".to_string())];
279 1 : let pg_params = json_to_pg_text(json);
280 1 : assert_eq!(pg_params, vec![Some("foo\"".to_owned())]);
281 :
282 1 : let json = vec![Value::Null];
283 1 : let pg_params = json_to_pg_text(json);
284 1 : assert_eq!(pg_params, vec![None]);
285 1 : }
286 :
287 : #[test]
288 1 : fn test_json_array_to_pg_array() {
289 1 : // atoms and escaping
290 1 : let json = "[true, false, null, \"NULL\", 42, \"foo\", \"bar\\\"-\\\\\"]";
291 1 : let json: Value = serde_json::from_str(json).unwrap();
292 1 : let pg_params = json_to_pg_text(vec![json]);
293 1 : assert_eq!(
294 1 : pg_params,
295 1 : vec![Some(
296 1 : "{true,false,NULL,\"NULL\",42,\"foo\",\"bar\\\"-\\\\\"}".to_owned()
297 1 : )]
298 1 : );
299 :
300 : // nested arrays
301 1 : let json = "[[true, false], [null, 42], [\"foo\", \"bar\\\"-\\\\\"]]";
302 1 : let json: Value = serde_json::from_str(json).unwrap();
303 1 : let pg_params = json_to_pg_text(vec![json]);
304 1 : assert_eq!(
305 1 : pg_params,
306 1 : vec![Some(
307 1 : "{{true,false},{NULL,42},{\"foo\",\"bar\\\"-\\\\\"}}".to_owned()
308 1 : )]
309 1 : );
310 : // array of objects
311 1 : let json = r#"[{"foo": 1},{"bar": 2}]"#;
312 1 : let json: Value = serde_json::from_str(json).unwrap();
313 1 : let pg_params = json_to_pg_text(vec![json]);
314 1 : assert_eq!(
315 1 : pg_params,
316 1 : vec![Some(r#"{"{\"foo\":1}","{\"bar\":2}"}"#.to_owned())]
317 1 : );
318 1 : }
319 :
320 : #[test]
321 1 : fn test_atomic_types_parse() {
322 1 : assert_eq!(
323 1 : pg_text_to_json(Some("foo"), &Type::TEXT).unwrap(),
324 1 : json!("foo")
325 1 : );
326 1 : assert_eq!(pg_text_to_json(None, &Type::TEXT).unwrap(), json!(null));
327 1 : assert_eq!(pg_text_to_json(Some("42"), &Type::INT4).unwrap(), json!(42));
328 1 : assert_eq!(pg_text_to_json(Some("42"), &Type::INT2).unwrap(), json!(42));
329 1 : assert_eq!(
330 1 : pg_text_to_json(Some("42"), &Type::INT8).unwrap(),
331 1 : json!("42")
332 1 : );
333 1 : assert_eq!(
334 1 : pg_text_to_json(Some("42.42"), &Type::FLOAT8).unwrap(),
335 1 : json!(42.42)
336 1 : );
337 1 : assert_eq!(
338 1 : pg_text_to_json(Some("42.42"), &Type::FLOAT4).unwrap(),
339 1 : json!(42.42)
340 1 : );
341 1 : assert_eq!(
342 1 : pg_text_to_json(Some("NaN"), &Type::FLOAT4).unwrap(),
343 1 : json!("NaN")
344 1 : );
345 1 : assert_eq!(
346 1 : pg_text_to_json(Some("Infinity"), &Type::FLOAT4).unwrap(),
347 1 : json!("Infinity")
348 1 : );
349 1 : assert_eq!(
350 1 : pg_text_to_json(Some("-Infinity"), &Type::FLOAT4).unwrap(),
351 1 : json!("-Infinity")
352 1 : );
353 :
354 1 : let json: Value =
355 1 : serde_json::from_str("{\"s\":\"str\",\"n\":42,\"f\":4.2,\"a\":[null,3,\"a\"]}")
356 1 : .unwrap();
357 1 : assert_eq!(
358 1 : pg_text_to_json(
359 1 : Some(r#"{"s":"str","n":42,"f":4.2,"a":[null,3,"a"]}"#),
360 1 : &Type::JSONB
361 1 : )
362 1 : .unwrap(),
363 1 : json
364 1 : );
365 1 : }
366 :
367 : #[test]
368 1 : fn test_pg_array_parse_text() {
369 4 : fn pt(pg_arr: &str) -> Value {
370 4 : pg_array_parse(pg_arr, &Type::TEXT).unwrap()
371 4 : }
372 1 : assert_eq!(
373 1 : pt(r#"{"aa\"\\\,a",cha,"bbbb"}"#),
374 1 : json!(["aa\"\\,a", "cha", "bbbb"])
375 1 : );
376 1 : assert_eq!(
377 1 : pt(r#"{{"foo","bar"},{"bee","bop"}}"#),
378 1 : json!([["foo", "bar"], ["bee", "bop"]])
379 1 : );
380 1 : assert_eq!(
381 1 : pt(r#"{{{{"foo",NULL,"bop",bup}}}}"#),
382 1 : json!([[[["foo", null, "bop", "bup"]]]])
383 1 : );
384 1 : assert_eq!(
385 1 : pt(r#"{{"1",2,3},{4,NULL,6},{NULL,NULL,NULL}}"#),
386 1 : json!([["1", "2", "3"], ["4", null, "6"], [null, null, null]])
387 1 : );
388 1 : }
389 :
390 : #[test]
391 1 : fn test_pg_array_parse_bool() {
392 4 : fn pb(pg_arr: &str) -> Value {
393 4 : pg_array_parse(pg_arr, &Type::BOOL).unwrap()
394 4 : }
395 1 : assert_eq!(pb(r#"{t,f,t}"#), json!([true, false, true]));
396 1 : assert_eq!(pb(r#"{{t,f,t}}"#), json!([[true, false, true]]));
397 1 : assert_eq!(
398 1 : pb(r#"{{t,f},{f,t}}"#),
399 1 : json!([[true, false], [false, true]])
400 1 : );
401 1 : assert_eq!(
402 1 : pb(r#"{{t,NULL},{NULL,f}}"#),
403 1 : json!([[true, null], [null, false]])
404 1 : );
405 1 : }
406 :
407 : #[test]
408 1 : fn test_pg_array_parse_numbers() {
409 9 : fn pn(pg_arr: &str, ty: &Type) -> Value {
410 9 : pg_array_parse(pg_arr, ty).unwrap()
411 9 : }
412 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::INT4), json!([1, 2, 3]));
413 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::INT2), json!([1, 2, 3]));
414 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::INT8), json!(["1", "2", "3"]));
415 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::FLOAT4), json!([1.0, 2.0, 3.0]));
416 1 : assert_eq!(pn(r#"{1,2,3}"#, &Type::FLOAT8), json!([1.0, 2.0, 3.0]));
417 1 : assert_eq!(
418 1 : pn(r#"{1.1,2.2,3.3}"#, &Type::FLOAT4),
419 1 : json!([1.1, 2.2, 3.3])
420 1 : );
421 1 : assert_eq!(
422 1 : pn(r#"{1.1,2.2,3.3}"#, &Type::FLOAT8),
423 1 : json!([1.1, 2.2, 3.3])
424 1 : );
425 1 : assert_eq!(
426 1 : pn(r#"{NaN,Infinity,-Infinity}"#, &Type::FLOAT4),
427 1 : json!(["NaN", "Infinity", "-Infinity"])
428 1 : );
429 1 : assert_eq!(
430 1 : pn(r#"{NaN,Infinity,-Infinity}"#, &Type::FLOAT8),
431 1 : json!(["NaN", "Infinity", "-Infinity"])
432 1 : );
433 1 : }
434 :
435 : #[test]
436 1 : fn test_pg_array_with_decoration() {
437 1 : fn p(pg_arr: &str) -> Value {
438 1 : pg_array_parse(pg_arr, &Type::INT2).unwrap()
439 1 : }
440 1 : assert_eq!(
441 1 : p(r#"[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}"#),
442 1 : json!([[[1, 2, 3], [4, 5, 6]]])
443 1 : );
444 1 : }
445 :
446 : #[test]
447 1 : fn test_pg_array_parse_json() {
448 4 : fn pt(pg_arr: &str) -> Value {
449 4 : pg_array_parse(pg_arr, &Type::JSONB).unwrap()
450 4 : }
451 1 : assert_eq!(pt(r#"{"{}"}"#), json!([{}]));
452 1 : assert_eq!(
453 1 : pt(r#"{"{\"foo\": 1, \"bar\": 2}"}"#),
454 1 : json!([{"foo": 1, "bar": 2}])
455 1 : );
456 1 : assert_eq!(
457 1 : pt(r#"{"{\"foo\": 1}", "{\"bar\": 2}"}"#),
458 1 : json!([{"foo": 1}, {"bar": 2}])
459 1 : );
460 1 : assert_eq!(
461 1 : pt(r#"{{"{\"foo\": 1}", "{\"bar\": 2}"}}"#),
462 1 : json!([[{"foo": 1}, {"bar": 2}]])
463 1 : );
464 1 : }
465 : }
|