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
54/// v7.39 (round 205, JSON_TABLE) — parse a document string into a
55/// JsonValue tree. Thin pub(crate) wrapper so the executor's
56/// JSON_TABLE arm can hold the parsed root across row iteration.
57pub(crate) fn parse_doc(src: &str) -> Result<JsonValue, EvalError> {
58    parse(src).map_err(|e| EvalError::TypeMismatch {
59        detail: alloc::format!("invalid JSON for JSON_TABLE: {e}"),
60    })
61}
62
63/// v7.39 (round 205, JSON_TABLE) — evaluate a jsonpath string over a
64/// pre-parsed JsonValue root, returning the ordered match set. The
65/// JSON_TABLE executor drives this for the row pattern (once per doc)
66/// and each column path (once per row item). `vars` carries PASSING
67/// variables the same way the jsonb_path_* functions do.
68pub(crate) fn json_table_path(
69    root: &JsonValue,
70    path: &str,
71    vars: Option<&JsonValue>,
72) -> Result<Vec<JsonValue>, EvalError> {
73    let (strict, steps) = parse_jsonpath_mode(path)?;
74    apply_jsonpath_mode(root, &steps, vars, strict)
75}
76
77impl JsonValue {
78    /// v7.39 (round 205) — the scalar text of a JSON value for column
79    /// coercion: a json string yields its inner text (so
80    /// `"2024-01-15"` coerces to a DATE by its content), numbers/bools
81    /// their literal, containers their json text.
82    pub(crate) fn scalar_text(&self) -> String {
83        self.as_text()
84    }
85
86    /// v7.39 (round 206) — the PG-canonical jsonb TEXT of this value
87    /// (spaces after `,` and `:`, strings quoted): a FORMAT JSON
88    /// column returns this, matching PG's `[1, 2, 3]` / `{"x": 1}` /
89    /// `"hi"` output.
90    pub(crate) fn canonical_json_text(&self) -> String {
91        let mut out = String::new();
92        write_json_canonical(self, &mut out);
93        out
94    }
95
96    /// v7.39 (round 205) — true for a JSON null (distinct from "no
97    /// match": the caller checks emptiness of the match set first).
98    pub(crate) fn is_json_null(&self) -> bool {
99        matches!(self, Self::Null)
100    }
101
102    fn as_text(&self) -> String {
103        match self {
104            Self::Null => "null".into(),
105            Self::Bool(b) => if *b { "true" } else { "false" }.into(),
106            Self::Number(x) => alloc::format!("{x}"),
107            Self::NumberText(s) | Self::String(s) => s.clone(),
108            Self::Array(_) | Self::Object(_) => self.to_json_text(),
109        }
110    }
111
112    pub(crate) fn to_json_text(&self) -> String {
113        let mut out = String::new();
114        write_json(self, &mut out);
115        out
116    }
117}
118
119fn write_json(v: &JsonValue, out: &mut String) {
120    match v {
121        JsonValue::Null => out.push_str("null"),
122        JsonValue::Bool(true) => out.push_str("true"),
123        JsonValue::Bool(false) => out.push_str("false"),
124        JsonValue::Number(x) => out.push_str(&alloc::format!("{x}")),
125        JsonValue::NumberText(s) => out.push_str(s),
126        JsonValue::String(s) => {
127            out.push('"');
128            for c in s.chars() {
129                match c {
130                    '"' => out.push_str("\\\""),
131                    '\\' => out.push_str("\\\\"),
132                    '\n' => out.push_str("\\n"),
133                    '\r' => out.push_str("\\r"),
134                    '\t' => out.push_str("\\t"),
135                    c if (c as u32) < 0x20 => {
136                        out.push_str(&alloc::format!("\\u{:04x}", c as u32));
137                    }
138                    c => out.push(c),
139                }
140            }
141            out.push('"');
142        }
143        JsonValue::Array(items) => {
144            out.push('[');
145            for (i, it) in items.iter().enumerate() {
146                if i > 0 {
147                    out.push(',');
148                }
149                write_json(it, out);
150            }
151            out.push(']');
152        }
153        JsonValue::Object(entries) => {
154            out.push('{');
155            for (i, (k, val)) in entries.iter().enumerate() {
156                if i > 0 {
157                    out.push(',');
158                }
159                write_json_string(k, out);
160                out.push(':');
161                write_json(val, out);
162            }
163            out.push('}');
164        }
165    }
166}
167
168/// Escape a string into a JSON string literal (shared by the verbatim
169/// and canonical serializers). Matches PG: `\n \r \t \" \\`, control
170/// chars as `\uXXXX`, everything else (incl. non-ASCII) verbatim UTF-8.
171fn write_json_string(s: &str, out: &mut String) {
172    out.push('"');
173    for c in s.chars() {
174        match c {
175            '"' => out.push_str("\\\""),
176            '\\' => out.push_str("\\\\"),
177            '\n' => out.push_str("\\n"),
178            '\r' => out.push_str("\\r"),
179            '\t' => out.push_str("\\t"),
180            c if (c as u32) < 0x20 => out.push_str(&alloc::format!("\\u{:04x}", c as u32)),
181            c => out.push(c),
182        }
183    }
184    out.push('"');
185}
186
187/// Canonicalise a jsonb text value the way PostgreSQL does on input:
188/// object keys sorted by (length, then bytewise) with duplicate keys
189/// collapsed last-wins, `, ` / `: ` whitespace, and numbers normalised
190/// to plain decimal (exponents expanded, `-0` → `0`, but trailing zeros
191/// from the input scale preserved — `1e2` → `100`, `1E-3` → `0.001`,
192/// `1.10` stays `1.10`). `json` keeps its input verbatim; only `jsonb`
193/// runs through this.
194pub fn canonicalize_jsonb(src: &str) -> Result<String, ParseError> {
195    let v = parse(src)?;
196    // v7.39 (round 619) — the canonical form is the source plus the spaces
197    // after `:` and `,`; sizing for that keeps the writer off the allocator.
198    let mut out = String::with_capacity(src.len() + src.len() / 4 + 8);
199    write_json_canonical(&v, &mut out);
200    Ok(out)
201}
202
203/// Canonicalise a `Value::Json` payload (a jsonb-typed result); any
204/// other value passes through untouched. Used to bring jsonb builder /
205/// mutator functions (`jsonb_build_object`, `to_jsonb`, `jsonb_set`, the
206/// `||` / `-` / `#-` operators, …) in line with PG, which always emits
207/// canonical jsonb from them. The `json_*` siblings stay verbatim.
208#[must_use]
209/// v7.39 (round 603) — the `JsonValue` a scalar becomes, when it becomes one
210/// simply.
211///
212/// `to_jsonb(5)` used to format the value into JSON text and then hand that
213/// text to `canonicalize_value`, which PARSES it and serialises it again —
214/// ten allocations a row for an integer, against one for the same projection
215/// without it, and `jsonb_build_object('a', id)` eighteen. The canonical
216/// form of these scalars is not in doubt, so they skip the round trip. `None`
217/// sends the caller down the text-then-reparse path, which is what anything
218/// richer (NUMERIC, dates, arrays, composites, already-JSON values) needs.
219fn simple_scalar_json(v: &Value<'_>) -> Option<JsonValue> {
220    Some(match v {
221        Value::Null => JsonValue::Null,
222        Value::Bool(b) => JsonValue::Bool(*b),
223        Value::SmallInt(n) => JsonValue::NumberText(alloc::format!("{n}")),
224        Value::Int(n) => JsonValue::NumberText(alloc::format!("{n}")),
225        Value::BigInt(n) => JsonValue::NumberText(alloc::format!("{n}")),
226        Value::Text(s) | Value::BpChar(s) => JsonValue::String(s.to_string()),
227        _ => return None,
228    })
229}
230
231/// v7.39 (round 603) — `to_jsonb` over a scalar, without the round trip.
232/// `None` when the argument is not one of the simple kinds.
233pub(crate) fn to_jsonb_scalar(v: &Value<'_>) -> Option<Value<'static>> {
234    // An integer's canonical jsonb IS its decimal spelling, and a bool's and
235    // NULL's are their keywords, so those need no `JsonValue` at all — which
236    // is the difference between two allocations and five.
237    match v {
238        Value::Null => return Some(Value::json(String::from("null"))),
239        Value::Bool(b) => {
240            return Some(Value::json(String::from(if *b { "true" } else { "false" })));
241        }
242        Value::SmallInt(n) => return Some(Value::json(alloc::format!("{n}"))),
243        Value::Int(n) => return Some(Value::json(alloc::format!("{n}"))),
244        Value::BigInt(n) => return Some(Value::json(alloc::format!("{n}"))),
245        _ => {}
246    }
247    simple_scalar_json(v).map(|jv| Value::json(json_canonical_string(&jv)))
248}
249
250/// v7.39 (round 603) — `jsonb_build_object` built directly as a value and
251/// serialised canonically once, instead of writing `json_build_object`'s
252/// spacing and re-parsing it to get jsonb's. The ordering, the last-wins
253/// duplicate rule and the number canonicalisation all still come from
254/// `write_json_canonical`, so this changes when the parse happens and
255/// nothing about what it produces. `None` when any argument is richer than
256/// the simple kinds.
257pub(crate) fn build_object_canonical(args: &[Value<'_>]) -> Option<Value<'static>> {
258    if !args.len().is_multiple_of(2) {
259        return None;
260    }
261    let mut entries: alloc::vec::Vec<(String, JsonValue)> =
262        alloc::vec::Vec::with_capacity(args.len() / 2);
263    for pair in args.chunks_exact(2) {
264        // A NULL key is an error the text path words; leave it there.
265        let key = match &pair[0] {
266            Value::Text(s) | Value::BpChar(s) => s.to_string(),
267            Value::SmallInt(n) => alloc::format!("{n}"),
268            Value::Int(n) => alloc::format!("{n}"),
269            Value::BigInt(n) => alloc::format!("{n}"),
270            _ => return None,
271        };
272        entries.push((key, simple_scalar_json(&pair[1])?));
273    }
274    Some(Value::json(json_canonical_string(&JsonValue::Object(
275        entries,
276    ))))
277}
278
279pub fn canonicalize_value(v: Value<'static>) -> Value<'static> {
280    match v {
281        Value::Json(s) => {
282            Value::json(canonicalize_jsonb(s.as_ref()).unwrap_or_else(|_| s.into_owned()))
283        }
284        other => other,
285    }
286}
287
288/// Render a sub-value extracted by the `->` / `#>` (jsonb) and `->>` /
289/// `#>>` (text) accessors. Containers are serialised canonically (PG
290/// re-emits the extracted jsonb in canonical form); a scalar under
291/// `as_text` returns its raw value — already canonical, since the source
292/// jsonb was canonicalised on input.
293fn accessor_result(v: &JsonValue, as_text: bool) -> Value<'static> {
294    if as_text && !matches!(v, JsonValue::Array(_) | JsonValue::Object(_)) {
295        return Value::text(v.as_text());
296    }
297    let s = json_canonical_string(v);
298    if as_text {
299        Value::text(s)
300    } else {
301        Value::json(s)
302    }
303}
304
305// ---- v7.38 (read01) — verbatim source extraction for the json accessors ----
306//
307// PG's `->` / `->>` / `#>` / `#>>` return the EXACT source text of the located
308// value, never a re-serialization: `('{"a":{ "b" : 1 }}'::json) -> 'a'` yields
309// `{ "b" : 1 }`, `2e2` stays `2e2`, and `{"k":1,"k":2}` keeps both members.
310// `jsonb` needs no special case — its stored text is already canonical, so
311// slicing that text yields canonical text, exactly as before.
312//
313// Only containers were wrong: SPG already passed scalars through verbatim.
314
315/// First index at or after `i` that is not JSON whitespace.
316fn skip_ws_at(b: &[u8], mut i: usize) -> usize {
317    while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\n' | b'\r') {
318        i += 1;
319    }
320    i
321}
322
323/// `i` sits on the opening quote; returns the index just past the closing
324/// quote. Escapes are skipped as a unit so `\"` does not end the string.
325fn scan_string(b: &[u8], i: usize) -> Option<usize> {
326    debug_assert_eq!(b.get(i), Some(&b'"'));
327    let mut j = i + 1;
328    while j < b.len() {
329        match b[j] {
330            b'\\' => j += 2,
331            b'"' => return Some(j + 1),
332            _ => j += 1,
333        }
334    }
335    None
336}
337
338/// `i` sits on the first byte of a JSON value; returns the index just past its
339/// last byte. Containers are matched by depth, ignoring braces inside strings;
340/// scalars run to the next structural byte. Multi-byte UTF-8 is safe: its
341/// continuation bytes are all >= 0x80 and never collide with the ASCII
342/// delimiters tested here.
343fn scan_value(b: &[u8], i: usize) -> Option<usize> {
344    match *b.get(i)? {
345        b'"' => scan_string(b, i),
346        open @ (b'{' | b'[') => {
347            let close = if open == b'{' { b'}' } else { b']' };
348            let mut depth = 0usize;
349            let mut j = i;
350            while j < b.len() {
351                match b[j] {
352                    b'"' => j = scan_string(b, j)?,
353                    c if c == open => {
354                        depth += 1;
355                        j += 1;
356                    }
357                    c if c == close => {
358                        depth -= 1;
359                        j += 1;
360                        if depth == 0 {
361                            return Some(j);
362                        }
363                    }
364                    _ => j += 1,
365                }
366            }
367            None
368        }
369        _ => {
370            let mut j = i;
371            while j < b.len() && !matches!(b[j], b',' | b'}' | b']' | b' ' | b'\t' | b'\n' | b'\r')
372            {
373                j += 1;
374            }
375            (j > i).then_some(j)
376        }
377    }
378}
379
380/// Decode a JSON string token (including its quotes) into its text value.
381fn decode_string_token(tok: &str) -> Option<String> {
382    match parse(tok).ok()? {
383        JsonValue::String(s) => Some(s),
384        _ => None,
385    }
386}
387
388/// v7.38.8 — does a JSON string TOKEN denote exactly `key`, without
389/// building the string it denotes?
390///
391/// `locate_member` compared keys by calling `decode_string_token` on
392/// each one, which runs the whole recursive-descent parser over the
393/// token and allocates a `String` — once per member, per row, per
394/// accessor, to answer a question that is usually a byte comparison.
395///
396/// A token with no backslash in it denotes its own inner bytes, so it
397/// can be compared in place. One with an escape defers to
398/// `decode_string_token`, so the two paths cannot disagree about what
399/// an escape means.
400fn key_token_eq(tok: &str, key: &str) -> bool {
401    let inner = match tok.strip_prefix('"').and_then(|t| t.strip_suffix('"')) {
402        Some(i) => i,
403        None => return false,
404    };
405    if inner.as_bytes().contains(&b'\\') {
406        return decode_string_token(tok).is_some_and(|d| d == key);
407    }
408    inner == key
409}
410
411/// Verbatim source slice of `key`'s value in the object encoded at `src`.
412/// PG resolves a duplicate key to the LAST occurrence, so the scan does not
413/// stop early. Keys are compared after unescaping (`{"A":1}` has key `A`).
414fn locate_member<'a>(src: &'a str, key: &str) -> Option<&'a str> {
415    let b = src.as_bytes();
416    let mut i = skip_ws_at(b, 0);
417    if b.get(i) != Some(&b'{') {
418        return None;
419    }
420    i += 1;
421    let mut found: Option<&'a str> = None;
422    loop {
423        i = skip_ws_at(b, i);
424        match b.get(i)? {
425            b'}' => return found,
426            b'"' => {}
427            _ => return None,
428        }
429        let key_end = scan_string(b, i)?;
430        let key_tok = src.get(i..key_end)?;
431        i = skip_ws_at(b, key_end);
432        if b.get(i) != Some(&b':') {
433            return None;
434        }
435        i = skip_ws_at(b, i + 1);
436        let val_end = scan_value(b, i)?;
437        if key_token_eq(key_tok, key) {
438            found = Some(src.get(i..val_end)?);
439        }
440        i = skip_ws_at(b, val_end);
441        match b.get(i)? {
442            b',' => i += 1,
443            b'}' => return found,
444            _ => return None,
445        }
446    }
447}
448
449/// Verbatim source slice of element `idx` in the array encoded at `src`.
450/// A negative index counts from the end, as in PG.
451fn locate_index(src: &str, idx: i64) -> Option<&str> {
452    let b = src.as_bytes();
453    let mut i = skip_ws_at(b, 0);
454    if b.get(i) != Some(&b'[') {
455        return None;
456    }
457    i += 1;
458    let mut spans: Vec<(usize, usize)> = Vec::new();
459    loop {
460        i = skip_ws_at(b, i);
461        if b.get(i)? == &b']' {
462            break;
463        }
464        let end = scan_value(b, i)?;
465        spans.push((i, end));
466        i = skip_ws_at(b, end);
467        match b.get(i)? {
468            b',' => i += 1,
469            b']' => break,
470            _ => return None,
471        }
472    }
473    let n = if idx >= 0 {
474        usize::try_from(idx).ok()?
475    } else {
476        usize::try_from(i64::try_from(spans.len()).ok()? + idx).ok()?
477    };
478    let (s, e) = *spans.get(n)?;
479    src.get(s..e)
480}
481
482/// Turn a located verbatim slice into the accessor's result. Containers and
483/// scalars alike keep their source text; only `->>` unwraps a string token and
484/// maps a JSON `null` to SQL NULL (`->` yields the JSON `null` itself).
485fn verbatim_accessor_result(slice: &str, as_text: bool) -> Value<'static> {
486    match slice.as_bytes().first() {
487        Some(b'n') if slice == "null" => {
488            if as_text {
489                Value::Null
490            } else {
491                Value::json("null")
492            }
493        }
494        Some(b'"') if as_text => decode_string_token(slice).map_or(Value::Null, Value::text),
495        _ if as_text => Value::text(slice.to_string()),
496        _ => Value::json(slice.to_string()),
497    }
498}
499
500/// Serialise a `JsonValue` in PG's canonical jsonb text form.
501fn json_canonical_string(v: &JsonValue) -> String {
502    let mut s = String::new();
503    write_json_canonical(v, &mut s);
504    s
505}
506
507fn write_json_canonical(v: &JsonValue, out: &mut String) {
508    match v {
509        JsonValue::Null => out.push_str("null"),
510        JsonValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
511        JsonValue::Number(x) => out.push_str(&canon_json_number(&alloc::format!("{x}"))),
512        JsonValue::NumberText(s) => out.push_str(&canon_json_number(s)),
513        JsonValue::String(s) => write_json_string(s, out),
514        JsonValue::Array(items) => {
515            out.push('[');
516            for (i, it) in items.iter().enumerate() {
517                if i > 0 {
518                    out.push_str(", ");
519                }
520                write_json_canonical(it, out);
521            }
522            out.push(']');
523        }
524        JsonValue::Object(entries) => {
525            // v7.39 (round 619) — one entry needs neither dedup nor sort, and
526            // the `Vec` those need was built for every object on every row.
527            if entries.len() <= 1 {
528                out.push('{');
529                if let Some((k, val)) = entries.first() {
530                    write_json_string(k, out);
531                    out.push_str(": ");
532                    write_json_canonical(val, out);
533                }
534                out.push('}');
535                return;
536            }
537            write_object_general(entries, out);
538        }
539    }
540}
541
542/// v7.39 (round 619) — the dedup-and-sort object writer. Split out so the
543/// one-entry shortcut above can be checked against it directly.
544fn write_object_general(entries: &[(String, JsonValue)], out: &mut String) {
545    {
546        {
547            // Duplicate keys collapse last-wins (keep the final value),
548            // preserving first-seen order only until the stable sort.
549            let mut deduped: Vec<(&String, &JsonValue)> = Vec::new();
550            for (k, val) in entries {
551                if let Some(slot) = deduped.iter_mut().find(|(mk, _)| *mk == k) {
552                    slot.1 = val;
553                } else {
554                    deduped.push((k, val));
555                }
556            }
557            deduped.sort_by(|a, b| {
558                a.0.len()
559                    .cmp(&b.0.len())
560                    .then_with(|| a.0.as_bytes().cmp(b.0.as_bytes()))
561            });
562            out.push('{');
563            for (i, (k, val)) in deduped.iter().enumerate() {
564                if i > 0 {
565                    out.push_str(", ");
566                }
567                write_json_string(k, out);
568                out.push_str(": ");
569                write_json_canonical(val, out);
570            }
571            out.push('}');
572        }
573    }
574}
575
576/// Render a JSON number lexeme in PostgreSQL's canonical jsonb form: a
577/// plain decimal with the exponent applied, `-0` normalised to `0`, and
578/// the input's fractional scale preserved. Digits are manipulated as
579/// strings so arbitrarily large numbers round-trip without overflow.
580/// v7.39 (round 619) — a plain integer lexeme is ALREADY the canonical form,
581/// so it is handed back borrowed instead of rebuilt.
582///
583/// The slow body below is unchanged and still decides every other shape; the
584/// two are asserted to agree over a generated set in this module's tests, so
585/// the shortcut is checked mechanically rather than by reading. Canonicalising
586/// `{"a":123}` allocated a `String` here for every number, on every row.
587fn canon_json_number(lexeme: &str) -> alloc::borrow::Cow<'_, str> {
588    let body = lexeme.strip_prefix('-').unwrap_or(lexeme);
589    if !body.is_empty()
590        && body.bytes().all(|b| b.is_ascii_digit())
591        // A leading zero is only canonical when the whole integer IS zero.
592        && (body == "0" || !body.starts_with('0'))
593        // `-0` canonicalises to `0`, so it is not a pass-through.
594        && !(lexeme.starts_with('-') && body == "0")
595    {
596        return alloc::borrow::Cow::Borrowed(lexeme);
597    }
598    alloc::borrow::Cow::Owned(canon_json_number_slow(lexeme))
599}
600
601fn canon_json_number_slow(lexeme: &str) -> String {
602    let neg = lexeme.starts_with('-');
603    let body = lexeme.trim_start_matches(['-', '+']);
604    // Split into mantissa (int '.' frac) and exponent.
605    let (mantissa, exp) = match body.split_once(['e', 'E']) {
606        Some((m, e)) => (m, e.parse::<i64>().unwrap_or(0)),
607        None => (body, 0),
608    };
609    let (int_part, frac_part) = match mantissa.split_once('.') {
610        Some((i, f)) => (i, f),
611        None => (mantissa, ""),
612    };
613    let digits: String = alloc::format!("{int_part}{frac_part}");
614    // `shift` = number of fractional digits in the output. Applying the
615    // exponent moves the point right by `exp`, i.e. reduces the fraction
616    // count by `exp`.
617    let shift = frac_part.len() as i64 - exp;
618    let all_zero = digits.bytes().all(|b| b == b'0');
619    let sign = if neg && !all_zero { "-" } else { "" };
620    let strip = |s: &str| -> String {
621        let t = s.trim_start_matches('0');
622        if t.is_empty() { "0".into() } else { t.into() }
623    };
624    if shift <= 0 {
625        // Integer: append `-shift` trailing zeros.
626        let zeros = "0".repeat((-shift) as usize);
627        alloc::format!("{sign}{}{zeros}", strip(&digits))
628    } else {
629        let shift = shift as usize;
630        let (int_str, frac_str) = if digits.len() > shift {
631            (
632                digits[..digits.len() - shift].to_string(),
633                digits[digits.len() - shift..].to_string(),
634            )
635        } else {
636            (
637                "0".to_string(),
638                alloc::format!("{}{}", "0".repeat(shift - digits.len()), digits),
639            )
640        };
641        alloc::format!("{sign}{}.{frac_str}", strip(&int_str))
642    }
643}
644
645/// v6.4.5 — PG `json #> path_text` / `json #>> path_text`. The
646/// right-hand side is a PG text-array literal `'{a,0,b}'` whose
647/// elements are walked left-to-right; each element is either an
648/// object key or (when it parses as a non-negative integer) an
649/// v7.37.43-T4.5 — set-returning function `jsonb_each_text(jsonb)`.
650/// PG semantics: for each (key, value) pair in the object, emit one
651/// row whose `key` column is the literal key and `value` column is
652/// the JSON value rendered as text (`null` → SQL NULL, primitives →
653/// their lexeme, nested objects/arrays → JSON text).
654///
655/// Returns the (key, value) tuples as a Vec ready for FROM-clause
656/// materialisation. Non-object inputs raise an error (PG's actual
657/// behaviour); `NULL` and empty object both produce 0 rows.
658pub fn jsonb_each_text_rows(arg: &Value) -> Result<Vec<(String, Option<String>)>, EvalError> {
659    each_rows(arg, true, "jsonb_each_text")
660}
661
662/// v7.37.17 (17.6 siblings) — shared body for the four `each` SRFs.
663/// `as_text` (the `*_each_text` forms) unwraps scalar values to
664/// their lexeme and maps JSON null → SQL NULL; the plain forms
665/// render every value (including JSON null) as compact JSON text,
666/// which the executor wraps as a jsonb-typed column.
667pub fn each_rows(
668    arg: &Value,
669    as_text: bool,
670    fn_name: &str,
671) -> Result<Vec<(String, Option<String>)>, EvalError> {
672    let src = match arg {
673        Value::Null => return Ok(Vec::new()),
674        Value::Json(s) | Value::Text(s) => s.as_ref(),
675        other => {
676            return Err(EvalError::TypeMismatch {
677                detail: alloc::format!(
678                    "{fn_name}: argument must be JSON / JSONB, got {}",
679                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
680                ),
681            });
682        }
683    };
684    let parsed = parse(src).map_err(|e| EvalError::TypeMismatch {
685        detail: alloc::format!("{fn_name}: invalid JSON: {e}"),
686    })?;
687    match parsed {
688        JsonValue::Object(entries) => {
689            let mut out: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
690            for (k, v) in entries {
691                let text = if as_text {
692                    match &v {
693                        JsonValue::Null => None,
694                        JsonValue::Bool(b) => Some(if *b {
695                            "true".to_string()
696                        } else {
697                            "false".to_string()
698                        }),
699                        JsonValue::Number(_) | JsonValue::NumberText(_) | JsonValue::String(_) => {
700                            Some(v.as_text())
701                        }
702                        JsonValue::Array(_) | JsonValue::Object(_) => {
703                            Some(json_canonical_string(&v))
704                        }
705                    }
706                } else {
707                    Some(json_canonical_string(&v))
708                };
709                out.push((k, text));
710            }
711            Ok(out)
712        }
713        other => Err(EvalError::TypeMismatch {
714            detail: alloc::format!("cannot call {fn_name} on a non-object ({other:?})"),
715        }),
716    }
717}
718
719/// v7.37.17 (17.6 siblings) — set-returning function
720/// `jsonb_array_elements[_text](json)`. PG semantics: one row per
721/// array element. `_text` renders scalars as their lexeme and JSON
722/// null as SQL NULL; the plain form renders every element (including
723/// JSON null) as compact JSON text. Non-array inputs raise an error
724/// (PG's actual behaviour); SQL NULL produces 0 rows.
725///
726/// Returns the element texts as a Vec ready for FROM-clause
727/// materialisation (via the unnest rewrite in the parser).
728pub fn array_element_rows(
729    arg: &Value,
730    as_text: bool,
731    fn_name: &str,
732) -> Result<Vec<Option<String>>, EvalError> {
733    let src = match arg {
734        Value::Null => return Ok(Vec::new()),
735        Value::Json(s) | Value::Text(s) => s.as_ref(),
736        other => {
737            return Err(EvalError::TypeMismatch {
738                detail: alloc::format!(
739                    "{fn_name}: argument must be JSON / JSONB, got {}",
740                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
741                ),
742            });
743        }
744    };
745    let parsed = parse(src).map_err(|e| EvalError::TypeMismatch {
746        detail: alloc::format!("{fn_name}: invalid JSON: {e}"),
747    })?;
748    match parsed {
749        JsonValue::Array(items) => {
750            let mut out: Vec<Option<String>> = Vec::with_capacity(items.len());
751            for v in items {
752                let text = if as_text {
753                    match &v {
754                        JsonValue::Null => None,
755                        JsonValue::Bool(b) => Some(if *b {
756                            "true".to_string()
757                        } else {
758                            "false".to_string()
759                        }),
760                        JsonValue::Number(_) | JsonValue::NumberText(_) | JsonValue::String(_) => {
761                            Some(v.as_text())
762                        }
763                        JsonValue::Array(_) | JsonValue::Object(_) => {
764                            Some(json_canonical_string(&v))
765                        }
766                    }
767                } else {
768                    // Plain form renders each element as canonical jsonb.
769                    Some(json_canonical_string(&v))
770                };
771                out.push(text);
772            }
773            Ok(out)
774        }
775        other => Err(EvalError::TypeMismatch {
776            detail: alloc::format!(
777                "cannot extract elements from a non-array ({fn_name}: got {other:?})"
778            ),
779        }),
780    }
781}
782
783/// array index. Missing or non-existent steps return `Value::Null`.
784pub fn path_walk(lhs: &Value, rhs: &Value, as_text: bool) -> Result<Value<'static>, EvalError> {
785    let src = match lhs {
786        Value::Json(s) | Value::Text(s) => s.as_ref(),
787        Value::Null => return Ok(Value::Null),
788        other => {
789            return Err(EvalError::TypeMismatch {
790                detail: alloc::format!(
791                    "JSON path walk: left side must be JSON or TEXT, got {}",
792                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
793                ),
794            });
795        }
796    };
797    // v7.39 (round 769, F31 tranche 5 #135) — PG accepts the path as a
798    // real TEXT[] value too (`doc #> ARRAY['a','b']`), not only the
799    // `'{a,b}'` literal; a NULL element yields NULL (no such key).
800    let owned_steps: Vec<String>;
801    let path: Vec<String> = match rhs {
802        Value::TextArray(items) => {
803            if items.iter().any(Option::is_none) {
804                return Ok(Value::Null);
805            }
806            owned_steps = items.iter().flatten().cloned().collect();
807            owned_steps
808        }
809        Value::Text(s) | Value::Json(s) => parse_text_array(s.as_ref())?,
810        Value::Null => return Ok(Value::Null),
811        other => {
812            return Err(EvalError::TypeMismatch {
813                detail: alloc::format!(
814                    "JSON path walk: right side must be TEXT, got {}",
815                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
816                ),
817            });
818        }
819    };
820    // Validate once, then narrow a VERBATIM source slice per step — PG's `#>` /
821    // `#>>` return the located value's original text, not a re-serialization.
822    validate_unless_known_json(lhs, src, "path walk")?;
823    let mut cur: &str = src;
824    for step in &path {
825        let at = skip_ws_at(cur.as_bytes(), 0);
826        let next = match cur.as_bytes().get(at) {
827            Some(b'{') => locate_member(cur, step),
828            Some(b'[') => match step.parse::<i64>() {
829                Ok(idx) => locate_index(cur, idx),
830                Err(_) => return Ok(Value::Null),
831            },
832            _ => return Ok(Value::Null),
833        };
834        cur = match next {
835            None => return Ok(Value::Null),
836            Some(slice) => slice,
837        };
838    }
839    Ok(verbatim_accessor_result(cur, as_text))
840}
841
842/// v6.4.5 — PG `json @> sub_json` containment. Returns BOOL.
843/// `lhs @> rhs` is true when every member of `rhs` is structurally
844/// contained in `lhs`:
845///   - Scalars: equal
846///   - Objects: every (key, value) in rhs exists in lhs with a
847///     containing value
848///   - Arrays: every element in rhs has a containing element in lhs
849/// v7.37.6-A — PG `jsonb ? text`. Returns BOOL: true iff the key
850/// exists at the top level of the document.
851///   - Object: true iff `key` is a member name.
852///   - Array:  true iff any element is exactly the JSON string `key`.
853///   - Scalar string: true iff the scalar equals `key`.
854///   - Other scalars / null: false.
855/// NULL on either side → NULL (SQL 3VL).
856pub fn key_exists(lhs: &Value, rhs: &Value) -> Result<Value<'static>, EvalError> {
857    let lhs_text = match lhs {
858        Value::Json(s) | Value::Text(s) => s.as_ref(),
859        Value::Null => return Ok(Value::Null),
860        other => {
861            return Err(EvalError::TypeMismatch {
862                detail: alloc::format!(
863                    "JSON ?: left side must be JSON or TEXT, got {}",
864                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
865                ),
866            });
867        }
868    };
869    let key = match rhs {
870        Value::Text(s) => s.as_ref(),
871        Value::Null => return Ok(Value::Null),
872        other => {
873            return Err(EvalError::TypeMismatch {
874                detail: alloc::format!(
875                    "JSON ?: right side must be TEXT, got {}",
876                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
877                ),
878            });
879        }
880    };
881    let doc = parse(lhs_text).map_err(|e| EvalError::TypeMismatch {
882        detail: alloc::format!("invalid JSON on left of ?: {e}"),
883    })?;
884    Ok(Value::Bool(node_has_key(&doc, key)))
885}
886
887fn node_has_key(v: &JsonValue, key: &str) -> bool {
888    match v {
889        JsonValue::Object(members) => members.iter().any(|(k, _)| k == key),
890        JsonValue::Array(items) => items
891            .iter()
892            .any(|item| matches!(item, JsonValue::String(s) if s == key)),
893        JsonValue::String(s) => s == key,
894        _ => false,
895    }
896}
897
898/// Helper for `?|` / `?&` — extract a Vec of keys from either a
899/// TEXT[] Value or a single TEXT Value (PG accepts both).
900fn collect_keys(v: &Value) -> Result<Option<Vec<String>>, EvalError> {
901    match v {
902        Value::Null => Ok(None),
903        Value::TextArray(items) => Ok(Some(items.iter().filter_map(|x| x.clone()).collect())),
904        Value::Text(s) => Ok(Some(alloc::vec![s.to_string()])),
905        other => Err(EvalError::TypeMismatch {
906            detail: alloc::format!(
907                "JSON ?|/?&: right side must be TEXT[] or TEXT, got {}",
908                crate::conversions::pg_type_name_for_error_opt(other.data_type())
909            ),
910        }),
911    }
912}
913
914/// v7.37.6-A — PG `jsonb ?| text[]`. Returns BOOL: true iff any one
915/// of the listed keys exists at the top level.
916pub fn keys_any(lhs: &Value, rhs: &Value) -> Result<Value<'static>, EvalError> {
917    let lhs_text = match lhs {
918        Value::Json(s) | Value::Text(s) => s.as_ref(),
919        Value::Null => return Ok(Value::Null),
920        other => {
921            return Err(EvalError::TypeMismatch {
922                detail: alloc::format!(
923                    "JSON ?|: left side must be JSON or TEXT, got {}",
924                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
925                ),
926            });
927        }
928    };
929    let Some(keys) = collect_keys(rhs)? else {
930        return Ok(Value::Null);
931    };
932    let doc = parse(lhs_text).map_err(|e| EvalError::TypeMismatch {
933        detail: alloc::format!("invalid JSON on left of ?|: {e}"),
934    })?;
935    Ok(Value::Bool(keys.iter().any(|k| node_has_key(&doc, k))))
936}
937
938/// v7.37.6-A — PG `jsonb ?& text[]`. Returns BOOL: true iff every
939/// one of the listed keys exists at the top level.
940pub fn keys_all(lhs: &Value, rhs: &Value) -> Result<Value<'static>, EvalError> {
941    let lhs_text = match lhs {
942        Value::Json(s) | Value::Text(s) => s.as_ref(),
943        Value::Null => return Ok(Value::Null),
944        other => {
945            return Err(EvalError::TypeMismatch {
946                detail: alloc::format!(
947                    "JSON ?&: left side must be JSON or TEXT, got {}",
948                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
949                ),
950            });
951        }
952    };
953    let Some(keys) = collect_keys(rhs)? else {
954        return Ok(Value::Null);
955    };
956    let doc = parse(lhs_text).map_err(|e| EvalError::TypeMismatch {
957        detail: alloc::format!("invalid JSON on left of ?&: {e}"),
958    })?;
959    Ok(Value::Bool(keys.iter().all(|k| node_has_key(&doc, k))))
960}
961
962pub fn contains(lhs: &Value, rhs: &Value) -> Result<Value<'static>, EvalError> {
963    let lhs_text = match lhs {
964        Value::Json(s) | Value::Text(s) => s.as_ref(),
965        Value::Null => return Ok(Value::Null),
966        other => {
967            return Err(EvalError::TypeMismatch {
968                detail: alloc::format!(
969                    "JSON @>: left side must be JSON or TEXT, got {}",
970                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
971                ),
972            });
973        }
974    };
975    let rhs_text = match rhs {
976        Value::Json(s) | Value::Text(s) => s.as_ref(),
977        Value::Null => return Ok(Value::Null),
978        other => {
979            return Err(EvalError::TypeMismatch {
980                detail: alloc::format!(
981                    "JSON @>: right side must be JSON or TEXT, got {}",
982                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
983                ),
984            });
985        }
986    };
987    let lhs_doc = parse(lhs_text).map_err(|e| EvalError::TypeMismatch {
988        detail: alloc::format!("invalid JSON on left of @>: {e}"),
989    })?;
990    let rhs_doc = parse(rhs_text).map_err(|e| EvalError::TypeMismatch {
991        detail: alloc::format!("invalid JSON on right of @>: {e}"),
992    })?;
993    // PG special case: a top-level array `@>` a non-array scalar is
994    // true when the scalar equals any element (flat equality). This
995    // applies ONLY at the top level — inside array/array containment
996    // PG still requires a scalar RHS element to match a *scalar* LHS
997    // element, so it must NOT be folded into `json_contains`'s
998    // recursion (`'[1,[2,3]]' @> '[2,3]'` stays false).
999    let result = match (&lhs_doc, &rhs_doc) {
1000        (JsonValue::Array(items), scalar)
1001            if !matches!(scalar, JsonValue::Array(_) | JsonValue::Object(_)) =>
1002        {
1003            items.iter().any(|it| json_eq(it, scalar))
1004        }
1005        _ => json_contains(&lhs_doc, &rhs_doc),
1006    };
1007    Ok(Value::Bool(result))
1008}
1009
1010/// `jsonb = jsonb` structural equality (PG18-compatible). PG's jsonb
1011/// equality is order-INDEPENDENT for object keys but order-SENSITIVE
1012/// for array elements, and it compares numbers by value (so
1013/// `'1'::jsonb = '1.0'::jsonb` is true). `json_eq` encodes those rules;
1014/// this parses both operands and delegates. Values reaching here through
1015/// the `::jsonb` cast / a jsonb column are already canonicalised (keys
1016/// sorted, duplicates collapsed), so object equality is exact.
1017pub fn equals(lhs: &Value, rhs: &Value) -> Result<bool, EvalError> {
1018    let lhs_text = match lhs {
1019        Value::Json(s) | Value::Text(s) => s.as_ref(),
1020        other => {
1021            return Err(EvalError::TypeMismatch {
1022                detail: alloc::format!(
1023                    "jsonb =: left side must be JSON or TEXT, got {}",
1024                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
1025                ),
1026            });
1027        }
1028    };
1029    let rhs_text = match rhs {
1030        Value::Json(s) | Value::Text(s) => s.as_ref(),
1031        other => {
1032            return Err(EvalError::TypeMismatch {
1033                detail: alloc::format!(
1034                    "jsonb =: right side must be JSON or TEXT, got {}",
1035                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
1036                ),
1037            });
1038        }
1039    };
1040    let lhs_doc = parse(lhs_text).map_err(|e| EvalError::TypeMismatch {
1041        detail: alloc::format!("invalid JSON on left of =: {e}"),
1042    })?;
1043    let rhs_doc = parse(rhs_text).map_err(|e| EvalError::TypeMismatch {
1044        detail: alloc::format!("invalid JSON on right of =: {e}"),
1045    })?;
1046    Ok(json_eq(&lhs_doc, &rhs_doc))
1047}
1048
1049fn json_contains(lhs: &JsonValue, rhs: &JsonValue) -> bool {
1050    match (lhs, rhs) {
1051        (JsonValue::Object(l), JsonValue::Object(r)) => r
1052            .iter()
1053            .all(|(rk, rv)| l.iter().any(|(lk, lv)| lk == rk && json_contains(lv, rv))),
1054        (JsonValue::Array(l), JsonValue::Array(r)) => {
1055            r.iter().all(|rv| l.iter().any(|lv| json_contains(lv, rv)))
1056        }
1057        _ => json_eq(lhs, rhs),
1058    }
1059}
1060
1061fn json_eq(a: &JsonValue, b: &JsonValue) -> bool {
1062    match (a, b) {
1063        (JsonValue::Null, JsonValue::Null) => true,
1064        (JsonValue::Bool(x), JsonValue::Bool(y)) => x == y,
1065        (JsonValue::String(x), JsonValue::String(y)) => x == y,
1066        // PG compares jsonb numbers by value, not by lexeme, so
1067        // `1` == `1.0` == `1e0` and `1.50` == `1.5`. Normalise both to
1068        // an exact numeric-equality key (canonical decimal with trailing
1069        // zeros stripped) rather than a lossy f64 subtraction.
1070        (
1071            JsonValue::Number(_) | JsonValue::NumberText(_),
1072            JsonValue::Number(_) | JsonValue::NumberText(_),
1073        ) => json_number_key(a) == json_number_key(b),
1074        (JsonValue::Array(x), JsonValue::Array(y)) => {
1075            x.len() == y.len() && x.iter().zip(y).all(|(a, b)| json_eq(a, b))
1076        }
1077        (JsonValue::Object(x), JsonValue::Object(y)) => {
1078            x.len() == y.len()
1079                && x.iter()
1080                    .all(|(k, v)| y.iter().any(|(k2, v2)| k == k2 && json_eq(v, v2)))
1081        }
1082        _ => false,
1083    }
1084}
1085
1086/// Normalise a JSON number to a key where numerically-equal values share
1087/// one string (`1` / `1.0` / `1e0` → `1`, `1.50` → `1.5`), so jsonb `=`
1088/// and containment compare numbers by value like PG — exactly, without
1089/// f64 rounding.
1090fn numeric_eq_key(lexeme: &str) -> String {
1091    let c = canon_json_number(lexeme);
1092    if c.contains('.') {
1093        c.trim_end_matches('0').trim_end_matches('.').to_string()
1094    } else {
1095        c.into_owned()
1096    }
1097}
1098
1099fn json_number_key(v: &JsonValue) -> Option<String> {
1100    match v {
1101        JsonValue::NumberText(s) => Some(numeric_eq_key(s)),
1102        JsonValue::Number(x) => Some(numeric_eq_key(&alloc::format!("{x}"))),
1103        _ => None,
1104    }
1105}
1106
1107/// Parse PG's text-array literal `'{a,b,c}'` into a Vec<String>.
1108/// Whitespace around elements is trimmed; quoted elements (`"x,y"`)
1109/// preserve embedded commas (minimal support — full PG array
1110/// escaping is OOS).
1111fn parse_text_array(s: &str) -> Result<Vec<String>, EvalError> {
1112    let trimmed = s.trim();
1113    let inner = if let Some(stripped) = trimmed.strip_prefix('{').and_then(|s| s.strip_suffix('}'))
1114    {
1115        stripped
1116    } else {
1117        return Err(EvalError::TypeMismatch {
1118            detail: alloc::format!("path walk: expected PG array literal `{{…}}`, got {s:?}"),
1119        });
1120    };
1121    if inner.trim().is_empty() {
1122        return Ok(Vec::new());
1123    }
1124    let mut out = Vec::new();
1125    let mut cur = String::new();
1126    let mut in_quotes = false;
1127    let mut chars = inner.chars().peekable();
1128    while let Some(c) = chars.next() {
1129        match c {
1130            '"' => in_quotes = !in_quotes,
1131            ',' if !in_quotes => {
1132                out.push(cur.trim().to_string());
1133                cur = String::new();
1134            }
1135            '\\' => {
1136                if let Some(&next) = chars.peek() {
1137                    cur.push(next);
1138                    chars.next();
1139                }
1140            }
1141            _ => cur.push(c),
1142        }
1143    }
1144    out.push(cur.trim().to_string());
1145    Ok(out)
1146}
1147
1148/// PG `json -> key` / `json ->> key`. `lhs` must be JSON or TEXT
1149/// containing JSON. `rhs` is either a TEXT key (object access) or
1150/// an INT index (array access). `as_text=true` for `->>` (returns
1151/// `Value::Text`); `false` for `->` (returns `Value::Json`).
1152/// v7.38.8 — validate a document only when it is not already known to be
1153/// one.
1154///
1155/// `Value::Json` reaches an accessor from a json/jsonb column or from a
1156/// cast, and both of those validate at their own boundary (the column
1157/// one only since v7.38.8 — before that a jsonb column could hold
1158/// `{bad`, and this is the guarantee that made re-validating here look
1159/// necessary). `Value::Text` is SPG's own leniency: PG has no
1160/// `text -> text` operator at all, so a text operand has passed through
1161/// no boundary and is checked here.
1162///
1163/// The cost this removes is the whole document, per row, per accessor:
1164/// the parse built a `JsonValue` tree — a Vec plus a String per member —
1165/// and threw it away, and the verbatim scan below did the real work. On
1166/// a four-member document that was 333 ns a row against PG's 7.5.
1167fn validate_unless_known_json(lhs: &Value, src: &str, what: &str) -> Result<(), EvalError> {
1168    if matches!(lhs, Value::Json(_)) {
1169        return Ok(());
1170    }
1171    parse(src).map(|_| ()).map_err(|e| EvalError::TypeMismatch {
1172        detail: alloc::format!("invalid JSON for {what}: {e}"),
1173    })
1174}
1175
1176pub fn path_get(lhs: &Value, rhs: &Value, as_text: bool) -> Result<Value<'static>, EvalError> {
1177    let src = match lhs {
1178        Value::Json(s) | Value::Text(s) => s.as_ref(),
1179        Value::Null => return Ok(Value::Null),
1180        other => {
1181            return Err(EvalError::TypeMismatch {
1182                detail: alloc::format!(
1183                    "JSON path operator: left side must be JSON or TEXT, got {}",
1184                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
1185                ),
1186            });
1187        }
1188    };
1189    // Validate the document (an invalid one still errors), then extract the
1190    // located value's VERBATIM source text — PG never re-serializes here.
1191    validate_unless_known_json(lhs, src, "path access")?;
1192    let located = match rhs {
1193        Value::Text(k) => locate_member(src, k),
1194        Value::Int(idx) => locate_index(src, i64::from(*idx)),
1195        Value::BigInt(idx) => locate_index(src, *idx),
1196        Value::Null => return Ok(Value::Null),
1197        _ => None,
1198    };
1199    Ok(located.map_or(Value::Null, |slice| {
1200        verbatim_accessor_result(slice, as_text)
1201    }))
1202}
1203
1204// ---- Tiny recursive-descent JSON parser ----
1205
1206#[derive(Debug)]
1207pub enum ParseError {
1208    Unexpected(char, usize),
1209    Truncated,
1210    InvalidEscape(usize),
1211    InvalidNumber(usize),
1212}
1213
1214impl core::fmt::Display for ParseError {
1215    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1216        match self {
1217            Self::Unexpected(c, p) => write!(f, "unexpected {c:?} at offset {p}"),
1218            Self::Truncated => f.write_str("unexpected end of JSON input"),
1219            Self::InvalidEscape(p) => write!(f, "invalid string escape at offset {p}"),
1220            Self::InvalidNumber(p) => write!(f, "invalid number at offset {p}"),
1221        }
1222    }
1223}
1224
1225pub fn parse(src: &str) -> Result<JsonValue, ParseError> {
1226    let bytes = src.as_bytes();
1227    let mut p = 0;
1228    skip_ws(bytes, &mut p);
1229    let value = parse_value(bytes, &mut p)?;
1230    skip_ws(bytes, &mut p);
1231    if p != bytes.len() {
1232        return Err(ParseError::Unexpected(bytes[p] as char, p));
1233    }
1234    Ok(value)
1235}
1236
1237/// v7.38 (read01 P6.24) — PG's `jsonb` total order (ORDER BY / DISTINCT /
1238/// btree). First by type rank `Null < String < Number < Boolean < Array <
1239/// Object`; then within a type: strings by content, numbers numerically,
1240/// booleans `false < true`, arrays by length then element-wise, objects by
1241/// pair-count then key/value pairwise (keys in canonical stored order).
1242/// Mirrors the observable behaviour of `jsonb.c`'s `compareJsonbContainers`.
1243#[must_use]
1244pub fn jsonb_compare(a: &JsonValue, b: &JsonValue) -> core::cmp::Ordering {
1245    use core::cmp::Ordering;
1246    fn rank(v: &JsonValue) -> u8 {
1247        match v {
1248            JsonValue::Null => 0,
1249            JsonValue::String(_) => 1,
1250            JsonValue::Number(_) | JsonValue::NumberText(_) => 2,
1251            JsonValue::Bool(_) => 3,
1252            JsonValue::Array(_) => 4,
1253            JsonValue::Object(_) => 5,
1254        }
1255    }
1256    fn num(v: &JsonValue) -> f64 {
1257        match v {
1258            JsonValue::Number(x) => *x,
1259            JsonValue::NumberText(s) => s.parse::<f64>().unwrap_or(0.0),
1260            _ => 0.0,
1261        }
1262    }
1263    let (ra, rb) = (rank(a), rank(b));
1264    if ra != rb {
1265        return ra.cmp(&rb);
1266    }
1267    match (a, b) {
1268        (JsonValue::String(x), JsonValue::String(y)) => x.cmp(y),
1269        (JsonValue::Bool(x), JsonValue::Bool(y)) => x.cmp(y),
1270        (
1271            JsonValue::Number(_) | JsonValue::NumberText(_),
1272            JsonValue::Number(_) | JsonValue::NumberText(_),
1273        ) => num(a).partial_cmp(&num(b)).unwrap_or(Ordering::Equal),
1274        (JsonValue::Array(x), JsonValue::Array(y)) => x.len().cmp(&y.len()).then_with(|| {
1275            x.iter()
1276                .zip(y.iter())
1277                .map(|(ea, eb)| jsonb_compare(ea, eb))
1278                .find(|o| *o != Ordering::Equal)
1279                .unwrap_or(Ordering::Equal)
1280        }),
1281        (JsonValue::Object(x), JsonValue::Object(y)) => x.len().cmp(&y.len()).then_with(|| {
1282            x.iter()
1283                .zip(y.iter())
1284                .map(|((ka, va), (kb, vb))| ka.cmp(kb).then_with(|| jsonb_compare(va, vb)))
1285                .find(|o| *o != Ordering::Equal)
1286                .unwrap_or(Ordering::Equal)
1287        }),
1288        // Same rank, both Null (or the impossible cross-variant) → equal.
1289        _ => Ordering::Equal,
1290    }
1291}
1292
1293fn skip_ws(bytes: &[u8], p: &mut usize) {
1294    while *p < bytes.len() && matches!(bytes[*p], b' ' | b'\t' | b'\n' | b'\r') {
1295        *p += 1;
1296    }
1297}
1298
1299fn parse_value(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
1300    skip_ws(bytes, p);
1301    if *p >= bytes.len() {
1302        return Err(ParseError::Truncated);
1303    }
1304    match bytes[*p] {
1305        b'{' => parse_object(bytes, p),
1306        b'[' => parse_array(bytes, p),
1307        b'"' => parse_string(bytes, p).map(JsonValue::String),
1308        b't' | b'f' => parse_bool(bytes, p),
1309        b'n' => parse_null(bytes, p),
1310        b'-' | b'0'..=b'9' => parse_number(bytes, p),
1311        c => Err(ParseError::Unexpected(c as char, *p)),
1312    }
1313}
1314
1315fn parse_object(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
1316    debug_assert_eq!(bytes[*p], b'{');
1317    *p += 1;
1318    let mut entries = Vec::new();
1319    skip_ws(bytes, p);
1320    if *p < bytes.len() && bytes[*p] == b'}' {
1321        *p += 1;
1322        return Ok(JsonValue::Object(entries));
1323    }
1324    loop {
1325        skip_ws(bytes, p);
1326        if *p >= bytes.len() || bytes[*p] != b'"' {
1327            return Err(ParseError::Unexpected(
1328                bytes.get(*p).copied().unwrap_or(0) as char,
1329                *p,
1330            ));
1331        }
1332        let key = parse_string(bytes, p)?;
1333        skip_ws(bytes, p);
1334        if *p >= bytes.len() || bytes[*p] != b':' {
1335            return Err(ParseError::Unexpected(
1336                bytes.get(*p).copied().unwrap_or(0) as char,
1337                *p,
1338            ));
1339        }
1340        *p += 1;
1341        let value = parse_value(bytes, p)?;
1342        entries.push((key, value));
1343        skip_ws(bytes, p);
1344        if *p >= bytes.len() {
1345            return Err(ParseError::Truncated);
1346        }
1347        match bytes[*p] {
1348            b',' => {
1349                *p += 1;
1350                continue;
1351            }
1352            b'}' => {
1353                *p += 1;
1354                return Ok(JsonValue::Object(entries));
1355            }
1356            c => return Err(ParseError::Unexpected(c as char, *p)),
1357        }
1358    }
1359}
1360
1361fn parse_array(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
1362    debug_assert_eq!(bytes[*p], b'[');
1363    *p += 1;
1364    let mut items = Vec::new();
1365    skip_ws(bytes, p);
1366    if *p < bytes.len() && bytes[*p] == b']' {
1367        *p += 1;
1368        return Ok(JsonValue::Array(items));
1369    }
1370    loop {
1371        items.push(parse_value(bytes, p)?);
1372        skip_ws(bytes, p);
1373        if *p >= bytes.len() {
1374            return Err(ParseError::Truncated);
1375        }
1376        match bytes[*p] {
1377            b',' => {
1378                *p += 1;
1379                continue;
1380            }
1381            b']' => {
1382                *p += 1;
1383                return Ok(JsonValue::Array(items));
1384            }
1385            c => return Err(ParseError::Unexpected(c as char, *p)),
1386        }
1387    }
1388}
1389
1390fn parse_string(bytes: &[u8], p: &mut usize) -> Result<String, ParseError> {
1391    debug_assert_eq!(bytes[*p], b'"');
1392    *p += 1;
1393    let mut out = String::new();
1394    while *p < bytes.len() {
1395        match bytes[*p] {
1396            b'"' => {
1397                *p += 1;
1398                return Ok(out);
1399            }
1400            b'\\' => {
1401                let start = *p;
1402                *p += 1;
1403                if *p >= bytes.len() {
1404                    return Err(ParseError::Truncated);
1405                }
1406                match bytes[*p] {
1407                    b'"' => {
1408                        out.push('"');
1409                        *p += 1;
1410                    }
1411                    b'\\' => {
1412                        out.push('\\');
1413                        *p += 1;
1414                    }
1415                    b'/' => {
1416                        out.push('/');
1417                        *p += 1;
1418                    }
1419                    b'b' => {
1420                        out.push('\u{08}');
1421                        *p += 1;
1422                    }
1423                    b'f' => {
1424                        out.push('\u{0c}');
1425                        *p += 1;
1426                    }
1427                    b'n' => {
1428                        out.push('\n');
1429                        *p += 1;
1430                    }
1431                    b'r' => {
1432                        out.push('\r');
1433                        *p += 1;
1434                    }
1435                    b't' => {
1436                        out.push('\t');
1437                        *p += 1;
1438                    }
1439                    b'u' => {
1440                        if *p + 5 > bytes.len() {
1441                            return Err(ParseError::Truncated);
1442                        }
1443                        let hex = &bytes[*p + 1..*p + 5];
1444                        let n = u32::from_str_radix(
1445                            core::str::from_utf8(hex)
1446                                .map_err(|_| ParseError::InvalidEscape(start))?,
1447                            16,
1448                        )
1449                        .map_err(|_| ParseError::InvalidEscape(start))?;
1450                        out.push(char::from_u32(n).ok_or(ParseError::InvalidEscape(start))?);
1451                        *p += 5;
1452                    }
1453                    _ => return Err(ParseError::InvalidEscape(start)),
1454                }
1455            }
1456            c if c < 0x20 => return Err(ParseError::Unexpected(c as char, *p)),
1457            _ => {
1458                // Multi-byte UTF-8: consume the whole codepoint.
1459                let s = core::str::from_utf8(&bytes[*p..])
1460                    .map_err(|_| ParseError::Unexpected(bytes[*p] as char, *p))?;
1461                let c = s.chars().next().unwrap();
1462                out.push(c);
1463                *p += c.len_utf8();
1464            }
1465        }
1466    }
1467    Err(ParseError::Truncated)
1468}
1469
1470fn parse_bool(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
1471    if bytes[*p..].starts_with(b"true") {
1472        *p += 4;
1473        Ok(JsonValue::Bool(true))
1474    } else if bytes[*p..].starts_with(b"false") {
1475        *p += 5;
1476        Ok(JsonValue::Bool(false))
1477    } else {
1478        Err(ParseError::Unexpected(bytes[*p] as char, *p))
1479    }
1480}
1481
1482fn parse_null(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
1483    if bytes[*p..].starts_with(b"null") {
1484        *p += 4;
1485        Ok(JsonValue::Null)
1486    } else {
1487        Err(ParseError::Unexpected(bytes[*p] as char, *p))
1488    }
1489}
1490
1491fn parse_number(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
1492    let start = *p;
1493    if bytes[*p] == b'-' {
1494        *p += 1;
1495    }
1496    while *p < bytes.len() && bytes[*p].is_ascii_digit() {
1497        *p += 1;
1498    }
1499    if *p < bytes.len() && bytes[*p] == b'.' {
1500        *p += 1;
1501        while *p < bytes.len() && bytes[*p].is_ascii_digit() {
1502            *p += 1;
1503        }
1504    }
1505    if *p < bytes.len() && matches!(bytes[*p], b'e' | b'E') {
1506        *p += 1;
1507        if *p < bytes.len() && matches!(bytes[*p], b'+' | b'-') {
1508            *p += 1;
1509        }
1510        while *p < bytes.len() && bytes[*p].is_ascii_digit() {
1511            *p += 1;
1512        }
1513    }
1514    let text = core::str::from_utf8(&bytes[start..*p])
1515        .map_err(|_| ParseError::InvalidNumber(start))?
1516        .to_string();
1517    // Validate the parse so the wire side can trust the value.
1518    if text.parse::<f64>().is_err() {
1519        return Err(ParseError::InvalidNumber(start));
1520    }
1521    Ok(JsonValue::NumberText(text))
1522}
1523
1524// ─── v7.17.0 Phase 3.9 — minimal JSONPath subset for jsonb_path_query ───
1525//
1526// Supported path syntax (PG-flavoured JSONPath subset):
1527//   * `$` — document root (required leading segment)
1528//   * `.field` — object field access (bare ident only; quoted form
1529//                `."field with space"` accepted)
1530//   * `[N]` — array index (non-negative integer; negative indices
1531//             out of v7.17 scope)
1532//   * `[*]` — array wildcard (fan-out — each element matched separately)
1533//   * Chained: `$.a.b[0].c[*].name`
1534//
1535// NOT supported (errors clearly):
1536//   * Filter expressions `? (@.price > 100)`
1537//   * Range slices `[1:3]`
1538//   * Recursive descent `..field`
1539//   * Functions `keyvalue()`, `size()`, etc.
1540//   * Path variables `$varname`
1541
1542/// v7.39 (jsonpath depth) — an array subscript bound: a plain index or
1543/// `last - N` (offset back from the final element).
1544#[derive(Debug, Clone, Copy)]
1545enum IdxBound {
1546    At(usize),
1547    FromLast(usize),
1548}
1549
1550impl IdxBound {
1551    /// Resolve against an array of `len` items; `None` = out of range.
1552    fn resolve(self, len: usize) -> Option<usize> {
1553        match self {
1554            Self::At(n) => (n < len).then_some(n),
1555            Self::FromLast(off) => len.checked_sub(1 + off),
1556        }
1557    }
1558}
1559
1560/// v7.39 (jsonpath depth) — numeric item methods.
1561#[derive(Debug, Clone, Copy)]
1562enum NumMethod {
1563    Abs,
1564    Floor,
1565    Ceiling,
1566    Double,
1567}
1568
1569#[derive(Debug, Clone)]
1570enum PathStep {
1571    Field(String),
1572    Index(IdxBound),
1573    Wildcard,
1574    // v7.38 (read01, T8) — SQL/JSON path filter sublanguage.
1575    /// `[N to M]` — an inclusive array-index range (bounds may be `last - k`).
1576    Range(IdxBound, IdxBound),
1577    /// `? (<predicate>)` — keep the current items whose accessor expression
1578    /// satisfies the (possibly `&&`/`||`-combined) predicate.
1579    Filter(FilterExpr),
1580    /// `.size()` — the length of an array (or 1 for a scalar, per PG lax mode).
1581    Size,
1582    /// `.type()` — the JSON type name of the current item.
1583    TypeOf,
1584    /// v7.39 — `.abs()` / `.floor()` / `.ceiling()` / `.double()`.
1585    Num(NumMethod),
1586    /// v7.39 — `.**` recursive descent: the item plus every descendant.
1587    RecursiveAll,
1588}
1589
1590#[derive(Debug, Clone)]
1591struct FilterPred {
1592    /// Accessor after `@`: empty = `@` itself, `["p"]` = `@.p`, etc.
1593    path: Vec<String>,
1594    op: FilterOp,
1595    val: FilterVal,
1596    /// v7.39 (read01 jsonpath.c) — `like_regex ... flag "izsq..."`.
1597    /// Only `i` affects evaluation today; the string round-trips
1598    /// through the canonical printer.
1599    regex_flags: Option<String>,
1600}
1601
1602/// A filter predicate tree — a single comparison or a `&&`/`||` combination.
1603#[derive(Debug, Clone)]
1604enum FilterExpr {
1605    Cmp(FilterPred),
1606    And(alloc::boxed::Box<FilterExpr>, alloc::boxed::Box<FilterExpr>),
1607    Or(alloc::boxed::Box<FilterExpr>, alloc::boxed::Box<FilterExpr>),
1608}
1609
1610#[derive(Debug, Clone, Copy)]
1611enum FilterOp {
1612    Gt,
1613    Lt,
1614    Ge,
1615    Le,
1616    Eq,
1617    Ne,
1618    /// v7.39 — `starts with "prefix"` (string operand only).
1619    StartsWith,
1620    /// v7.39 — `like_regex "pattern"` (POSIX search, unanchored).
1621    LikeRegex,
1622}
1623
1624#[derive(Debug, Clone)]
1625enum FilterVal {
1626    Num(f64),
1627    Str(String),
1628    Bool(bool),
1629    /// v7.39 — the `null` literal (`@ == null` matches JSON null only).
1630    Null,
1631    /// v7.39 — a `$name` variable reference, resolved from the `vars`
1632    /// document at evaluation time.
1633    Var(String),
1634}
1635
1636/// v7.39 (round 235) — parse a jsonpath, returning its MODE alongside the
1637/// steps. Before this round the leading `strict` / `lax` word was stripped
1638/// and thrown away, so every path evaluated with (incomplete) lax
1639/// semantics and `strict` was silently a no-op.
1640fn parse_jsonpath_mode(p: &str) -> Result<(bool, Vec<PathStep>), EvalError> {
1641    let trimmed = p.trim_start();
1642    let (strict, p) = if let Some(rest) = trimmed.strip_prefix("strict") {
1643        (true, rest.trim_start())
1644    } else if let Some(rest) = trimmed.strip_prefix("lax") {
1645        (false, rest.trim_start())
1646    } else {
1647        (false, trimmed)
1648    };
1649    let chars: Vec<char> = p.chars().collect();
1650    let mut i = 0;
1651    if i >= chars.len() || chars[i] != '$' {
1652        return Err(EvalError::TypeMismatch {
1653            detail: alloc::format!("jsonpath must start with '$', got {p:?}"),
1654        });
1655    }
1656    i += 1;
1657    let mut steps: Vec<PathStep> = Vec::new();
1658    while i < chars.len() {
1659        match chars[i] {
1660            '.' => {
1661                i += 1;
1662                // v7.39 — `.**` recursive descent (visits the item and
1663                // every descendant; a following `.field` then selects).
1664                if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
1665                    i += 2;
1666                    steps.push(PathStep::RecursiveAll);
1667                    continue;
1668                }
1669                if i < chars.len() && chars[i] == '"' {
1670                    i += 1;
1671                    let start = i;
1672                    while i < chars.len() && chars[i] != '"' {
1673                        i += 1;
1674                    }
1675                    if i >= chars.len() {
1676                        return Err(EvalError::TypeMismatch {
1677                            detail: "jsonpath: unterminated quoted field".into(),
1678                        });
1679                    }
1680                    steps.push(PathStep::Field(chars[start..i].iter().collect()));
1681                    i += 1;
1682                } else {
1683                    let start = i;
1684                    while i < chars.len()
1685                        && chars[i] != '.'
1686                        && chars[i] != '['
1687                        && chars[i] != '('
1688                        && !chars[i].is_whitespace()
1689                    {
1690                        i += 1;
1691                    }
1692                    if start == i {
1693                        return Err(EvalError::TypeMismatch {
1694                            detail: "jsonpath: missing field name after '.'".into(),
1695                        });
1696                    }
1697                    let name: String = chars[start..i].iter().collect();
1698                    // v7.38 (read01, T8) — `.size()` / `.type()` item methods.
1699                    if i < chars.len() && chars[i] == '(' {
1700                        i += 1;
1701                        while i < chars.len() && chars[i] != ')' {
1702                            i += 1;
1703                        }
1704                        if i >= chars.len() {
1705                            return Err(EvalError::TypeMismatch {
1706                                detail: "jsonpath: unterminated method call".into(),
1707                            });
1708                        }
1709                        i += 1; // )
1710                        match name.as_str() {
1711                            "size" => steps.push(PathStep::Size),
1712                            "type" => steps.push(PathStep::TypeOf),
1713                            // v7.39 — numeric item methods.
1714                            "abs" => steps.push(PathStep::Num(NumMethod::Abs)),
1715                            "floor" => steps.push(PathStep::Num(NumMethod::Floor)),
1716                            "ceiling" => steps.push(PathStep::Num(NumMethod::Ceiling)),
1717                            "double" => steps.push(PathStep::Num(NumMethod::Double)),
1718                            other => {
1719                                return Err(EvalError::TypeMismatch {
1720                                    detail: alloc::format!(
1721                                        "jsonpath: unsupported method .{other}()"
1722                                    ),
1723                                });
1724                            }
1725                        }
1726                    } else {
1727                        steps.push(PathStep::Field(name));
1728                    }
1729                }
1730            }
1731            '?' => {
1732                // v7.38 (read01, T8) — filter `? ( @... <op> <literal> )`.
1733                i += 1;
1734                let (pred, ni) = parse_filter_pred(&chars, i)?;
1735                i = ni;
1736                steps.push(PathStep::Filter(pred));
1737            }
1738            '[' => {
1739                i += 1;
1740                if i < chars.len() && chars[i] == '*' {
1741                    i += 1;
1742                    if i >= chars.len() || chars[i] != ']' {
1743                        return Err(EvalError::TypeMismatch {
1744                            detail: "jsonpath: expected ']' after '[*'".into(),
1745                        });
1746                    }
1747                    i += 1;
1748                    steps.push(PathStep::Wildcard);
1749                } else {
1750                    // v7.39 — a bound is `N` or `last[ - K]`.
1751                    let mut parse_bound = |i: &mut usize| -> Result<IdxBound, EvalError> {
1752                        jp_skip_ws(&chars, i);
1753                        if chars[*i..].starts_with(&['l', 'a', 's', 't']) {
1754                            *i += 4;
1755                            jp_skip_ws(&chars, i);
1756                            if *i < chars.len() && chars[*i] == '-' {
1757                                *i += 1;
1758                                jp_skip_ws(&chars, i);
1759                                let s = *i;
1760                                while *i < chars.len() && chars[*i].is_ascii_digit() {
1761                                    *i += 1;
1762                                }
1763                                let off: usize =
1764                                    chars[s..*i].iter().collect::<String>().parse().map_err(
1765                                        |_| EvalError::TypeMismatch {
1766                                            detail: "jsonpath: invalid `last - N` offset".into(),
1767                                        },
1768                                    )?;
1769                                return Ok(IdxBound::FromLast(off));
1770                            }
1771                            return Ok(IdxBound::FromLast(0));
1772                        }
1773                        let s = *i;
1774                        while *i < chars.len() && chars[*i].is_ascii_digit() {
1775                            *i += 1;
1776                        }
1777                        if s == *i {
1778                            return Err(EvalError::TypeMismatch {
1779                                detail: "jsonpath: expected `N`, `last[ - K]` or `*` subscript"
1780                                    .into(),
1781                            });
1782                        }
1783                        Ok(IdxBound::At(
1784                            chars[s..*i]
1785                                .iter()
1786                                .collect::<String>()
1787                                .parse()
1788                                .map_err(|_| EvalError::TypeMismatch {
1789                                    detail: "jsonpath: invalid array index".into(),
1790                                })?,
1791                        ))
1792                    };
1793                    let idx = parse_bound(&mut i)?;
1794                    // v7.38 (read01, T8) — `[N to M]` inclusive range.
1795                    while i < chars.len() && chars[i].is_whitespace() {
1796                        i += 1;
1797                    }
1798                    if i + 1 < chars.len() && chars[i] == 't' && chars[i + 1] == 'o' {
1799                        i += 2;
1800                        let hi = parse_bound(&mut i)?;
1801                        while i < chars.len() && chars[i].is_whitespace() {
1802                            i += 1;
1803                        }
1804                        if i >= chars.len() || chars[i] != ']' {
1805                            return Err(EvalError::TypeMismatch {
1806                                detail: "jsonpath: expected ']' after range".into(),
1807                            });
1808                        }
1809                        i += 1;
1810                        steps.push(PathStep::Range(idx, hi));
1811                    } else {
1812                        if i >= chars.len() || chars[i] != ']' {
1813                            return Err(EvalError::TypeMismatch {
1814                                detail: "jsonpath: expected ']' after array index".into(),
1815                            });
1816                        }
1817                        i += 1;
1818                        steps.push(PathStep::Index(idx));
1819                    }
1820                }
1821            }
1822            c if c.is_whitespace() => {
1823                i += 1;
1824            }
1825            c => {
1826                return Err(EvalError::TypeMismatch {
1827                    detail: alloc::format!(
1828                        "jsonpath: unexpected char '{c}' (supports `$.field`, `[N]`, `[N to M]`, `[*]`, `? (...)`, `.size()`, `.type()`)"
1829                    ),
1830                });
1831            }
1832        }
1833    }
1834    Ok((strict, steps))
1835}
1836
1837/// Lax-mode convenience for the callers that only need the steps.
1838fn parse_jsonpath(p: &str) -> Result<Vec<PathStep>, EvalError> {
1839    parse_jsonpath_mode(p).map(|(_, steps)| steps)
1840}
1841
1842fn jp_skip_ws(chars: &[char], i: &mut usize) {
1843    while *i < chars.len() && chars[*i].is_whitespace() {
1844        *i += 1;
1845    }
1846}
1847
1848/// v7.38 (read01, T8) — parse a filter body `( <expr> )` starting just after
1849/// the `?`, where `<expr>` is a comparison of a `@` accessor against a literal,
1850/// optionally combined with `&&` / `||` and grouped with parentheses.
1851fn parse_filter_pred(chars: &[char], mut i: usize) -> Result<(FilterExpr, usize), EvalError> {
1852    let err = |m: &str| EvalError::TypeMismatch {
1853        detail: alloc::format!("jsonpath filter: {m}"),
1854    };
1855    jp_skip_ws(chars, &mut i);
1856    if i >= chars.len() || chars[i] != '(' {
1857        return Err(err("expected '(' after '?'"));
1858    }
1859    i += 1;
1860    let (expr, ni) = parse_filter_or(chars, i)?;
1861    i = ni;
1862    jp_skip_ws(chars, &mut i);
1863    if i >= chars.len() || chars[i] != ')' {
1864        return Err(err("expected ')' to close the filter"));
1865    }
1866    i += 1;
1867    Ok((expr, i))
1868}
1869
1870/// `<and> ( '||' <and> )*`
1871fn parse_filter_or(chars: &[char], i: usize) -> Result<(FilterExpr, usize), EvalError> {
1872    let (mut left, mut i) = parse_filter_and(chars, i)?;
1873    loop {
1874        jp_skip_ws(chars, &mut i);
1875        if i + 1 < chars.len() && chars[i] == '|' && chars[i + 1] == '|' {
1876            i += 2;
1877            let (right, ni) = parse_filter_and(chars, i)?;
1878            i = ni;
1879            left = FilterExpr::Or(alloc::boxed::Box::new(left), alloc::boxed::Box::new(right));
1880        } else {
1881            return Ok((left, i));
1882        }
1883    }
1884}
1885
1886/// `<atom> ( '&&' <atom> )*`
1887fn parse_filter_and(chars: &[char], i: usize) -> Result<(FilterExpr, usize), EvalError> {
1888    let (mut left, mut i) = parse_filter_atom(chars, i)?;
1889    loop {
1890        jp_skip_ws(chars, &mut i);
1891        if i + 1 < chars.len() && chars[i] == '&' && chars[i + 1] == '&' {
1892            i += 2;
1893            let (right, ni) = parse_filter_atom(chars, i)?;
1894            i = ni;
1895            left = FilterExpr::And(alloc::boxed::Box::new(left), alloc::boxed::Box::new(right));
1896        } else {
1897            return Ok((left, i));
1898        }
1899    }
1900}
1901
1902/// `'(' <or> ')'` | `@[.field]* <op> <literal>`
1903fn parse_filter_atom(chars: &[char], mut i: usize) -> Result<(FilterExpr, usize), EvalError> {
1904    let err = |m: &str| EvalError::TypeMismatch {
1905        detail: alloc::format!("jsonpath filter: {m}"),
1906    };
1907    jp_skip_ws(chars, &mut i);
1908    if i < chars.len() && chars[i] == '(' {
1909        i += 1;
1910        let (expr, ni) = parse_filter_or(chars, i)?;
1911        i = ni;
1912        jp_skip_ws(chars, &mut i);
1913        if i >= chars.len() || chars[i] != ')' {
1914            return Err(err("expected ')' in grouped predicate"));
1915        }
1916        i += 1;
1917        return Ok((expr, i));
1918    }
1919    if i >= chars.len() || chars[i] != '@' {
1920        return Err(err("only `@`-based predicates are supported"));
1921    }
1922    i += 1;
1923    let mut path: Vec<String> = Vec::new();
1924    while i < chars.len() && chars[i] == '.' {
1925        i += 1;
1926        let start = i;
1927        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
1928            i += 1;
1929        }
1930        path.push(chars[start..i].iter().collect());
1931    }
1932    let (op, val, regex_flags, ni) = parse_cmp_and_literal(chars, i)?;
1933    i = ni;
1934    Ok((
1935        FilterExpr::Cmp(FilterPred {
1936            path,
1937            op,
1938            val,
1939            regex_flags,
1940        }),
1941        i,
1942    ))
1943}
1944
1945/// Parse a comparison operator and its literal operand (`> 8`, `== "b"`,
1946/// `>= 3`) starting at `i`; returns the op, the literal and the new index.
1947/// Shared by the `? (...)` filter parser and the top-level `@@` predicate.
1948fn parse_cmp_and_literal(
1949    chars: &[char],
1950    mut i: usize,
1951) -> Result<(FilterOp, FilterVal, Option<String>, usize), EvalError> {
1952    let err = |m: &str| EvalError::TypeMismatch {
1953        detail: alloc::format!("jsonpath predicate: {m}"),
1954    };
1955    while i < chars.len() && chars[i].is_whitespace() {
1956        i += 1;
1957    }
1958    let kw = |i: usize, w: &str| -> bool {
1959        let wc: Vec<char> = w.chars().collect();
1960        chars[i..].starts_with(&wc)
1961    };
1962    let op = if i + 1 < chars.len() && chars[i] == '>' && chars[i + 1] == '=' {
1963        i += 2;
1964        FilterOp::Ge
1965    } else if i + 1 < chars.len() && chars[i] == '<' && chars[i + 1] == '=' {
1966        i += 2;
1967        FilterOp::Le
1968    } else if i + 1 < chars.len() && chars[i] == '=' && chars[i + 1] == '=' {
1969        i += 2;
1970        FilterOp::Eq
1971    } else if i + 1 < chars.len() && chars[i] == '!' && chars[i + 1] == '=' {
1972        i += 2;
1973        FilterOp::Ne
1974    } else if i < chars.len() && chars[i] == '>' {
1975        i += 1;
1976        FilterOp::Gt
1977    } else if i < chars.len() && chars[i] == '<' {
1978        i += 1;
1979        FilterOp::Lt
1980    // v7.39 — `starts with "prefix"` / `like_regex "pattern"`.
1981    } else if kw(i, "starts") {
1982        i += 6;
1983        while i < chars.len() && chars[i].is_whitespace() {
1984            i += 1;
1985        }
1986        if !kw(i, "with") {
1987            return Err(err("expected `with` after `starts`"));
1988        }
1989        i += 4;
1990        FilterOp::StartsWith
1991    } else if kw(i, "like_regex") {
1992        i += 10;
1993        FilterOp::LikeRegex
1994    } else {
1995        return Err(err(
1996            "expected a comparison operator (> < >= <= == != starts with like_regex)",
1997        ));
1998    };
1999    while i < chars.len() && chars[i].is_whitespace() {
2000        i += 1;
2001    }
2002    let val = if i < chars.len() && chars[i] == '"' {
2003        i += 1;
2004        let start = i;
2005        while i < chars.len() && chars[i] != '"' {
2006            i += 1;
2007        }
2008        if i >= chars.len() {
2009            return Err(err("unterminated string literal"));
2010        }
2011        let s: String = chars[start..i].iter().collect();
2012        i += 1;
2013        FilterVal::Str(s)
2014    } else if chars[i..].starts_with(&['t', 'r', 'u', 'e']) {
2015        i += 4;
2016        FilterVal::Bool(true)
2017    } else if chars[i..].starts_with(&['f', 'a', 'l', 's', 'e']) {
2018        i += 5;
2019        FilterVal::Bool(false)
2020    // v7.39 — `null` literal and `$name` variable references.
2021    } else if chars[i..].starts_with(&['n', 'u', 'l', 'l']) {
2022        i += 4;
2023        FilterVal::Null
2024    } else if i < chars.len() && chars[i] == '$' {
2025        i += 1;
2026        let start = i;
2027        while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
2028            i += 1;
2029        }
2030        if start == i {
2031            return Err(err("expected a variable name after '$'"));
2032        }
2033        FilterVal::Var(chars[start..i].iter().collect())
2034    } else {
2035        let start = i;
2036        if i < chars.len() && (chars[i] == '-' || chars[i] == '+') {
2037            i += 1;
2038        }
2039        while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
2040            i += 1;
2041        }
2042        let num: f64 = chars[start..i]
2043            .iter()
2044            .collect::<String>()
2045            .parse()
2046            .map_err(|_| err("invalid numeric literal"))?;
2047        FilterVal::Num(num)
2048    };
2049    // v7.39 (read01 jsonpath.c) — optional `flag "..."` after a
2050    // like_regex pattern.
2051    let mut flags: Option<String> = None;
2052    if matches!(op, FilterOp::LikeRegex) {
2053        let mut j = i;
2054        while j < chars.len() && chars[j].is_whitespace() {
2055            j += 1;
2056        }
2057        if chars[j..].starts_with(&['f', 'l', 'a', 'g']) {
2058            j += 4;
2059            while j < chars.len() && chars[j].is_whitespace() {
2060                j += 1;
2061            }
2062            if j < chars.len() && chars[j] == '"' {
2063                j += 1;
2064                let start = j;
2065                while j < chars.len() && chars[j] != '"' {
2066                    j += 1;
2067                }
2068                if j < chars.len() {
2069                    flags = Some(chars[start..j].iter().collect());
2070                    j += 1;
2071                    i = j;
2072                }
2073            }
2074        }
2075    }
2076    Ok((op, val, flags, i))
2077}
2078
2079/// v7.38 (read01, T8) — the PG `.type()` name of a JSON value.
2080fn json_type_name(v: &JsonValue) -> &'static str {
2081    match v {
2082        JsonValue::Null => "null",
2083        JsonValue::Bool(_) => "boolean",
2084        JsonValue::Number(_) | JsonValue::NumberText(_) => "number",
2085        JsonValue::String(_) => "string",
2086        JsonValue::Array(_) => "array",
2087        JsonValue::Object(_) => "object",
2088    }
2089}
2090
2091/// Numeric value of a JSON scalar for a filter comparison, if it is a number.
2092fn json_num(v: &JsonValue) -> Option<f64> {
2093    match v {
2094        JsonValue::Number(n) => Some(*n),
2095        JsonValue::NumberText(s) => s.parse().ok(),
2096        _ => None,
2097    }
2098}
2099
2100/// Resolve `@.a.b` (the accessor `path`) starting from `node`.
2101fn resolve_accessor<'a>(node: &'a JsonValue, path: &[String]) -> Option<&'a JsonValue> {
2102    let mut cur = node;
2103    for key in path {
2104        match cur {
2105            JsonValue::Object(entries) => {
2106                cur = &entries.iter().find(|(k, _)| k == key)?.1;
2107            }
2108            _ => return None,
2109        }
2110    }
2111    Some(cur)
2112}
2113
2114/// Evaluate a filter predicate against the current item. `vars` is the
2115/// jsonb `vars` document (third argument of the jsonb_path_* family);
2116/// `$name` operands resolve against its top-level keys.
2117fn filter_matches(node: &JsonValue, pred: &FilterPred, vars: Option<&JsonValue>) -> bool {
2118    let Some(target) = resolve_accessor(node, &pred.path) else {
2119        return false;
2120    };
2121    // v7.39 — a `$name` operand becomes the literal it refers to.
2122    let resolved;
2123    let val = match &pred.val {
2124        FilterVal::Var(name) => {
2125            let Some(JsonValue::Object(entries)) = vars else {
2126                return false;
2127            };
2128            let Some((_, v)) = entries.iter().find(|(k, _)| k == name) else {
2129                return false;
2130            };
2131            resolved = match v {
2132                JsonValue::Number(n) => FilterVal::Num(*n),
2133                JsonValue::NumberText(s) => match s.parse::<f64>() {
2134                    Ok(n) => FilterVal::Num(n),
2135                    Err(_) => return false,
2136                },
2137                JsonValue::String(s) => FilterVal::Str(s.clone()),
2138                JsonValue::Bool(b) => FilterVal::Bool(*b),
2139                JsonValue::Null => FilterVal::Null,
2140                _ => return false,
2141            };
2142            &resolved
2143        }
2144        other => other,
2145    };
2146    match val {
2147        FilterVal::Num(rhs) => match json_num(target) {
2148            Some(lhs) => match pred.op {
2149                FilterOp::Gt => lhs > *rhs,
2150                FilterOp::Lt => lhs < *rhs,
2151                FilterOp::Ge => lhs >= *rhs,
2152                FilterOp::Le => lhs <= *rhs,
2153                FilterOp::Eq => lhs == *rhs,
2154                FilterOp::Ne => lhs != *rhs,
2155                FilterOp::StartsWith | FilterOp::LikeRegex => false,
2156            },
2157            None => false,
2158        },
2159        FilterVal::Str(rhs) => match target {
2160            JsonValue::String(lhs) => match pred.op {
2161                FilterOp::Eq => lhs == rhs,
2162                FilterOp::Ne => lhs != rhs,
2163                FilterOp::Gt => lhs.as_str() > rhs.as_str(),
2164                FilterOp::Lt => lhs.as_str() < rhs.as_str(),
2165                FilterOp::Ge => lhs.as_str() >= rhs.as_str(),
2166                FilterOp::Le => lhs.as_str() <= rhs.as_str(),
2167                // v7.39 — string pattern predicates.
2168                FilterOp::StartsWith => lhs.starts_with(rhs.as_str()),
2169                FilterOp::LikeRegex => {
2170                    // v7.39 (read01 jsonpath.c) — the `i` flag folds case
2171                    // (other flags round-trip but don't alter matching yet).
2172                    if pred.regex_flags.as_deref().is_some_and(|f| f.contains('i')) {
2173                        crate::eval::regex_is_match(&rhs.to_lowercase(), &lhs.to_lowercase())
2174                            .unwrap_or(false)
2175                    } else {
2176                        crate::eval::regex_is_match(rhs, lhs).unwrap_or(false)
2177                    }
2178                }
2179            },
2180            _ => false,
2181        },
2182        FilterVal::Bool(rhs) => match target {
2183            JsonValue::Bool(lhs) => match pred.op {
2184                FilterOp::Eq => lhs == rhs,
2185                FilterOp::Ne => lhs != rhs,
2186                _ => false,
2187            },
2188            _ => false,
2189        },
2190        // v7.39 — `== null` matches JSON null only; `!= null` any non-null.
2191        FilterVal::Null => match pred.op {
2192            FilterOp::Eq => matches!(target, JsonValue::Null),
2193            FilterOp::Ne => !matches!(target, JsonValue::Null),
2194            _ => false,
2195        },
2196        FilterVal::Var(_) => false, // resolved above
2197    }
2198}
2199
2200/// Evaluate a (possibly `&&`/`||`-combined) filter predicate tree.
2201fn filter_expr_matches(node: &JsonValue, expr: &FilterExpr, vars: Option<&JsonValue>) -> bool {
2202    match expr {
2203        FilterExpr::Cmp(pred) => filter_matches(node, pred, vars),
2204        FilterExpr::And(a, b) => {
2205            filter_expr_matches(node, a, vars) && filter_expr_matches(node, b, vars)
2206        }
2207        FilterExpr::Or(a, b) => {
2208            filter_expr_matches(node, a, vars) || filter_expr_matches(node, b, vars)
2209        }
2210    }
2211}
2212
2213/// Lax evaluation, for the callers that never carried a mode.
2214fn apply_jsonpath(
2215    root: &JsonValue,
2216    steps: &[PathStep],
2217    vars: Option<&JsonValue>,
2218) -> Vec<JsonValue> {
2219    apply_jsonpath_mode(root, steps, vars, false).unwrap_or_default()
2220}
2221
2222/// v7.39 (round 235) — jsonpath evaluation with PG's two modes.
2223///
2224/// LAX (the default) is forgiving in two specific ways SPG did not
2225/// implement: a member accessor auto-UNWRAPS an array and applies to each
2226/// element (`lax $.a` over `[{"a":1}]` yields 1), and an array accessor
2227/// auto-WRAPS a non-array into a one-element array (`lax $[*]` over `1`
2228/// yields 1, and over `{"a":1}` yields the object). Both used to return
2229/// nothing.
2230///
2231/// STRICT reports what lax quietly skips. Wording probed off PG18.4:
2232/// a missing object key, an out-of-bounds subscript, a wildcard on a
2233/// non-array, a member accessor on a non-object. Filters never error in
2234/// either mode — a predicate that matches nothing is simply empty.
2235fn apply_jsonpath_mode(
2236    root: &JsonValue,
2237    steps: &[PathStep],
2238    vars: Option<&JsonValue>,
2239    strict: bool,
2240) -> Result<Vec<JsonValue>, EvalError> {
2241    let err = |m: alloc::string::String| Err(EvalError::TypeMismatch { detail: m });
2242    let mut cur: Vec<JsonValue> = alloc::vec![root.clone()];
2243    for step in steps {
2244        // LAX auto-unwrap / auto-wrap, applied to the inputs of this step.
2245        if !strict {
2246            match step {
2247                // A member accessor looks inside an array's elements.
2248                PathStep::Field(_) => {
2249                    let mut flat: Vec<JsonValue> = Vec::new();
2250                    for node in cur {
2251                        match node {
2252                            JsonValue::Array(items) => flat.extend(items),
2253                            other => flat.push(other),
2254                        }
2255                    }
2256                    cur = flat;
2257                }
2258                // An array accessor treats a non-array as a single element.
2259                PathStep::Wildcard | PathStep::Index(_) | PathStep::Range(..) => {
2260                    cur = cur
2261                        .into_iter()
2262                        .map(|n| match n {
2263                            arr @ JsonValue::Array(_) => arr,
2264                            other => JsonValue::Array(alloc::vec![other]),
2265                        })
2266                        .collect();
2267                }
2268                _ => {}
2269            }
2270        } else {
2271            // STRICT refuses the shapes lax would have adapted.
2272            for node in &cur {
2273                match step {
2274                    PathStep::Field(k) => match node {
2275                        JsonValue::Object(entries) => {
2276                            if !entries.iter().any(|(name, _)| name == k) {
2277                                return err(alloc::format!(
2278                                    "JSON object does not contain key \"{k}\""
2279                                ));
2280                            }
2281                        }
2282                        _ => {
2283                            return err(
2284                                "jsonpath member accessor can only be applied to an object".into(),
2285                            );
2286                        }
2287                    },
2288                    PathStep::Wildcard => {
2289                        if !matches!(node, JsonValue::Array(_)) {
2290                            return err(
2291                                "jsonpath wildcard array accessor can only be applied to an array"
2292                                    .into(),
2293                            );
2294                        }
2295                    }
2296                    PathStep::Index(idx) => match node {
2297                        JsonValue::Array(items) => {
2298                            if idx.resolve(items.len()).is_none_or(|p| p >= items.len()) {
2299                                return err("jsonpath array subscript is out of bounds".into());
2300                            }
2301                        }
2302                        _ => {
2303                            return err(
2304                                "jsonpath array accessor can only be applied to an array".into()
2305                            );
2306                        }
2307                    },
2308                    PathStep::Range(lo, hi) => match node {
2309                        JsonValue::Array(items) => {
2310                            let n = items.len();
2311                            if lo.resolve(n).is_none_or(|p| p >= n)
2312                                || hi.resolve(n).is_none_or(|p| p >= n)
2313                            {
2314                                return err("jsonpath array subscript is out of bounds".into());
2315                            }
2316                        }
2317                        _ => {
2318                            return err(
2319                                "jsonpath array accessor can only be applied to an array".into()
2320                            );
2321                        }
2322                    },
2323                    _ => {}
2324                }
2325            }
2326        }
2327        let mut next: Vec<JsonValue> = Vec::new();
2328        for node in &cur {
2329            match (step, node) {
2330                (PathStep::Field(k), JsonValue::Object(entries)) => {
2331                    if let Some((_, v)) = entries.iter().find(|(name, _)| name == k) {
2332                        next.push(v.clone());
2333                    }
2334                }
2335                (PathStep::Index(idx), JsonValue::Array(items)) => {
2336                    if let Some(pos) = idx.resolve(items.len())
2337                        && let Some(v) = items.get(pos)
2338                    {
2339                        next.push(v.clone());
2340                    }
2341                }
2342                (PathStep::Wildcard, JsonValue::Array(items)) => {
2343                    next.extend(items.iter().cloned());
2344                }
2345                // v7.38 (read01, T8) — range / filter / methods.
2346                (PathStep::Range(lo, hi), JsonValue::Array(items)) => {
2347                    if let (Some(a), Some(b)) = (lo.resolve(items.len()), hi.resolve(items.len())) {
2348                        for idx in a..=b {
2349                            if let Some(v) = items.get(idx) {
2350                                next.push(v.clone());
2351                            }
2352                        }
2353                    }
2354                }
2355                (PathStep::Filter(expr), node) => {
2356                    if filter_expr_matches(node, expr, vars) {
2357                        next.push(node.clone());
2358                    }
2359                }
2360                (PathStep::Size, JsonValue::Array(items)) => {
2361                    next.push(JsonValue::Number(items.len() as f64));
2362                }
2363                // PG lax mode: `.size()` of a non-array is 1.
2364                (PathStep::Size, _) => next.push(JsonValue::Number(1.0)),
2365                (PathStep::TypeOf, node) => {
2366                    next.push(JsonValue::String(json_type_name(node).into()));
2367                }
2368                // v7.39 — `.**`: the item itself plus all descendants,
2369                // document order.
2370                (PathStep::RecursiveAll, node) => {
2371                    fn descend(v: &JsonValue, out: &mut Vec<JsonValue>) {
2372                        out.push(v.clone());
2373                        match v {
2374                            JsonValue::Object(entries) => {
2375                                for (_, child) in entries {
2376                                    descend(child, out);
2377                                }
2378                            }
2379                            JsonValue::Array(items) => {
2380                                for child in items {
2381                                    descend(child, out);
2382                                }
2383                            }
2384                            _ => {}
2385                        }
2386                    }
2387                    descend(node, &mut next);
2388                }
2389                // v7.39 — numeric item methods (lax: non-numbers drop out).
2390                (PathStep::Num(m), node) => {
2391                    let n = match m {
2392                        // `.double()` also accepts numeric strings.
2393                        NumMethod::Double => match node {
2394                            JsonValue::String(s) => s.parse::<f64>().ok(),
2395                            other => json_num(other),
2396                        },
2397                        _ => json_num(node),
2398                    };
2399                    if let Some(x) = n {
2400                        let out = match m {
2401                            NumMethod::Abs => x.abs(),
2402                            NumMethod::Floor => x.floor(),
2403                            NumMethod::Ceiling => x.ceil(),
2404                            NumMethod::Double => x,
2405                        };
2406                        next.push(JsonValue::Number(out));
2407                    }
2408                }
2409                _ => {} // no match at this branch
2410            }
2411        }
2412        cur = next;
2413        if cur.is_empty() {
2414            return Ok(Vec::new());
2415        }
2416    }
2417    Ok(cur)
2418}
2419
2420/// v7.38 (read01, T8) — evaluate a top-level jsonpath boolean predicate like
2421/// `$.a > 3` (the form the `@@` operator / jsonb_path_match takes). Returns
2422/// `Some(bool)` when the path is a top-level comparison, or `None` to let the
2423/// caller fall back to the ordinary path-query match (`$.a ? (...)` etc.).
2424pub fn path_predicate(doc: &Value, path: &Value) -> Result<Option<bool>, EvalError> {
2425    path_predicate_vars(doc, path, None)
2426}
2427
2428/// v7.39 — `path_predicate` with a jsonb `vars` document.
2429pub fn path_predicate_vars(
2430    doc: &Value,
2431    path: &Value,
2432    vars: Option<&JsonValue>,
2433) -> Result<Option<bool>, EvalError> {
2434    let (src, ptext) = match (doc, path) {
2435        (Value::Null, _) | (_, Value::Null) => return Ok(None),
2436        (Value::Json(s) | Value::Text(s), Value::Text(p) | Value::Json(p)) => (s, p),
2437        _ => return Ok(None),
2438    };
2439    // v7.39 — top-level `exists(<path>)` predicate form.
2440    let trimmed = ptext.trim();
2441    if let Some(inner) = trimmed
2442        .strip_prefix("exists")
2443        .map(str::trim_start)
2444        .and_then(|r| r.strip_prefix('('))
2445        .and_then(|r| r.strip_suffix(')'))
2446    {
2447        let (strict, steps) = parse_jsonpath_mode(inner.trim())?;
2448        let root = parse(src).map_err(|e| EvalError::TypeMismatch {
2449            detail: alloc::format!("{e}"),
2450        })?;
2451        return Ok(Some(
2452            !apply_jsonpath_mode(&root, &steps, vars, strict)?.is_empty(),
2453        ));
2454    }
2455    let chars: Vec<char> = ptext.chars().collect();
2456    // Find a top-level comparison operator — depth 0, outside quotes, so a `>`
2457    // inside a `? (...)` filter or `[...]` does not count.
2458    let mut depth = 0i32;
2459    let mut i = 0;
2460    let mut op_at = None;
2461    while i < chars.len() {
2462        match chars[i] {
2463            '(' | '[' => depth += 1,
2464            ')' | ']' => depth -= 1,
2465            '"' => {
2466                i += 1;
2467                while i < chars.len() && chars[i] != '"' {
2468                    i += 1;
2469                }
2470            }
2471            '>' | '<' | '=' | '!' if depth == 0 => {
2472                op_at = Some(i);
2473                break;
2474            }
2475            _ => {}
2476        }
2477        i += 1;
2478    }
2479    let Some(pos) = op_at else { return Ok(None) };
2480    let left: String = chars[..pos].iter().collect();
2481    let (strict, steps) = parse_jsonpath_mode(left.trim())?;
2482    let (op, val, regex_flags, _) = parse_cmp_and_literal(&chars, pos)?;
2483    let root = parse(src).map_err(|e| EvalError::TypeMismatch {
2484        detail: alloc::format!("{e}"),
2485    })?;
2486    // v7.39 (round 235) — a strict refusal travels out of the predicate
2487    // too; the `@@` / jsonb_path_match callers turn it into NULL.
2488    let results = apply_jsonpath_mode(&root, &steps, vars, strict)?;
2489    let pred = FilterPred {
2490        path: Vec::new(),
2491        op,
2492        val,
2493        regex_flags,
2494    };
2495    Ok(Some(results.iter().any(|v| filter_matches(v, &pred, vars))))
2496}
2497
2498/// v7.17.0 Phase 3.9 — `jsonb_path_query(doc, path)` — returns the
2499/// matched JSON values as a TextArray (each element is the JSON
2500/// encoding of one match).
2501pub fn path_query(doc: &Value, path: &Value) -> Result<Value<'static>, EvalError> {
2502    path_query_vars(doc, path, None)
2503}
2504
2505/// v7.39 — parse the `vars` argument of the jsonb_path_* family into a
2506/// JsonValue object (NULL → no vars).
2507pub fn parse_path_vars(v: &Value) -> Result<Option<JsonValue>, EvalError> {
2508    match v {
2509        Value::Null => Ok(None),
2510        Value::Json(s) | Value::Text(s) => {
2511            let parsed = parse(s).map_err(|e| EvalError::TypeMismatch {
2512                detail: alloc::format!("invalid jsonpath vars document: {e}"),
2513            })?;
2514            if !matches!(parsed, JsonValue::Object(_)) {
2515                return Err(EvalError::TypeMismatch {
2516                    detail: "jsonpath vars must be a JSON object".into(),
2517                });
2518            }
2519            Ok(Some(parsed))
2520        }
2521        other => Err(EvalError::TypeMismatch {
2522            detail: alloc::format!(
2523                "jsonpath vars must be jsonb, got {}",
2524                crate::conversions::pg_type_name_for_error_opt(other.data_type())
2525            ),
2526        }),
2527    }
2528}
2529
2530/// v7.39 — `path_query` with a jsonb `vars` document ($name references).
2531pub fn path_query_vars(
2532    doc: &Value,
2533    path: &Value,
2534    vars: Option<&JsonValue>,
2535) -> Result<Value<'static>, EvalError> {
2536    let (src, path_text) = match (doc, path) {
2537        (Value::Null, _) | (_, Value::Null) => return Ok(Value::Null),
2538        (Value::Json(s) | Value::Text(s), Value::Text(p) | Value::Json(p)) => (s, p),
2539        _ => {
2540            return Err(EvalError::TypeMismatch {
2541                detail: "jsonb_path_query() expects (JSON, TEXT)".into(),
2542            });
2543        }
2544    };
2545    let root = parse(src).map_err(|e| EvalError::TypeMismatch {
2546        detail: alloc::format!("invalid JSON for jsonb_path_query: {e}"),
2547    })?;
2548    // v7.39 — a top-level `exists(...)` path yields a single boolean.
2549    let trimmed = path_text.trim();
2550    if let Some(inner) = trimmed
2551        .strip_prefix("exists")
2552        .map(str::trim_start)
2553        .and_then(|r| r.strip_prefix('('))
2554        .and_then(|r| r.strip_suffix(')'))
2555    {
2556        let steps = parse_jsonpath(inner.trim())?;
2557        let hit = !apply_jsonpath(&root, &steps, vars).is_empty();
2558        return Ok(Value::TextArray(alloc::vec![Some(
2559            if hit { "true" } else { "false" }.into()
2560        )]));
2561    }
2562    // v7.39 (round 235) — the query family propagates a strict-mode
2563    // refusal; only path_match / `@?` / `@@` suppress it (see below).
2564    let (strict, steps) = parse_jsonpath_mode(path_text)?;
2565    let matches = apply_jsonpath_mode(&root, &steps, vars, strict)?;
2566    let arr: Vec<Option<String>> = matches
2567        .into_iter()
2568        .map(|v| Some(json_canonical_string(&v)))
2569        .collect();
2570    Ok(Value::TextArray(arr))
2571}
2572
2573/// v7.17.0 Phase 3.9 — `jsonb_path_query_first(doc, path)` returns
2574/// the first matched JSON value as a Json, or NULL on no match.
2575pub fn path_query_first(doc: &Value, path: &Value) -> Result<Value<'static>, EvalError> {
2576    path_query_first_vars(doc, path, None)
2577}
2578
2579/// v7.39 — `path_query_first` with a jsonb `vars` document.
2580pub fn path_query_first_vars(
2581    doc: &Value,
2582    path: &Value,
2583    vars: Option<&JsonValue>,
2584) -> Result<Value<'static>, EvalError> {
2585    let q = path_query_vars(doc, path, vars)?;
2586    match q {
2587        Value::TextArray(items) => {
2588            if let Some(Some(first)) = items.into_iter().next() {
2589                Ok(Value::json(first))
2590            } else {
2591                Ok(Value::Null)
2592            }
2593        }
2594        other => Ok(other),
2595    }
2596}
2597
2598/// v7.17.0 Phase 3.9 — `jsonb_path_query_array(doc, path)` returns
2599/// the matched values wrapped as a single JSON array.
2600pub fn path_query_array(doc: &Value, path: &Value) -> Result<Value<'static>, EvalError> {
2601    path_query_array_vars(doc, path, None)
2602}
2603
2604/// v7.39 — `path_query_array` with a jsonb `vars` document.
2605pub fn path_query_array_vars(
2606    doc: &Value,
2607    path: &Value,
2608    vars: Option<&JsonValue>,
2609) -> Result<Value<'static>, EvalError> {
2610    let q = path_query_vars(doc, path, vars)?;
2611    let arr = match q {
2612        Value::TextArray(items) => {
2613            let mut buf = String::from("[");
2614            let mut first = true;
2615            for s in items.into_iter().flatten() {
2616                if !first {
2617                    buf.push_str(", ");
2618                }
2619                buf.push_str(&s);
2620                first = false;
2621            }
2622            buf.push(']');
2623            Value::json(buf)
2624        }
2625        other => other,
2626    };
2627    // jsonb_path_query_array yields a jsonb array — emit PG-canonical
2628    // text (`[1, 2, 3]`, `, ` after each element) instead of the raw
2629    // `,`-joined buffer. Matches jsonb_agg / jsonb_build_array output.
2630    Ok(canonicalize_value(arr))
2631}
2632
2633// ─── v7.17.0 Phase 3.P0-28 — JSON builder family ───────────────
2634//
2635// Surface: to_json / to_jsonb, json_build_object / jsonb_build_object,
2636// json_build_array / jsonb_build_array, jsonb_set, jsonb_insert.
2637//
2638// PG `json` vs `jsonb` differ in storage shape only — both surface
2639// as Value::Json textually. The pair just shares an implementation.
2640
2641/// Encode a Value as its canonical JSON text (no surrounding quotes
2642/// for non-strings). Used by every builder below.
2643///
2644/// Rules:
2645///   * NULL → "null" (json literal; NOT SQL NULL).
2646///   * BOOL → "true" / "false".
2647///   * Numbers → bare decimal text (BigInt prints exact 64-bit form).
2648///   * Text → quoted+escaped JSON string.
2649///   * Json/Jsonb → pass-through (assumed valid; parser is forgiving).
2650///   * Arrays → "[..,..]" with element-wise encoding.
2651///   * Bytes / Date / Timestamp / Uuid / Numeric → quoted textual
2652///     form via Display; PG canonical text shape.
2653/// v7.39 (read01 jsonpath.c) — canonicalize a jsonpath literal the way
2654/// PG's jsonpath output function does: `lax` is the implicit default and
2655/// is not printed, `strict` is; field accessors always print quoted
2656/// (`$."a"`); filters print as `?(@ <op> <val>)` with spaces around the
2657/// operator; `last - k` keeps its spaces. Errors surface as 22P02-shaped
2658/// syntax errors.
2659pub fn jsonpath_canonical(input: &str) -> Result<String, EvalError> {
2660    let trimmed = input.trim();
2661    let (strict, body) = if let Some(rest) = trimmed.strip_prefix("strict ") {
2662        (true, rest.trim_start())
2663    } else if let Some(rest) = trimmed.strip_prefix("lax ") {
2664        (false, rest.trim_start())
2665    } else {
2666        (false, trimmed)
2667    };
2668    let steps = parse_jsonpath(body).map_err(|_| {
2669        // PG reports the first offending token; the first character is
2670        // a close-enough stand-in for the common shapes.
2671        let tok: String = body.chars().take(1).collect();
2672        EvalError::TypeMismatch {
2673            detail: alloc::format!("syntax error at or near {tok:?} of jsonpath input"),
2674        }
2675    })?;
2676    let mut out = String::new();
2677    if strict {
2678        out.push_str("strict ");
2679    }
2680    out.push('$');
2681    fn idx(b: &IdxBound, out: &mut String) {
2682        match b {
2683            IdxBound::At(n) => {
2684                let _ = core::fmt::Write::write_fmt(out, format_args!("{n}"));
2685            }
2686            IdxBound::FromLast(0) => out.push_str("last"),
2687            IdxBound::FromLast(k) => {
2688                let _ = core::fmt::Write::write_fmt(out, format_args!("last - {k}"));
2689            }
2690        }
2691    }
2692    fn fval(v: &FilterVal, out: &mut String) {
2693        match v {
2694            FilterVal::Num(x) => {
2695                if x.fract() == 0.0 && x.abs() < 1e15 {
2696                    let _ = core::fmt::Write::write_fmt(out, format_args!("{}", *x as i64));
2697                } else {
2698                    let _ = core::fmt::Write::write_fmt(out, format_args!("{x}"));
2699                }
2700            }
2701            FilterVal::Str(s) => {
2702                let _ = core::fmt::Write::write_fmt(out, format_args!("{s:?}"));
2703            }
2704            FilterVal::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
2705            FilterVal::Null => out.push_str("null"),
2706            FilterVal::Var(n) => {
2707                let _ = core::fmt::Write::write_fmt(out, format_args!("$\"{n}\""));
2708            }
2709        }
2710    }
2711    fn fexpr(e: &FilterExpr, out: &mut String) {
2712        match e {
2713            FilterExpr::Cmp(p) => {
2714                out.push('@');
2715                for seg in &p.path {
2716                    let _ = core::fmt::Write::write_fmt(out, format_args!(".\"{seg}\""));
2717                }
2718                let op = match p.op {
2719                    FilterOp::Gt => " > ",
2720                    FilterOp::Lt => " < ",
2721                    FilterOp::Ge => " >= ",
2722                    FilterOp::Le => " <= ",
2723                    FilterOp::Eq => " == ",
2724                    FilterOp::Ne => " != ",
2725                    FilterOp::StartsWith => " starts with ",
2726                    FilterOp::LikeRegex => " like_regex ",
2727                };
2728                out.push_str(op);
2729                fval(&p.val, out);
2730                if let Some(f) = &p.regex_flags {
2731                    let _ = core::fmt::Write::write_fmt(out, format_args!(" flag \"{f}\""));
2732                }
2733            }
2734            FilterExpr::And(l, r) => {
2735                fexpr(l, out);
2736                out.push_str(" && ");
2737                fexpr(r, out);
2738            }
2739            FilterExpr::Or(l, r) => {
2740                fexpr(l, out);
2741                out.push_str(" || ");
2742                fexpr(r, out);
2743            }
2744        }
2745    }
2746    for st in &steps {
2747        match st {
2748            PathStep::Field(f) => {
2749                let _ = core::fmt::Write::write_fmt(&mut out, format_args!(".\"{f}\""));
2750            }
2751            PathStep::Index(b) => {
2752                out.push('[');
2753                idx(b, &mut out);
2754                out.push(']');
2755            }
2756            PathStep::Wildcard => out.push_str("[*]"),
2757            PathStep::Range(a, b) => {
2758                out.push('[');
2759                idx(a, &mut out);
2760                out.push_str(" to ");
2761                idx(b, &mut out);
2762                out.push(']');
2763            }
2764            PathStep::Filter(e) => {
2765                out.push_str("?(");
2766                fexpr(e, &mut out);
2767                out.push(')');
2768            }
2769            PathStep::Size => out.push_str(".size()"),
2770            PathStep::TypeOf => out.push_str(".type()"),
2771            PathStep::Num(m) => out.push_str(match m {
2772                NumMethod::Abs => ".abs()",
2773                NumMethod::Floor => ".floor()",
2774                NumMethod::Ceiling => ".ceiling()",
2775                NumMethod::Double => ".double()",
2776            }),
2777            PathStep::RecursiveAll => out.push_str(".**"),
2778        }
2779    }
2780    Ok(out)
2781}
2782
2783pub fn value_to_json_text(v: &Value) -> String {
2784    let mut out = String::new();
2785    encode_value_into(v, &mut out);
2786    out
2787}
2788
2789fn encode_value_into(v: &Value, out: &mut String) {
2790    match v {
2791        Value::Null => out.push_str("null"),
2792        Value::Bool(true) => out.push_str("true"),
2793        Value::Bool(false) => out.push_str("false"),
2794        Value::SmallInt(n) => out.push_str(&alloc::format!("{n}")),
2795        Value::Int(n) => out.push_str(&alloc::format!("{n}")),
2796        Value::BigInt(n) => out.push_str(&alloc::format!("{n}")),
2797        // v7.39 (read01 json.c) — non-finite floats are not legal JSON
2798        // numbers; PG quotes the canonical spellings ("NaN"/"Infinity").
2799        Value::Float(x) if !x.is_finite() => {
2800            let txt = if x.is_nan() {
2801                "NaN"
2802            } else if *x > 0.0 {
2803                "Infinity"
2804            } else {
2805                "-Infinity"
2806            };
2807            write_json(&JsonValue::String(txt.into()), out);
2808        }
2809        Value::Float(x) => out.push_str(&alloc::format!("{x}")),
2810        Value::Real(x) if !x.is_finite() => {
2811            let txt = if x.is_nan() {
2812                "NaN"
2813            } else if *x > 0.0 {
2814                "Infinity"
2815            } else {
2816                "-Infinity"
2817            };
2818            write_json(&JsonValue::String(txt.into()), out);
2819        }
2820        Value::Numeric {
2821            scaled,
2822            scale,
2823            kind,
2824        } => {
2825            use spg_storage::NumericKind as NK;
2826            match kind {
2827                NK::NaN => write_json(&JsonValue::String("NaN".into()), out),
2828                NK::PosInf => write_json(&JsonValue::String("Infinity".into()), out),
2829                NK::NegInf => write_json(&JsonValue::String("-Infinity".into()), out),
2830                // Render the exact decimal text — same shape display uses.
2831                NK::Finite => out.push_str(&render_numeric(*scaled, *scale)),
2832            }
2833        }
2834        Value::Text(s) => write_json(&JsonValue::String(s.to_string()), out),
2835        Value::Json(s) => {
2836            // Pass through verbatim; re-parsing would re-format and
2837            // drift `1.0` → `1` etc. PG's to_json on a json input is
2838            // identity.
2839            out.push_str(s);
2840        }
2841        // v7.38 (read01, T9) — a composite encodes as a JSON object keyed by
2842        // field name (`to_json(row(1,'a'))` → `{"f1":1,"f2":"a"}`).
2843        Value::Composite(fields) => {
2844            out.push('{');
2845            for (i, (name, fv)) in fields.iter().enumerate() {
2846                if i > 0 {
2847                    out.push(',');
2848                }
2849                write_json(&JsonValue::String(name.clone()), out);
2850                out.push(':');
2851                encode_value_into(fv, out);
2852            }
2853            out.push('}');
2854        }
2855        Value::TextArray(items) => {
2856            out.push('[');
2857            for (i, it) in items.iter().enumerate() {
2858                if i > 0 {
2859                    out.push(',');
2860                }
2861                match it {
2862                    Some(s) => write_json(&JsonValue::String(s.clone()), out),
2863                    None => out.push_str("null"),
2864                }
2865            }
2866            out.push(']');
2867        }
2868        Value::IntArray(items) => {
2869            out.push('[');
2870            for (i, it) in items.iter().enumerate() {
2871                if i > 0 {
2872                    out.push(',');
2873                }
2874                match it {
2875                    Some(n) => out.push_str(&alloc::format!("{n}")),
2876                    None => out.push_str("null"),
2877                }
2878            }
2879            out.push(']');
2880        }
2881        Value::BigIntArray(items) => {
2882            out.push('[');
2883            for (i, it) in items.iter().enumerate() {
2884                if i > 0 {
2885                    out.push(',');
2886                }
2887                match it {
2888                    Some(n) => out.push_str(&alloc::format!("{n}")),
2889                    None => out.push_str("null"),
2890                }
2891            }
2892            out.push(']');
2893        }
2894        // PG's to_json spells a timestamp in ISO 8601 with a `T`
2895        // separator (`2020-01-15T10:30:00`), unlike the space-separated
2896        // text-out form, so it needs its own arm ahead of the catch-all.
2897        Value::Timestamp(_) => {
2898            let txt = crate::eval::values::value_to_text(v).replacen(' ', "T", 1);
2899            write_json(&JsonValue::String(txt), out);
2900        }
2901        // Fall-through: every other type (Date / Interval / Uuid / Bytea /
2902        // Time / Money / …) renders via the canonical PG-faithful text
2903        // renderer, wrapped as a JSON string — never a Rust debug dump.
2904        //
2905        // v7.39 (read01 round 76) — but an ARRAY is a JSON array, not a
2906        // JSON string. The arms above cover only text/int/bigint arrays;
2907        // every other element type (bool / float / numeric / date / uuid /
2908        // …) and every 2-D matrix used to reach this fall-through and come
2909        // out quoted (`to_jsonb(ARRAY[[1,2]])` → `"{{1,2}}"`). Route them
2910        // through the shared element menu, recursing per element so nesting
2911        // and per-type spelling both stay canonical.
2912        other => {
2913            if let Some(elems) = crate::eval::values::array_elements(other) {
2914                out.push('[');
2915                for (i, e) in elems.iter().enumerate() {
2916                    if i > 0 {
2917                        out.push(',');
2918                    }
2919                    encode_value_into(e, out);
2920                }
2921                out.push(']');
2922                return;
2923            }
2924            let txt = crate::eval::values::value_to_text(other);
2925            write_json(&JsonValue::String(txt), out);
2926        }
2927    }
2928}
2929
2930fn render_numeric(scaled: i128, scale: u16) -> String {
2931    let neg = scaled < 0;
2932    let mag_str = alloc::format!("{}", scaled.unsigned_abs());
2933    let s = scale as usize;
2934    let body = if s == 0 {
2935        mag_str
2936    } else if mag_str.len() > s {
2937        let p = mag_str.len() - s;
2938        alloc::format!("{}.{}", &mag_str[..p], &mag_str[p..])
2939    } else {
2940        let pad = s - mag_str.len();
2941        alloc::format!("0.{}{}", "0".repeat(pad), mag_str)
2942    };
2943    if neg { alloc::format!("-{body}") } else { body }
2944}
2945
2946/// `json_build_object(k, v, k, v, …)` — variadic, even-length.
2947/// NULL key → error (PG: "argument cannot be null"). Values encoded
2948/// via `value_to_json_text`. Returns Value::Json.
2949/// v7.37.17 (17.6 siblings) — `jsonb_concat(a, b)` — function form
2950/// of the `||` operator. Object + object merges keys (right wins on
2951/// duplicates); array + array appends; array + scalar appends the
2952/// scalar; scalar + scalar makes a 2-element array (PG semantics).
2953pub fn concat(lhs: &Value, rhs: &Value) -> Result<Value<'static>, EvalError> {
2954    concat_inner(lhs, rhs).map(canonicalize_value)
2955}
2956
2957fn concat_inner(lhs: &Value, rhs: &Value) -> Result<Value<'static>, EvalError> {
2958    let (a_src, b_src) = match (lhs, rhs) {
2959        (Value::Null, _) | (_, Value::Null) => return Ok(Value::Null),
2960        (Value::Json(a) | Value::Text(a), Value::Json(b) | Value::Text(b)) => {
2961            (a.as_ref(), b.as_ref())
2962        }
2963        _ => {
2964            return Err(EvalError::TypeMismatch {
2965                detail: "jsonb_concat() expects (JSON, JSON)".into(),
2966            });
2967        }
2968    };
2969    let a = parse(a_src).map_err(|e| EvalError::TypeMismatch {
2970        detail: alloc::format!("invalid JSON lhs for concat: {e}"),
2971    })?;
2972    let b = parse(b_src).map_err(|e| EvalError::TypeMismatch {
2973        detail: alloc::format!("invalid JSON rhs for concat: {e}"),
2974    })?;
2975    let merged = match (a, b) {
2976        (JsonValue::Object(mut ea), JsonValue::Object(eb)) => {
2977            // Right side wins on duplicate keys.
2978            for (k, v) in eb {
2979                if let Some(slot) = ea.iter_mut().find(|(ek, _)| *ek == k) {
2980                    slot.1 = v;
2981                } else {
2982                    ea.push((k, v));
2983                }
2984            }
2985            JsonValue::Object(ea)
2986        }
2987        (JsonValue::Array(mut ia), JsonValue::Array(ib)) => {
2988            ia.extend(ib);
2989            JsonValue::Array(ia)
2990        }
2991        (JsonValue::Array(mut ia), scalar) => {
2992            ia.push(scalar);
2993            JsonValue::Array(ia)
2994        }
2995        (scalar, JsonValue::Array(ib)) => {
2996            let mut out = alloc::vec![scalar];
2997            out.extend(ib);
2998            JsonValue::Array(out)
2999        }
3000        (sa, sb) => JsonValue::Array(alloc::vec![sa, sb]),
3001    };
3002    Ok(Value::json(merged.to_json_text()))
3003}
3004
3005/// v7.37.17 (17.6 siblings) — `jsonb_delete(doc, key)` — function
3006/// form of the `-` operator. Removes an object key or an array
3007/// element (by text match for objects, by index for arrays).
3008pub fn delete_key(lhs: &Value, rhs: &Value) -> Result<Value<'static>, EvalError> {
3009    delete_key_inner(lhs, rhs).map(canonicalize_value)
3010}
3011
3012fn delete_key_inner(lhs: &Value, rhs: &Value) -> Result<Value<'static>, EvalError> {
3013    let src = match lhs {
3014        Value::Null => return Ok(Value::Null),
3015        Value::Json(s) | Value::Text(s) => s.as_ref(),
3016        _ => {
3017            return Err(EvalError::TypeMismatch {
3018                detail: "jsonb_delete() expects JSON lhs".into(),
3019            });
3020        }
3021    };
3022    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
3023        detail: alloc::format!("invalid JSON for delete: {e}"),
3024    })?;
3025    let out = match (doc, rhs) {
3026        (_, Value::Null) => return Ok(Value::Null),
3027        (JsonValue::Object(entries), Value::Text(key)) => {
3028            let filtered: Vec<(String, JsonValue)> = entries
3029                .into_iter()
3030                .filter(|(k, _)| k != key.as_ref())
3031                .collect();
3032            JsonValue::Object(filtered)
3033        }
3034        // PG `jsonb - text[]` removes every listed key from an object.
3035        (JsonValue::Object(entries), Value::TextArray(keys)) => {
3036            let filtered: Vec<(String, JsonValue)> = entries
3037                .into_iter()
3038                .filter(|(k, _)| !keys.iter().any(|kk| kk.as_deref() == Some(k.as_str())))
3039                .collect();
3040            JsonValue::Object(filtered)
3041        }
3042        (JsonValue::Array(items), Value::Int(idx)) => {
3043            let n = *idx;
3044            let len = items.len() as i64;
3045            let real = if n >= 0 {
3046                i64::from(n)
3047            } else {
3048                len + i64::from(n)
3049            };
3050            let filtered: Vec<JsonValue> = items
3051                .into_iter()
3052                .enumerate()
3053                .filter(|(i, _)| *i as i64 != real)
3054                .map(|(_, v)| v)
3055                .collect();
3056            JsonValue::Array(filtered)
3057        }
3058        // v7.39 (round 234) — this used to be a silent catch-all
3059        // (`(other, _) => other`), so every unsupported combination handed
3060        // the document back untouched. PG names each one (probed 18.4):
3061        // deleting from a scalar has nowhere to delete from, and an
3062        // integer index is meaningless on an object.
3063        (JsonValue::Object(_), Value::Int(_) | Value::SmallInt(_) | Value::BigInt(_)) => {
3064            return Err(EvalError::TypeMismatch {
3065                detail: "cannot delete from object using integer index".into(),
3066            });
3067        }
3068        (other, _) if !matches!(other, JsonValue::Object(_) | JsonValue::Array(_)) => {
3069            return Err(EvalError::TypeMismatch {
3070                detail: "cannot delete from scalar".into(),
3071            });
3072        }
3073        // An array minus a key, or any other container/operand pairing PG
3074        // accepts as a no-op, keeps the document.
3075        (other, _) => other,
3076    };
3077    Ok(Value::json(out.to_json_text()))
3078}
3079
3080pub fn build_object(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3081    if !args.len().is_multiple_of(2) {
3082        return Err(EvalError::TypeMismatch {
3083            detail: alloc::format!(
3084                "json_build_object() needs an even number of args, got {}",
3085                args.len()
3086            ),
3087        });
3088    }
3089    let mut out = String::from("{");
3090    let mut first = true;
3091    for pair in args.chunks_exact(2) {
3092        if !first {
3093            // v7.38 (read01, T-json-ws) — PG's json_build_object uses `, `
3094            // between pairs and ` : ` (spaces both sides) around the colon;
3095            // jsonb_build_object canonicalises this to `: `.
3096            out.push_str(", ");
3097        }
3098        first = false;
3099        let key = match &pair[0] {
3100            Value::Null => {
3101                return Err(EvalError::TypeMismatch {
3102                    detail: "json_build_object() key cannot be NULL".into(),
3103                });
3104            }
3105            Value::Text(s) | Value::Json(s) => s.to_string(),
3106            other => format_value_as_text(other),
3107        };
3108        write_json(&JsonValue::String(key), &mut out);
3109        out.push_str(" : ");
3110        encode_value_into(&pair[1], &mut out);
3111    }
3112    out.push('}');
3113    Ok(Value::json(out))
3114}
3115
3116/// `json_build_array(...)` — variadic; empty → "[]". Each arg
3117/// encoded via `value_to_json_text`.
3118pub fn build_array(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3119    let mut out = String::from("[");
3120    for (i, v) in args.iter().enumerate() {
3121        if i > 0 {
3122            // v7.38 (read01, T-json-ws) — PG's json_build_array separates
3123            // elements with `, ` (the jsonb variant canonicalises to the same
3124            // spacing). to_json / array_to_json stay compact via other paths.
3125            out.push_str(", ");
3126        }
3127        encode_value_into(v, &mut out);
3128    }
3129    out.push(']');
3130    Ok(Value::json(out))
3131}
3132
3133fn format_value_as_text(v: &Value) -> String {
3134    match v {
3135        Value::SmallInt(n) => alloc::format!("{n}"),
3136        Value::Int(n) => alloc::format!("{n}"),
3137        Value::BigInt(n) => alloc::format!("{n}"),
3138        Value::Float(x) => alloc::format!("{x}"),
3139        Value::Bool(b) => alloc::format!("{b}"),
3140        other => alloc::format!("{other:?}"),
3141    }
3142}
3143
3144/// `jsonb_set(target, path, new_value [, create_missing])` — replace
3145/// at PG text-array path. `create_missing` defaults to true.
3146///
3147///   * Path step on object: treated as key. If missing & create_missing
3148///     → insert; else no-op.
3149///   * Path step on array: integer index, negative counts from end.
3150///     Out-of-range with create_missing → append; without → no-op.
3151///   * Type mismatch (e.g. step on a scalar) → no-op (PG semantics).
3152pub fn set(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3153    if !(3..=4).contains(&args.len()) {
3154        return Err(EvalError::TypeMismatch {
3155            detail: alloc::format!("jsonb_set() takes 3 or 4 args, got {}", args.len()),
3156        });
3157    }
3158    if args.iter().take(3).any(|v| matches!(v, Value::Null)) {
3159        return Ok(Value::Null);
3160    }
3161    let create_missing = match args.get(3) {
3162        None | Some(Value::Null) => true,
3163        Some(Value::Bool(b)) => *b,
3164        Some(other) => {
3165            return Err(EvalError::TypeMismatch {
3166                detail: alloc::format!(
3167                    "jsonb_set() create_missing must be BOOL, got {}",
3168                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
3169                ),
3170            });
3171        }
3172    };
3173    let doc_text = json_text_arg(&args[0], "jsonb_set", "target")?;
3174    let path = path_text_arg(&args[1], "jsonb_set")?;
3175    let new_text = json_text_arg(&args[2], "jsonb_set", "new_value")?;
3176    let mut root = parse(doc_text).map_err(|e| EvalError::TypeMismatch {
3177        detail: alloc::format!("jsonb_set(): invalid JSON target — {e}"),
3178    })?;
3179    let new_val = parse(new_text).map_err(|e| EvalError::TypeMismatch {
3180        detail: alloc::format!("jsonb_set(): invalid JSON new_value — {e}"),
3181    })?;
3182    // v7.39 (round 234) — PG's edge rules for the modification family,
3183    // probed against 18.4. An EMPTY path is a no-op (SPG replaced the whole
3184    // document with the new value — silently wrong), and a SCALAR target
3185    // has nowhere to put a path (SPG returned the scalar unchanged).
3186    if path.is_empty() {
3187        return Ok(Value::json(root.to_json_text()));
3188    }
3189    if is_json_scalar(&root) {
3190        return Err(EvalError::TypeMismatch {
3191            detail: "cannot set path in scalar".into(),
3192        });
3193    }
3194    set_at_path(&mut root, &path, new_val, create_missing);
3195    Ok(Value::json(root.to_json_text()))
3196}
3197
3198/// v7.37.17 (17.6 siblings) — `jsonb_delete_path(doc, path[])` —
3199/// function form of the `#-` operator. Removes the value at the
3200/// nested path; missing path leaves the doc unchanged.
3201pub fn delete_path(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3202    delete_path_inner(args).map(canonicalize_value)
3203}
3204
3205fn delete_path_inner(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3206    if args.len() != 2 {
3207        return Err(EvalError::TypeMismatch {
3208            detail: alloc::format!("jsonb_delete_path() takes 2 args, got {}", args.len()),
3209        });
3210    }
3211    if args.iter().any(|v| matches!(v, Value::Null)) {
3212        return Ok(Value::Null);
3213    }
3214    let doc_text = json_text_arg(&args[0], "jsonb_delete_path", "target")?;
3215    let path = path_text_arg(&args[1], "jsonb_delete_path")?;
3216    let mut root = parse(doc_text).map_err(|e| EvalError::TypeMismatch {
3217        detail: alloc::format!("jsonb_delete_path(): invalid JSON target — {e}"),
3218    })?;
3219    // v7.39 (round 234) — `#-` on a scalar is an error in PG; SPG handed
3220    // the scalar back unchanged.
3221    if is_json_scalar(&root) && !path.is_empty() {
3222        return Err(EvalError::TypeMismatch {
3223            detail: "cannot delete path in scalar".into(),
3224        });
3225    }
3226    delete_at_path(&mut root, &path);
3227    Ok(Value::json(root.to_json_text()))
3228}
3229
3230fn delete_at_path(node: &mut JsonValue, path: &[String]) {
3231    if path.is_empty() {
3232        return;
3233    }
3234    let step = &path[0];
3235    if path.len() == 1 {
3236        // Terminal step — remove here.
3237        match node {
3238            JsonValue::Object(entries) => {
3239                entries.retain(|(k, _)| k != step);
3240            }
3241            JsonValue::Array(items) => {
3242                if let Ok(idx) = step.parse::<i64>() {
3243                    let len = items.len() as i64;
3244                    let real = if idx >= 0 { idx } else { len + idx };
3245                    if real >= 0 && real < len {
3246                        items.remove(real as usize);
3247                    }
3248                }
3249            }
3250            _ => {}
3251        }
3252        return;
3253    }
3254    // Navigate deeper.
3255    match node {
3256        JsonValue::Object(entries) => {
3257            if let Some((_, child)) = entries.iter_mut().find(|(k, _)| k == step) {
3258                delete_at_path(child, &path[1..]);
3259            }
3260        }
3261        JsonValue::Array(items) => {
3262            if let Ok(idx) = step.parse::<i64>() {
3263                let len = items.len() as i64;
3264                let real = if idx >= 0 { idx } else { len + idx };
3265                if real >= 0 && real < len {
3266                    delete_at_path(&mut items[real as usize], &path[1..]);
3267                }
3268            }
3269        }
3270        _ => {}
3271    }
3272}
3273
3274fn set_at_path(node: &mut JsonValue, path: &[String], new_val: JsonValue, create_missing: bool) {
3275    if path.is_empty() {
3276        *node = new_val;
3277        return;
3278    }
3279    let step = &path[0];
3280    let rest = &path[1..];
3281    match node {
3282        JsonValue::Object(entries) => {
3283            if let Some(pos) = entries.iter().position(|(k, _)| k == step) {
3284                if rest.is_empty() {
3285                    entries[pos].1 = new_val;
3286                } else {
3287                    set_at_path(&mut entries[pos].1, rest, new_val, create_missing);
3288                }
3289            } else if create_missing && rest.is_empty() {
3290                entries.push((step.clone(), new_val));
3291            }
3292            // Missing intermediate path with create_missing — PG only
3293            // creates the LEAF, never intermediate parents. No-op.
3294        }
3295        JsonValue::Array(items) => {
3296            let Some(idx) = resolve_array_index(step, items.len()) else {
3297                if create_missing && rest.is_empty() {
3298                    // PG: positive overshoot appends, negative prepends.
3299                    if let Ok(n) = step.parse::<i64>() {
3300                        if n < 0 {
3301                            items.insert(0, new_val);
3302                        } else {
3303                            items.push(new_val);
3304                        }
3305                    }
3306                }
3307                return;
3308            };
3309            if rest.is_empty() {
3310                items[idx] = new_val;
3311            } else {
3312                set_at_path(&mut items[idx], rest, new_val, create_missing);
3313            }
3314        }
3315        _ => {
3316            // Scalar — no replacement possible at non-empty path.
3317        }
3318    }
3319}
3320
3321fn resolve_array_index(step: &str, len: usize) -> Option<usize> {
3322    let n = step.parse::<i64>().ok()?;
3323    if n >= 0 {
3324        let i = n as usize;
3325        if i < len { Some(i) } else { None }
3326    } else {
3327        let from_end = len as i64 + n;
3328        if from_end >= 0 {
3329            Some(from_end as usize)
3330        } else {
3331            None
3332        }
3333    }
3334}
3335
3336/// `jsonb_insert(target, path, new_value [, insert_after])` —
3337/// insert at path. `insert_after` defaults to false.
3338///
3339///   * Array parent: insert before (or after) the index. Out-of-range
3340///     positive index → append; out-of-range negative → prepend.
3341///   * Object parent: key must NOT exist (PG raises). insert_after
3342///     has no effect for objects.
3343pub fn insert(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3344    if !(3..=4).contains(&args.len()) {
3345        return Err(EvalError::TypeMismatch {
3346            detail: alloc::format!("jsonb_insert() takes 3 or 4 args, got {}", args.len()),
3347        });
3348    }
3349    if args.iter().take(3).any(|v| matches!(v, Value::Null)) {
3350        return Ok(Value::Null);
3351    }
3352    let insert_after = match args.get(3) {
3353        None | Some(Value::Null) => false,
3354        Some(Value::Bool(b)) => *b,
3355        Some(other) => {
3356            return Err(EvalError::TypeMismatch {
3357                detail: alloc::format!(
3358                    "jsonb_insert() insert_after must be BOOL, got {}",
3359                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
3360                ),
3361            });
3362        }
3363    };
3364    let doc_text = json_text_arg(&args[0], "jsonb_insert", "target")?;
3365    let path = path_text_arg(&args[1], "jsonb_insert")?;
3366    let new_text = json_text_arg(&args[2], "jsonb_insert", "new_value")?;
3367    let mut root = parse(doc_text).map_err(|e| EvalError::TypeMismatch {
3368        detail: alloc::format!("jsonb_insert(): invalid JSON target — {e}"),
3369    })?;
3370    // v7.39 (round 234) — PG returns the document untouched for an empty
3371    // path (SPG raised its own error) and refuses a scalar target with the
3372    // same wording jsonb_set uses.
3373    if path.is_empty() {
3374        return Ok(Value::json(root.to_json_text()));
3375    }
3376    if is_json_scalar(&root) {
3377        return Err(EvalError::TypeMismatch {
3378            detail: "cannot set path in scalar".into(),
3379        });
3380    }
3381    let new_val = parse(new_text).map_err(|e| EvalError::TypeMismatch {
3382        detail: alloc::format!("jsonb_insert(): invalid JSON new_value — {e}"),
3383    })?;
3384    insert_at_path(&mut root, &path, new_val, insert_after)?;
3385    Ok(Value::json(root.to_json_text()))
3386}
3387
3388fn insert_at_path(
3389    node: &mut JsonValue,
3390    path: &[String],
3391    new_val: JsonValue,
3392    insert_after: bool,
3393) -> Result<(), EvalError> {
3394    debug_assert!(!path.is_empty());
3395    if path.len() == 1 {
3396        let step = &path[0];
3397        match node {
3398            JsonValue::Object(entries) => {
3399                if entries.iter().any(|(k, _)| k == step) {
3400                    return Err(EvalError::TypeMismatch {
3401                        detail: alloc::format!(
3402                            "jsonb_insert(): cannot replace existing key {step:?}"
3403                        ),
3404                    });
3405                }
3406                entries.push((step.clone(), new_val));
3407                Ok(())
3408            }
3409            JsonValue::Array(items) => {
3410                let Ok(n) = step.parse::<i64>() else {
3411                    return Err(EvalError::TypeMismatch {
3412                        detail: alloc::format!(
3413                            "jsonb_insert(): array step must be integer, got {step:?}"
3414                        ),
3415                    });
3416                };
3417                let mut idx = if n >= 0 {
3418                    let i = n as usize;
3419                    if i > items.len() { items.len() } else { i }
3420                } else {
3421                    let from_end = items.len() as i64 + n;
3422                    if from_end < 0 { 0 } else { from_end as usize }
3423                };
3424                if insert_after && idx < items.len() {
3425                    idx += 1;
3426                }
3427                items.insert(idx, new_val);
3428                Ok(())
3429            }
3430            _ => Err(EvalError::TypeMismatch {
3431                detail: "jsonb_insert(): parent at path is a scalar".into(),
3432            }),
3433        }
3434    } else {
3435        let step = &path[0];
3436        let rest = &path[1..];
3437        match node {
3438            JsonValue::Object(entries) => {
3439                if let Some(pos) = entries.iter().position(|(k, _)| k == step) {
3440                    insert_at_path(&mut entries[pos].1, rest, new_val, insert_after)
3441                } else {
3442                    Err(EvalError::TypeMismatch {
3443                        detail: alloc::format!("jsonb_insert(): path {step:?} does not exist"),
3444                    })
3445                }
3446            }
3447            JsonValue::Array(items) => {
3448                let Some(idx) = resolve_array_index(step, items.len()) else {
3449                    return Err(EvalError::TypeMismatch {
3450                        detail: alloc::format!("jsonb_insert(): array index {step:?} out of range"),
3451                    });
3452                };
3453                insert_at_path(&mut items[idx], rest, new_val, insert_after)
3454            }
3455            _ => Err(EvalError::TypeMismatch {
3456                detail: "jsonb_insert(): parent at path is a scalar".into(),
3457            }),
3458        }
3459    }
3460}
3461
3462fn json_text_arg<'a>(v: &'a Value, fname: &str, role: &str) -> Result<&'a str, EvalError> {
3463    match v {
3464        Value::Json(s) | Value::Text(s) => Ok(s.as_ref()),
3465        other => Err(EvalError::TypeMismatch {
3466            detail: alloc::format!(
3467                "{fname}() {role} must be JSON or TEXT, got {}",
3468                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3469            ),
3470        }),
3471    }
3472}
3473
3474fn path_text_arg(v: &Value, fname: &str) -> Result<Vec<String>, EvalError> {
3475    match v {
3476        Value::Text(s) | Value::Json(s) => parse_text_array(s.as_ref()),
3477        Value::TextArray(items) => Ok(items
3478            .iter()
3479            .map(|o| o.clone().unwrap_or_default())
3480            .collect()),
3481        other => Err(EvalError::TypeMismatch {
3482            detail: alloc::format!(
3483                "{fname}() path must be TEXT[] or TEXT, got {}",
3484                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3485            ),
3486        }),
3487    }
3488}
3489
3490#[cfg(test)]
3491mod tests {
3492    use super::*;
3493
3494    fn canon(s: &str) -> String {
3495        canonicalize_jsonb(s).unwrap()
3496    }
3497
3498    #[test]
3499    fn canon_number_rules() {
3500        // Values from live PG 18.4 jsonb.
3501        assert_eq!(canon_json_number("1.0"), "1.0");
3502        assert_eq!(canon_json_number("1e2"), "100");
3503        assert_eq!(canon_json_number("1.10"), "1.10");
3504        assert_eq!(canon_json_number("100.00"), "100.00");
3505        assert_eq!(canon_json_number("0.5"), "0.5");
3506        assert_eq!(canon_json_number("-0"), "0");
3507        assert_eq!(canon_json_number("1E-3"), "0.001");
3508        assert_eq!(canon_json_number("42"), "42");
3509        assert_eq!(canon_json_number("-2.5"), "-2.5");
3510        assert_eq!(canon_json_number("2.5e3"), "2500");
3511    }
3512
3513    #[test]
3514    fn canon_key_order_dedup_and_whitespace() {
3515        // Keys sort by (length, bytes); ""/a/b/z/aa. PG 18.4.
3516        assert_eq!(
3517            canon(r#"{"b":1,"a":2,"aa":3,"":9,"z":4}"#),
3518            r#"{"": 9, "a": 2, "b": 1, "z": 4, "aa": 3}"#
3519        );
3520        // Duplicate keys collapse last-wins.
3521        assert_eq!(canon(r#"{"a":1,"a":2,"a":3}"#), r#"{"a": 3}"#);
3522        // Arrays get `, ` and are not reordered.
3523        assert_eq!(canon("[3,2,1]"), "[3, 2, 1]");
3524    }
3525
3526    #[test]
3527    fn json_number_equality_by_value() {
3528        let eq = |a: &str, b: &str| json_eq(&parse(a).unwrap(), &parse(b).unwrap());
3529        assert!(eq("1", "1.0"));
3530        assert!(eq("1.50", "1.5"));
3531        assert!(eq("1e3", "1000.00"));
3532        assert!(eq("0", "-0"));
3533        assert!(eq("2.5e3", "2500"));
3534        assert!(!eq("1.5", "1.6"));
3535        // Inside arrays / objects.
3536        assert!(eq("[1, 2.0]", "[1.0, 2]"));
3537        assert!(eq(r#"{"a":1}"#, r#"{"a":1.0}"#));
3538    }
3539
3540    #[test]
3541    fn canon_nested_and_scalars() {
3542        assert_eq!(
3543            canon(r#"{"x":{"b":1,"a":2},"y":[3,{"d":1,"c":2}]}"#),
3544            r#"{"x": {"a": 2, "b": 1}, "y": [3, {"c": 2, "d": 1}]}"#
3545        );
3546        assert_eq!(canon("  true "), "true");
3547        assert_eq!(canon(" 42 "), "42");
3548        assert_eq!(canon("{}"), "{}");
3549        assert_eq!(canon("[]"), "[]");
3550        // Non-ASCII stays verbatim UTF-8; escapes preserved.
3551        assert_eq!(
3552            canon(r#"{"e":"café","t":"a\nb"}"#),
3553            r#"{"e": "café", "t": "a\nb"}"#
3554        );
3555    }
3556
3557    #[test]
3558    fn parse_atoms() {
3559        assert_eq!(parse("null").unwrap(), JsonValue::Null);
3560        assert_eq!(parse("true").unwrap(), JsonValue::Bool(true));
3561        assert_eq!(parse("false").unwrap(), JsonValue::Bool(false));
3562        assert_eq!(
3563            parse("\"hello\"").unwrap(),
3564            JsonValue::String("hello".into())
3565        );
3566        assert!(matches!(
3567            parse("42").unwrap(),
3568            JsonValue::NumberText(ref s) if s == "42"
3569        ));
3570    }
3571
3572    #[test]
3573    fn parse_nested() {
3574        let doc = parse(r#"{"a":1,"b":[true,null,"x"]}"#).unwrap();
3575        let JsonValue::Object(entries) = doc else {
3576            panic!("expected object");
3577        };
3578        assert_eq!(entries.len(), 2);
3579        assert_eq!(entries[0].0, "a");
3580        assert_eq!(entries[1].0, "b");
3581    }
3582
3583    #[test]
3584    fn parse_string_escapes() {
3585        let s = parse(r#""he said \"hi\" and\\then\n""#).unwrap();
3586        assert_eq!(s, JsonValue::String("he said \"hi\" and\\then\n".into()));
3587    }
3588
3589    #[test]
3590    fn parse_unicode_escape() {
3591        assert_eq!(parse(r#""é""#).unwrap(), JsonValue::String("é".into()));
3592    }
3593
3594    #[test]
3595    fn path_object_key_returns_value() {
3596        let doc = Value::json::<String>(r#"{"name":"alice","age":30}"#.into());
3597        let key = Value::text("name");
3598        let v = path_get(&doc, &key, true).unwrap();
3599        assert_eq!(v, Value::text("alice"));
3600        let v = path_get(&doc, &key, false).unwrap();
3601        assert_eq!(v, Value::json("\"alice\""));
3602    }
3603
3604    #[test]
3605    fn path_array_index_supports_negative() {
3606        let doc = Value::json("[10,20,30]");
3607        let v = path_get(&doc, &Value::Int(1), true).unwrap();
3608        assert_eq!(v, Value::text("20"));
3609        let v = path_get(&doc, &Value::Int(-1), true).unwrap();
3610        assert_eq!(v, Value::text("30"));
3611    }
3612
3613    #[test]
3614    fn path_missing_key_returns_null() {
3615        let doc = Value::json::<String>(r#"{"a":1}"#.into());
3616        let v = path_get(&doc, &Value::text("missing"), true).unwrap();
3617        assert_eq!(v, Value::Null);
3618    }
3619
3620    #[test]
3621    fn path_get_nested_subtree_is_verbatim() {
3622        // v7.38 (read01) — PG returns the located value's EXACT source text, so
3623        // a compact source stays compact (verified against PG18.4: `->` on this
3624        // doc yields `{"x":[1,2]}`, not the canonical `{"x": [1, 2]}`). A jsonb
3625        // column reaches here already canonicalized, so slicing it still yields
3626        // canonical text.
3627        let doc = Value::json::<String>(r#"{"k":{"x":[1,2]}}"#.into());
3628        let v = path_get(&doc, &Value::text("k"), false).unwrap();
3629        assert_eq!(v, Value::json::<String>(r#"{"x":[1,2]}"#.into()));
3630
3631        // A canonical (jsonb-shaped) source slices back to canonical text.
3632        let canon = Value::json::<String>(r#"{"k": {"x": [1, 2]}}"#.into());
3633        let v = path_get(&canon, &Value::text("k"), false).unwrap();
3634        assert_eq!(v, Value::json::<String>(r#"{"x": [1, 2]}"#.into()));
3635
3636        // Whitespace, number lexemes and duplicate keys all survive; a
3637        // duplicate key resolves to the LAST occurrence, as in PG.
3638        let raw = Value::json::<String>(r#"{"a":{ "y" : 2e2 },"k":1,"k":2}"#.into());
3639        assert_eq!(
3640            path_get(&raw, &Value::text("a"), false).unwrap(),
3641            Value::json::<String>(r#"{ "y" : 2e2 }"#.into())
3642        );
3643        assert_eq!(
3644            path_get(&raw, &Value::text("k"), false).unwrap(),
3645            Value::json::<String>("2".into())
3646        );
3647
3648        // `->` on a JSON null yields the JSON null; `->>` yields SQL NULL.
3649        let n = Value::json::<String>(r#"{"a":null}"#.into());
3650        assert_eq!(
3651            path_get(&n, &Value::text("a"), false).unwrap(),
3652            Value::json::<String>("null".into())
3653        );
3654        assert_eq!(path_get(&n, &Value::text("a"), true).unwrap(), Value::Null);
3655    }
3656}
3657
3658/// v7.37.17 (17.6 siblings) — one step of a MySQL JSON path
3659/// (`$.key`, `$."quoted key"`, `$[0]`).
3660#[derive(Debug)]
3661pub enum MysqlPathStep {
3662    Key(String),
3663    Index(usize),
3664}
3665
3666/// Parse a MySQL JSON path. Supports `$`, `.key`, `."quoted key"`
3667/// and `[N]`; wildcard steps (`*`, `[*]`, `**`) error honestly —
3668/// they return multiple matches per document and need a different
3669/// walker shape.
3670pub fn mysql_path_steps(path: &str) -> Result<Vec<MysqlPathStep>, EvalError> {
3671    let chars: Vec<char> = path.trim().chars().collect();
3672    if chars.first() != Some(&'$') {
3673        return Err(EvalError::TypeMismatch {
3674            detail: alloc::format!("invalid JSON path expression (must start with $): {path:?}"),
3675        });
3676    }
3677    let mut steps = Vec::new();
3678    let mut i = 1;
3679    while i < chars.len() {
3680        match chars[i] {
3681            '.' => {
3682                i += 1;
3683                if i < chars.len() && chars[i] == '"' {
3684                    i += 1;
3685                    let mut key = String::new();
3686                    while i < chars.len() && chars[i] != '"' {
3687                        if chars[i] == '\\' && i + 1 < chars.len() {
3688                            i += 1;
3689                        }
3690                        key.push(chars[i]);
3691                        i += 1;
3692                    }
3693                    if i >= chars.len() {
3694                        return Err(EvalError::TypeMismatch {
3695                            detail: alloc::format!(
3696                                "invalid JSON path expression (unterminated quote): {path:?}"
3697                            ),
3698                        });
3699                    }
3700                    i += 1; // closing quote
3701                    steps.push(MysqlPathStep::Key(key));
3702                } else {
3703                    let mut key = String::new();
3704                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
3705                        key.push(chars[i]);
3706                        i += 1;
3707                    }
3708                    if key.is_empty() {
3709                        return Err(EvalError::TypeMismatch {
3710                            detail: alloc::format!(
3711                                "unsupported JSON path step at position {i} in {path:?} \
3712                                 (wildcards are not supported)"
3713                            ),
3714                        });
3715                    }
3716                    steps.push(MysqlPathStep::Key(key));
3717                }
3718            }
3719            '[' => {
3720                i += 1;
3721                let mut num = String::new();
3722                while i < chars.len() && chars[i] != ']' {
3723                    num.push(chars[i]);
3724                    i += 1;
3725                }
3726                if i >= chars.len() {
3727                    return Err(EvalError::TypeMismatch {
3728                        detail: alloc::format!(
3729                            "invalid JSON path expression (unterminated bracket): {path:?}"
3730                        ),
3731                    });
3732                }
3733                i += 1; // ]
3734                let idx: usize = num.trim().parse().map_err(|_| EvalError::TypeMismatch {
3735                    detail: alloc::format!(
3736                        "unsupported JSON path index {num:?} in {path:?} \
3737                         (wildcards are not supported)"
3738                    ),
3739                })?;
3740                steps.push(MysqlPathStep::Index(idx));
3741            }
3742            other => {
3743                return Err(EvalError::TypeMismatch {
3744                    detail: alloc::format!(
3745                        "invalid JSON path expression (unexpected {other:?}): {path:?}"
3746                    ),
3747                });
3748            }
3749        }
3750    }
3751    Ok(steps)
3752}
3753
3754/// Walk a parsed JSON document along a MySQL path. Returns None
3755/// when any step misses.
3756pub fn mysql_path_get<'a>(doc: &'a JsonValue, steps: &[MysqlPathStep]) -> Option<&'a JsonValue> {
3757    let mut cur = doc;
3758    for step in steps {
3759        match (step, cur) {
3760            (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
3761                cur = members.iter().find(|(mk, _)| mk == k).map(|(_, v)| v)?;
3762            }
3763            (MysqlPathStep::Index(idx), JsonValue::Array(items)) => {
3764                cur = items.get(*idx)?;
3765            }
3766            // MySQL: a non-array auto-wraps as a one-element array
3767            // for [0].
3768            (MysqlPathStep::Index(0), scalar) => {
3769                cur = scalar;
3770            }
3771            _ => return None,
3772        }
3773    }
3774    Some(cur)
3775}
3776
3777/// v7.37.17 (17.6 siblings) — MySQL JSON_EXTRACT(doc, path...).
3778/// One path → the value at that path (or SQL NULL when it misses);
3779/// several paths → a JSON array of the values that matched (NULL
3780/// when none did).
3781pub fn mysql_json_extract(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3782    if args.len() < 2 {
3783        return Err(EvalError::TypeMismatch {
3784            detail: alloc::format!(
3785                "json_extract() takes a document and at least one path, got {} args",
3786                args.len()
3787            ),
3788        });
3789    }
3790    if args.iter().any(|a| matches!(a, Value::Null)) {
3791        return Ok(Value::Null);
3792    }
3793    let src = match &args[0] {
3794        Value::Json(s) | Value::Text(s) => s.as_ref(),
3795        other => {
3796            return Err(EvalError::TypeMismatch {
3797                detail: alloc::format!(
3798                    "json_extract() document must be json, got {}",
3799                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
3800                ),
3801            });
3802        }
3803    };
3804    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
3805        detail: alloc::format!("json_extract(): invalid JSON: {e}"),
3806    })?;
3807    let mut hits: Vec<String> = Vec::new();
3808    for path_v in &args[1..] {
3809        let Value::Text(p) = path_v else {
3810            return Err(EvalError::TypeMismatch {
3811                detail: alloc::format!(
3812                    "json_extract() paths must be text, got {}",
3813                    crate::conversions::pg_type_name_for_error_opt(path_v.data_type())
3814                ),
3815            });
3816        };
3817        let steps = mysql_path_steps(p)?;
3818        if let Some(v) = mysql_path_get(&doc, &steps) {
3819            hits.push(v.to_json_text());
3820        }
3821    }
3822    match (args.len() - 1, hits.len()) {
3823        (_, 0) => Ok(Value::Null),
3824        (1, _) => Ok(Value::Json(alloc::borrow::Cow::Owned(
3825            hits.into_iter().next().unwrap(),
3826        ))),
3827        _ => {
3828            let mut out = String::from("[");
3829            for (i, h) in hits.iter().enumerate() {
3830                if i > 0 {
3831                    out.push_str(", ");
3832                }
3833                out.push_str(h);
3834            }
3835            out.push(']');
3836            Ok(Value::Json(alloc::borrow::Cow::Owned(out)))
3837        }
3838    }
3839}
3840
3841/// v7.37.17 (17.6 siblings) — MySQL JSON_CONTAINS_PATH(doc,
3842/// 'one'|'all', path...).
3843pub fn mysql_json_contains_path(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3844    if args.len() < 3 {
3845        return Err(EvalError::TypeMismatch {
3846            detail: alloc::format!(
3847                "json_contains_path() takes a document, one/all, and at least one path, got {} args",
3848                args.len()
3849            ),
3850        });
3851    }
3852    if args.iter().any(|a| matches!(a, Value::Null)) {
3853        return Ok(Value::Null);
3854    }
3855    let src = match &args[0] {
3856        Value::Json(s) | Value::Text(s) => s.as_ref(),
3857        other => {
3858            return Err(EvalError::TypeMismatch {
3859                detail: alloc::format!(
3860                    "json_contains_path() document must be json, got {}",
3861                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
3862                ),
3863            });
3864        }
3865    };
3866    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
3867        detail: alloc::format!("json_contains_path(): invalid JSON: {e}"),
3868    })?;
3869    let mode = match &args[1] {
3870        Value::Text(m) if m.eq_ignore_ascii_case("one") => false,
3871        Value::Text(m) if m.eq_ignore_ascii_case("all") => true,
3872        other => {
3873            return Err(EvalError::TypeMismatch {
3874                detail: alloc::format!(
3875                    "json_contains_path() second arg must be 'one' or 'all', got {other:?}"
3876                ),
3877            });
3878        }
3879    };
3880    let mut found_any = false;
3881    let mut found_all = true;
3882    for path_v in &args[2..] {
3883        let Value::Text(p) = path_v else {
3884            return Err(EvalError::TypeMismatch {
3885                detail: alloc::format!(
3886                    "json_contains_path() paths must be text, got {}",
3887                    crate::conversions::pg_type_name_for_error_opt(path_v.data_type())
3888                ),
3889            });
3890        };
3891        let steps = mysql_path_steps(p)?;
3892        if mysql_path_get(&doc, &steps).is_some() {
3893            found_any = true;
3894        } else {
3895            found_all = false;
3896        }
3897    }
3898    Ok(Value::Bool(if mode { found_all } else { found_any }))
3899}
3900
3901/// v7.37.17 (17.6 siblings) — convert a SQL value into a JsonValue
3902/// for the MySQL JSON mutation functions (SQL text becomes a JSON
3903/// string; JSON passes through parsed).
3904fn value_to_jsonvalue(v: &Value) -> Result<JsonValue, EvalError> {
3905    Ok(match v {
3906        Value::Null => JsonValue::Null,
3907        Value::Bool(b) => JsonValue::Bool(*b),
3908        Value::Json(s) => parse(s).map_err(|e| EvalError::TypeMismatch {
3909            detail: alloc::format!("invalid JSON value: {e}"),
3910        })?,
3911        Value::Text(s) => JsonValue::String(s.to_string()),
3912        // v7.38 (read01, T9) — a composite becomes a JSON object keyed by field
3913        // name (`row_to_json(row(1,'a'))` → `{"f1":1,"f2":"a"}`).
3914        Value::Composite(fields) => {
3915            let mut entries = alloc::vec::Vec::with_capacity(fields.len());
3916            for (name, fv) in fields.iter() {
3917                entries.push((name.clone(), value_to_jsonvalue(fv)?));
3918            }
3919            JsonValue::Object(entries)
3920        }
3921        other => {
3922            // Numbers and everything else render through the
3923            // to_json text form, then parse back.
3924            let text = value_to_json_text(other);
3925            parse(&text).map_err(|e| EvalError::TypeMismatch {
3926                detail: alloc::format!("invalid JSON value: {e}"),
3927            })?
3928        }
3929    })
3930}
3931
3932#[derive(Clone, Copy, PartialEq, Debug)]
3933enum MutateMode {
3934    /// json_set — replace existing, create missing.
3935    Set,
3936    /// json_insert — create missing only.
3937    Insert,
3938    /// json_replace — replace existing only.
3939    Replace,
3940}
3941
3942/// Apply one path mutation. Missing intermediate steps are a no-op
3943/// (MySQL: only the final step may be created).
3944fn mutate_at(cur: &mut JsonValue, steps: &[MysqlPathStep], mode: MutateMode, newval: &JsonValue) {
3945    match steps {
3946        [] => {
3947            if matches!(mode, MutateMode::Set | MutateMode::Replace) {
3948                *cur = newval.clone();
3949            }
3950        }
3951        [last] => match (last, &mut *cur) {
3952            (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
3953                if let Some(slot) = members.iter_mut().find(|(mk, _)| mk == k) {
3954                    if matches!(mode, MutateMode::Set | MutateMode::Replace) {
3955                        slot.1 = newval.clone();
3956                    }
3957                } else if matches!(mode, MutateMode::Set | MutateMode::Insert) {
3958                    members.push((k.clone(), newval.clone()));
3959                }
3960            }
3961            (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
3962                if *i < items.len() {
3963                    if matches!(mode, MutateMode::Set | MutateMode::Replace) {
3964                        items[*i] = newval.clone();
3965                    }
3966                } else if matches!(mode, MutateMode::Set | MutateMode::Insert) {
3967                    // Index past the end appends (MySQL semantics).
3968                    items.push(newval.clone());
3969                }
3970            }
3971            // Scalar auto-wraps as a one-element array: [0] exists.
3972            (MysqlPathStep::Index(0), scalar) => {
3973                if matches!(mode, MutateMode::Set | MutateMode::Replace) {
3974                    *scalar = newval.clone();
3975                }
3976            }
3977            _ => {}
3978        },
3979        [head, rest @ ..] => match (head, cur) {
3980            (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
3981                if let Some(slot) = members.iter_mut().find(|(mk, _)| mk == k) {
3982                    mutate_at(&mut slot.1, rest, mode, newval);
3983                }
3984            }
3985            (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
3986                if let Some(slot) = items.get_mut(*i) {
3987                    mutate_at(slot, rest, mode, newval);
3988                }
3989            }
3990            _ => {}
3991        },
3992    }
3993}
3994
3995fn mysql_json_mutate(
3996    args: &[Value<'_>],
3997    mode: MutateMode,
3998    fn_name: &str,
3999) -> Result<Value<'static>, EvalError> {
4000    if args.len() < 3 || args.len() % 2 == 0 {
4001        return Err(EvalError::TypeMismatch {
4002            detail: alloc::format!(
4003                "{fn_name}() takes a document plus (path, value) pairs, got {} args",
4004                args.len()
4005            ),
4006        });
4007    }
4008    if matches!(args[0], Value::Null) {
4009        return Ok(Value::Null);
4010    }
4011    let src = match &args[0] {
4012        Value::Json(s) | Value::Text(s) => s.as_ref(),
4013        other => {
4014            return Err(EvalError::TypeMismatch {
4015                detail: alloc::format!(
4016                    "{fn_name}() document must be json, got {}",
4017                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4018                ),
4019            });
4020        }
4021    };
4022    let mut doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4023        detail: alloc::format!("{fn_name}(): invalid JSON: {e}"),
4024    })?;
4025    for pair in args[1..].chunks(2) {
4026        let Value::Text(p) = &pair[0] else {
4027            if matches!(pair[0], Value::Null) {
4028                return Ok(Value::Null);
4029            }
4030            return Err(EvalError::TypeMismatch {
4031                detail: alloc::format!(
4032                    "{fn_name}() paths must be text, got {}",
4033                    crate::conversions::pg_type_name_for_error_opt(pair[0].data_type())
4034                ),
4035            });
4036        };
4037        let steps = mysql_path_steps(p)?;
4038        let newval = value_to_jsonvalue(&pair[1])?;
4039        mutate_at(&mut doc, &steps, mode, &newval);
4040    }
4041    // v7.39 (round 392) — MariaDB renders JSON with `": "` / `", "` spacing
4042    // (`{"a": 1, "b": 2}`); canonicalise so JSON_SET / INSERT / REPLACE /
4043    // REMOVE match, like JSON_OBJECT (r391).
4044    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4045        doc.to_json_text(),
4046    ))))
4047}
4048
4049/// v7.37.17 (17.6 siblings) — MySQL JSON_SET / JSON_INSERT /
4050/// JSON_REPLACE ('$.x'-path forms; the PG jsonb_set text-array
4051/// spelling stays on crate::json::set).
4052pub fn mysql_json_set(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4053    mysql_json_mutate(args, MutateMode::Set, "json_set")
4054}
4055
4056pub fn mysql_json_insert(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4057    mysql_json_mutate(args, MutateMode::Insert, "json_insert")
4058}
4059
4060pub fn mysql_json_replace(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4061    mysql_json_mutate(args, MutateMode::Replace, "json_replace")
4062}
4063
4064/// v7.37.17 (17.6 siblings) — MySQL JSON_REMOVE(doc, path...).
4065/// Removing the root path `$` errors, as in MySQL.
4066pub fn mysql_json_remove(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4067    if args.len() < 2 {
4068        return Err(EvalError::TypeMismatch {
4069            detail: alloc::format!(
4070                "json_remove() takes a document and at least one path, got {} args",
4071                args.len()
4072            ),
4073        });
4074    }
4075    if args.iter().any(|a| matches!(a, Value::Null)) {
4076        return Ok(Value::Null);
4077    }
4078    let src = match &args[0] {
4079        Value::Json(s) | Value::Text(s) => s.as_ref(),
4080        other => {
4081            return Err(EvalError::TypeMismatch {
4082                detail: alloc::format!(
4083                    "json_remove() document must be json, got {}",
4084                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4085                ),
4086            });
4087        }
4088    };
4089    let mut doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4090        detail: alloc::format!("json_remove(): invalid JSON: {e}"),
4091    })?;
4092    fn remove_at(cur: &mut JsonValue, steps: &[MysqlPathStep]) {
4093        match steps {
4094            [] => {}
4095            [last] => match (last, cur) {
4096                (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4097                    members.retain(|(mk, _)| mk != k);
4098                }
4099                (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
4100                    if *i < items.len() {
4101                        items.remove(*i);
4102                    }
4103                }
4104                _ => {}
4105            },
4106            [head, rest @ ..] => match (head, cur) {
4107                (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4108                    if let Some(slot) = members.iter_mut().find(|(mk, _)| mk == k) {
4109                        remove_at(&mut slot.1, rest);
4110                    }
4111                }
4112                (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
4113                    if let Some(slot) = items.get_mut(*i) {
4114                        remove_at(slot, rest);
4115                    }
4116                }
4117                _ => {}
4118            },
4119        }
4120    }
4121    for path_v in &args[1..] {
4122        let Value::Text(p) = path_v else {
4123            return Err(EvalError::TypeMismatch {
4124                detail: alloc::format!(
4125                    "json_remove() paths must be text, got {}",
4126                    crate::conversions::pg_type_name_for_error_opt(path_v.data_type())
4127                ),
4128            });
4129        };
4130        let steps = mysql_path_steps(p)?;
4131        if steps.is_empty() {
4132            return Err(EvalError::TypeMismatch {
4133                detail: "The path expression '$' is not allowed in this context".into(),
4134            });
4135        }
4136        remove_at(&mut doc, &steps);
4137    }
4138    // v7.39 (round 392) — MariaDB's `": "` / `", "` JSON render spacing.
4139    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4140        doc.to_json_text(),
4141    ))))
4142}
4143
4144/// Apply `f` to the value AT the full path (not its parent). Missing
4145/// steps are a no-op.
4146fn modify_at(cur: &mut JsonValue, steps: &[MysqlPathStep], f: &mut dyn FnMut(&mut JsonValue)) {
4147    match steps {
4148        [] => f(cur),
4149        [head, rest @ ..] => match (head, cur) {
4150            (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4151                if let Some(slot) = members.iter_mut().find(|(mk, _)| mk == k) {
4152                    modify_at(&mut slot.1, rest, f);
4153                }
4154            }
4155            (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
4156                if let Some(slot) = items.get_mut(*i) {
4157                    modify_at(slot, rest, f);
4158                }
4159            }
4160            _ => {}
4161        },
4162    }
4163}
4164
4165/// Shared arg plumbing for the (doc, path, value)-pairs mutators.
4166fn mysql_doc_and_pairs<'a>(
4167    args: &'a [Value<'_>],
4168    fn_name: &str,
4169) -> Result<Option<(JsonValue, &'a [Value<'a>])>, EvalError> {
4170    if args.len() < 3 || args.len() % 2 == 0 {
4171        return Err(EvalError::TypeMismatch {
4172            detail: alloc::format!(
4173                "{fn_name}() takes a document plus (path, value) pairs, got {} args",
4174                args.len()
4175            ),
4176        });
4177    }
4178    if args.iter().any(|a| matches!(a, Value::Null)) {
4179        return Ok(None);
4180    }
4181    let src = match &args[0] {
4182        Value::Json(s) | Value::Text(s) => s.as_ref(),
4183        other => {
4184            return Err(EvalError::TypeMismatch {
4185                detail: alloc::format!(
4186                    "{fn_name}() document must be json, got {}",
4187                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4188                ),
4189            });
4190        }
4191    };
4192    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4193        detail: alloc::format!("{fn_name}(): invalid JSON: {e}"),
4194    })?;
4195    Ok(Some((doc, &args[1..])))
4196}
4197
4198/// v7.37.17 (17.6 siblings) — MySQL JSON_ARRAY_APPEND(doc, path,
4199/// val, ...). The value at path gains `val` at the end; a non-array
4200/// value wraps as `[old, val]` (MySQL semantics).
4201pub fn mysql_json_array_append(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4202    let Some((mut doc, pairs)) = mysql_doc_and_pairs(args, "json_array_append")? else {
4203        return Ok(Value::Null);
4204    };
4205    for pair in pairs.chunks(2) {
4206        let Value::Text(p) = &pair[0] else {
4207            return Err(EvalError::TypeMismatch {
4208                detail: alloc::format!(
4209                    "json_array_append() paths must be text, got {}",
4210                    crate::conversions::pg_type_name_for_error_opt(pair[0].data_type())
4211                ),
4212            });
4213        };
4214        let steps = mysql_path_steps(p)?;
4215        let newval = value_to_jsonvalue(&pair[1])?;
4216        modify_at(&mut doc, &steps, &mut |v| match v {
4217            JsonValue::Array(items) => items.push(newval.clone()),
4218            other => {
4219                let old = core::mem::replace(other, JsonValue::Null);
4220                *other = JsonValue::Array(alloc::vec![old, newval.clone()]);
4221            }
4222        });
4223    }
4224    // v7.39 (round 392) — MariaDB's `": "` / `", "` JSON render spacing.
4225    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4226        doc.to_json_text(),
4227    ))))
4228}
4229
4230/// v7.37.17 (17.6 siblings) — MySQL JSON_ARRAY_INSERT(doc, path,
4231/// val, ...). The path must end in `[N]`; the value is inserted at
4232/// position N in the parent array, shifting later elements right
4233/// (past-the-end appends). A non-array parent is a no-op.
4234pub fn mysql_json_array_insert(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4235    let Some((mut doc, pairs)) = mysql_doc_and_pairs(args, "json_array_insert")? else {
4236        return Ok(Value::Null);
4237    };
4238    for pair in pairs.chunks(2) {
4239        let Value::Text(p) = &pair[0] else {
4240            return Err(EvalError::TypeMismatch {
4241                detail: alloc::format!(
4242                    "json_array_insert() paths must be text, got {}",
4243                    crate::conversions::pg_type_name_for_error_opt(pair[0].data_type())
4244                ),
4245            });
4246        };
4247        let steps = mysql_path_steps(p)?;
4248        let Some(MysqlPathStep::Index(idx)) = steps.last() else {
4249            return Err(EvalError::TypeMismatch {
4250                detail: alloc::format!(
4251                    "json_array_insert() path must end with an array index: {p:?}"
4252                ),
4253            });
4254        };
4255        let idx = *idx;
4256        let newval = value_to_jsonvalue(&pair[1])?;
4257        modify_at(&mut doc, &steps[..steps.len() - 1], &mut |v| {
4258            if let JsonValue::Array(items) = v {
4259                let at = idx.min(items.len());
4260                items.insert(at, newval.clone());
4261            }
4262        });
4263    }
4264    // v7.39 (round 392) — MariaDB's `": "` / `", "` JSON render spacing.
4265    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4266        doc.to_json_text(),
4267    ))))
4268}
4269
4270/// MySQL JSON containment recursion: candidate object ⊆ target
4271/// object (same keys, contained values); each candidate array
4272/// element contained in some target array element; a candidate
4273/// scalar is contained in an array when it equals some element.
4274fn mysql_contains(target: &JsonValue, cand: &JsonValue) -> bool {
4275    match (target, cand) {
4276        (JsonValue::Object(t), JsonValue::Object(c)) => c
4277            .iter()
4278            .all(|(ck, cv)| t.iter().any(|(tk, tv)| tk == ck && mysql_contains(tv, cv))),
4279        (JsonValue::Array(t), JsonValue::Array(c)) => {
4280            c.iter().all(|cv| t.iter().any(|tv| mysql_contains(tv, cv)))
4281        }
4282        (JsonValue::Array(t), scalar) => t.iter().any(|tv| mysql_contains(tv, scalar)),
4283        // Numbers compare numerically across the two lexeme forms.
4284        (JsonValue::Number(a), JsonValue::NumberText(b))
4285        | (JsonValue::NumberText(b), JsonValue::Number(a)) => {
4286            b.parse::<f64>().map(|x| x == *a).unwrap_or(false)
4287        }
4288        (JsonValue::NumberText(a), JsonValue::NumberText(b)) => {
4289            a == b
4290                || (a.parse::<f64>().ok().zip(b.parse::<f64>().ok()))
4291                    .map(|(x, y)| x == y)
4292                    .unwrap_or(false)
4293        }
4294        (a, b) => a == b,
4295    }
4296}
4297
4298/// v7.37.17 (17.6 siblings) — MySQL JSON_CONTAINS(target, candidate
4299/// [, path]).
4300pub fn mysql_json_contains(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4301    if !matches!(args.len(), 2 | 3) {
4302        return Err(EvalError::TypeMismatch {
4303            detail: alloc::format!("json_contains() takes 2 or 3 args, got {}", args.len()),
4304        });
4305    }
4306    if args.iter().any(|a| matches!(a, Value::Null)) {
4307        return Ok(Value::Null);
4308    }
4309    let parse_arg = |v: &Value<'_>, which: &str| -> Result<JsonValue, EvalError> {
4310        match v {
4311            Value::Json(s) | Value::Text(s) => parse(s).map_err(|e| EvalError::TypeMismatch {
4312                detail: alloc::format!("json_contains(): invalid {which} JSON: {e}"),
4313            }),
4314            other => Err(EvalError::TypeMismatch {
4315                detail: alloc::format!(
4316                    "json_contains() {which} must be json, got {}",
4317                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4318                ),
4319            }),
4320        }
4321    };
4322    let target = parse_arg(&args[0], "target")?;
4323    let cand = parse_arg(&args[1], "candidate")?;
4324    let effective = match args.get(2) {
4325        None => Some(&target),
4326        Some(Value::Text(p)) => {
4327            let steps = mysql_path_steps(p)?;
4328            mysql_path_get(&target, &steps)
4329        }
4330        Some(other) => {
4331            return Err(EvalError::TypeMismatch {
4332                detail: alloc::format!(
4333                    "json_contains() path must be text, got {}",
4334                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4335                ),
4336            });
4337        }
4338    };
4339    match effective {
4340        None => Ok(Value::Null),
4341        Some(t) => Ok(Value::Bool(mysql_contains(t, &cand))),
4342    }
4343}
4344
4345/// RFC 7396 merge-patch: a non-object patch replaces the target;
4346/// an object patch merges key-by-key, with JSON null values
4347/// removing keys.
4348fn merge_patch(target: JsonValue, patch: JsonValue) -> JsonValue {
4349    let JsonValue::Object(patch_members) = patch else {
4350        return patch;
4351    };
4352    let mut out = match target {
4353        JsonValue::Object(members) => members,
4354        _ => Vec::new(),
4355    };
4356    for (k, v) in patch_members {
4357        if matches!(v, JsonValue::Null) {
4358            out.retain(|(mk, _)| *mk != k);
4359        } else if let Some(slot) = out.iter_mut().find(|(mk, _)| *mk == k) {
4360            let old = core::mem::replace(&mut slot.1, JsonValue::Null);
4361            slot.1 = merge_patch(old, v);
4362        } else {
4363            // Merging into a missing key still strips nested nulls.
4364            out.push((k, merge_patch(JsonValue::Null, v)));
4365        }
4366    }
4367    JsonValue::Object(out)
4368}
4369
4370/// MySQL JSON_MERGE_PRESERVE pairwise rule: arrays concatenate,
4371/// objects merge with duplicate-key values merged recursively,
4372/// scalars combine into arrays (a non-array beside an array wraps
4373/// first).
4374fn merge_preserve(a: JsonValue, b: JsonValue) -> JsonValue {
4375    match (a, b) {
4376        (JsonValue::Object(mut ma), JsonValue::Object(mb)) => {
4377            for (k, v) in mb {
4378                if let Some(pos) = ma.iter().position(|(mk, _)| *mk == k) {
4379                    let (_, old) = ma.remove(pos);
4380                    ma.insert(pos, (k, merge_preserve(old, v)));
4381                } else {
4382                    ma.push((k, v));
4383                }
4384            }
4385            JsonValue::Object(ma)
4386        }
4387        (JsonValue::Array(mut xs), JsonValue::Array(ys)) => {
4388            xs.extend(ys);
4389            JsonValue::Array(xs)
4390        }
4391        (JsonValue::Array(mut xs), scalar) => {
4392            xs.push(scalar);
4393            JsonValue::Array(xs)
4394        }
4395        (scalar, JsonValue::Array(ys)) => {
4396            let mut xs = alloc::vec![scalar];
4397            xs.extend(ys);
4398            JsonValue::Array(xs)
4399        }
4400        (sa, sb) => JsonValue::Array(alloc::vec![sa, sb]),
4401    }
4402}
4403
4404fn mysql_json_merge(
4405    args: &[Value<'_>],
4406    fn_name: &str,
4407    combine: fn(JsonValue, JsonValue) -> JsonValue,
4408) -> Result<Value<'static>, EvalError> {
4409    if args.len() < 2 {
4410        return Err(EvalError::TypeMismatch {
4411            detail: alloc::format!("{fn_name}() takes at least 2 documents, got {}", args.len()),
4412        });
4413    }
4414    if args.iter().any(|a| matches!(a, Value::Null)) {
4415        return Ok(Value::Null);
4416    }
4417    let mut acc: Option<JsonValue> = None;
4418    for arg in args {
4419        let src = match arg {
4420            Value::Json(s) | Value::Text(s) => s.as_ref(),
4421            other => {
4422                return Err(EvalError::TypeMismatch {
4423                    detail: alloc::format!(
4424                        "{fn_name}() arguments must be json, got {}",
4425                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
4426                    ),
4427                });
4428            }
4429        };
4430        let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4431            detail: alloc::format!("{fn_name}(): invalid JSON: {e}"),
4432        })?;
4433        acc = Some(match acc {
4434            None => doc,
4435            Some(prev) => combine(prev, doc),
4436        });
4437    }
4438    // v7.39 (round 392) — MariaDB's `": "` / `", "` JSON render spacing.
4439    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4440        acc.unwrap().to_json_text(),
4441    ))))
4442}
4443
4444/// v7.37.17 (17.6 siblings) — MySQL JSON_MERGE_PATCH (RFC 7396).
4445pub fn mysql_json_merge_patch(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4446    mysql_json_merge(args, "json_merge_patch", merge_patch)
4447}
4448
4449/// v7.37.17 (17.6 siblings) — MySQL JSON_MERGE_PRESERVE (and its
4450/// deprecated JSON_MERGE alias).
4451pub fn mysql_json_merge_preserve(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4452    mysql_json_merge(args, "json_merge_preserve", merge_preserve)
4453}
4454
4455/// v7.37.17 (17.6 siblings) — MySQL JSON_OVERLAPS(d1, d2): arrays
4456/// share any element; objects share any key-value pair; scalars
4457/// compare equal; an array vs a scalar checks membership.
4458pub fn mysql_json_overlaps(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4459    if args.len() != 2 {
4460        return Err(EvalError::TypeMismatch {
4461            detail: alloc::format!("json_overlaps() takes 2 args, got {}", args.len()),
4462        });
4463    }
4464    if args.iter().any(|a| matches!(a, Value::Null)) {
4465        return Ok(Value::Null);
4466    }
4467    let parse_arg = |v: &Value<'_>| -> Result<JsonValue, EvalError> {
4468        match v {
4469            Value::Json(s) | Value::Text(s) => parse(s).map_err(|e| EvalError::TypeMismatch {
4470                detail: alloc::format!("json_overlaps(): invalid JSON: {e}"),
4471            }),
4472            other => Err(EvalError::TypeMismatch {
4473                detail: alloc::format!(
4474                    "json_overlaps() arguments must be json, got {}",
4475                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4476                ),
4477            }),
4478        }
4479    };
4480    let a = parse_arg(&args[0])?;
4481    let b = parse_arg(&args[1])?;
4482    let overlaps = match (&a, &b) {
4483        (JsonValue::Array(xs), JsonValue::Array(ys)) => xs.iter().any(|x| {
4484            ys.iter()
4485                .any(|y| mysql_contains(x, y) && mysql_contains(y, x))
4486        }),
4487        (JsonValue::Object(ma), JsonValue::Object(mb)) => ma.iter().any(|(k, v)| {
4488            mb.iter()
4489                .any(|(k2, v2)| k == k2 && mysql_contains(v, v2) && mysql_contains(v2, v))
4490        }),
4491        (JsonValue::Array(xs), scalar) | (scalar, JsonValue::Array(xs)) => xs
4492            .iter()
4493            .any(|x| mysql_contains(x, scalar) && mysql_contains(scalar, x)),
4494        (sa, sb) => mysql_contains(sa, sb) && mysql_contains(sb, sa),
4495    };
4496    Ok(Value::Bool(overlaps))
4497}
4498
4499/// SQL LIKE matcher for json_search: `%` any run, `_` one char,
4500/// `escape` literalises the next char.
4501fn like_match(text: &[char], pat: &[char], escape: char) -> bool {
4502    match pat {
4503        [] => text.is_empty(),
4504        ['%', rest @ ..] => (0..=text.len()).any(|skip| like_match(&text[skip..], rest, escape)),
4505        ['_', rest @ ..] => !text.is_empty() && like_match(&text[1..], rest, escape),
4506        [e, lit, rest @ ..] if *e == escape => {
4507            text.first() == Some(lit) && like_match(&text[1..], rest, escape)
4508        }
4509        [c, rest @ ..] => text.first() == Some(c) && like_match(&text[1..], rest, escape),
4510    }
4511}
4512
4513/// Render one MySQL path step onto a path string. Identifier-shaped
4514/// keys render bare (`$.a`); anything else quotes (`$."a b"`).
4515fn push_path_step(out: &mut String, step_key: Option<&str>, step_idx: Option<usize>) {
4516    if let Some(k) = step_key {
4517        let ident_shaped = !k.is_empty()
4518            && k.chars().all(|c| c.is_alphanumeric() || c == '_')
4519            && !k.chars().next().unwrap().is_numeric();
4520        if ident_shaped {
4521            out.push('.');
4522            out.push_str(k);
4523        } else {
4524            out.push_str(".\"");
4525            for c in k.chars() {
4526                if c == '"' || c == '\\' {
4527                    out.push('\\');
4528                }
4529                out.push(c);
4530            }
4531            out.push('"');
4532        }
4533    }
4534    if let Some(i) = step_idx {
4535        out.push('[');
4536        out.push_str(&alloc::format!("{i}"));
4537        out.push(']');
4538    }
4539}
4540
4541/// v7.37.17 (17.6 siblings) — MySQL JSON_SEARCH(doc, 'one'|'all',
4542/// pattern [, escape [, path...]]). Returns the path of the first
4543/// string value LIKE-matching the pattern ('one') or a JSON array
4544/// of all such paths ('all'); NULL when nothing matches. The
4545/// optional path args narrow where the walk starts.
4546pub fn mysql_json_search(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4547    if args.len() < 3 {
4548        return Err(EvalError::TypeMismatch {
4549            detail: alloc::format!(
4550                "json_search() takes doc, one/all, pattern [, escape [, path...]], got {} args",
4551                args.len()
4552            ),
4553        });
4554    }
4555    if args[..3].iter().any(|a| matches!(a, Value::Null)) {
4556        return Ok(Value::Null);
4557    }
4558    let src = match &args[0] {
4559        Value::Json(s) | Value::Text(s) => s.as_ref(),
4560        other => {
4561            return Err(EvalError::TypeMismatch {
4562                detail: alloc::format!(
4563                    "json_search() document must be json, got {}",
4564                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4565                ),
4566            });
4567        }
4568    };
4569    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4570        detail: alloc::format!("json_search(): invalid JSON: {e}"),
4571    })?;
4572    let one = match &args[1] {
4573        Value::Text(m) if m.eq_ignore_ascii_case("one") => true,
4574        Value::Text(m) if m.eq_ignore_ascii_case("all") => false,
4575        other => {
4576            return Err(EvalError::TypeMismatch {
4577                detail: alloc::format!(
4578                    "json_search() second arg must be 'one' or 'all', got {other:?}"
4579                ),
4580            });
4581        }
4582    };
4583    let Value::Text(pattern) = &args[2] else {
4584        return Err(EvalError::TypeMismatch {
4585            detail: alloc::format!(
4586                "json_search() pattern must be text, got {}",
4587                crate::conversions::pg_type_name_for_error_opt(args[2].data_type())
4588            ),
4589        });
4590    };
4591    let escape = match args.get(3) {
4592        None | Some(Value::Null) => '\\',
4593        Some(Value::Text(e)) if e.chars().count() == 1 => e.chars().next().unwrap(),
4594        Some(other) => {
4595            return Err(EvalError::TypeMismatch {
4596                detail: alloc::format!(
4597                    "json_search() escape must be a single character, got {other:?}"
4598                ),
4599            });
4600        }
4601    };
4602    let pat: Vec<char> = pattern.chars().collect();
4603    fn walk(
4604        v: &JsonValue,
4605        path: &str,
4606        pat: &[char],
4607        escape: char,
4608        hits: &mut Vec<String>,
4609        stop_at_one: bool,
4610    ) {
4611        if stop_at_one && !hits.is_empty() {
4612            return;
4613        }
4614        match v {
4615            JsonValue::String(s) => {
4616                let chars: Vec<char> = s.chars().collect();
4617                if like_match(&chars, pat, escape) {
4618                    hits.push(path.to_string());
4619                }
4620            }
4621            JsonValue::Object(members) => {
4622                for (k, mv) in members {
4623                    let mut p = path.to_string();
4624                    push_path_step(&mut p, Some(k), None);
4625                    walk(mv, &p, pat, escape, hits, stop_at_one);
4626                }
4627            }
4628            JsonValue::Array(items) => {
4629                for (i, iv) in items.iter().enumerate() {
4630                    let mut p = path.to_string();
4631                    push_path_step(&mut p, None, Some(i));
4632                    walk(iv, &p, pat, escape, hits, stop_at_one);
4633                }
4634            }
4635            _ => {}
4636        }
4637    }
4638    let mut hits: Vec<String> = Vec::new();
4639    let start_paths: Vec<String> = args
4640        .get(4..)
4641        .unwrap_or(&[])
4642        .iter()
4643        .map(|v| match v {
4644            Value::Text(p) => Ok(p.to_string()),
4645            other => Err(EvalError::TypeMismatch {
4646                detail: alloc::format!(
4647                    "json_search() paths must be text, got {}",
4648                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4649                ),
4650            }),
4651        })
4652        .collect::<Result<_, _>>()?;
4653    if start_paths.is_empty() {
4654        walk(&doc, "$", &pat, escape, &mut hits, one);
4655    } else {
4656        for p in &start_paths {
4657            let steps = mysql_path_steps(p)?;
4658            if let Some(sub) = mysql_path_get(&doc, &steps) {
4659                walk(sub, p.trim(), &pat, escape, &mut hits, one);
4660            }
4661        }
4662    }
4663    match hits.len() {
4664        0 => Ok(Value::Null),
4665        1 => Ok(Value::Json(alloc::borrow::Cow::Owned(
4666            JsonValue::String(hits.into_iter().next().unwrap()).to_json_text(),
4667        ))),
4668        _ => {
4669            let arr = JsonValue::Array(hits.into_iter().map(JsonValue::String).collect());
4670            Ok(Value::Json(alloc::borrow::Cow::Owned(arr.to_json_text())))
4671        }
4672    }
4673}
4674
4675/// v7.37.17 (17.6 siblings) — MySQL JSON_VALUE(doc, path). Returns
4676/// the scalar at the path as unquoted text (MySQL's default
4677/// RETURNING VARCHAR); containers render as JSON text; a miss is
4678/// NULL. The RETURNING clause is parser syntax and queued.
4679pub fn mysql_json_value(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4680    if args.len() != 2 {
4681        return Err(EvalError::TypeMismatch {
4682            detail: alloc::format!("json_value() takes 2 args, got {}", args.len()),
4683        });
4684    }
4685    if args.iter().any(|a| matches!(a, Value::Null)) {
4686        return Ok(Value::Null);
4687    }
4688    let src = match &args[0] {
4689        Value::Json(s) | Value::Text(s) => s.as_ref(),
4690        other => {
4691            return Err(EvalError::TypeMismatch {
4692                detail: alloc::format!(
4693                    "json_value() document must be json, got {}",
4694                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4695                ),
4696            });
4697        }
4698    };
4699    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4700        detail: alloc::format!("json_value(): invalid JSON: {e}"),
4701    })?;
4702    let Value::Text(p) = &args[1] else {
4703        return Err(EvalError::TypeMismatch {
4704            detail: alloc::format!(
4705                "json_value() path must be text, got {}",
4706                crate::conversions::pg_type_name_for_error_opt(args[1].data_type())
4707            ),
4708        });
4709    };
4710    let steps = mysql_path_steps(p)?;
4711    match mysql_path_get(&doc, &steps) {
4712        None => Ok(Value::Null),
4713        Some(JsonValue::Null) => Ok(Value::Null),
4714        Some(v) => Ok(Value::text(v.as_text())),
4715    }
4716}
4717
4718/// v7.39 (round 234) — a JSON scalar (string / number / boolean / null),
4719/// i.e. anything that isn't a container. PG refuses every path-based
4720/// modification against one: there is nowhere for a path to point.
4721fn is_json_scalar(v: &JsonValue) -> bool {
4722    !matches!(v, JsonValue::Object(_) | JsonValue::Array(_))
4723}
4724
4725#[cfg(test)]
4726mod round619_number_fast_path {
4727    use super::*;
4728
4729    /// v7.39 (round 619) — the borrowed shortcut has to be the same string
4730    /// the full canonicaliser builds, for every lexeme either might see.
4731    /// Checked over a generated set rather than by reading the two.
4732    #[test]
4733    fn fast_path_agrees_with_the_full_canonicaliser() {
4734        let mut cases: Vec<String> = Vec::new();
4735        for sign in ["", "-"] {
4736            for body in [
4737                "0",
4738                "1",
4739                "7",
4740                "10",
4741                "123",
4742                "0123",
4743                "00",
4744                "000",
4745                "9223372036854775807",
4746                "170141183460469231731687303715884105727",
4747                "1.0",
4748                "1.5",
4749                "0.5",
4750                ".5",
4751                "1.",
4752                "1e3",
4753                "1E3",
4754                "1e-3",
4755                "1.5e2",
4756                "1.50",
4757                "100",
4758                "0.0",
4759                "0.00",
4760                "10.010",
4761                "1e0",
4762                "1e+3",
4763                "0e0",
4764                "12345678901234567890.12345678901234567890",
4765            ] {
4766                cases.push(alloc::format!("{sign}{body}"));
4767            }
4768        }
4769        for c in &cases {
4770            assert_eq!(
4771                canon_json_number(c).as_ref(),
4772                canon_json_number_slow(c).as_str(),
4773                "lexeme {c:?} canonicalises differently through the shortcut"
4774            );
4775        }
4776    }
4777
4778    /// The single-entry object writer has to spell what the sorting one does.
4779    #[test]
4780    fn one_entry_object_writes_what_the_general_writer_writes() {
4781        for src in [
4782            r#"{"a":1}"#,
4783            r#"{"":1}"#,
4784            r#"{"a":{"b":2}}"#,
4785            r#"{"a":[1,2,3]}"#,
4786            r#"{"a\"b":"c\\d"}"#,
4787            r#"{"日本":"語"}"#,
4788            r#"{"a":null}"#,
4789            r#"{}"#,
4790        ] {
4791            let JsonValue::Object(entries) = parse(src).expect("valid json") else {
4792                panic!("{src} is not an object");
4793            };
4794            let mut fast = String::new();
4795            write_json_canonical(&JsonValue::Object(entries.clone()), &mut fast);
4796            let mut general = String::new();
4797            write_object_general(&entries, &mut general);
4798            assert_eq!(fast, general, "{src}");
4799        }
4800    }
4801}