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