Skip to main content

spg_engine/
json.rs

1// Recursive-descent JSON parser. Several lints are inherent to the
2// hand-rolled byte-scan style and don't add clarity here.
3#![allow(
4    clippy::cast_lossless,
5    clippy::cast_possible_truncation,
6    clippy::cast_possible_wrap,
7    clippy::cast_sign_loss,
8    clippy::doc_markdown,
9    clippy::format_push_string,
10    clippy::needless_continue,
11    clippy::needless_range_loop,
12    clippy::single_match,
13    clippy::uninlined_format_args
14)]
15
16//! v4.14 minimal JSON parser for the `->` / `->>` operators.
17//!
18//! Hand-rolled, no external dep — same policy as the rest of the
19//! engine. Supports the JSON grammar from RFC 8259: objects,
20//! arrays, strings (with `\"` / `\\` / `\/` / `\b` / `\f` / `\n`
21//! / `\r` / `\t` / `\uXXXX` escapes), numbers, true / false /
22//! null. The parser returns a tree we walk by key (object) or
23//! integer index (array); accesses that miss return `Value::Null`
24//! per PG semantics.
25//!
26//! `path_get(doc, key, as_text)` is the public entry. When
27//! `as_text` is true (`->>` operator), JSON strings unwrap to
28//! raw text and other scalars render as their canonical text;
29//! when false (`->`), the result is wrapped back into a Json
30//! value (the inner subtree rendered to its canonical JSON
31//! string form).
32
33use alloc::string::{String, ToString};
34use alloc::vec::Vec;
35
36use spg_storage::Value;
37
38use crate::eval::EvalError;
39
40#[derive(Debug, Clone, PartialEq)]
41pub enum JsonValue {
42    Null,
43    Bool(bool),
44    Number(f64),
45    /// Original numeric text, so integer round-trips don't drift to
46    /// `1.0`. We render either the raw lexeme (when present) or
47    /// `Number`'s default formatting.
48    NumberText(String),
49    String(String),
50    Array(Vec<JsonValue>),
51    Object(Vec<(String, JsonValue)>),
52}
53
54impl JsonValue {
55    fn as_text(&self) -> String {
56        match self {
57            Self::Null => "null".into(),
58            Self::Bool(b) => if *b { "true" } else { "false" }.into(),
59            Self::Number(x) => alloc::format!("{x}"),
60            Self::NumberText(s) | Self::String(s) => s.clone(),
61            Self::Array(_) | Self::Object(_) => self.to_json_text(),
62        }
63    }
64
65    fn to_json_text(&self) -> String {
66        let mut out = String::new();
67        write_json(self, &mut out);
68        out
69    }
70}
71
72fn write_json(v: &JsonValue, out: &mut String) {
73    match v {
74        JsonValue::Null => out.push_str("null"),
75        JsonValue::Bool(true) => out.push_str("true"),
76        JsonValue::Bool(false) => out.push_str("false"),
77        JsonValue::Number(x) => out.push_str(&alloc::format!("{x}")),
78        JsonValue::NumberText(s) => out.push_str(s),
79        JsonValue::String(s) => {
80            out.push('"');
81            for c in s.chars() {
82                match c {
83                    '"' => out.push_str("\\\""),
84                    '\\' => out.push_str("\\\\"),
85                    '\n' => out.push_str("\\n"),
86                    '\r' => out.push_str("\\r"),
87                    '\t' => out.push_str("\\t"),
88                    c if (c as u32) < 0x20 => {
89                        out.push_str(&alloc::format!("\\u{:04x}", c as u32));
90                    }
91                    c => out.push(c),
92                }
93            }
94            out.push('"');
95        }
96        JsonValue::Array(items) => {
97            out.push('[');
98            for (i, it) in items.iter().enumerate() {
99                if i > 0 {
100                    out.push(',');
101                }
102                write_json(it, out);
103            }
104            out.push(']');
105        }
106        JsonValue::Object(entries) => {
107            out.push('{');
108            for (i, (k, val)) in entries.iter().enumerate() {
109                if i > 0 {
110                    out.push(',');
111                }
112                write_json(&JsonValue::String(k.clone()), out);
113                out.push(':');
114                write_json(val, out);
115            }
116            out.push('}');
117        }
118    }
119}
120
121/// v6.4.5 — PG `json #> path_text` / `json #>> path_text`. The
122/// right-hand side is a PG text-array literal `'{a,0,b}'` whose
123/// elements are walked left-to-right; each element is either an
124/// object key or (when it parses as a non-negative integer) an
125/// array index. Missing or non-existent steps return `Value::Null`.
126pub fn path_walk(lhs: &Value, rhs: &Value, as_text: bool) -> Result<Value, EvalError> {
127    let src = match lhs {
128        Value::Json(s) | Value::Text(s) => s.as_str(),
129        Value::Null => return Ok(Value::Null),
130        other => {
131            return Err(EvalError::TypeMismatch {
132                detail: alloc::format!(
133                    "JSON path walk: left side must be JSON or TEXT, got {:?}",
134                    other.data_type()
135                ),
136            });
137        }
138    };
139    let path_text = match rhs {
140        Value::Text(s) | Value::Json(s) => s.as_str(),
141        Value::Null => return Ok(Value::Null),
142        other => {
143            return Err(EvalError::TypeMismatch {
144                detail: alloc::format!(
145                    "JSON path walk: right side must be TEXT, got {:?}",
146                    other.data_type()
147                ),
148            });
149        }
150    };
151    let path = parse_text_array(path_text)?;
152    let mut cur = parse(src).map_err(|e| EvalError::TypeMismatch {
153        detail: alloc::format!("invalid JSON for path walk: {e}"),
154    })?;
155    for step in &path {
156        let next = match (&cur, step.as_str()) {
157            (JsonValue::Object(entries), key) => entries
158                .iter()
159                .find(|(k, _)| k == key)
160                .map(|(_, v)| v.clone()),
161            (JsonValue::Array(items), key) => {
162                let Ok(idx) = key.parse::<i64>() else {
163                    return Ok(Value::Null);
164                };
165                if idx >= 0 {
166                    items.get(idx as usize).cloned()
167                } else {
168                    let from_end = items.len() as i64 + idx;
169                    if from_end >= 0 {
170                        items.get(from_end as usize).cloned()
171                    } else {
172                        None
173                    }
174                }
175            }
176            _ => return Ok(Value::Null),
177        };
178        cur = match next {
179            None => return Ok(Value::Null),
180            Some(v) => v,
181        };
182    }
183    if matches!(cur, JsonValue::Null) {
184        return Ok(Value::Null);
185    }
186    if as_text {
187        Ok(Value::Text(cur.as_text()))
188    } else {
189        Ok(Value::Json(cur.to_json_text()))
190    }
191}
192
193/// v6.4.5 — PG `json @> sub_json` containment. Returns BOOL.
194/// `lhs @> rhs` is true when every member of `rhs` is structurally
195/// contained in `lhs`:
196///   - Scalars: equal
197///   - Objects: every (key, value) in rhs exists in lhs with a
198///     containing value
199///   - Arrays: every element in rhs has a containing element in lhs
200pub fn contains(lhs: &Value, rhs: &Value) -> Result<Value, EvalError> {
201    let lhs_text = match lhs {
202        Value::Json(s) | Value::Text(s) => s.as_str(),
203        Value::Null => return Ok(Value::Null),
204        other => {
205            return Err(EvalError::TypeMismatch {
206                detail: alloc::format!(
207                    "JSON @>: left side must be JSON or TEXT, got {:?}",
208                    other.data_type()
209                ),
210            });
211        }
212    };
213    let rhs_text = match rhs {
214        Value::Json(s) | Value::Text(s) => s.as_str(),
215        Value::Null => return Ok(Value::Null),
216        other => {
217            return Err(EvalError::TypeMismatch {
218                detail: alloc::format!(
219                    "JSON @>: right side must be JSON or TEXT, got {:?}",
220                    other.data_type()
221                ),
222            });
223        }
224    };
225    let lhs_doc = parse(lhs_text).map_err(|e| EvalError::TypeMismatch {
226        detail: alloc::format!("invalid JSON on left of @>: {e}"),
227    })?;
228    let rhs_doc = parse(rhs_text).map_err(|e| EvalError::TypeMismatch {
229        detail: alloc::format!("invalid JSON on right of @>: {e}"),
230    })?;
231    Ok(Value::Bool(json_contains(&lhs_doc, &rhs_doc)))
232}
233
234fn json_contains(lhs: &JsonValue, rhs: &JsonValue) -> bool {
235    match (lhs, rhs) {
236        (JsonValue::Object(l), JsonValue::Object(r)) => r
237            .iter()
238            .all(|(rk, rv)| l.iter().any(|(lk, lv)| lk == rk && json_contains(lv, rv))),
239        (JsonValue::Array(l), JsonValue::Array(r)) => {
240            r.iter().all(|rv| l.iter().any(|lv| json_contains(lv, rv)))
241        }
242        _ => json_eq(lhs, rhs),
243    }
244}
245
246fn json_eq(a: &JsonValue, b: &JsonValue) -> bool {
247    match (a, b) {
248        (JsonValue::Null, JsonValue::Null) => true,
249        (JsonValue::Bool(x), JsonValue::Bool(y)) => x == y,
250        (JsonValue::String(x), JsonValue::String(y)) => x == y,
251        (JsonValue::Number(x), JsonValue::Number(y)) => (x - y).abs() < 1e-12,
252        (JsonValue::NumberText(x), JsonValue::NumberText(y)) => x == y,
253        (JsonValue::NumberText(x), JsonValue::Number(y))
254        | (JsonValue::Number(y), JsonValue::NumberText(x)) => {
255            x.parse::<f64>().is_ok_and(|xn| (xn - y).abs() < 1e-12)
256        }
257        (JsonValue::Array(x), JsonValue::Array(y)) => {
258            x.len() == y.len() && x.iter().zip(y).all(|(a, b)| json_eq(a, b))
259        }
260        (JsonValue::Object(x), JsonValue::Object(y)) => {
261            x.len() == y.len()
262                && x.iter()
263                    .all(|(k, v)| y.iter().any(|(k2, v2)| k == k2 && json_eq(v, v2)))
264        }
265        _ => false,
266    }
267}
268
269/// Parse PG's text-array literal `'{a,b,c}'` into a Vec<String>.
270/// Whitespace around elements is trimmed; quoted elements (`"x,y"`)
271/// preserve embedded commas (minimal support — full PG array
272/// escaping is OOS).
273fn parse_text_array(s: &str) -> Result<Vec<String>, EvalError> {
274    let trimmed = s.trim();
275    let inner = if let Some(stripped) = trimmed.strip_prefix('{').and_then(|s| s.strip_suffix('}'))
276    {
277        stripped
278    } else {
279        return Err(EvalError::TypeMismatch {
280            detail: alloc::format!("path walk: expected PG array literal `{{…}}`, got {s:?}"),
281        });
282    };
283    if inner.trim().is_empty() {
284        return Ok(Vec::new());
285    }
286    let mut out = Vec::new();
287    let mut cur = String::new();
288    let mut in_quotes = false;
289    let mut chars = inner.chars().peekable();
290    while let Some(c) = chars.next() {
291        match c {
292            '"' => in_quotes = !in_quotes,
293            ',' if !in_quotes => {
294                out.push(cur.trim().to_string());
295                cur = String::new();
296            }
297            '\\' => {
298                if let Some(&next) = chars.peek() {
299                    cur.push(next);
300                    chars.next();
301                }
302            }
303            _ => cur.push(c),
304        }
305    }
306    out.push(cur.trim().to_string());
307    Ok(out)
308}
309
310/// PG `json -> key` / `json ->> key`. `lhs` must be JSON or TEXT
311/// containing JSON. `rhs` is either a TEXT key (object access) or
312/// an INT index (array access). `as_text=true` for `->>` (returns
313/// `Value::Text`); `false` for `->` (returns `Value::Json`).
314pub fn path_get(lhs: &Value, rhs: &Value, as_text: bool) -> Result<Value, EvalError> {
315    let src = match lhs {
316        Value::Json(s) | Value::Text(s) => s.as_str(),
317        Value::Null => return Ok(Value::Null),
318        other => {
319            return Err(EvalError::TypeMismatch {
320                detail: alloc::format!(
321                    "JSON path operator: left side must be JSON or TEXT, got {:?}",
322                    other.data_type()
323                ),
324            });
325        }
326    };
327    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
328        detail: alloc::format!("invalid JSON for path access: {e}"),
329    })?;
330    let inner = match (&doc, rhs) {
331        (JsonValue::Object(entries), Value::Text(k)) => entries
332            .iter()
333            .find(|(name, _)| name == k)
334            .map(|(_, v)| v.clone()),
335        (JsonValue::Array(items), Value::Int(idx)) => {
336            let n = *idx;
337            if n >= 0 {
338                items.get(n as usize).cloned()
339            } else {
340                let from_end = items.len() as i64 + i64::from(n);
341                if from_end >= 0 {
342                    items.get(from_end as usize).cloned()
343                } else {
344                    None
345                }
346            }
347        }
348        (JsonValue::Array(items), Value::BigInt(idx)) => {
349            let n = *idx;
350            if n >= 0 {
351                items.get(n as usize).cloned()
352            } else {
353                let from_end = items.len() as i64 + n;
354                if from_end >= 0 {
355                    items.get(from_end as usize).cloned()
356                } else {
357                    None
358                }
359            }
360        }
361        (_, Value::Null) => return Ok(Value::Null),
362        _ => None,
363    };
364    match inner {
365        None | Some(JsonValue::Null) => Ok(Value::Null),
366        Some(v) => {
367            if as_text {
368                Ok(Value::Text(v.as_text()))
369            } else {
370                Ok(Value::Json(v.to_json_text()))
371            }
372        }
373    }
374}
375
376// ---- Tiny recursive-descent JSON parser ----
377
378#[derive(Debug)]
379pub enum ParseError {
380    Unexpected(char, usize),
381    Truncated,
382    InvalidEscape(usize),
383    InvalidNumber(usize),
384}
385
386impl core::fmt::Display for ParseError {
387    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
388        match self {
389            Self::Unexpected(c, p) => write!(f, "unexpected {c:?} at offset {p}"),
390            Self::Truncated => f.write_str("unexpected end of JSON input"),
391            Self::InvalidEscape(p) => write!(f, "invalid string escape at offset {p}"),
392            Self::InvalidNumber(p) => write!(f, "invalid number at offset {p}"),
393        }
394    }
395}
396
397pub fn parse(src: &str) -> Result<JsonValue, ParseError> {
398    let bytes = src.as_bytes();
399    let mut p = 0;
400    skip_ws(bytes, &mut p);
401    let value = parse_value(bytes, &mut p)?;
402    skip_ws(bytes, &mut p);
403    if p != bytes.len() {
404        return Err(ParseError::Unexpected(bytes[p] as char, p));
405    }
406    Ok(value)
407}
408
409fn skip_ws(bytes: &[u8], p: &mut usize) {
410    while *p < bytes.len() && matches!(bytes[*p], b' ' | b'\t' | b'\n' | b'\r') {
411        *p += 1;
412    }
413}
414
415fn parse_value(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
416    skip_ws(bytes, p);
417    if *p >= bytes.len() {
418        return Err(ParseError::Truncated);
419    }
420    match bytes[*p] {
421        b'{' => parse_object(bytes, p),
422        b'[' => parse_array(bytes, p),
423        b'"' => parse_string(bytes, p).map(JsonValue::String),
424        b't' | b'f' => parse_bool(bytes, p),
425        b'n' => parse_null(bytes, p),
426        b'-' | b'0'..=b'9' => parse_number(bytes, p),
427        c => Err(ParseError::Unexpected(c as char, *p)),
428    }
429}
430
431fn parse_object(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
432    debug_assert_eq!(bytes[*p], b'{');
433    *p += 1;
434    let mut entries = Vec::new();
435    skip_ws(bytes, p);
436    if *p < bytes.len() && bytes[*p] == b'}' {
437        *p += 1;
438        return Ok(JsonValue::Object(entries));
439    }
440    loop {
441        skip_ws(bytes, p);
442        if *p >= bytes.len() || bytes[*p] != b'"' {
443            return Err(ParseError::Unexpected(
444                bytes.get(*p).copied().unwrap_or(0) as char,
445                *p,
446            ));
447        }
448        let key = parse_string(bytes, p)?;
449        skip_ws(bytes, p);
450        if *p >= bytes.len() || bytes[*p] != b':' {
451            return Err(ParseError::Unexpected(
452                bytes.get(*p).copied().unwrap_or(0) as char,
453                *p,
454            ));
455        }
456        *p += 1;
457        let value = parse_value(bytes, p)?;
458        entries.push((key, value));
459        skip_ws(bytes, p);
460        if *p >= bytes.len() {
461            return Err(ParseError::Truncated);
462        }
463        match bytes[*p] {
464            b',' => {
465                *p += 1;
466                continue;
467            }
468            b'}' => {
469                *p += 1;
470                return Ok(JsonValue::Object(entries));
471            }
472            c => return Err(ParseError::Unexpected(c as char, *p)),
473        }
474    }
475}
476
477fn parse_array(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
478    debug_assert_eq!(bytes[*p], b'[');
479    *p += 1;
480    let mut items = Vec::new();
481    skip_ws(bytes, p);
482    if *p < bytes.len() && bytes[*p] == b']' {
483        *p += 1;
484        return Ok(JsonValue::Array(items));
485    }
486    loop {
487        items.push(parse_value(bytes, p)?);
488        skip_ws(bytes, p);
489        if *p >= bytes.len() {
490            return Err(ParseError::Truncated);
491        }
492        match bytes[*p] {
493            b',' => {
494                *p += 1;
495                continue;
496            }
497            b']' => {
498                *p += 1;
499                return Ok(JsonValue::Array(items));
500            }
501            c => return Err(ParseError::Unexpected(c as char, *p)),
502        }
503    }
504}
505
506fn parse_string(bytes: &[u8], p: &mut usize) -> Result<String, ParseError> {
507    debug_assert_eq!(bytes[*p], b'"');
508    *p += 1;
509    let mut out = String::new();
510    while *p < bytes.len() {
511        match bytes[*p] {
512            b'"' => {
513                *p += 1;
514                return Ok(out);
515            }
516            b'\\' => {
517                let start = *p;
518                *p += 1;
519                if *p >= bytes.len() {
520                    return Err(ParseError::Truncated);
521                }
522                match bytes[*p] {
523                    b'"' => {
524                        out.push('"');
525                        *p += 1;
526                    }
527                    b'\\' => {
528                        out.push('\\');
529                        *p += 1;
530                    }
531                    b'/' => {
532                        out.push('/');
533                        *p += 1;
534                    }
535                    b'b' => {
536                        out.push('\u{08}');
537                        *p += 1;
538                    }
539                    b'f' => {
540                        out.push('\u{0c}');
541                        *p += 1;
542                    }
543                    b'n' => {
544                        out.push('\n');
545                        *p += 1;
546                    }
547                    b'r' => {
548                        out.push('\r');
549                        *p += 1;
550                    }
551                    b't' => {
552                        out.push('\t');
553                        *p += 1;
554                    }
555                    b'u' => {
556                        if *p + 5 > bytes.len() {
557                            return Err(ParseError::Truncated);
558                        }
559                        let hex = &bytes[*p + 1..*p + 5];
560                        let n = u32::from_str_radix(
561                            core::str::from_utf8(hex)
562                                .map_err(|_| ParseError::InvalidEscape(start))?,
563                            16,
564                        )
565                        .map_err(|_| ParseError::InvalidEscape(start))?;
566                        out.push(char::from_u32(n).ok_or(ParseError::InvalidEscape(start))?);
567                        *p += 5;
568                    }
569                    _ => return Err(ParseError::InvalidEscape(start)),
570                }
571            }
572            c if c < 0x20 => return Err(ParseError::Unexpected(c as char, *p)),
573            _ => {
574                // Multi-byte UTF-8: consume the whole codepoint.
575                let s = core::str::from_utf8(&bytes[*p..])
576                    .map_err(|_| ParseError::Unexpected(bytes[*p] as char, *p))?;
577                let c = s.chars().next().unwrap();
578                out.push(c);
579                *p += c.len_utf8();
580            }
581        }
582    }
583    Err(ParseError::Truncated)
584}
585
586fn parse_bool(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
587    if bytes[*p..].starts_with(b"true") {
588        *p += 4;
589        Ok(JsonValue::Bool(true))
590    } else if bytes[*p..].starts_with(b"false") {
591        *p += 5;
592        Ok(JsonValue::Bool(false))
593    } else {
594        Err(ParseError::Unexpected(bytes[*p] as char, *p))
595    }
596}
597
598fn parse_null(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
599    if bytes[*p..].starts_with(b"null") {
600        *p += 4;
601        Ok(JsonValue::Null)
602    } else {
603        Err(ParseError::Unexpected(bytes[*p] as char, *p))
604    }
605}
606
607fn parse_number(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
608    let start = *p;
609    if bytes[*p] == b'-' {
610        *p += 1;
611    }
612    while *p < bytes.len() && bytes[*p].is_ascii_digit() {
613        *p += 1;
614    }
615    if *p < bytes.len() && bytes[*p] == b'.' {
616        *p += 1;
617        while *p < bytes.len() && bytes[*p].is_ascii_digit() {
618            *p += 1;
619        }
620    }
621    if *p < bytes.len() && matches!(bytes[*p], b'e' | b'E') {
622        *p += 1;
623        if *p < bytes.len() && matches!(bytes[*p], b'+' | b'-') {
624            *p += 1;
625        }
626        while *p < bytes.len() && bytes[*p].is_ascii_digit() {
627            *p += 1;
628        }
629    }
630    let text = core::str::from_utf8(&bytes[start..*p])
631        .map_err(|_| ParseError::InvalidNumber(start))?
632        .to_string();
633    // Validate the parse so the wire side can trust the value.
634    if text.parse::<f64>().is_err() {
635        return Err(ParseError::InvalidNumber(start));
636    }
637    Ok(JsonValue::NumberText(text))
638}
639
640// ─── v7.17.0 Phase 3.9 — minimal JSONPath subset for jsonb_path_query ───
641//
642// Supported path syntax (PG-flavoured JSONPath subset):
643//   * `$` — document root (required leading segment)
644//   * `.field` — object field access (bare ident only; quoted form
645//                `."field with space"` accepted)
646//   * `[N]` — array index (non-negative integer; negative indices
647//             out of v7.17 scope)
648//   * `[*]` — array wildcard (fan-out — each element matched separately)
649//   * Chained: `$.a.b[0].c[*].name`
650//
651// NOT supported (errors clearly):
652//   * Filter expressions `? (@.price > 100)`
653//   * Range slices `[1:3]`
654//   * Recursive descent `..field`
655//   * Functions `keyvalue()`, `size()`, etc.
656//   * Path variables `$varname`
657
658#[derive(Debug, Clone)]
659enum PathStep {
660    Field(String),
661    Index(usize),
662    Wildcard,
663}
664
665fn parse_jsonpath(p: &str) -> Result<Vec<PathStep>, EvalError> {
666    let chars: Vec<char> = p.chars().collect();
667    let mut i = 0;
668    if i >= chars.len() || chars[i] != '$' {
669        return Err(EvalError::TypeMismatch {
670            detail: alloc::format!("jsonpath must start with '$', got {p:?}"),
671        });
672    }
673    i += 1;
674    let mut steps: Vec<PathStep> = Vec::new();
675    while i < chars.len() {
676        match chars[i] {
677            '.' => {
678                i += 1;
679                if i < chars.len() && chars[i] == '"' {
680                    i += 1;
681                    let start = i;
682                    while i < chars.len() && chars[i] != '"' {
683                        i += 1;
684                    }
685                    if i >= chars.len() {
686                        return Err(EvalError::TypeMismatch {
687                            detail: "jsonpath: unterminated quoted field".into(),
688                        });
689                    }
690                    steps.push(PathStep::Field(chars[start..i].iter().collect()));
691                    i += 1;
692                } else {
693                    let start = i;
694                    while i < chars.len()
695                        && chars[i] != '.'
696                        && chars[i] != '['
697                        && !chars[i].is_whitespace()
698                    {
699                        i += 1;
700                    }
701                    if start == i {
702                        return Err(EvalError::TypeMismatch {
703                            detail: "jsonpath: missing field name after '.'".into(),
704                        });
705                    }
706                    steps.push(PathStep::Field(chars[start..i].iter().collect()));
707                }
708            }
709            '[' => {
710                i += 1;
711                if i < chars.len() && chars[i] == '*' {
712                    i += 1;
713                    if i >= chars.len() || chars[i] != ']' {
714                        return Err(EvalError::TypeMismatch {
715                            detail: "jsonpath: expected ']' after '[*'".into(),
716                        });
717                    }
718                    i += 1;
719                    steps.push(PathStep::Wildcard);
720                } else {
721                    let start = i;
722                    while i < chars.len() && chars[i].is_ascii_digit() {
723                        i += 1;
724                    }
725                    if start == i {
726                        return Err(EvalError::TypeMismatch {
727                            detail:
728                                "jsonpath: only `[N]` (non-negative) or `[*]` supported in v7.17"
729                                    .into(),
730                        });
731                    }
732                    let idx: usize =
733                        chars[start..i]
734                            .iter()
735                            .collect::<String>()
736                            .parse()
737                            .map_err(|_| EvalError::TypeMismatch {
738                                detail: "jsonpath: invalid array index".into(),
739                            })?;
740                    if i >= chars.len() || chars[i] != ']' {
741                        return Err(EvalError::TypeMismatch {
742                            detail: "jsonpath: expected ']' after array index".into(),
743                        });
744                    }
745                    i += 1;
746                    steps.push(PathStep::Index(idx));
747                }
748            }
749            c if c.is_whitespace() => {
750                i += 1;
751            }
752            c => {
753                return Err(EvalError::TypeMismatch {
754                    detail: alloc::format!(
755                        "jsonpath: unexpected char '{c}' (v7.17 supports `$.field`, `[N]`, `[*]` only)"
756                    ),
757                });
758            }
759        }
760    }
761    Ok(steps)
762}
763
764fn apply_jsonpath(root: &JsonValue, steps: &[PathStep]) -> Vec<JsonValue> {
765    let mut cur: Vec<JsonValue> = alloc::vec![root.clone()];
766    for step in steps {
767        let mut next: Vec<JsonValue> = Vec::new();
768        for node in &cur {
769            match (step, node) {
770                (PathStep::Field(k), JsonValue::Object(entries)) => {
771                    if let Some((_, v)) = entries.iter().find(|(name, _)| name == k) {
772                        next.push(v.clone());
773                    }
774                }
775                (PathStep::Index(idx), JsonValue::Array(items)) => {
776                    if let Some(v) = items.get(*idx) {
777                        next.push(v.clone());
778                    }
779                }
780                (PathStep::Wildcard, JsonValue::Array(items)) => {
781                    next.extend(items.iter().cloned());
782                }
783                _ => {} // no match at this branch
784            }
785        }
786        cur = next;
787        if cur.is_empty() {
788            return Vec::new();
789        }
790    }
791    cur
792}
793
794/// v7.17.0 Phase 3.9 — `jsonb_path_query(doc, path)` — returns the
795/// matched JSON values as a TextArray (each element is the JSON
796/// encoding of one match).
797pub fn path_query(doc: &Value, path: &Value) -> Result<Value, EvalError> {
798    let (src, path_text) = match (doc, path) {
799        (Value::Null, _) | (_, Value::Null) => return Ok(Value::Null),
800        (Value::Json(s) | Value::Text(s), Value::Text(p) | Value::Json(p)) => (s, p),
801        _ => {
802            return Err(EvalError::TypeMismatch {
803                detail: "jsonb_path_query() expects (JSON, TEXT)".into(),
804            });
805        }
806    };
807    let root = parse(src).map_err(|e| EvalError::TypeMismatch {
808        detail: alloc::format!("invalid JSON for jsonb_path_query: {e}"),
809    })?;
810    let steps = parse_jsonpath(path_text)?;
811    let matches = apply_jsonpath(&root, &steps);
812    let arr: Vec<Option<String>> = matches
813        .into_iter()
814        .map(|v| Some(v.to_json_text()))
815        .collect();
816    Ok(Value::TextArray(arr))
817}
818
819/// v7.17.0 Phase 3.9 — `jsonb_path_query_first(doc, path)` returns
820/// the first matched JSON value as a Json, or NULL on no match.
821pub fn path_query_first(doc: &Value, path: &Value) -> Result<Value, EvalError> {
822    let q = path_query(doc, path)?;
823    match q {
824        Value::TextArray(items) => {
825            if let Some(Some(first)) = items.into_iter().next() {
826                Ok(Value::Json(first))
827            } else {
828                Ok(Value::Null)
829            }
830        }
831        other => Ok(other),
832    }
833}
834
835/// v7.17.0 Phase 3.9 — `jsonb_path_query_array(doc, path)` returns
836/// the matched values wrapped as a single JSON array.
837pub fn path_query_array(doc: &Value, path: &Value) -> Result<Value, EvalError> {
838    let q = path_query(doc, path)?;
839    match q {
840        Value::TextArray(items) => {
841            let mut buf = String::from("[");
842            let mut first = true;
843            for s in items.into_iter().flatten() {
844                if !first {
845                    buf.push(',');
846                }
847                buf.push_str(&s);
848                first = false;
849            }
850            buf.push(']');
851            Ok(Value::Json(buf))
852        }
853        other => Ok(other),
854    }
855}
856
857// ─── v7.17.0 Phase 3.P0-28 — JSON builder family ───────────────
858//
859// Surface: to_json / to_jsonb, json_build_object / jsonb_build_object,
860// json_build_array / jsonb_build_array, jsonb_set, jsonb_insert.
861//
862// PG `json` vs `jsonb` differ in storage shape only — both surface
863// as Value::Json textually. The pair just shares an implementation.
864
865/// Encode a Value as its canonical JSON text (no surrounding quotes
866/// for non-strings). Used by every builder below.
867///
868/// Rules:
869///   * NULL → "null" (json literal; NOT SQL NULL).
870///   * BOOL → "true" / "false".
871///   * Numbers → bare decimal text (BigInt prints exact 64-bit form).
872///   * Text → quoted+escaped JSON string.
873///   * Json/Jsonb → pass-through (assumed valid; parser is forgiving).
874///   * Arrays → "[..,..]" with element-wise encoding.
875///   * Bytes / Date / Timestamp / Uuid / Numeric → quoted textual
876///     form via Display; PG canonical text shape.
877pub fn value_to_json_text(v: &Value) -> String {
878    let mut out = String::new();
879    encode_value_into(v, &mut out);
880    out
881}
882
883fn encode_value_into(v: &Value, out: &mut String) {
884    match v {
885        Value::Null => out.push_str("null"),
886        Value::Bool(true) => out.push_str("true"),
887        Value::Bool(false) => out.push_str("false"),
888        Value::SmallInt(n) => out.push_str(&alloc::format!("{n}")),
889        Value::Int(n) => out.push_str(&alloc::format!("{n}")),
890        Value::BigInt(n) => out.push_str(&alloc::format!("{n}")),
891        Value::Float(x) => out.push_str(&alloc::format!("{x}")),
892        Value::Numeric { scaled, scale } => {
893            // Render the exact decimal text — same shape display uses.
894            out.push_str(&render_numeric(*scaled, *scale));
895        }
896        Value::Text(s) => write_json(&JsonValue::String(s.clone()), out),
897        Value::Json(s) => {
898            // Pass through verbatim; re-parsing would re-format and
899            // drift `1.0` → `1` etc. PG's to_json on a json input is
900            // identity.
901            out.push_str(s);
902        }
903        Value::TextArray(items) => {
904            out.push('[');
905            for (i, it) in items.iter().enumerate() {
906                if i > 0 {
907                    out.push(',');
908                }
909                match it {
910                    Some(s) => write_json(&JsonValue::String(s.clone()), out),
911                    None => out.push_str("null"),
912                }
913            }
914            out.push(']');
915        }
916        Value::IntArray(items) => {
917            out.push('[');
918            for (i, it) in items.iter().enumerate() {
919                if i > 0 {
920                    out.push(',');
921                }
922                match it {
923                    Some(n) => out.push_str(&alloc::format!("{n}")),
924                    None => out.push_str("null"),
925                }
926            }
927            out.push(']');
928        }
929        Value::BigIntArray(items) => {
930            out.push('[');
931            for (i, it) in items.iter().enumerate() {
932                if i > 0 {
933                    out.push(',');
934                }
935                match it {
936                    Some(n) => out.push_str(&alloc::format!("{n}")),
937                    None => out.push_str("null"),
938                }
939            }
940            out.push(']');
941        }
942        // Fall-through: render via Debug-stripped textual form,
943        // wrapped as a JSON string. Date/Timestamp/Uuid/Bytes/etc.
944        // PG itself stringifies these to their text-out form.
945        other => {
946            let txt = alloc::format!("{other:?}");
947            write_json(&JsonValue::String(txt), out);
948        }
949    }
950}
951
952fn render_numeric(scaled: i128, scale: u8) -> String {
953    let neg = scaled < 0;
954    let mag_str = alloc::format!("{}", scaled.unsigned_abs());
955    let s = scale as usize;
956    let body = if s == 0 {
957        mag_str
958    } else if mag_str.len() > s {
959        let p = mag_str.len() - s;
960        alloc::format!("{}.{}", &mag_str[..p], &mag_str[p..])
961    } else {
962        let pad = s - mag_str.len();
963        alloc::format!("0.{}{}", "0".repeat(pad), mag_str)
964    };
965    if neg { alloc::format!("-{body}") } else { body }
966}
967
968/// `json_build_object(k, v, k, v, …)` — variadic, even-length.
969/// NULL key → error (PG: "argument cannot be null"). Values encoded
970/// via `value_to_json_text`. Returns Value::Json.
971pub fn build_object(args: &[Value]) -> Result<Value, EvalError> {
972    if !args.len().is_multiple_of(2) {
973        return Err(EvalError::TypeMismatch {
974            detail: alloc::format!(
975                "json_build_object() needs an even number of args, got {}",
976                args.len()
977            ),
978        });
979    }
980    let mut out = String::from("{");
981    let mut first = true;
982    for pair in args.chunks_exact(2) {
983        if !first {
984            out.push(',');
985        }
986        first = false;
987        let key = match &pair[0] {
988            Value::Null => {
989                return Err(EvalError::TypeMismatch {
990                    detail: "json_build_object() key cannot be NULL".into(),
991                });
992            }
993            Value::Text(s) | Value::Json(s) => s.clone(),
994            other => format_value_as_text(other),
995        };
996        write_json(&JsonValue::String(key), &mut out);
997        out.push(':');
998        encode_value_into(&pair[1], &mut out);
999    }
1000    out.push('}');
1001    Ok(Value::Json(out))
1002}
1003
1004/// `json_build_array(...)` — variadic; empty → "[]". Each arg
1005/// encoded via `value_to_json_text`.
1006pub fn build_array(args: &[Value]) -> Result<Value, EvalError> {
1007    let mut out = String::from("[");
1008    for (i, v) in args.iter().enumerate() {
1009        if i > 0 {
1010            out.push(',');
1011        }
1012        encode_value_into(v, &mut out);
1013    }
1014    out.push(']');
1015    Ok(Value::Json(out))
1016}
1017
1018fn format_value_as_text(v: &Value) -> String {
1019    match v {
1020        Value::SmallInt(n) => alloc::format!("{n}"),
1021        Value::Int(n) => alloc::format!("{n}"),
1022        Value::BigInt(n) => alloc::format!("{n}"),
1023        Value::Float(x) => alloc::format!("{x}"),
1024        Value::Bool(b) => alloc::format!("{b}"),
1025        other => alloc::format!("{other:?}"),
1026    }
1027}
1028
1029/// `jsonb_set(target, path, new_value [, create_missing])` — replace
1030/// at PG text-array path. `create_missing` defaults to true.
1031///
1032///   * Path step on object: treated as key. If missing & create_missing
1033///     → insert; else no-op.
1034///   * Path step on array: integer index, negative counts from end.
1035///     Out-of-range with create_missing → append; without → no-op.
1036///   * Type mismatch (e.g. step on a scalar) → no-op (PG semantics).
1037pub fn set(args: &[Value]) -> Result<Value, EvalError> {
1038    if !(3..=4).contains(&args.len()) {
1039        return Err(EvalError::TypeMismatch {
1040            detail: alloc::format!("jsonb_set() takes 3 or 4 args, got {}", args.len()),
1041        });
1042    }
1043    if args.iter().take(3).any(|v| matches!(v, Value::Null)) {
1044        return Ok(Value::Null);
1045    }
1046    let create_missing = match args.get(3) {
1047        None | Some(Value::Null) => true,
1048        Some(Value::Bool(b)) => *b,
1049        Some(other) => {
1050            return Err(EvalError::TypeMismatch {
1051                detail: alloc::format!(
1052                    "jsonb_set() create_missing must be BOOL, got {:?}",
1053                    other.data_type()
1054                ),
1055            });
1056        }
1057    };
1058    let doc_text = json_text_arg(&args[0], "jsonb_set", "target")?;
1059    let path = path_text_arg(&args[1], "jsonb_set")?;
1060    let new_text = json_text_arg(&args[2], "jsonb_set", "new_value")?;
1061    let mut root = parse(doc_text).map_err(|e| EvalError::TypeMismatch {
1062        detail: alloc::format!("jsonb_set(): invalid JSON target — {e}"),
1063    })?;
1064    let new_val = parse(new_text).map_err(|e| EvalError::TypeMismatch {
1065        detail: alloc::format!("jsonb_set(): invalid JSON new_value — {e}"),
1066    })?;
1067    set_at_path(&mut root, &path, new_val, create_missing);
1068    Ok(Value::Json(root.to_json_text()))
1069}
1070
1071fn set_at_path(node: &mut JsonValue, path: &[String], new_val: JsonValue, create_missing: bool) {
1072    if path.is_empty() {
1073        *node = new_val;
1074        return;
1075    }
1076    let step = &path[0];
1077    let rest = &path[1..];
1078    match node {
1079        JsonValue::Object(entries) => {
1080            if let Some(pos) = entries.iter().position(|(k, _)| k == step) {
1081                if rest.is_empty() {
1082                    entries[pos].1 = new_val;
1083                } else {
1084                    set_at_path(&mut entries[pos].1, rest, new_val, create_missing);
1085                }
1086            } else if create_missing && rest.is_empty() {
1087                entries.push((step.clone(), new_val));
1088            }
1089            // Missing intermediate path with create_missing — PG only
1090            // creates the LEAF, never intermediate parents. No-op.
1091        }
1092        JsonValue::Array(items) => {
1093            let Some(idx) = resolve_array_index(step, items.len()) else {
1094                if create_missing && rest.is_empty() {
1095                    // PG: positive overshoot appends, negative prepends.
1096                    if let Ok(n) = step.parse::<i64>() {
1097                        if n < 0 {
1098                            items.insert(0, new_val);
1099                        } else {
1100                            items.push(new_val);
1101                        }
1102                    }
1103                }
1104                return;
1105            };
1106            if rest.is_empty() {
1107                items[idx] = new_val;
1108            } else {
1109                set_at_path(&mut items[idx], rest, new_val, create_missing);
1110            }
1111        }
1112        _ => {
1113            // Scalar — no replacement possible at non-empty path.
1114        }
1115    }
1116}
1117
1118fn resolve_array_index(step: &str, len: usize) -> Option<usize> {
1119    let n = step.parse::<i64>().ok()?;
1120    if n >= 0 {
1121        let i = n as usize;
1122        if i < len { Some(i) } else { None }
1123    } else {
1124        let from_end = len as i64 + n;
1125        if from_end >= 0 {
1126            Some(from_end as usize)
1127        } else {
1128            None
1129        }
1130    }
1131}
1132
1133/// `jsonb_insert(target, path, new_value [, insert_after])` —
1134/// insert at path. `insert_after` defaults to false.
1135///
1136///   * Array parent: insert before (or after) the index. Out-of-range
1137///     positive index → append; out-of-range negative → prepend.
1138///   * Object parent: key must NOT exist (PG raises). insert_after
1139///     has no effect for objects.
1140pub fn insert(args: &[Value]) -> Result<Value, EvalError> {
1141    if !(3..=4).contains(&args.len()) {
1142        return Err(EvalError::TypeMismatch {
1143            detail: alloc::format!("jsonb_insert() takes 3 or 4 args, got {}", args.len()),
1144        });
1145    }
1146    if args.iter().take(3).any(|v| matches!(v, Value::Null)) {
1147        return Ok(Value::Null);
1148    }
1149    let insert_after = match args.get(3) {
1150        None | Some(Value::Null) => false,
1151        Some(Value::Bool(b)) => *b,
1152        Some(other) => {
1153            return Err(EvalError::TypeMismatch {
1154                detail: alloc::format!(
1155                    "jsonb_insert() insert_after must be BOOL, got {:?}",
1156                    other.data_type()
1157                ),
1158            });
1159        }
1160    };
1161    let doc_text = json_text_arg(&args[0], "jsonb_insert", "target")?;
1162    let path = path_text_arg(&args[1], "jsonb_insert")?;
1163    let new_text = json_text_arg(&args[2], "jsonb_insert", "new_value")?;
1164    if path.is_empty() {
1165        return Err(EvalError::TypeMismatch {
1166            detail: "jsonb_insert(): path cannot be empty".into(),
1167        });
1168    }
1169    let mut root = parse(doc_text).map_err(|e| EvalError::TypeMismatch {
1170        detail: alloc::format!("jsonb_insert(): invalid JSON target — {e}"),
1171    })?;
1172    let new_val = parse(new_text).map_err(|e| EvalError::TypeMismatch {
1173        detail: alloc::format!("jsonb_insert(): invalid JSON new_value — {e}"),
1174    })?;
1175    insert_at_path(&mut root, &path, new_val, insert_after)?;
1176    Ok(Value::Json(root.to_json_text()))
1177}
1178
1179fn insert_at_path(
1180    node: &mut JsonValue,
1181    path: &[String],
1182    new_val: JsonValue,
1183    insert_after: bool,
1184) -> Result<(), EvalError> {
1185    debug_assert!(!path.is_empty());
1186    if path.len() == 1 {
1187        let step = &path[0];
1188        match node {
1189            JsonValue::Object(entries) => {
1190                if entries.iter().any(|(k, _)| k == step) {
1191                    return Err(EvalError::TypeMismatch {
1192                        detail: alloc::format!(
1193                            "jsonb_insert(): cannot replace existing key {step:?}"
1194                        ),
1195                    });
1196                }
1197                entries.push((step.clone(), new_val));
1198                Ok(())
1199            }
1200            JsonValue::Array(items) => {
1201                let Ok(n) = step.parse::<i64>() else {
1202                    return Err(EvalError::TypeMismatch {
1203                        detail: alloc::format!(
1204                            "jsonb_insert(): array step must be integer, got {step:?}"
1205                        ),
1206                    });
1207                };
1208                let mut idx = if n >= 0 {
1209                    let i = n as usize;
1210                    if i > items.len() { items.len() } else { i }
1211                } else {
1212                    let from_end = items.len() as i64 + n;
1213                    if from_end < 0 { 0 } else { from_end as usize }
1214                };
1215                if insert_after && idx < items.len() {
1216                    idx += 1;
1217                }
1218                items.insert(idx, new_val);
1219                Ok(())
1220            }
1221            _ => Err(EvalError::TypeMismatch {
1222                detail: "jsonb_insert(): parent at path is a scalar".into(),
1223            }),
1224        }
1225    } else {
1226        let step = &path[0];
1227        let rest = &path[1..];
1228        match node {
1229            JsonValue::Object(entries) => {
1230                if let Some(pos) = entries.iter().position(|(k, _)| k == step) {
1231                    insert_at_path(&mut entries[pos].1, rest, new_val, insert_after)
1232                } else {
1233                    Err(EvalError::TypeMismatch {
1234                        detail: alloc::format!("jsonb_insert(): path {step:?} does not exist"),
1235                    })
1236                }
1237            }
1238            JsonValue::Array(items) => {
1239                let Some(idx) = resolve_array_index(step, items.len()) else {
1240                    return Err(EvalError::TypeMismatch {
1241                        detail: alloc::format!("jsonb_insert(): array index {step:?} out of range"),
1242                    });
1243                };
1244                insert_at_path(&mut items[idx], rest, new_val, insert_after)
1245            }
1246            _ => Err(EvalError::TypeMismatch {
1247                detail: "jsonb_insert(): parent at path is a scalar".into(),
1248            }),
1249        }
1250    }
1251}
1252
1253fn json_text_arg<'a>(v: &'a Value, fname: &str, role: &str) -> Result<&'a str, EvalError> {
1254    match v {
1255        Value::Json(s) | Value::Text(s) => Ok(s.as_str()),
1256        other => Err(EvalError::TypeMismatch {
1257            detail: alloc::format!(
1258                "{fname}() {role} must be JSON or TEXT, got {:?}",
1259                other.data_type()
1260            ),
1261        }),
1262    }
1263}
1264
1265fn path_text_arg(v: &Value, fname: &str) -> Result<Vec<String>, EvalError> {
1266    match v {
1267        Value::Text(s) | Value::Json(s) => parse_text_array(s.as_str()),
1268        Value::TextArray(items) => Ok(items
1269            .iter()
1270            .map(|o| o.clone().unwrap_or_default())
1271            .collect()),
1272        other => Err(EvalError::TypeMismatch {
1273            detail: alloc::format!(
1274                "{fname}() path must be TEXT[] or TEXT, got {:?}",
1275                other.data_type()
1276            ),
1277        }),
1278    }
1279}
1280
1281#[cfg(test)]
1282mod tests {
1283    use super::*;
1284
1285    #[test]
1286    fn parse_atoms() {
1287        assert_eq!(parse("null").unwrap(), JsonValue::Null);
1288        assert_eq!(parse("true").unwrap(), JsonValue::Bool(true));
1289        assert_eq!(parse("false").unwrap(), JsonValue::Bool(false));
1290        assert_eq!(
1291            parse("\"hello\"").unwrap(),
1292            JsonValue::String("hello".into())
1293        );
1294        assert!(matches!(
1295            parse("42").unwrap(),
1296            JsonValue::NumberText(ref s) if s == "42"
1297        ));
1298    }
1299
1300    #[test]
1301    fn parse_nested() {
1302        let doc = parse(r#"{"a":1,"b":[true,null,"x"]}"#).unwrap();
1303        let JsonValue::Object(entries) = doc else {
1304            panic!("expected object");
1305        };
1306        assert_eq!(entries.len(), 2);
1307        assert_eq!(entries[0].0, "a");
1308        assert_eq!(entries[1].0, "b");
1309    }
1310
1311    #[test]
1312    fn parse_string_escapes() {
1313        let s = parse(r#""he said \"hi\" and\\then\n""#).unwrap();
1314        assert_eq!(s, JsonValue::String("he said \"hi\" and\\then\n".into()));
1315    }
1316
1317    #[test]
1318    fn parse_unicode_escape() {
1319        assert_eq!(parse(r#""é""#).unwrap(), JsonValue::String("é".into()));
1320    }
1321
1322    #[test]
1323    fn path_object_key_returns_value() {
1324        let doc = Value::Json(r#"{"name":"alice","age":30}"#.into());
1325        let key = Value::Text("name".into());
1326        let v = path_get(&doc, &key, true).unwrap();
1327        assert_eq!(v, Value::Text("alice".into()));
1328        let v = path_get(&doc, &key, false).unwrap();
1329        assert_eq!(v, Value::Json("\"alice\"".into()));
1330    }
1331
1332    #[test]
1333    fn path_array_index_supports_negative() {
1334        let doc = Value::Json("[10,20,30]".into());
1335        let v = path_get(&doc, &Value::Int(1), true).unwrap();
1336        assert_eq!(v, Value::Text("20".into()));
1337        let v = path_get(&doc, &Value::Int(-1), true).unwrap();
1338        assert_eq!(v, Value::Text("30".into()));
1339    }
1340
1341    #[test]
1342    fn path_missing_key_returns_null() {
1343        let doc = Value::Json(r#"{"a":1}"#.into());
1344        let v = path_get(&doc, &Value::Text("missing".into()), true).unwrap();
1345        assert_eq!(v, Value::Null);
1346    }
1347
1348    #[test]
1349    fn path_get_nested_subtree_renders_back() {
1350        let doc = Value::Json(r#"{"k":{"x":[1,2]}}"#.into());
1351        let v = path_get(&doc, &Value::Text("k".into()), false).unwrap();
1352        assert_eq!(v, Value::Json("{\"x\":[1,2]}".into()));
1353    }
1354}