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