Skip to main content

uqa_sql/expr/
json_strip.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` 18 signatures and textual JSON handling for null stripping.
8
9use uqa_core::{
10    json::{decode_json_string_with_control, JsonReadError},
11    memory::{Produced, ProductionControl, ProductionString, ProductionVec},
12    Value,
13};
14
15use crate::error::{Result, SQLError};
16
17use super::validate_named_argument_order_with_control;
18
19const PARAMETER_NAMES: [&str; 2] = ["target", "strip_in_arrays"];
20
21/// Map call-order arguments onto the declared `(target, strip_in_arrays DEFAULT false)` slots. `None` means the arity or a named argument does not select either catalogued overload.
22pub fn argument_positions(
23    name: &str,
24    argument_names: &[Option<&str>],
25) -> Result<Option<Vec<usize>>> {
26    argument_positions_with_control(name, argument_names, &ProductionControl::uncontrolled()).map(
27        |positions| {
28            positions.map(|positions| {
29                positions
30                    .into_uncontrolled()
31                    .expect("ordinary JSON null-stripping argument positions")
32            })
33        },
34    )
35}
36
37pub fn argument_positions_with_control(
38    name: &str,
39    argument_names: &[Option<&str>],
40    control: &ProductionControl<'_>,
41) -> Result<Option<Produced<Vec<usize>>>> {
42    control.check()?;
43    validate_named_argument_order_with_control(argument_names.iter().copied(), control)?;
44    let function = name
45        .get(..11)
46        .filter(|prefix| prefix.eq_ignore_ascii_case("pg_catalog."))
47        .map_or(name, |_| &name[11..]);
48    if !(function.eq_ignore_ascii_case("json_strip_nulls")
49        || function.eq_ignore_ascii_case("jsonb_strip_nulls"))
50        || !(1..=2).contains(&argument_names.len())
51    {
52        return Ok(None);
53    }
54    let mut occupied = [false; PARAMETER_NAMES.len()];
55    let mut positions = ProductionVec::new(*control);
56    positions.reserve(argument_names.len())?;
57    let mut positional = 0usize;
58    for argument_name in argument_names {
59        let position = if let Some(argument_name) = argument_name {
60            PARAMETER_NAMES
61                .iter()
62                .position(|candidate| candidate == argument_name)
63        } else {
64            let position = positional;
65            positional += 1;
66            Some(position)
67        };
68        let Some(position) = position.filter(|position| *position < occupied.len()) else {
69            return Ok(None);
70        };
71        if occupied[position] {
72            return Ok(None);
73        }
74        occupied[position] = true;
75        positions.push_copy(position)?;
76    }
77    Ok(occupied[0].then(|| positions.finish()).transpose()?)
78}
79
80pub(super) fn reorder_named_values_with_control(
81    function: &str,
82    call_args: &[(Option<String>, Value)],
83    control: &ProductionControl<'_>,
84) -> Result<Option<Produced<Vec<Value>>>> {
85    let names = super::call_arguments::evaluated_argument_names_with_control(call_args, control)?;
86    let positions = match argument_positions_with_control(function, &names, control) {
87        Ok(Some(positions)) => positions,
88        Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => return Err(error),
89        Ok(None) | Err(_) => return Ok(None),
90    };
91    let mut values = [None; PARAMETER_NAMES.len()];
92    for ((_, value), position) in call_args.iter().zip(positions.iter().copied()) {
93        values[position] = Some(value);
94    }
95    let default = Value::Bool(false);
96    values[1].get_or_insert(&default);
97    let mut output = ProductionVec::new(*control);
98    output.reserve(values.len())?;
99    for value in values {
100        let Some(value) = value else { return Ok(None) };
101        output.push_produced(control.copy_value(value)?)?;
102    }
103    Ok(Some(output.finish()?))
104}
105
106pub(super) fn invalid_json_input(input: &str) -> SQLError {
107    SQLError::Routine {
108        sqlstate: "22P02".into(),
109        message: format!("invalid input syntax for type json: \"{input}\""),
110    }
111}
112
113/// Remove JSON nulls without converting textual `json` through a map-backed value. `PostgreSQL`'s `json` result preserves object order, duplicate keys, and number lexemes while compacting whitespace and decoding JSON string escapes.
114#[cfg(test)]
115pub(super) fn strip_json_nulls_text(input: &str, strip_in_arrays: bool) -> Result<String> {
116    Ok(strip_json_nulls_text_with_control(
117        input,
118        strip_in_arrays,
119        &ProductionControl::uncontrolled(),
120    )?
121    .into_uncontrolled()
122    .expect("ordinary JSON stripping has no lease"))
123}
124
125pub(super) fn strip_json_nulls_text_with_control(
126    input: &str,
127    strip_in_arrays: bool,
128    control: &ProductionControl<'_>,
129) -> Result<Produced<String>> {
130    let mut parser = JsonStripParser {
131        input,
132        position: 0,
133        strip_in_arrays,
134        control: *control,
135    };
136    let rendered = parser.parse_value(0)?;
137    parser.skip_whitespace()?;
138    if parser.position != input.len() {
139        return Err(invalid_json_input(input));
140    }
141    Ok(rendered.text)
142}
143
144struct RenderedJson {
145    text: Produced<String>,
146    is_null: bool,
147}
148
149struct JsonStripParser<'a, 'c> {
150    input: &'a str,
151    position: usize,
152    strip_in_arrays: bool,
153    control: ProductionControl<'c>,
154}
155
156impl JsonStripParser<'_, '_> {
157    const MAX_DEPTH: usize = 128;
158
159    fn parse_value(&mut self, depth: usize) -> Result<RenderedJson> {
160        self.control.check()?;
161        if depth > Self::MAX_DEPTH {
162            return Err(invalid_json_input(self.input));
163        }
164        self.skip_whitespace()?;
165        match self.peek() {
166            Some(b'{') => self.parse_object(depth),
167            Some(b'[') => self.parse_array(depth),
168            Some(b'"') => self.parse_string().map(|text| RenderedJson {
169                text,
170                is_null: false,
171            }),
172            Some(b't') => self.parse_literal("true", false),
173            Some(b'f') => self.parse_literal("false", false),
174            Some(b'n') => self.parse_literal("null", true),
175            Some(b'-' | b'0'..=b'9') => self.parse_number(),
176            _ => Err(invalid_json_input(self.input)),
177        }
178    }
179
180    fn parse_object(&mut self, depth: usize) -> Result<RenderedJson> {
181        self.position += 1;
182        self.skip_whitespace()?;
183        let mut fields = ProductionString::new(self.control);
184        fields.push('{')?;
185        let mut emitted = false;
186        if self.consume(b'}') {
187            fields.push('}')?;
188            return Ok(RenderedJson {
189                text: fields.finish()?,
190                is_null: false,
191            });
192        }
193        loop {
194            self.skip_whitespace()?;
195            if self.peek() != Some(b'"') {
196                return Err(invalid_json_input(self.input));
197            }
198            let key = self.parse_string()?;
199            self.skip_whitespace()?;
200            if !self.consume(b':') {
201                return Err(invalid_json_input(self.input));
202            }
203            let value = self.parse_value(depth + 1)?;
204            if !value.is_null {
205                if emitted {
206                    fields.push(',')?;
207                }
208                fields.push_str(&key)?;
209                fields.push(':')?;
210                fields.push_str(&value.text)?;
211                emitted = true;
212            }
213            self.skip_whitespace()?;
214            if self.consume(b'}') {
215                break;
216            }
217            if !self.consume(b',') {
218                return Err(invalid_json_input(self.input));
219            }
220        }
221        fields.push('}')?;
222        Ok(RenderedJson {
223            text: fields.finish()?,
224            is_null: false,
225        })
226    }
227
228    fn parse_array(&mut self, depth: usize) -> Result<RenderedJson> {
229        self.position += 1;
230        self.skip_whitespace()?;
231        let mut elements = ProductionString::new(self.control);
232        elements.push('[')?;
233        let mut emitted = false;
234        if self.consume(b']') {
235            elements.push(']')?;
236            return Ok(RenderedJson {
237                text: elements.finish()?,
238                is_null: false,
239            });
240        }
241        loop {
242            let value = self.parse_value(depth + 1)?;
243            if !self.strip_in_arrays || !value.is_null {
244                if emitted {
245                    elements.push(',')?;
246                }
247                elements.push_str(&value.text)?;
248                emitted = true;
249            }
250            self.skip_whitespace()?;
251            if self.consume(b']') {
252                break;
253            }
254            if !self.consume(b',') {
255                return Err(invalid_json_input(self.input));
256            }
257        }
258        elements.push(']')?;
259        Ok(RenderedJson {
260            text: elements.finish()?,
261            is_null: false,
262        })
263    }
264
265    fn parse_string(&mut self) -> Result<Produced<String>> {
266        let start = self.position;
267        self.position += 1;
268        while let Some(byte) = self.peek() {
269            self.control.check()?;
270            match byte {
271                b'"' => {
272                    self.position += 1;
273                    let source = &self.input[start..self.position];
274                    let decoded = decode_json_string_with_control(source.as_bytes(), &self.control)
275                        .map_err(|error| match error {
276                            JsonReadError::InvalidJson => invalid_json_input(self.input),
277                            JsonReadError::Memory(error) => error.into(),
278                            JsonReadError::Cancelled(error) => error.into(),
279                        })?;
280                    return super::json::quote_with_control(&decoded, &self.control);
281                }
282                b'\\' => {
283                    self.position += 1;
284                    if self.peek().is_none() {
285                        return Err(invalid_json_input(self.input));
286                    }
287                    self.position += 1;
288                }
289                _ => self.position += 1,
290            }
291        }
292        Err(invalid_json_input(self.input))
293    }
294
295    fn parse_literal(&mut self, literal: &str, is_null: bool) -> Result<RenderedJson> {
296        if !self.input[self.position..].starts_with(literal) {
297            return Err(invalid_json_input(self.input));
298        }
299        self.position += literal.len();
300        Ok(RenderedJson {
301            text: self.control.copy_text(literal)?,
302            is_null,
303        })
304    }
305
306    fn parse_number(&mut self) -> Result<RenderedJson> {
307        let start = self.position;
308        self.consume(b'-');
309        match self.peek() {
310            Some(b'0') => self.position += 1,
311            Some(b'1'..=b'9') => {
312                self.position += 1;
313                self.consume_digits()?;
314            }
315            _ => return Err(invalid_json_input(self.input)),
316        }
317        if self.consume(b'.') {
318            let digits = self.position;
319            self.consume_digits()?;
320            if digits == self.position {
321                return Err(invalid_json_input(self.input));
322            }
323        }
324        if matches!(self.peek(), Some(b'e' | b'E')) {
325            self.position += 1;
326            if matches!(self.peek(), Some(b'+' | b'-')) {
327                self.position += 1;
328            }
329            let digits = self.position;
330            self.consume_digits()?;
331            if digits == self.position {
332                return Err(invalid_json_input(self.input));
333            }
334        }
335        Ok(RenderedJson {
336            text: self.control.copy_text(&self.input[start..self.position])?,
337            is_null: false,
338        })
339    }
340
341    fn consume_digits(&mut self) -> Result<()> {
342        while matches!(self.peek(), Some(b'0'..=b'9')) {
343            self.control.check()?;
344            self.position += 1;
345        }
346        Ok(())
347    }
348
349    fn skip_whitespace(&mut self) -> Result<()> {
350        while matches!(self.peek(), Some(b' ' | b'\n' | b'\r' | b'\t')) {
351            self.control.check()?;
352            self.position += 1;
353        }
354        Ok(())
355    }
356
357    fn consume(&mut self, expected: u8) -> bool {
358        if self.peek() == Some(expected) {
359            self.position += 1;
360            true
361        } else {
362            false
363        }
364    }
365
366    fn peek(&self) -> Option<u8> {
367        self.input.as_bytes().get(self.position).copied()
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::{argument_positions, strip_json_nulls_text};
374
375    #[test]
376    fn json_strip_positions_accept_the_default_and_declaration_order_names() {
377        assert_eq!(
378            argument_positions("json_strip_nulls", &[None]).unwrap(),
379            Some(vec![0])
380        );
381        assert_eq!(
382            argument_positions(
383                "jsonb_strip_nulls",
384                &[Some("strip_in_arrays"), Some("target")]
385            )
386            .unwrap(),
387            Some(vec![1, 0])
388        );
389        assert_eq!(
390            argument_positions("json_strip_nulls", &[Some("strip_in_arrays")]).unwrap(),
391            None
392        );
393        assert_eq!(
394            argument_positions("json_strip_nulls", &[Some("unknown"), Some("target")]).unwrap(),
395            None
396        );
397    }
398
399    #[test]
400    fn textual_json_null_stripping_preserves_order_duplicates_and_number_lexemes() {
401        let input = r#" { "z" : 1.2300e+02, "a" : null, "z" : 2, "s" : "\u0061", "nested" : [null,{"drop":null,"keep":3}] } "#;
402        assert_eq!(
403            strip_json_nulls_text(input, false).unwrap(),
404            r#"{"z":1.2300e+02,"z":2,"s":"a","nested":[null,{"keep":3}]}"#
405        );
406        assert_eq!(
407            strip_json_nulls_text(input, true).unwrap(),
408            r#"{"z":1.2300e+02,"z":2,"s":"a","nested":[{"keep":3}]}"#
409        );
410        assert_eq!(strip_json_nulls_text("null", true).unwrap(), "null");
411    }
412
413    #[test]
414    fn textual_json_null_stripping_rejects_malformed_input_with_json_sqlstate() {
415        for input in [r#"{"a":}"#, r#"{"a":01}"#, r"[1,]", r#""\uD800""#] {
416            assert_eq!(
417                strip_json_nulls_text(input, false).unwrap_err().sqlstate(),
418                Some("22P02")
419            );
420        }
421    }
422}