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::TypeMismatch {
3528            detail: alloc::format!("jsonb_set() takes 3 or 4 args, got {}", args.len()),
3529        });
3530    }
3531    if args.iter().take(3).any(|v| matches!(v, Value::Null)) {
3532        return Ok(Value::Null);
3533    }
3534    let create_missing = match args.get(3) {
3535        None | Some(Value::Null) => true,
3536        Some(Value::Bool(b)) => *b,
3537        Some(other) => {
3538            return Err(EvalError::TypeMismatch {
3539                detail: alloc::format!(
3540                    "jsonb_set() create_missing must be BOOL, got {}",
3541                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
3542                ),
3543            });
3544        }
3545    };
3546    let doc_text = json_text_arg(&args[0], "jsonb_set", "target")?;
3547    let path = path_text_arg(&args[1], "jsonb_set")?;
3548    let new_text = json_text_arg(&args[2], "jsonb_set", "new_value")?;
3549    let mut root = parse(doc_text).map_err(|e| EvalError::TypeMismatch {
3550        detail: alloc::format!("jsonb_set(): invalid JSON target — {e}"),
3551    })?;
3552    let new_val = parse(new_text).map_err(|e| EvalError::TypeMismatch {
3553        detail: alloc::format!("jsonb_set(): invalid JSON new_value — {e}"),
3554    })?;
3555    // v7.39 (round 234) — PG's edge rules for the modification family,
3556    // probed against 18.4. An EMPTY path is a no-op (SPG replaced the whole
3557    // document with the new value — silently wrong), and a SCALAR target
3558    // has nowhere to put a path (SPG returned the scalar unchanged).
3559    if path.is_empty() {
3560        return Ok(Value::json(root.to_json_text()));
3561    }
3562    if is_json_scalar(&root) {
3563        return Err(EvalError::TypeMismatch {
3564            detail: "cannot set path in scalar".into(),
3565        });
3566    }
3567    set_at_path(&mut root, &path, new_val, create_missing);
3568    Ok(Value::json(root.to_json_text()))
3569}
3570
3571/// v7.37.17 (17.6 siblings) — `jsonb_delete_path(doc, path[])` —
3572/// function form of the `#-` operator. Removes the value at the
3573/// nested path; missing path leaves the doc unchanged.
3574pub fn delete_path(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3575    delete_path_inner(args).map(canonicalize_value)
3576}
3577
3578fn delete_path_inner(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3579    if args.len() != 2 {
3580        return Err(EvalError::TypeMismatch {
3581            detail: alloc::format!("jsonb_delete_path() takes 2 args, got {}", args.len()),
3582        });
3583    }
3584    if args.iter().any(|v| matches!(v, Value::Null)) {
3585        return Ok(Value::Null);
3586    }
3587    let doc_text = json_text_arg(&args[0], "jsonb_delete_path", "target")?;
3588    let path = path_text_arg(&args[1], "jsonb_delete_path")?;
3589    let mut root = parse(doc_text).map_err(|e| EvalError::TypeMismatch {
3590        detail: alloc::format!("jsonb_delete_path(): invalid JSON target — {e}"),
3591    })?;
3592    // v7.39 (round 234) — `#-` on a scalar is an error in PG; SPG handed
3593    // the scalar back unchanged.
3594    if is_json_scalar(&root) && !path.is_empty() {
3595        return Err(EvalError::TypeMismatch {
3596            detail: "cannot delete path in scalar".into(),
3597        });
3598    }
3599    delete_at_path(&mut root, &path);
3600    Ok(Value::json(root.to_json_text()))
3601}
3602
3603fn delete_at_path(node: &mut JsonValue, path: &[String]) {
3604    if path.is_empty() {
3605        return;
3606    }
3607    let step = &path[0];
3608    if path.len() == 1 {
3609        // Terminal step — remove here.
3610        match node {
3611            JsonValue::Object(entries) => {
3612                entries.retain(|(k, _)| k != step);
3613            }
3614            JsonValue::Array(items) => {
3615                if let Ok(idx) = step.parse::<i64>() {
3616                    let len = items.len() as i64;
3617                    let real = if idx >= 0 { idx } else { len + idx };
3618                    if real >= 0 && real < len {
3619                        items.remove(real as usize);
3620                    }
3621                }
3622            }
3623            _ => {}
3624        }
3625        return;
3626    }
3627    // Navigate deeper.
3628    match node {
3629        JsonValue::Object(entries) => {
3630            if let Some((_, child)) = entries.iter_mut().find(|(k, _)| k == step) {
3631                delete_at_path(child, &path[1..]);
3632            }
3633        }
3634        JsonValue::Array(items) => {
3635            if let Ok(idx) = step.parse::<i64>() {
3636                let len = items.len() as i64;
3637                let real = if idx >= 0 { idx } else { len + idx };
3638                if real >= 0 && real < len {
3639                    delete_at_path(&mut items[real as usize], &path[1..]);
3640                }
3641            }
3642        }
3643        _ => {}
3644    }
3645}
3646
3647fn set_at_path(node: &mut JsonValue, path: &[String], new_val: JsonValue, create_missing: bool) {
3648    if path.is_empty() {
3649        *node = new_val;
3650        return;
3651    }
3652    let step = &path[0];
3653    let rest = &path[1..];
3654    match node {
3655        JsonValue::Object(entries) => {
3656            if let Some(pos) = entries.iter().position(|(k, _)| k == step) {
3657                if rest.is_empty() {
3658                    entries[pos].1 = new_val;
3659                } else {
3660                    set_at_path(&mut entries[pos].1, rest, new_val, create_missing);
3661                }
3662            } else if create_missing && rest.is_empty() {
3663                entries.push((step.clone(), new_val));
3664            }
3665            // Missing intermediate path with create_missing — PG only
3666            // creates the LEAF, never intermediate parents. No-op.
3667        }
3668        JsonValue::Array(items) => {
3669            let Some(idx) = resolve_array_index(step, items.len()) else {
3670                if create_missing && rest.is_empty() {
3671                    // PG: positive overshoot appends, negative prepends.
3672                    if let Ok(n) = step.parse::<i64>() {
3673                        if n < 0 {
3674                            items.insert(0, new_val);
3675                        } else {
3676                            items.push(new_val);
3677                        }
3678                    }
3679                }
3680                return;
3681            };
3682            if rest.is_empty() {
3683                items[idx] = new_val;
3684            } else {
3685                set_at_path(&mut items[idx], rest, new_val, create_missing);
3686            }
3687        }
3688        _ => {
3689            // Scalar — no replacement possible at non-empty path.
3690        }
3691    }
3692}
3693
3694fn resolve_array_index(step: &str, len: usize) -> Option<usize> {
3695    let n = step.parse::<i64>().ok()?;
3696    if n >= 0 {
3697        let i = n as usize;
3698        if i < len { Some(i) } else { None }
3699    } else {
3700        let from_end = len as i64 + n;
3701        if from_end >= 0 {
3702            Some(from_end as usize)
3703        } else {
3704            None
3705        }
3706    }
3707}
3708
3709/// `jsonb_insert(target, path, new_value [, insert_after])` —
3710/// insert at path. `insert_after` defaults to false.
3711///
3712///   * Array parent: insert before (or after) the index. Out-of-range
3713///     positive index → append; out-of-range negative → prepend.
3714///   * Object parent: key must NOT exist (PG raises). insert_after
3715///     has no effect for objects.
3716pub fn insert(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
3717    if !(3..=4).contains(&args.len()) {
3718        return Err(EvalError::TypeMismatch {
3719            detail: alloc::format!("jsonb_insert() takes 3 or 4 args, got {}", args.len()),
3720        });
3721    }
3722    if args.iter().take(3).any(|v| matches!(v, Value::Null)) {
3723        return Ok(Value::Null);
3724    }
3725    let insert_after = match args.get(3) {
3726        None | Some(Value::Null) => false,
3727        Some(Value::Bool(b)) => *b,
3728        Some(other) => {
3729            return Err(EvalError::TypeMismatch {
3730                detail: alloc::format!(
3731                    "jsonb_insert() insert_after must be BOOL, got {}",
3732                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
3733                ),
3734            });
3735        }
3736    };
3737    let doc_text = json_text_arg(&args[0], "jsonb_insert", "target")?;
3738    let path = path_text_arg(&args[1], "jsonb_insert")?;
3739    let new_text = json_text_arg(&args[2], "jsonb_insert", "new_value")?;
3740    let mut root = parse(doc_text).map_err(|e| EvalError::TypeMismatch {
3741        detail: alloc::format!("jsonb_insert(): invalid JSON target — {e}"),
3742    })?;
3743    // v7.39 (round 234) — PG returns the document untouched for an empty
3744    // path (SPG raised its own error) and refuses a scalar target with the
3745    // same wording jsonb_set uses.
3746    if path.is_empty() {
3747        return Ok(Value::json(root.to_json_text()));
3748    }
3749    if is_json_scalar(&root) {
3750        return Err(EvalError::TypeMismatch {
3751            detail: "cannot set path in scalar".into(),
3752        });
3753    }
3754    let new_val = parse(new_text).map_err(|e| EvalError::TypeMismatch {
3755        detail: alloc::format!("jsonb_insert(): invalid JSON new_value — {e}"),
3756    })?;
3757    insert_at_path(&mut root, &path, new_val, insert_after)?;
3758    Ok(Value::json(root.to_json_text()))
3759}
3760
3761fn insert_at_path(
3762    node: &mut JsonValue,
3763    path: &[String],
3764    new_val: JsonValue,
3765    insert_after: bool,
3766) -> Result<(), EvalError> {
3767    debug_assert!(!path.is_empty());
3768    if path.len() == 1 {
3769        let step = &path[0];
3770        match node {
3771            JsonValue::Object(entries) => {
3772                if entries.iter().any(|(k, _)| k == step) {
3773                    return Err(EvalError::TypeMismatch {
3774                        detail: alloc::format!(
3775                            "jsonb_insert(): cannot replace existing key {step:?}"
3776                        ),
3777                    });
3778                }
3779                entries.push((step.clone(), new_val));
3780                Ok(())
3781            }
3782            JsonValue::Array(items) => {
3783                let Ok(n) = step.parse::<i64>() else {
3784                    return Err(EvalError::TypeMismatch {
3785                        detail: alloc::format!(
3786                            "jsonb_insert(): array step must be integer, got {step:?}"
3787                        ),
3788                    });
3789                };
3790                let mut idx = if n >= 0 {
3791                    let i = n as usize;
3792                    if i > items.len() { items.len() } else { i }
3793                } else {
3794                    let from_end = items.len() as i64 + n;
3795                    if from_end < 0 { 0 } else { from_end as usize }
3796                };
3797                if insert_after && idx < items.len() {
3798                    idx += 1;
3799                }
3800                items.insert(idx, new_val);
3801                Ok(())
3802            }
3803            _ => Err(EvalError::TypeMismatch {
3804                detail: "jsonb_insert(): parent at path is a scalar".into(),
3805            }),
3806        }
3807    } else {
3808        let step = &path[0];
3809        let rest = &path[1..];
3810        match node {
3811            JsonValue::Object(entries) => {
3812                if let Some(pos) = entries.iter().position(|(k, _)| k == step) {
3813                    insert_at_path(&mut entries[pos].1, rest, new_val, insert_after)
3814                } else {
3815                    Err(EvalError::TypeMismatch {
3816                        detail: alloc::format!("jsonb_insert(): path {step:?} does not exist"),
3817                    })
3818                }
3819            }
3820            JsonValue::Array(items) => {
3821                let Some(idx) = resolve_array_index(step, items.len()) else {
3822                    return Err(EvalError::TypeMismatch {
3823                        detail: alloc::format!("jsonb_insert(): array index {step:?} out of range"),
3824                    });
3825                };
3826                insert_at_path(&mut items[idx], rest, new_val, insert_after)
3827            }
3828            _ => Err(EvalError::TypeMismatch {
3829                detail: "jsonb_insert(): parent at path is a scalar".into(),
3830            }),
3831        }
3832    }
3833}
3834
3835fn json_text_arg<'a>(v: &'a Value, fname: &str, role: &str) -> Result<&'a str, EvalError> {
3836    match v {
3837        Value::Json(s) | Value::Text(s) => Ok(s.as_ref()),
3838        other => Err(EvalError::TypeMismatch {
3839            detail: alloc::format!(
3840                "{fname}() {role} must be JSON or TEXT, got {}",
3841                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3842            ),
3843        }),
3844    }
3845}
3846
3847fn path_text_arg(v: &Value, fname: &str) -> Result<Vec<String>, EvalError> {
3848    match v {
3849        Value::Text(s) | Value::Json(s) => parse_text_array(s.as_ref()),
3850        Value::TextArray(items) => Ok(items
3851            .iter()
3852            .map(|o| o.clone().unwrap_or_default())
3853            .collect()),
3854        other => Err(EvalError::TypeMismatch {
3855            detail: alloc::format!(
3856                "{fname}() path must be TEXT[] or TEXT, got {}",
3857                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3858            ),
3859        }),
3860    }
3861}
3862
3863#[cfg(test)]
3864mod tests {
3865    use super::*;
3866
3867    fn canon(s: &str) -> String {
3868        canonicalize_jsonb(s).unwrap()
3869    }
3870
3871    #[test]
3872    fn canon_number_rules() {
3873        // Values from live PG 18.4 jsonb.
3874        assert_eq!(canon_json_number("1.0"), "1.0");
3875        assert_eq!(canon_json_number("1e2"), "100");
3876        assert_eq!(canon_json_number("1.10"), "1.10");
3877        assert_eq!(canon_json_number("100.00"), "100.00");
3878        assert_eq!(canon_json_number("0.5"), "0.5");
3879        assert_eq!(canon_json_number("-0"), "0");
3880        assert_eq!(canon_json_number("1E-3"), "0.001");
3881        assert_eq!(canon_json_number("42"), "42");
3882        assert_eq!(canon_json_number("-2.5"), "-2.5");
3883        assert_eq!(canon_json_number("2.5e3"), "2500");
3884    }
3885
3886    #[test]
3887    fn canon_key_order_dedup_and_whitespace() {
3888        // Keys sort by (length, bytes); ""/a/b/z/aa. PG 18.4.
3889        assert_eq!(
3890            canon(r#"{"b":1,"a":2,"aa":3,"":9,"z":4}"#),
3891            r#"{"": 9, "a": 2, "b": 1, "z": 4, "aa": 3}"#
3892        );
3893        // Duplicate keys collapse last-wins.
3894        assert_eq!(canon(r#"{"a":1,"a":2,"a":3}"#), r#"{"a": 3}"#);
3895        // Arrays get `, ` and are not reordered.
3896        assert_eq!(canon("[3,2,1]"), "[3, 2, 1]");
3897    }
3898
3899    #[test]
3900    fn json_number_equality_by_value() {
3901        let eq = |a: &str, b: &str| json_eq(&parse(a).unwrap(), &parse(b).unwrap());
3902        assert!(eq("1", "1.0"));
3903        assert!(eq("1.50", "1.5"));
3904        assert!(eq("1e3", "1000.00"));
3905        assert!(eq("0", "-0"));
3906        assert!(eq("2.5e3", "2500"));
3907        assert!(!eq("1.5", "1.6"));
3908        // Inside arrays / objects.
3909        assert!(eq("[1, 2.0]", "[1.0, 2]"));
3910        assert!(eq(r#"{"a":1}"#, r#"{"a":1.0}"#));
3911    }
3912
3913    #[test]
3914    fn canon_nested_and_scalars() {
3915        assert_eq!(
3916            canon(r#"{"x":{"b":1,"a":2},"y":[3,{"d":1,"c":2}]}"#),
3917            r#"{"x": {"a": 2, "b": 1}, "y": [3, {"c": 2, "d": 1}]}"#
3918        );
3919        assert_eq!(canon("  true "), "true");
3920        assert_eq!(canon(" 42 "), "42");
3921        assert_eq!(canon("{}"), "{}");
3922        assert_eq!(canon("[]"), "[]");
3923        // Non-ASCII stays verbatim UTF-8; escapes preserved.
3924        assert_eq!(
3925            canon(r#"{"e":"café","t":"a\nb"}"#),
3926            r#"{"e": "café", "t": "a\nb"}"#
3927        );
3928    }
3929
3930    #[test]
3931    fn parse_atoms() {
3932        assert_eq!(parse("null").unwrap(), JsonValue::Null);
3933        assert_eq!(parse("true").unwrap(), JsonValue::Bool(true));
3934        assert_eq!(parse("false").unwrap(), JsonValue::Bool(false));
3935        assert_eq!(
3936            parse("\"hello\"").unwrap(),
3937            JsonValue::String("hello".into())
3938        );
3939        assert!(matches!(
3940            parse("42").unwrap(),
3941            JsonValue::NumberText(ref s) if s == "42"
3942        ));
3943    }
3944
3945    #[test]
3946    fn parse_nested() {
3947        let doc = parse(r#"{"a":1,"b":[true,null,"x"]}"#).unwrap();
3948        let JsonValue::Object(entries) = doc else {
3949            panic!("expected object");
3950        };
3951        assert_eq!(entries.len(), 2);
3952        assert_eq!(entries[0].0, "a");
3953        assert_eq!(entries[1].0, "b");
3954    }
3955
3956    #[test]
3957    fn parse_string_escapes() {
3958        let s = parse(r#""he said \"hi\" and\\then\n""#).unwrap();
3959        assert_eq!(s, JsonValue::String("he said \"hi\" and\\then\n".into()));
3960    }
3961
3962    #[test]
3963    fn parse_unicode_escape() {
3964        assert_eq!(parse(r#""é""#).unwrap(), JsonValue::String("é".into()));
3965    }
3966
3967    #[test]
3968    fn path_object_key_returns_value() {
3969        let doc = Value::json::<String>(r#"{"name":"alice","age":30}"#.into());
3970        let key = Value::text("name");
3971        let v = path_get(&doc, &key, true).unwrap();
3972        assert_eq!(v, Value::text("alice"));
3973        let v = path_get(&doc, &key, false).unwrap();
3974        assert_eq!(v, Value::json("\"alice\""));
3975    }
3976
3977    #[test]
3978    fn path_array_index_supports_negative() {
3979        let doc = Value::json("[10,20,30]");
3980        let v = path_get(&doc, &Value::Int(1), true).unwrap();
3981        assert_eq!(v, Value::text("20"));
3982        let v = path_get(&doc, &Value::Int(-1), true).unwrap();
3983        assert_eq!(v, Value::text("30"));
3984    }
3985
3986    #[test]
3987    fn path_missing_key_returns_null() {
3988        let doc = Value::json::<String>(r#"{"a":1}"#.into());
3989        let v = path_get(&doc, &Value::text("missing"), true).unwrap();
3990        assert_eq!(v, Value::Null);
3991    }
3992
3993    #[test]
3994    fn path_get_nested_subtree_is_verbatim() {
3995        // v7.38 (read01) — PG returns the located value's EXACT source text, so
3996        // a compact source stays compact (verified against PG18.4: `->` on this
3997        // doc yields `{"x":[1,2]}`, not the canonical `{"x": [1, 2]}`). A jsonb
3998        // column reaches here already canonicalized, so slicing it still yields
3999        // canonical text.
4000        let doc = Value::json::<String>(r#"{"k":{"x":[1,2]}}"#.into());
4001        let v = path_get(&doc, &Value::text("k"), false).unwrap();
4002        assert_eq!(v, Value::json::<String>(r#"{"x":[1,2]}"#.into()));
4003
4004        // A canonical (jsonb-shaped) source slices back to canonical text.
4005        let canon = Value::json::<String>(r#"{"k": {"x": [1, 2]}}"#.into());
4006        let v = path_get(&canon, &Value::text("k"), false).unwrap();
4007        assert_eq!(v, Value::json::<String>(r#"{"x": [1, 2]}"#.into()));
4008
4009        // Whitespace, number lexemes and duplicate keys all survive; a
4010        // duplicate key resolves to the LAST occurrence, as in PG.
4011        let raw = Value::json::<String>(r#"{"a":{ "y" : 2e2 },"k":1,"k":2}"#.into());
4012        assert_eq!(
4013            path_get(&raw, &Value::text("a"), false).unwrap(),
4014            Value::json::<String>(r#"{ "y" : 2e2 }"#.into())
4015        );
4016        assert_eq!(
4017            path_get(&raw, &Value::text("k"), false).unwrap(),
4018            Value::json::<String>("2".into())
4019        );
4020
4021        // `->` on a JSON null yields the JSON null; `->>` yields SQL NULL.
4022        let n = Value::json::<String>(r#"{"a":null}"#.into());
4023        assert_eq!(
4024            path_get(&n, &Value::text("a"), false).unwrap(),
4025            Value::json::<String>("null".into())
4026        );
4027        assert_eq!(path_get(&n, &Value::text("a"), true).unwrap(), Value::Null);
4028    }
4029}
4030
4031/// v7.37.17 (17.6 siblings) — one step of a MySQL JSON path
4032/// (`$.key`, `$."quoted key"`, `$[0]`).
4033#[derive(Debug)]
4034pub enum MysqlPathStep {
4035    Key(String),
4036    Index(usize),
4037}
4038
4039/// Parse a MySQL JSON path. Supports `$`, `.key`, `."quoted key"`
4040/// and `[N]`; wildcard steps (`*`, `[*]`, `**`) error honestly —
4041/// they return multiple matches per document and need a different
4042/// walker shape.
4043pub fn mysql_path_steps(path: &str) -> Result<Vec<MysqlPathStep>, EvalError> {
4044    let chars: Vec<char> = path.trim().chars().collect();
4045    if chars.first() != Some(&'$') {
4046        return Err(EvalError::TypeMismatch {
4047            detail: alloc::format!("invalid JSON path expression (must start with $): {path:?}"),
4048        });
4049    }
4050    let mut steps = Vec::new();
4051    let mut i = 1;
4052    while i < chars.len() {
4053        match chars[i] {
4054            '.' => {
4055                i += 1;
4056                if i < chars.len() && chars[i] == '"' {
4057                    i += 1;
4058                    let mut key = String::new();
4059                    while i < chars.len() && chars[i] != '"' {
4060                        if chars[i] == '\\' && i + 1 < chars.len() {
4061                            i += 1;
4062                        }
4063                        key.push(chars[i]);
4064                        i += 1;
4065                    }
4066                    if i >= chars.len() {
4067                        return Err(EvalError::TypeMismatch {
4068                            detail: alloc::format!(
4069                                "invalid JSON path expression (unterminated quote): {path:?}"
4070                            ),
4071                        });
4072                    }
4073                    i += 1; // closing quote
4074                    steps.push(MysqlPathStep::Key(key));
4075                } else {
4076                    let mut key = String::new();
4077                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
4078                        key.push(chars[i]);
4079                        i += 1;
4080                    }
4081                    if key.is_empty() {
4082                        return Err(EvalError::TypeMismatch {
4083                            detail: alloc::format!(
4084                                "unsupported JSON path step at position {i} in {path:?} \
4085                                 (wildcards are not supported)"
4086                            ),
4087                        });
4088                    }
4089                    steps.push(MysqlPathStep::Key(key));
4090                }
4091            }
4092            '[' => {
4093                i += 1;
4094                let mut num = String::new();
4095                while i < chars.len() && chars[i] != ']' {
4096                    num.push(chars[i]);
4097                    i += 1;
4098                }
4099                if i >= chars.len() {
4100                    return Err(EvalError::TypeMismatch {
4101                        detail: alloc::format!(
4102                            "invalid JSON path expression (unterminated bracket): {path:?}"
4103                        ),
4104                    });
4105                }
4106                i += 1; // ]
4107                let idx: usize = num.trim().parse().map_err(|_| EvalError::TypeMismatch {
4108                    detail: alloc::format!(
4109                        "unsupported JSON path index {num:?} in {path:?} \
4110                         (wildcards are not supported)"
4111                    ),
4112                })?;
4113                steps.push(MysqlPathStep::Index(idx));
4114            }
4115            other => {
4116                return Err(EvalError::TypeMismatch {
4117                    detail: alloc::format!(
4118                        "invalid JSON path expression (unexpected {other:?}): {path:?}"
4119                    ),
4120                });
4121            }
4122        }
4123    }
4124    Ok(steps)
4125}
4126
4127/// Walk a parsed JSON document along a MySQL path. Returns None
4128/// when any step misses.
4129pub fn mysql_path_get<'a>(doc: &'a JsonValue, steps: &[MysqlPathStep]) -> Option<&'a JsonValue> {
4130    let mut cur = doc;
4131    for step in steps {
4132        match (step, cur) {
4133            (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4134                cur = members.iter().find(|(mk, _)| mk == k).map(|(_, v)| v)?;
4135            }
4136            (MysqlPathStep::Index(idx), JsonValue::Array(items)) => {
4137                cur = items.get(*idx)?;
4138            }
4139            // MySQL: a non-array auto-wraps as a one-element array
4140            // for [0].
4141            (MysqlPathStep::Index(0), scalar) => {
4142                cur = scalar;
4143            }
4144            _ => return None,
4145        }
4146    }
4147    Some(cur)
4148}
4149
4150/// v7.37.17 (17.6 siblings) — MySQL JSON_EXTRACT(doc, path...).
4151/// One path → the value at that path (or SQL NULL when it misses);
4152/// several paths → a JSON array of the values that matched (NULL
4153/// when none did).
4154pub fn mysql_json_extract(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4155    if args.len() < 2 {
4156        return Err(EvalError::TypeMismatch {
4157            detail: alloc::format!(
4158                "json_extract() takes a document and at least one path, got {} args",
4159                args.len()
4160            ),
4161        });
4162    }
4163    if args.iter().any(|a| matches!(a, Value::Null)) {
4164        return Ok(Value::Null);
4165    }
4166    let src = match &args[0] {
4167        Value::Json(s) | Value::Text(s) => s.as_ref(),
4168        other => {
4169            return Err(EvalError::TypeMismatch {
4170                detail: alloc::format!(
4171                    "json_extract() document must be json, got {}",
4172                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4173                ),
4174            });
4175        }
4176    };
4177    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4178        detail: alloc::format!("json_extract(): invalid JSON: {e}"),
4179    })?;
4180    let mut hits: Vec<String> = Vec::new();
4181    for path_v in &args[1..] {
4182        let Value::Text(p) = path_v else {
4183            return Err(EvalError::TypeMismatch {
4184                detail: alloc::format!(
4185                    "json_extract() paths must be text, got {}",
4186                    crate::conversions::pg_type_name_for_error_opt(path_v.data_type())
4187                ),
4188            });
4189        };
4190        let steps = mysql_path_steps(p)?;
4191        if let Some(v) = mysql_path_get(&doc, &steps) {
4192            hits.push(v.to_json_text());
4193        }
4194    }
4195    match (args.len() - 1, hits.len()) {
4196        (_, 0) => Ok(Value::Null),
4197        (1, _) => Ok(Value::Json(alloc::borrow::Cow::Owned(
4198            hits.into_iter().next().unwrap(),
4199        ))),
4200        _ => {
4201            let mut out = String::from("[");
4202            for (i, h) in hits.iter().enumerate() {
4203                if i > 0 {
4204                    out.push_str(", ");
4205                }
4206                out.push_str(h);
4207            }
4208            out.push(']');
4209            Ok(Value::Json(alloc::borrow::Cow::Owned(out)))
4210        }
4211    }
4212}
4213
4214/// v7.37.17 (17.6 siblings) — MySQL JSON_CONTAINS_PATH(doc,
4215/// 'one'|'all', path...).
4216pub fn mysql_json_contains_path(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4217    if args.len() < 3 {
4218        return Err(EvalError::TypeMismatch {
4219            detail: alloc::format!(
4220                "json_contains_path() takes a document, one/all, and at least one path, got {} args",
4221                args.len()
4222            ),
4223        });
4224    }
4225    if args.iter().any(|a| matches!(a, Value::Null)) {
4226        return Ok(Value::Null);
4227    }
4228    let src = match &args[0] {
4229        Value::Json(s) | Value::Text(s) => s.as_ref(),
4230        other => {
4231            return Err(EvalError::TypeMismatch {
4232                detail: alloc::format!(
4233                    "json_contains_path() document must be json, got {}",
4234                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4235                ),
4236            });
4237        }
4238    };
4239    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4240        detail: alloc::format!("json_contains_path(): invalid JSON: {e}"),
4241    })?;
4242    let mode = match &args[1] {
4243        Value::Text(m) if m.eq_ignore_ascii_case("one") => false,
4244        Value::Text(m) if m.eq_ignore_ascii_case("all") => true,
4245        other => {
4246            return Err(EvalError::TypeMismatch {
4247                detail: alloc::format!(
4248                    "json_contains_path() second arg must be 'one' or 'all', got {other:?}"
4249                ),
4250            });
4251        }
4252    };
4253    let mut found_any = false;
4254    let mut found_all = true;
4255    for path_v in &args[2..] {
4256        let Value::Text(p) = path_v else {
4257            return Err(EvalError::TypeMismatch {
4258                detail: alloc::format!(
4259                    "json_contains_path() paths must be text, got {}",
4260                    crate::conversions::pg_type_name_for_error_opt(path_v.data_type())
4261                ),
4262            });
4263        };
4264        let steps = mysql_path_steps(p)?;
4265        if mysql_path_get(&doc, &steps).is_some() {
4266            found_any = true;
4267        } else {
4268            found_all = false;
4269        }
4270    }
4271    Ok(Value::Bool(if mode { found_all } else { found_any }))
4272}
4273
4274/// v7.37.17 (17.6 siblings) — convert a SQL value into a JsonValue
4275/// for the MySQL JSON mutation functions (SQL text becomes a JSON
4276/// string; JSON passes through parsed).
4277fn value_to_jsonvalue(v: &Value) -> Result<JsonValue, EvalError> {
4278    Ok(match v {
4279        Value::Null => JsonValue::Null,
4280        Value::Bool(b) => JsonValue::Bool(*b),
4281        Value::Json(s) => parse(s).map_err(|e| EvalError::TypeMismatch {
4282            detail: alloc::format!("invalid JSON value: {e}"),
4283        })?,
4284        Value::Text(s) => JsonValue::String(s.to_string()),
4285        // v7.38 (read01, T9) — a composite becomes a JSON object keyed by field
4286        // name (`row_to_json(row(1,'a'))` → `{"f1":1,"f2":"a"}`).
4287        Value::Composite(fields) => {
4288            let mut entries = alloc::vec::Vec::with_capacity(fields.len());
4289            for (name, fv) in fields.iter() {
4290                entries.push((name.clone(), value_to_jsonvalue(fv)?));
4291            }
4292            JsonValue::Object(entries)
4293        }
4294        other => {
4295            // Numbers and everything else render through the
4296            // to_json text form, then parse back.
4297            let text = value_to_json_text(other);
4298            parse(&text).map_err(|e| EvalError::TypeMismatch {
4299                detail: alloc::format!("invalid JSON value: {e}"),
4300            })?
4301        }
4302    })
4303}
4304
4305#[derive(Clone, Copy, PartialEq, Debug)]
4306enum MutateMode {
4307    /// json_set — replace existing, create missing.
4308    Set,
4309    /// json_insert — create missing only.
4310    Insert,
4311    /// json_replace — replace existing only.
4312    Replace,
4313}
4314
4315/// Apply one path mutation. Missing intermediate steps are a no-op
4316/// (MySQL: only the final step may be created).
4317fn mutate_at(cur: &mut JsonValue, steps: &[MysqlPathStep], mode: MutateMode, newval: &JsonValue) {
4318    match steps {
4319        [] => {
4320            if matches!(mode, MutateMode::Set | MutateMode::Replace) {
4321                *cur = newval.clone();
4322            }
4323        }
4324        [last] => match (last, &mut *cur) {
4325            (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4326                if let Some(slot) = members.iter_mut().find(|(mk, _)| mk == k) {
4327                    if matches!(mode, MutateMode::Set | MutateMode::Replace) {
4328                        slot.1 = newval.clone();
4329                    }
4330                } else if matches!(mode, MutateMode::Set | MutateMode::Insert) {
4331                    members.push((k.clone(), newval.clone()));
4332                }
4333            }
4334            (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
4335                if *i < items.len() {
4336                    if matches!(mode, MutateMode::Set | MutateMode::Replace) {
4337                        items[*i] = newval.clone();
4338                    }
4339                } else if matches!(mode, MutateMode::Set | MutateMode::Insert) {
4340                    // Index past the end appends (MySQL semantics).
4341                    items.push(newval.clone());
4342                }
4343            }
4344            // Scalar auto-wraps as a one-element array: [0] exists.
4345            (MysqlPathStep::Index(0), scalar) => {
4346                if matches!(mode, MutateMode::Set | MutateMode::Replace) {
4347                    *scalar = newval.clone();
4348                }
4349            }
4350            _ => {}
4351        },
4352        [head, rest @ ..] => match (head, cur) {
4353            (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4354                if let Some(slot) = members.iter_mut().find(|(mk, _)| mk == k) {
4355                    mutate_at(&mut slot.1, rest, mode, newval);
4356                }
4357            }
4358            (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
4359                if let Some(slot) = items.get_mut(*i) {
4360                    mutate_at(slot, rest, mode, newval);
4361                }
4362            }
4363            _ => {}
4364        },
4365    }
4366}
4367
4368fn mysql_json_mutate(
4369    args: &[Value<'_>],
4370    mode: MutateMode,
4371    fn_name: &str,
4372) -> Result<Value<'static>, EvalError> {
4373    if args.len() < 3 || args.len() % 2 == 0 {
4374        return Err(EvalError::TypeMismatch {
4375            detail: alloc::format!(
4376                "{fn_name}() takes a document plus (path, value) pairs, got {} args",
4377                args.len()
4378            ),
4379        });
4380    }
4381    if matches!(args[0], Value::Null) {
4382        return Ok(Value::Null);
4383    }
4384    let src = match &args[0] {
4385        Value::Json(s) | Value::Text(s) => s.as_ref(),
4386        other => {
4387            return Err(EvalError::TypeMismatch {
4388                detail: alloc::format!(
4389                    "{fn_name}() document must be json, got {}",
4390                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4391                ),
4392            });
4393        }
4394    };
4395    let mut doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4396        detail: alloc::format!("{fn_name}(): invalid JSON: {e}"),
4397    })?;
4398    for pair in args[1..].chunks(2) {
4399        let Value::Text(p) = &pair[0] else {
4400            if matches!(pair[0], Value::Null) {
4401                return Ok(Value::Null);
4402            }
4403            return Err(EvalError::TypeMismatch {
4404                detail: alloc::format!(
4405                    "{fn_name}() paths must be text, got {}",
4406                    crate::conversions::pg_type_name_for_error_opt(pair[0].data_type())
4407                ),
4408            });
4409        };
4410        let steps = mysql_path_steps(p)?;
4411        let newval = value_to_jsonvalue(&pair[1])?;
4412        mutate_at(&mut doc, &steps, mode, &newval);
4413    }
4414    // v7.39 (round 392) — MariaDB renders JSON with `": "` / `", "` spacing
4415    // (`{"a": 1, "b": 2}`); canonicalise so JSON_SET / INSERT / REPLACE /
4416    // REMOVE match, like JSON_OBJECT (r391).
4417    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4418        doc.to_json_text(),
4419    ))))
4420}
4421
4422/// v7.37.17 (17.6 siblings) — MySQL JSON_SET / JSON_INSERT /
4423/// JSON_REPLACE ('$.x'-path forms; the PG jsonb_set text-array
4424/// spelling stays on crate::json::set).
4425pub fn mysql_json_set(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4426    mysql_json_mutate(args, MutateMode::Set, "json_set")
4427}
4428
4429pub fn mysql_json_insert(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4430    mysql_json_mutate(args, MutateMode::Insert, "json_insert")
4431}
4432
4433pub fn mysql_json_replace(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4434    mysql_json_mutate(args, MutateMode::Replace, "json_replace")
4435}
4436
4437/// v7.37.17 (17.6 siblings) — MySQL JSON_REMOVE(doc, path...).
4438/// Removing the root path `$` errors, as in MySQL.
4439pub fn mysql_json_remove(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4440    if args.len() < 2 {
4441        return Err(EvalError::TypeMismatch {
4442            detail: alloc::format!(
4443                "json_remove() takes a document and at least one path, got {} args",
4444                args.len()
4445            ),
4446        });
4447    }
4448    if args.iter().any(|a| matches!(a, Value::Null)) {
4449        return Ok(Value::Null);
4450    }
4451    let src = match &args[0] {
4452        Value::Json(s) | Value::Text(s) => s.as_ref(),
4453        other => {
4454            return Err(EvalError::TypeMismatch {
4455                detail: alloc::format!(
4456                    "json_remove() document must be json, got {}",
4457                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4458                ),
4459            });
4460        }
4461    };
4462    let mut doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4463        detail: alloc::format!("json_remove(): invalid JSON: {e}"),
4464    })?;
4465    fn remove_at(cur: &mut JsonValue, steps: &[MysqlPathStep]) {
4466        match steps {
4467            [] => {}
4468            [last] => match (last, cur) {
4469                (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4470                    members.retain(|(mk, _)| mk != k);
4471                }
4472                (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
4473                    if *i < items.len() {
4474                        items.remove(*i);
4475                    }
4476                }
4477                _ => {}
4478            },
4479            [head, rest @ ..] => match (head, cur) {
4480                (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4481                    if let Some(slot) = members.iter_mut().find(|(mk, _)| mk == k) {
4482                        remove_at(&mut slot.1, rest);
4483                    }
4484                }
4485                (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
4486                    if let Some(slot) = items.get_mut(*i) {
4487                        remove_at(slot, rest);
4488                    }
4489                }
4490                _ => {}
4491            },
4492        }
4493    }
4494    for path_v in &args[1..] {
4495        let Value::Text(p) = path_v else {
4496            return Err(EvalError::TypeMismatch {
4497                detail: alloc::format!(
4498                    "json_remove() paths must be text, got {}",
4499                    crate::conversions::pg_type_name_for_error_opt(path_v.data_type())
4500                ),
4501            });
4502        };
4503        let steps = mysql_path_steps(p)?;
4504        if steps.is_empty() {
4505            return Err(EvalError::TypeMismatch {
4506                detail: "The path expression '$' is not allowed in this context".into(),
4507            });
4508        }
4509        remove_at(&mut doc, &steps);
4510    }
4511    // v7.39 (round 392) — MariaDB's `": "` / `", "` JSON render spacing.
4512    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4513        doc.to_json_text(),
4514    ))))
4515}
4516
4517/// Apply `f` to the value AT the full path (not its parent). Missing
4518/// steps are a no-op.
4519fn modify_at(cur: &mut JsonValue, steps: &[MysqlPathStep], f: &mut dyn FnMut(&mut JsonValue)) {
4520    match steps {
4521        [] => f(cur),
4522        [head, rest @ ..] => match (head, cur) {
4523            (MysqlPathStep::Key(k), JsonValue::Object(members)) => {
4524                if let Some(slot) = members.iter_mut().find(|(mk, _)| mk == k) {
4525                    modify_at(&mut slot.1, rest, f);
4526                }
4527            }
4528            (MysqlPathStep::Index(i), JsonValue::Array(items)) => {
4529                if let Some(slot) = items.get_mut(*i) {
4530                    modify_at(slot, rest, f);
4531                }
4532            }
4533            _ => {}
4534        },
4535    }
4536}
4537
4538/// Shared arg plumbing for the (doc, path, value)-pairs mutators.
4539fn mysql_doc_and_pairs<'a>(
4540    args: &'a [Value<'_>],
4541    fn_name: &str,
4542) -> Result<Option<(JsonValue, &'a [Value<'a>])>, EvalError> {
4543    if args.len() < 3 || args.len() % 2 == 0 {
4544        return Err(EvalError::TypeMismatch {
4545            detail: alloc::format!(
4546                "{fn_name}() takes a document plus (path, value) pairs, got {} args",
4547                args.len()
4548            ),
4549        });
4550    }
4551    if args.iter().any(|a| matches!(a, Value::Null)) {
4552        return Ok(None);
4553    }
4554    let src = match &args[0] {
4555        Value::Json(s) | Value::Text(s) => s.as_ref(),
4556        other => {
4557            return Err(EvalError::TypeMismatch {
4558                detail: alloc::format!(
4559                    "{fn_name}() document must be json, got {}",
4560                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4561                ),
4562            });
4563        }
4564    };
4565    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4566        detail: alloc::format!("{fn_name}(): invalid JSON: {e}"),
4567    })?;
4568    Ok(Some((doc, &args[1..])))
4569}
4570
4571/// v7.37.17 (17.6 siblings) — MySQL JSON_ARRAY_APPEND(doc, path,
4572/// val, ...). The value at path gains `val` at the end; a non-array
4573/// value wraps as `[old, val]` (MySQL semantics).
4574pub fn mysql_json_array_append(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4575    let Some((mut doc, pairs)) = mysql_doc_and_pairs(args, "json_array_append")? else {
4576        return Ok(Value::Null);
4577    };
4578    for pair in pairs.chunks(2) {
4579        let Value::Text(p) = &pair[0] else {
4580            return Err(EvalError::TypeMismatch {
4581                detail: alloc::format!(
4582                    "json_array_append() paths must be text, got {}",
4583                    crate::conversions::pg_type_name_for_error_opt(pair[0].data_type())
4584                ),
4585            });
4586        };
4587        let steps = mysql_path_steps(p)?;
4588        let newval = value_to_jsonvalue(&pair[1])?;
4589        modify_at(&mut doc, &steps, &mut |v| match v {
4590            JsonValue::Array(items) => items.push(newval.clone()),
4591            other => {
4592                let old = core::mem::replace(other, JsonValue::Null);
4593                *other = JsonValue::Array(alloc::vec![old, newval.clone()]);
4594            }
4595        });
4596    }
4597    // v7.39 (round 392) — MariaDB's `": "` / `", "` JSON render spacing.
4598    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4599        doc.to_json_text(),
4600    ))))
4601}
4602
4603/// v7.37.17 (17.6 siblings) — MySQL JSON_ARRAY_INSERT(doc, path,
4604/// val, ...). The path must end in `[N]`; the value is inserted at
4605/// position N in the parent array, shifting later elements right
4606/// (past-the-end appends). A non-array parent is a no-op.
4607pub fn mysql_json_array_insert(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4608    let Some((mut doc, pairs)) = mysql_doc_and_pairs(args, "json_array_insert")? else {
4609        return Ok(Value::Null);
4610    };
4611    for pair in pairs.chunks(2) {
4612        let Value::Text(p) = &pair[0] else {
4613            return Err(EvalError::TypeMismatch {
4614                detail: alloc::format!(
4615                    "json_array_insert() paths must be text, got {}",
4616                    crate::conversions::pg_type_name_for_error_opt(pair[0].data_type())
4617                ),
4618            });
4619        };
4620        let steps = mysql_path_steps(p)?;
4621        let Some(MysqlPathStep::Index(idx)) = steps.last() else {
4622            return Err(EvalError::TypeMismatch {
4623                detail: alloc::format!(
4624                    "json_array_insert() path must end with an array index: {p:?}"
4625                ),
4626            });
4627        };
4628        let idx = *idx;
4629        let newval = value_to_jsonvalue(&pair[1])?;
4630        modify_at(&mut doc, &steps[..steps.len() - 1], &mut |v| {
4631            if let JsonValue::Array(items) = v {
4632                let at = idx.min(items.len());
4633                items.insert(at, newval.clone());
4634            }
4635        });
4636    }
4637    // v7.39 (round 392) — MariaDB's `": "` / `", "` JSON render spacing.
4638    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4639        doc.to_json_text(),
4640    ))))
4641}
4642
4643/// MySQL JSON containment recursion: candidate object ⊆ target
4644/// object (same keys, contained values); each candidate array
4645/// element contained in some target array element; a candidate
4646/// scalar is contained in an array when it equals some element.
4647fn mysql_contains(target: &JsonValue, cand: &JsonValue) -> bool {
4648    match (target, cand) {
4649        (JsonValue::Object(t), JsonValue::Object(c)) => c
4650            .iter()
4651            .all(|(ck, cv)| t.iter().any(|(tk, tv)| tk == ck && mysql_contains(tv, cv))),
4652        (JsonValue::Array(t), JsonValue::Array(c)) => {
4653            c.iter().all(|cv| t.iter().any(|tv| mysql_contains(tv, cv)))
4654        }
4655        (JsonValue::Array(t), scalar) => t.iter().any(|tv| mysql_contains(tv, scalar)),
4656        // Numbers compare numerically across the two lexeme forms.
4657        (JsonValue::Number(a), JsonValue::NumberText(b))
4658        | (JsonValue::NumberText(b), JsonValue::Number(a)) => {
4659            b.parse::<f64>().map(|x| x == *a).unwrap_or(false)
4660        }
4661        (JsonValue::NumberText(a), JsonValue::NumberText(b)) => {
4662            a == b
4663                || (a.parse::<f64>().ok().zip(b.parse::<f64>().ok()))
4664                    .map(|(x, y)| x == y)
4665                    .unwrap_or(false)
4666        }
4667        (a, b) => a == b,
4668    }
4669}
4670
4671/// v7.37.17 (17.6 siblings) — MySQL JSON_CONTAINS(target, candidate
4672/// [, path]).
4673pub fn mysql_json_contains(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4674    if !matches!(args.len(), 2 | 3) {
4675        return Err(EvalError::TypeMismatch {
4676            detail: alloc::format!("json_contains() takes 2 or 3 args, got {}", args.len()),
4677        });
4678    }
4679    if args.iter().any(|a| matches!(a, Value::Null)) {
4680        return Ok(Value::Null);
4681    }
4682    let parse_arg = |v: &Value<'_>, which: &str| -> Result<JsonValue, EvalError> {
4683        match v {
4684            Value::Json(s) | Value::Text(s) => parse(s).map_err(|e| EvalError::TypeMismatch {
4685                detail: alloc::format!("json_contains(): invalid {which} JSON: {e}"),
4686            }),
4687            other => Err(EvalError::TypeMismatch {
4688                detail: alloc::format!(
4689                    "json_contains() {which} must be json, got {}",
4690                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4691                ),
4692            }),
4693        }
4694    };
4695    let target = parse_arg(&args[0], "target")?;
4696    let cand = parse_arg(&args[1], "candidate")?;
4697    let effective = match args.get(2) {
4698        None => Some(&target),
4699        Some(Value::Text(p)) => {
4700            let steps = mysql_path_steps(p)?;
4701            mysql_path_get(&target, &steps)
4702        }
4703        Some(other) => {
4704            return Err(EvalError::TypeMismatch {
4705                detail: alloc::format!(
4706                    "json_contains() path must be text, got {}",
4707                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4708                ),
4709            });
4710        }
4711    };
4712    match effective {
4713        None => Ok(Value::Null),
4714        Some(t) => Ok(Value::Bool(mysql_contains(t, &cand))),
4715    }
4716}
4717
4718/// RFC 7396 merge-patch: a non-object patch replaces the target;
4719/// an object patch merges key-by-key, with JSON null values
4720/// removing keys.
4721fn merge_patch(target: JsonValue, patch: JsonValue) -> JsonValue {
4722    let JsonValue::Object(patch_members) = patch else {
4723        return patch;
4724    };
4725    let mut out = match target {
4726        JsonValue::Object(members) => members,
4727        _ => Vec::new(),
4728    };
4729    for (k, v) in patch_members {
4730        if matches!(v, JsonValue::Null) {
4731            out.retain(|(mk, _)| *mk != k);
4732        } else if let Some(slot) = out.iter_mut().find(|(mk, _)| *mk == k) {
4733            let old = core::mem::replace(&mut slot.1, JsonValue::Null);
4734            slot.1 = merge_patch(old, v);
4735        } else {
4736            // Merging into a missing key still strips nested nulls.
4737            out.push((k, merge_patch(JsonValue::Null, v)));
4738        }
4739    }
4740    JsonValue::Object(out)
4741}
4742
4743/// MySQL JSON_MERGE_PRESERVE pairwise rule: arrays concatenate,
4744/// objects merge with duplicate-key values merged recursively,
4745/// scalars combine into arrays (a non-array beside an array wraps
4746/// first).
4747fn merge_preserve(a: JsonValue, b: JsonValue) -> JsonValue {
4748    match (a, b) {
4749        (JsonValue::Object(mut ma), JsonValue::Object(mb)) => {
4750            for (k, v) in mb {
4751                if let Some(pos) = ma.iter().position(|(mk, _)| *mk == k) {
4752                    let (_, old) = ma.remove(pos);
4753                    ma.insert(pos, (k, merge_preserve(old, v)));
4754                } else {
4755                    ma.push((k, v));
4756                }
4757            }
4758            JsonValue::Object(ma)
4759        }
4760        (JsonValue::Array(mut xs), JsonValue::Array(ys)) => {
4761            xs.extend(ys);
4762            JsonValue::Array(xs)
4763        }
4764        (JsonValue::Array(mut xs), scalar) => {
4765            xs.push(scalar);
4766            JsonValue::Array(xs)
4767        }
4768        (scalar, JsonValue::Array(ys)) => {
4769            let mut xs = alloc::vec![scalar];
4770            xs.extend(ys);
4771            JsonValue::Array(xs)
4772        }
4773        (sa, sb) => JsonValue::Array(alloc::vec![sa, sb]),
4774    }
4775}
4776
4777fn mysql_json_merge(
4778    args: &[Value<'_>],
4779    fn_name: &str,
4780    combine: fn(JsonValue, JsonValue) -> JsonValue,
4781) -> Result<Value<'static>, EvalError> {
4782    if args.len() < 2 {
4783        return Err(EvalError::TypeMismatch {
4784            detail: alloc::format!("{fn_name}() takes at least 2 documents, got {}", args.len()),
4785        });
4786    }
4787    if args.iter().any(|a| matches!(a, Value::Null)) {
4788        return Ok(Value::Null);
4789    }
4790    let mut acc: Option<JsonValue> = None;
4791    for arg in args {
4792        let src = match arg {
4793            Value::Json(s) | Value::Text(s) => s.as_ref(),
4794            other => {
4795                return Err(EvalError::TypeMismatch {
4796                    detail: alloc::format!(
4797                        "{fn_name}() arguments must be json, got {}",
4798                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
4799                    ),
4800                });
4801            }
4802        };
4803        let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4804            detail: alloc::format!("{fn_name}(): invalid JSON: {e}"),
4805        })?;
4806        acc = Some(match acc {
4807            None => doc,
4808            Some(prev) => combine(prev, doc),
4809        });
4810    }
4811    // v7.39 (round 392) — MariaDB's `": "` / `", "` JSON render spacing.
4812    Ok(canonicalize_value(Value::Json(alloc::borrow::Cow::Owned(
4813        acc.unwrap().to_json_text(),
4814    ))))
4815}
4816
4817/// v7.37.17 (17.6 siblings) — MySQL JSON_MERGE_PATCH (RFC 7396).
4818pub fn mysql_json_merge_patch(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4819    mysql_json_merge(args, "json_merge_patch", merge_patch)
4820}
4821
4822/// v7.37.17 (17.6 siblings) — MySQL JSON_MERGE_PRESERVE (and its
4823/// deprecated JSON_MERGE alias).
4824pub fn mysql_json_merge_preserve(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4825    mysql_json_merge(args, "json_merge_preserve", merge_preserve)
4826}
4827
4828/// v7.37.17 (17.6 siblings) — MySQL JSON_OVERLAPS(d1, d2): arrays
4829/// share any element; objects share any key-value pair; scalars
4830/// compare equal; an array vs a scalar checks membership.
4831pub fn mysql_json_overlaps(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4832    if args.len() != 2 {
4833        return Err(EvalError::TypeMismatch {
4834            detail: alloc::format!("json_overlaps() takes 2 args, got {}", args.len()),
4835        });
4836    }
4837    if args.iter().any(|a| matches!(a, Value::Null)) {
4838        return Ok(Value::Null);
4839    }
4840    let parse_arg = |v: &Value<'_>| -> Result<JsonValue, EvalError> {
4841        match v {
4842            Value::Json(s) | Value::Text(s) => parse(s).map_err(|e| EvalError::TypeMismatch {
4843                detail: alloc::format!("json_overlaps(): invalid JSON: {e}"),
4844            }),
4845            other => Err(EvalError::TypeMismatch {
4846                detail: alloc::format!(
4847                    "json_overlaps() arguments must be json, got {}",
4848                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4849                ),
4850            }),
4851        }
4852    };
4853    let a = parse_arg(&args[0])?;
4854    let b = parse_arg(&args[1])?;
4855    let overlaps = match (&a, &b) {
4856        (JsonValue::Array(xs), JsonValue::Array(ys)) => xs.iter().any(|x| {
4857            ys.iter()
4858                .any(|y| mysql_contains(x, y) && mysql_contains(y, x))
4859        }),
4860        (JsonValue::Object(ma), JsonValue::Object(mb)) => ma.iter().any(|(k, v)| {
4861            mb.iter()
4862                .any(|(k2, v2)| k == k2 && mysql_contains(v, v2) && mysql_contains(v2, v))
4863        }),
4864        (JsonValue::Array(xs), scalar) | (scalar, JsonValue::Array(xs)) => xs
4865            .iter()
4866            .any(|x| mysql_contains(x, scalar) && mysql_contains(scalar, x)),
4867        (sa, sb) => mysql_contains(sa, sb) && mysql_contains(sb, sa),
4868    };
4869    Ok(Value::Bool(overlaps))
4870}
4871
4872/// SQL LIKE matcher for json_search: `%` any run, `_` one char,
4873/// `escape` literalises the next char.
4874fn like_match(text: &[char], pat: &[char], escape: char) -> bool {
4875    match pat {
4876        [] => text.is_empty(),
4877        ['%', rest @ ..] => (0..=text.len()).any(|skip| like_match(&text[skip..], rest, escape)),
4878        ['_', rest @ ..] => !text.is_empty() && like_match(&text[1..], rest, escape),
4879        [e, lit, rest @ ..] if *e == escape => {
4880            text.first() == Some(lit) && like_match(&text[1..], rest, escape)
4881        }
4882        [c, rest @ ..] => text.first() == Some(c) && like_match(&text[1..], rest, escape),
4883    }
4884}
4885
4886/// Render one MySQL path step onto a path string. Identifier-shaped
4887/// keys render bare (`$.a`); anything else quotes (`$."a b"`).
4888fn push_path_step(out: &mut String, step_key: Option<&str>, step_idx: Option<usize>) {
4889    if let Some(k) = step_key {
4890        let ident_shaped = !k.is_empty()
4891            && k.chars().all(|c| c.is_alphanumeric() || c == '_')
4892            && !k.chars().next().unwrap().is_numeric();
4893        if ident_shaped {
4894            out.push('.');
4895            out.push_str(k);
4896        } else {
4897            out.push_str(".\"");
4898            for c in k.chars() {
4899                if c == '"' || c == '\\' {
4900                    out.push('\\');
4901                }
4902                out.push(c);
4903            }
4904            out.push('"');
4905        }
4906    }
4907    if let Some(i) = step_idx {
4908        out.push('[');
4909        out.push_str(&alloc::format!("{i}"));
4910        out.push(']');
4911    }
4912}
4913
4914/// v7.37.17 (17.6 siblings) — MySQL JSON_SEARCH(doc, 'one'|'all',
4915/// pattern [, escape [, path...]]). Returns the path of the first
4916/// string value LIKE-matching the pattern ('one') or a JSON array
4917/// of all such paths ('all'); NULL when nothing matches. The
4918/// optional path args narrow where the walk starts.
4919pub fn mysql_json_search(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
4920    if args.len() < 3 {
4921        return Err(EvalError::TypeMismatch {
4922            detail: alloc::format!(
4923                "json_search() takes doc, one/all, pattern [, escape [, path...]], got {} args",
4924                args.len()
4925            ),
4926        });
4927    }
4928    if args[..3].iter().any(|a| matches!(a, Value::Null)) {
4929        return Ok(Value::Null);
4930    }
4931    let src = match &args[0] {
4932        Value::Json(s) | Value::Text(s) => s.as_ref(),
4933        other => {
4934            return Err(EvalError::TypeMismatch {
4935                detail: alloc::format!(
4936                    "json_search() document must be json, got {}",
4937                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
4938                ),
4939            });
4940        }
4941    };
4942    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
4943        detail: alloc::format!("json_search(): invalid JSON: {e}"),
4944    })?;
4945    let one = match &args[1] {
4946        Value::Text(m) if m.eq_ignore_ascii_case("one") => true,
4947        Value::Text(m) if m.eq_ignore_ascii_case("all") => false,
4948        other => {
4949            return Err(EvalError::TypeMismatch {
4950                detail: alloc::format!(
4951                    "json_search() second arg must be 'one' or 'all', got {other:?}"
4952                ),
4953            });
4954        }
4955    };
4956    let Value::Text(pattern) = &args[2] else {
4957        return Err(EvalError::TypeMismatch {
4958            detail: alloc::format!(
4959                "json_search() pattern must be text, got {}",
4960                crate::conversions::pg_type_name_for_error_opt(args[2].data_type())
4961            ),
4962        });
4963    };
4964    let escape = match args.get(3) {
4965        None | Some(Value::Null) => '\\',
4966        Some(Value::Text(e)) if e.chars().count() == 1 => e.chars().next().unwrap(),
4967        Some(other) => {
4968            return Err(EvalError::TypeMismatch {
4969                detail: alloc::format!(
4970                    "json_search() escape must be a single character, got {other:?}"
4971                ),
4972            });
4973        }
4974    };
4975    let pat: Vec<char> = pattern.chars().collect();
4976    fn walk(
4977        v: &JsonValue,
4978        path: &str,
4979        pat: &[char],
4980        escape: char,
4981        hits: &mut Vec<String>,
4982        stop_at_one: bool,
4983    ) {
4984        if stop_at_one && !hits.is_empty() {
4985            return;
4986        }
4987        match v {
4988            JsonValue::String(s) => {
4989                let chars: Vec<char> = s.chars().collect();
4990                if like_match(&chars, pat, escape) {
4991                    hits.push(path.to_string());
4992                }
4993            }
4994            JsonValue::Object(members) => {
4995                for (k, mv) in members {
4996                    let mut p = path.to_string();
4997                    push_path_step(&mut p, Some(k), None);
4998                    walk(mv, &p, pat, escape, hits, stop_at_one);
4999                }
5000            }
5001            JsonValue::Array(items) => {
5002                for (i, iv) in items.iter().enumerate() {
5003                    let mut p = path.to_string();
5004                    push_path_step(&mut p, None, Some(i));
5005                    walk(iv, &p, pat, escape, hits, stop_at_one);
5006                }
5007            }
5008            _ => {}
5009        }
5010    }
5011    let mut hits: Vec<String> = Vec::new();
5012    let start_paths: Vec<String> = args
5013        .get(4..)
5014        .unwrap_or(&[])
5015        .iter()
5016        .map(|v| match v {
5017            Value::Text(p) => Ok(p.to_string()),
5018            other => Err(EvalError::TypeMismatch {
5019                detail: alloc::format!(
5020                    "json_search() paths must be text, got {}",
5021                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
5022                ),
5023            }),
5024        })
5025        .collect::<Result<_, _>>()?;
5026    if start_paths.is_empty() {
5027        walk(&doc, "$", &pat, escape, &mut hits, one);
5028    } else {
5029        for p in &start_paths {
5030            let steps = mysql_path_steps(p)?;
5031            if let Some(sub) = mysql_path_get(&doc, &steps) {
5032                walk(sub, p.trim(), &pat, escape, &mut hits, one);
5033            }
5034        }
5035    }
5036    match hits.len() {
5037        0 => Ok(Value::Null),
5038        1 => Ok(Value::Json(alloc::borrow::Cow::Owned(
5039            JsonValue::String(hits.into_iter().next().unwrap()).to_json_text(),
5040        ))),
5041        _ => {
5042            let arr = JsonValue::Array(hits.into_iter().map(JsonValue::String).collect());
5043            Ok(Value::Json(alloc::borrow::Cow::Owned(arr.to_json_text())))
5044        }
5045    }
5046}
5047
5048/// v7.37.17 (17.6 siblings) — MySQL JSON_VALUE(doc, path). Returns
5049/// the scalar at the path as unquoted text (MySQL's default
5050/// RETURNING VARCHAR); containers render as JSON text; a miss is
5051/// NULL. The RETURNING clause is parser syntax and queued.
5052pub fn mysql_json_value(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
5053    if args.len() != 2 {
5054        return Err(EvalError::TypeMismatch {
5055            detail: alloc::format!("json_value() takes 2 args, got {}", args.len()),
5056        });
5057    }
5058    if args.iter().any(|a| matches!(a, Value::Null)) {
5059        return Ok(Value::Null);
5060    }
5061    let src = match &args[0] {
5062        Value::Json(s) | Value::Text(s) => s.as_ref(),
5063        other => {
5064            return Err(EvalError::TypeMismatch {
5065                detail: alloc::format!(
5066                    "json_value() document must be json, got {}",
5067                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
5068                ),
5069            });
5070        }
5071    };
5072    let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
5073        detail: alloc::format!("json_value(): invalid JSON: {e}"),
5074    })?;
5075    let Value::Text(p) = &args[1] else {
5076        return Err(EvalError::TypeMismatch {
5077            detail: alloc::format!(
5078                "json_value() path must be text, got {}",
5079                crate::conversions::pg_type_name_for_error_opt(args[1].data_type())
5080            ),
5081        });
5082    };
5083    let steps = mysql_path_steps(p)?;
5084    match mysql_path_get(&doc, &steps) {
5085        None => Ok(Value::Null),
5086        Some(JsonValue::Null) => Ok(Value::Null),
5087        Some(v) => Ok(Value::text(v.as_text())),
5088    }
5089}
5090
5091/// v7.39 (round 234) — a JSON scalar (string / number / boolean / null),
5092/// i.e. anything that isn't a container. PG refuses every path-based
5093/// modification against one: there is nowhere for a path to point.
5094fn is_json_scalar(v: &JsonValue) -> bool {
5095    !matches!(v, JsonValue::Object(_) | JsonValue::Array(_))
5096}
5097
5098#[cfg(test)]
5099mod round619_number_fast_path {
5100    use super::*;
5101
5102    /// v7.39 (round 619) — the borrowed shortcut has to be the same string
5103    /// the full canonicaliser builds, for every lexeme either might see.
5104    /// Checked over a generated set rather than by reading the two.
5105    #[test]
5106    fn fast_path_agrees_with_the_full_canonicaliser() {
5107        let mut cases: Vec<String> = Vec::new();
5108        for sign in ["", "-"] {
5109            for body in [
5110                "0",
5111                "1",
5112                "7",
5113                "10",
5114                "123",
5115                "0123",
5116                "00",
5117                "000",
5118                "9223372036854775807",
5119                "170141183460469231731687303715884105727",
5120                "1.0",
5121                "1.5",
5122                "0.5",
5123                ".5",
5124                "1.",
5125                "1e3",
5126                "1E3",
5127                "1e-3",
5128                "1.5e2",
5129                "1.50",
5130                "100",
5131                "0.0",
5132                "0.00",
5133                "10.010",
5134                "1e0",
5135                "1e+3",
5136                "0e0",
5137                "12345678901234567890.12345678901234567890",
5138            ] {
5139                cases.push(alloc::format!("{sign}{body}"));
5140            }
5141        }
5142        for c in &cases {
5143            assert_eq!(
5144                canon_json_number(c).as_ref(),
5145                canon_json_number_slow(c).as_str(),
5146                "lexeme {c:?} canonicalises differently through the shortcut"
5147            );
5148        }
5149    }
5150
5151    /// The single-entry object writer has to spell what the sorting one does.
5152    #[test]
5153    fn one_entry_object_writes_what_the_general_writer_writes() {
5154        for src in [
5155            r#"{"a":1}"#,
5156            r#"{"":1}"#,
5157            r#"{"a":{"b":2}}"#,
5158            r#"{"a":[1,2,3]}"#,
5159            r#"{"a\"b":"c\\d"}"#,
5160            r#"{"日本":"語"}"#,
5161            r#"{"a":null}"#,
5162            r#"{}"#,
5163        ] {
5164            let JsonValue::Object(entries) = parse(src).expect("valid json") else {
5165                panic!("{src} is not an object");
5166            };
5167            let mut fast = String::new();
5168            write_json_canonical(&JsonValue::Object(entries.clone()), &mut fast);
5169            let mut general = String::new();
5170            write_object_general(&entries, &mut general);
5171            assert_eq!(fast, general, "{src}");
5172        }
5173    }
5174}