LCOV - code coverage report
Current view: top level - proxy/src/serverless - json.rs (source / functions) Coverage Total Hit
Test: e5024a5c05016c30dec7897aca22d1040a340f63.info Lines: 90.0 % 329 296
Test Date: 2024-11-20 11:45:54 Functions: 71.9 % 32 23

            Line data    Source code
       1              : use serde_json::{Map, Value};
       2              : use tokio_postgres::types::{Kind, Type};
       3              : use tokio_postgres::Row;
       4              : 
       5              : //
       6              : // Convert json non-string types to strings, so that they can be passed to Postgres
       7              : // as parameters.
       8              : //
       9            7 : pub(crate) fn json_to_pg_text(json: Vec<Value>) -> Vec<Option<String>> {
      10            7 :     json.iter().map(json_value_to_pg_text).collect()
      11            7 : }
      12              : 
      13            8 : fn json_value_to_pg_text(value: &Value) -> Option<String> {
      14            8 :     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            1 :         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            8 : }
      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            0 : #[derive(Debug, thiserror::Error)]
      62              : pub(crate) enum JsonConversionError {
      63              :     #[error("internal error compute returned invalid data: {0}")]
      64              :     AsTextError(tokio_postgres::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 :             (i, c) = pg_array_chr.next().unwrap();
     208          400 :         }
     209              : 
     210           62 :         match c {
     211           56 :             '{' if !quote => {
     212           56 :                 level += 1;
     213           56 :                 if level > 1 {
     214           17 :                     let (res, off) = pg_array_parse_inner(&pg_array[i..], elem_type, true)?;
     215           17 :                     entries.push(res);
     216          195 :                     for _ in 0..off - 1 {
     217          195 :                         pg_array_chr.next();
     218          195 :                     }
     219           39 :                 }
     220              :             }
     221           56 :             '}' if !quote => {
     222           56 :                 level -= 1;
     223           56 :                 if level == 0 {
     224           39 :                     push_checked(&mut entry, &mut entries, elem_type)?;
     225           39 :                     if nested {
     226           17 :                         return Ok((Value::Array(entries), i));
     227           22 :                     }
     228           17 :                 }
     229              :             }
     230           30 :             '"' if !escaped => {
     231           30 :                 if quote {
     232              :                     // end of quoted string, so push it manually without any checks
     233              :                     // for emptiness or nulls
     234           15 :                     entries.push(pg_text_to_json(Some(&entry), elem_type)?);
     235           15 :                     entry.clear();
     236           15 :                 }
     237           30 :                 quote = !quote;
     238              :             }
     239           51 :             ',' if !quote => {
     240           51 :                 push_checked(&mut entry, &mut entries, elem_type)?;
     241              :             }
     242          222 :             _ => {
     243          222 :                 entry.push(c);
     244          222 :             }
     245              :         }
     246              :     }
     247              : 
     248           22 :     if level != 0 {
     249            0 :         return Err(JsonConversionError::UnbalancedArray);
     250           22 :     }
     251           22 : 
     252           22 :     Ok((Value::Array(entries), 0))
     253           39 : }
     254              : 
     255              : #[cfg(test)]
     256              : mod tests {
     257              :     use serde_json::json;
     258              : 
     259              :     use super::*;
     260              : 
     261              :     #[test]
     262            1 :     fn test_atomic_types_to_pg_params() {
     263            1 :         let json = vec![Value::Bool(true), Value::Bool(false)];
     264            1 :         let pg_params = json_to_pg_text(json);
     265            1 :         assert_eq!(
     266            1 :             pg_params,
     267            1 :             vec![Some("true".to_owned()), Some("false".to_owned())]
     268            1 :         );
     269              : 
     270            1 :         let json = vec![Value::Number(serde_json::Number::from(42))];
     271            1 :         let pg_params = json_to_pg_text(json);
     272            1 :         assert_eq!(pg_params, vec![Some("42".to_owned())]);
     273              : 
     274            1 :         let json = vec![Value::String("foo\"".to_string())];
     275            1 :         let pg_params = json_to_pg_text(json);
     276            1 :         assert_eq!(pg_params, vec![Some("foo\"".to_owned())]);
     277              : 
     278            1 :         let json = vec![Value::Null];
     279            1 :         let pg_params = json_to_pg_text(json);
     280            1 :         assert_eq!(pg_params, vec![None]);
     281            1 :     }
     282              : 
     283              :     #[test]
     284            1 :     fn test_json_array_to_pg_array() {
     285            1 :         // atoms and escaping
     286            1 :         let json = "[true, false, null, \"NULL\", 42, \"foo\", \"bar\\\"-\\\\\"]";
     287            1 :         let json: Value = serde_json::from_str(json).unwrap();
     288            1 :         let pg_params = json_to_pg_text(vec![json]);
     289            1 :         assert_eq!(
     290            1 :             pg_params,
     291            1 :             vec![Some(
     292            1 :                 "{true,false,NULL,\"NULL\",42,\"foo\",\"bar\\\"-\\\\\"}".to_owned()
     293            1 :             )]
     294            1 :         );
     295              : 
     296              :         // nested arrays
     297            1 :         let json = "[[true, false], [null, 42], [\"foo\", \"bar\\\"-\\\\\"]]";
     298            1 :         let json: Value = serde_json::from_str(json).unwrap();
     299            1 :         let pg_params = json_to_pg_text(vec![json]);
     300            1 :         assert_eq!(
     301            1 :             pg_params,
     302            1 :             vec![Some(
     303            1 :                 "{{true,false},{NULL,42},{\"foo\",\"bar\\\"-\\\\\"}}".to_owned()
     304            1 :             )]
     305            1 :         );
     306              :         // array of objects
     307            1 :         let json = r#"[{"foo": 1},{"bar": 2}]"#;
     308            1 :         let json: Value = serde_json::from_str(json).unwrap();
     309            1 :         let pg_params = json_to_pg_text(vec![json]);
     310            1 :         assert_eq!(
     311            1 :             pg_params,
     312            1 :             vec![Some(r#"{"{\"foo\":1}","{\"bar\":2}"}"#.to_owned())]
     313            1 :         );
     314            1 :     }
     315              : 
     316              :     #[test]
     317            1 :     fn test_atomic_types_parse() {
     318            1 :         assert_eq!(
     319            1 :             pg_text_to_json(Some("foo"), &Type::TEXT).unwrap(),
     320            1 :             json!("foo")
     321            1 :         );
     322            1 :         assert_eq!(pg_text_to_json(None, &Type::TEXT).unwrap(), json!(null));
     323            1 :         assert_eq!(pg_text_to_json(Some("42"), &Type::INT4).unwrap(), json!(42));
     324            1 :         assert_eq!(pg_text_to_json(Some("42"), &Type::INT2).unwrap(), json!(42));
     325            1 :         assert_eq!(
     326            1 :             pg_text_to_json(Some("42"), &Type::INT8).unwrap(),
     327            1 :             json!("42")
     328            1 :         );
     329            1 :         assert_eq!(
     330            1 :             pg_text_to_json(Some("42.42"), &Type::FLOAT8).unwrap(),
     331            1 :             json!(42.42)
     332            1 :         );
     333            1 :         assert_eq!(
     334            1 :             pg_text_to_json(Some("42.42"), &Type::FLOAT4).unwrap(),
     335            1 :             json!(42.42)
     336            1 :         );
     337            1 :         assert_eq!(
     338            1 :             pg_text_to_json(Some("NaN"), &Type::FLOAT4).unwrap(),
     339            1 :             json!("NaN")
     340            1 :         );
     341            1 :         assert_eq!(
     342            1 :             pg_text_to_json(Some("Infinity"), &Type::FLOAT4).unwrap(),
     343            1 :             json!("Infinity")
     344            1 :         );
     345            1 :         assert_eq!(
     346            1 :             pg_text_to_json(Some("-Infinity"), &Type::FLOAT4).unwrap(),
     347            1 :             json!("-Infinity")
     348            1 :         );
     349              : 
     350            1 :         let json: Value =
     351            1 :             serde_json::from_str("{\"s\":\"str\",\"n\":42,\"f\":4.2,\"a\":[null,3,\"a\"]}")
     352            1 :                 .unwrap();
     353            1 :         assert_eq!(
     354            1 :             pg_text_to_json(
     355            1 :                 Some(r#"{"s":"str","n":42,"f":4.2,"a":[null,3,"a"]}"#),
     356            1 :                 &Type::JSONB
     357            1 :             )
     358            1 :             .unwrap(),
     359            1 :             json
     360            1 :         );
     361            1 :     }
     362              : 
     363              :     #[test]
     364            1 :     fn test_pg_array_parse_text() {
     365            4 :         fn pt(pg_arr: &str) -> Value {
     366            4 :             pg_array_parse(pg_arr, &Type::TEXT).unwrap()
     367            4 :         }
     368            1 :         assert_eq!(
     369            1 :             pt(r#"{"aa\"\\\,a",cha,"bbbb"}"#),
     370            1 :             json!(["aa\"\\,a", "cha", "bbbb"])
     371            1 :         );
     372            1 :         assert_eq!(
     373            1 :             pt(r#"{{"foo","bar"},{"bee","bop"}}"#),
     374            1 :             json!([["foo", "bar"], ["bee", "bop"]])
     375            1 :         );
     376            1 :         assert_eq!(
     377            1 :             pt(r#"{{{{"foo",NULL,"bop",bup}}}}"#),
     378            1 :             json!([[[["foo", null, "bop", "bup"]]]])
     379            1 :         );
     380            1 :         assert_eq!(
     381            1 :             pt(r#"{{"1",2,3},{4,NULL,6},{NULL,NULL,NULL}}"#),
     382            1 :             json!([["1", "2", "3"], ["4", null, "6"], [null, null, null]])
     383            1 :         );
     384            1 :     }
     385              : 
     386              :     #[test]
     387            1 :     fn test_pg_array_parse_bool() {
     388            4 :         fn pb(pg_arr: &str) -> Value {
     389            4 :             pg_array_parse(pg_arr, &Type::BOOL).unwrap()
     390            4 :         }
     391            1 :         assert_eq!(pb(r#"{t,f,t}"#), json!([true, false, true]));
     392            1 :         assert_eq!(pb(r#"{{t,f,t}}"#), json!([[true, false, true]]));
     393            1 :         assert_eq!(
     394            1 :             pb(r#"{{t,f},{f,t}}"#),
     395            1 :             json!([[true, false], [false, true]])
     396            1 :         );
     397            1 :         assert_eq!(
     398            1 :             pb(r#"{{t,NULL},{NULL,f}}"#),
     399            1 :             json!([[true, null], [null, false]])
     400            1 :         );
     401            1 :     }
     402              : 
     403              :     #[test]
     404            1 :     fn test_pg_array_parse_numbers() {
     405            9 :         fn pn(pg_arr: &str, ty: &Type) -> Value {
     406            9 :             pg_array_parse(pg_arr, ty).unwrap()
     407            9 :         }
     408            1 :         assert_eq!(pn(r#"{1,2,3}"#, &Type::INT4), json!([1, 2, 3]));
     409            1 :         assert_eq!(pn(r#"{1,2,3}"#, &Type::INT2), json!([1, 2, 3]));
     410            1 :         assert_eq!(pn(r#"{1,2,3}"#, &Type::INT8), json!(["1", "2", "3"]));
     411            1 :         assert_eq!(pn(r#"{1,2,3}"#, &Type::FLOAT4), json!([1.0, 2.0, 3.0]));
     412            1 :         assert_eq!(pn(r#"{1,2,3}"#, &Type::FLOAT8), json!([1.0, 2.0, 3.0]));
     413            1 :         assert_eq!(
     414            1 :             pn(r#"{1.1,2.2,3.3}"#, &Type::FLOAT4),
     415            1 :             json!([1.1, 2.2, 3.3])
     416            1 :         );
     417            1 :         assert_eq!(
     418            1 :             pn(r#"{1.1,2.2,3.3}"#, &Type::FLOAT8),
     419            1 :             json!([1.1, 2.2, 3.3])
     420            1 :         );
     421            1 :         assert_eq!(
     422            1 :             pn(r#"{NaN,Infinity,-Infinity}"#, &Type::FLOAT4),
     423            1 :             json!(["NaN", "Infinity", "-Infinity"])
     424            1 :         );
     425            1 :         assert_eq!(
     426            1 :             pn(r#"{NaN,Infinity,-Infinity}"#, &Type::FLOAT8),
     427            1 :             json!(["NaN", "Infinity", "-Infinity"])
     428            1 :         );
     429            1 :     }
     430              : 
     431              :     #[test]
     432            1 :     fn test_pg_array_with_decoration() {
     433            1 :         fn p(pg_arr: &str) -> Value {
     434            1 :             pg_array_parse(pg_arr, &Type::INT2).unwrap()
     435            1 :         }
     436            1 :         assert_eq!(
     437            1 :             p(r#"[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}"#),
     438            1 :             json!([[[1, 2, 3], [4, 5, 6]]])
     439            1 :         );
     440            1 :     }
     441              : 
     442              :     #[test]
     443            1 :     fn test_pg_array_parse_json() {
     444            4 :         fn pt(pg_arr: &str) -> Value {
     445            4 :             pg_array_parse(pg_arr, &Type::JSONB).unwrap()
     446            4 :         }
     447            1 :         assert_eq!(pt(r#"{"{}"}"#), json!([{}]));
     448            1 :         assert_eq!(
     449            1 :             pt(r#"{"{\"foo\": 1, \"bar\": 2}"}"#),
     450            1 :             json!([{"foo": 1, "bar": 2}])
     451            1 :         );
     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 :     }
     461              : }
        

Generated by: LCOV version 2.1-beta