Skip to main content

spg_engine/
json.rs

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