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