Skip to main content

spg_engine/eval/
values.rs

1//! Value-utility free functions split out of `eval.rs` (cut 36): pure
2//! functions over `Value`(s) that support the evaluator and aggregates —
3//! `value_cmp_for_min_max` (MIN/MAX ordering), `value_to_f64` (numeric
4//! coercion), `values_equal_for_nullif` (NULLIF equality),
5//! `gen_random_uuid_bytes` (UUID v4), and the central `value_to_text`
6//! renderer. These reach the canonical formatters (re-exported from
7//! `eval::format` / `eval::textsearch`), `crate::conversions`, the PRNG
8//! (`eval::math`), and `civil_from_days` through `use super::*`.
9
10use super::*;
11
12/// Compare two values for min/max selection. Returns Equal when
13/// values are equal (including cross-numeric-width), Less when
14/// a < b, Greater when a > b. NULL handling is upstream.
15pub(super) fn value_cmp_for_min_max(a: &Value, b: &Value, mysql: bool) -> core::cmp::Ordering {
16    use core::cmp::Ordering;
17    // v7.39 (round 412) — GREATEST / LEAST over text under the MySQL default
18    // collation compares by the folded form (case- and accent-insensitive,
19    // PAD SPACE), matching ORDER BY / MIN / MAX.
20    if mysql {
21        // v7.38.18 — each side on its own type; see `mysql_fold_value`.
22        if let (Some(x), Some(y)) = (
23            spg_storage::mysql_fold_value(a),
24            spg_storage::mysql_fold_value(b),
25        ) {
26            return x.cmp(&y);
27        }
28    }
29    // v7.38 (read01, T3.C3) — a NUMERIC beyond i128 orders via exact bignum.
30    if let Some(ord) = crate::orderby::numeric_bignum_cmp(a, b) {
31        return ord;
32    }
33    // v7.38 (read01, T6.P3) — min()/max() over NUMERIC honor the special total
34    // order -Inf < finite < +Inf < NaN, ahead of the f64 widen (which reads a
35    // special's canonical 0 as the number 0).
36    {
37        use spg_storage::NumericKind as NK;
38        let kind = |v: &Value| -> Option<NK> {
39            match v {
40                Value::Numeric { kind, .. } => Some(*kind),
41                Value::Int(_) | Value::BigInt(_) | Value::SmallInt(_) => Some(NK::Finite),
42                _ => None,
43            }
44        };
45        if let (Some(lk), Some(rk)) = (kind(a), kind(b)) {
46            if lk != NK::Finite || rk != NK::Finite {
47                let rank = |k: NK| match k {
48                    NK::NegInf => -2,
49                    NK::Finite => 0,
50                    NK::PosInf => 1,
51                    NK::NaN => 2,
52                };
53                return rank(lk).cmp(&rank(rk));
54            }
55        }
56    }
57    // Integer-widen first (covers SmallInt vs Int vs BigInt).
58    let a_int = match a {
59        Value::SmallInt(x) => Some(i64::from(*x)),
60        Value::Int(x) => Some(i64::from(*x)),
61        Value::BigInt(x) => Some(*x),
62        _ => None,
63    };
64    let b_int = match b {
65        Value::SmallInt(x) => Some(i64::from(*x)),
66        Value::Int(x) => Some(i64::from(*x)),
67        Value::BigInt(x) => Some(*x),
68        _ => None,
69    };
70    if let (Some(av), Some(bv)) = (a_int, b_int) {
71        return av.cmp(&bv);
72    }
73    // Float-widen.
74    let a_f = value_to_f64(a);
75    let b_f = value_to_f64(b);
76    if let (Some(av), Some(bv)) = (a_f, b_f) {
77        return av.partial_cmp(&bv).unwrap_or(Ordering::Equal);
78    }
79    // Text/Text and the remaining ordered types. The fallthrough
80    // used to swallow dates/timestamps/intervals as Equal, making
81    // greatest()/least() silently keep the first argument.
82    match (a, b) {
83        (Value::Text(av), Value::Text(bv)) => av.cmp(bv),
84        (Value::Bytes(av), Value::Bytes(bv)) => av.cmp(bv),
85        (Value::Date(av), Value::Date(bv)) => av.cmp(bv),
86        (Value::Timestamp(av), Value::Timestamp(bv)) => av.cmp(bv),
87        // Date vs timestamp: lift the date to midnight micros.
88        (Value::Date(av), Value::Timestamp(bv)) => {
89            (i64::from(*av).saturating_mul(86_400_000_000)).cmp(bv)
90        }
91        (Value::Timestamp(av), Value::Date(bv)) => {
92            av.cmp(&i64::from(*bv).saturating_mul(86_400_000_000))
93        }
94        (Value::Time(av), Value::Time(bv)) => av.cmp(bv),
95        (Value::Bool(av), Value::Bool(bv)) => av.cmp(bv),
96        // Intervals order by their justified total microseconds
97        // (months at 30 days, PG's comparison convention).
98        (
99            Value::Interval {
100                months: am,
101                days: ad,
102                micros: au,
103                kind: akind,
104            },
105            Value::Interval {
106                months: bm,
107                days: bd,
108                micros: bu,
109                kind: bkind,
110            },
111        ) => {
112            let total = |m: i32, d: i32, u: i64| -> i128 {
113                i128::from(m) * 30 * 86_400_000_000 + i128::from(d) * 86_400_000_000 + i128::from(u)
114            };
115            akind
116                .rank()
117                .cmp(&bkind.rank())
118                .then_with(|| total(*am, *ad, *au).cmp(&total(*bm, *bd, *bu)))
119        }
120        // v7.39 (round 511) — a tid orders by block then offset. GREATEST /
121        // LEAST share this comparator with min/max's, and both used to reach
122        // the `_ => Equal` below.
123        (Value::Tid(b1, o1), Value::Tid(b2, o2)) => b1.cmp(b2).then(o1.cmp(o2)),
124        (Value::Xid(a), Value::Xid(b)) => a.cmp(b),
125        (Value::Cid(a), Value::Cid(b)) => a.cmp(b),
126        // v7.39 (round 516) — anything with no arm here asks the comparison
127        // the OPERATORS use instead of answering Equal.
128        //
129        // `_ => Equal` is not a neutral default in a min/max comparator: it
130        // silently keeps whichever value arrived first. That is how round
131        // 511's `max(ctid)` answered `(0,1)`, and how `network_larger`
132        // answered the SMALLER address here — inet has a `network_cmp` in
133        // the operator path and had no arm in this one. Delegating means a
134        // type only has to teach the engine its order once.
135        _ => crate::eval::binop::compare(spg_sql::ast::BinOp::Lt, a, b)
136            .ok()
137            .and_then(|v| match v {
138                Value::Bool(true) => Some(Ordering::Less),
139                Value::Bool(false) => {
140                    match crate::eval::binop::compare(spg_sql::ast::BinOp::Gt, a, b) {
141                        Ok(Value::Bool(true)) => Some(Ordering::Greater),
142                        Ok(Value::Bool(false)) => Some(Ordering::Equal),
143                        _ => None,
144                    }
145                }
146                _ => None,
147            })
148            .unwrap_or(Ordering::Equal),
149    }
150}
151
152pub(super) fn value_to_f64(v: &Value) -> Option<f64> {
153    match v {
154        Value::Float(x) => Some(*x),
155        Value::Real(x) => Some(f64::from(*x)),
156        Value::SmallInt(x) => Some(f64::from(*x)),
157        Value::Int(x) => Some(f64::from(*x)),
158        Value::BigInt(x) => Some(*x as f64),
159        Value::Numeric { scaled, scale, .. } => {
160            Some((*scaled as f64) / f64_powi(10.0, i32::from(*scale)))
161        }
162        _ => None,
163    }
164}
165
166/// PG-style equality for nullif. Handles cross-numeric-width
167/// comparison (Int vs BigInt vs SmallInt vs Float vs Numeric);
168/// text matches text exactly; everything else uses derived
169/// PartialEq.
170pub(super) fn values_equal_for_nullif(a: &Value, b: &Value) -> bool {
171    // Same-type fast path.
172    if a == b {
173        return true;
174    }
175    // Cross-int widening: SmallInt / Int / BigInt all comparable.
176    let a_int = match a {
177        Value::SmallInt(x) => Some(i64::from(*x)),
178        Value::Int(x) => Some(i64::from(*x)),
179        Value::BigInt(x) => Some(*x),
180        _ => None,
181    };
182    let b_int = match b {
183        Value::SmallInt(x) => Some(i64::from(*x)),
184        Value::Int(x) => Some(i64::from(*x)),
185        Value::BigInt(x) => Some(*x),
186        _ => None,
187    };
188    if let (Some(a), Some(b)) = (a_int, b_int) {
189        return a == b;
190    }
191    // Float / Numeric: widen to f64.
192    let a_f = match a {
193        Value::Float(x) => Some(*x),
194        Value::SmallInt(x) => Some(f64::from(*x)),
195        Value::Int(x) => Some(f64::from(*x)),
196        Value::BigInt(x) => Some(*x as f64),
197        Value::Numeric { scaled, scale, .. } => {
198            Some((*scaled as f64) / f64_powi(10.0, i32::from(*scale)))
199        }
200        _ => None,
201    };
202    let b_f = match b {
203        Value::Float(x) => Some(*x),
204        Value::SmallInt(x) => Some(f64::from(*x)),
205        Value::Int(x) => Some(f64::from(*x)),
206        Value::BigInt(x) => Some(*x as f64),
207        Value::Numeric { scaled, scale, .. } => {
208            Some((*scaled as f64) / f64_powi(10.0, i32::from(*scale)))
209        }
210        _ => None,
211    };
212    if let (Some(a), Some(b)) = (a_f, b_f) {
213        return a == b;
214    }
215    false
216}
217
218/// v7.17.0 — generate a RFC 4122 v4 (random) UUID. Layout: 16
219/// random bytes with the version nibble (high nibble of byte 6)
220/// pinned to `0100` (= 4) and the variant top bits (high two bits
221/// of byte 8) pinned to `10` — exactly what PG's
222/// `gen_random_uuid()` and the historical uuid-ossp
223/// `uuid_generate_v4()` produce.
224pub fn gen_random_uuid_bytes() -> [u8; 16] {
225    let mut out = [0u8; 16];
226    let hi = prng_next_u64().to_be_bytes();
227    let lo = prng_next_u64().to_be_bytes();
228    out[..8].copy_from_slice(&hi);
229    out[8..].copy_from_slice(&lo);
230    // Version 4: top nibble of byte 6 must be 0100.
231    out[6] = (out[6] & 0x0f) | 0x40;
232    // Variant 1 (RFC 4122): top two bits of byte 8 must be 10.
233    out[8] = (out[8] & 0x3f) | 0x80;
234    out
235}
236
237/// v7.39 (round 425) — render a value the way a MySQL client expects a
238/// column with a DECLARED fractional-seconds precision to look: EXACTLY
239/// `fsp` fractional digits, zero-padded (`DATETIME(3)` shows `.250`, and
240/// `.000` for a whole second), or none at all at precision 0. PG's renderer
241/// trims trailing zeros, which is right for PG and wrong for MySQL — the
242/// stored instant is identical either way.
243///
244/// `fsp` is `ColumnSchema.mysql_fsp`, which is `None` for every PG column
245/// and for any expression that reads no MySQL temporal column; those fall
246/// straight through to [`value_to_text`].
247#[must_use]
248pub fn value_to_text_with_fsp(v: &Value, fsp: Option<u8>) -> String {
249    let Some(fsp) = fsp else {
250        return value_to_text(v);
251    };
252    let (whole, micros) = match v {
253        Value::Timestamp(us) => (
254            crate::eval::format_timestamp(us.div_euclid(1_000_000) * 1_000_000),
255            us.rem_euclid(1_000_000),
256        ),
257        Value::Time(us) => (
258            crate::eval::format_time(us.div_euclid(1_000_000) * 1_000_000),
259            us.rem_euclid(1_000_000),
260        ),
261        other => return value_to_text(other),
262    };
263    if fsp == 0 {
264        return whole;
265    }
266    let digits = usize::from(fsp.min(6));
267    // `micros` is already truncated to the column's precision on write, so
268    // this only ever pads — it never drops a digit the caller could see.
269    let frac = format!("{micros:06}");
270    format!("{whole}.{}", &frac[..digits])
271}
272
273pub fn value_to_text(v: &Value) -> String {
274    value_to_text_styled(v, &crate::eval::RenderStyle::default())
275}
276
277/// 7.38.1 S4.1 (D5, MATRIX #18) — the COLUMN-AWARE canonical renderer.
278/// `timestamptz` stores the same i64 UTC microseconds as `timestamp`,
279/// so tz-ness lives only in the column type; a renderer that sees the
280/// value alone cannot append PG's offset suffix, and embedded output
281/// silently disagreed with the wire (`2026-01-05 09:00:00` vs
282/// `…09:00:00+00`). Callers that hold the result's `ColumnSchema` —
283/// the embedded surface, the sqllogictest runner, the diff/oracle
284/// harnesses — render through HERE so embedded and wire speak the
285/// same text. PG's own out-functions are type-addressed; this is the
286/// same shape.
287pub fn value_to_text_typed(v: &Value, dt: &spg_storage::DataType) -> String {
288    value_to_text_typed_styled(v, dt, &crate::eval::RenderStyle::default())
289}
290
291/// As [`value_to_text_typed`], under a session [`RenderStyle`].
292pub fn value_to_text_typed_styled(
293    v: &Value,
294    dt: &spg_storage::DataType,
295    style: &crate::eval::RenderStyle,
296) -> String {
297    match (dt, v) {
298        (spg_storage::DataType::Timestamptz, Value::Timestamp(us)) => {
299            crate::eval::format_timestamptz_styled(*us, style)
300        }
301        _ => value_to_text_styled(v, style),
302    }
303}
304
305/// v7.39 (GUC knife 3) — the canonical renderer under a session
306/// `RenderStyle` (DateStyle / IntervalStyle / extra_float_digits).
307/// `value_to_text` is the default-style shorthand.
308pub fn value_to_text_styled(v: &Value, style: &crate::eval::RenderStyle) -> String {
309    match v {
310        // v7.5.0 — Value is #[non_exhaustive]; any future variant
311        // without explicit text rendering hits the Debug fallback
312        // at the end.
313        Value::SmallInt(n) => format!("{n}"),
314        Value::Int(n) => format!("{n}"),
315        Value::BigInt(n) => format!("{n}"),
316        // PG `float8out`: shortest round-trip, scientific notation past
317        // the ±exponent thresholds, `Infinity` / `-Infinity` / `NaN` for
318        // the non-finite values.
319        Value::Float(x) => crate::eval::format_float_styled(*x, style),
320        // v7.38 (read01, T-float4) — PG float4out (f32 shortest round-trip).
321        Value::Real(x) => crate::eval::format_real_styled(*x, style),
322        // v7.38 (read01, T11) — bpchar renders blank-padded (the stored form,
323        // as PG's wire display). The ::text CAST strips (handled in cast.rs).
324        Value::BpChar(s) => s.to_string(),
325        // v4.9: JSON renders identically to Text — both are raw UTF-8.
326        Value::Text(s) | Value::Json(s) => s.to_string(),
327        Value::Bool(b) => (if *b { "true" } else { "false" }).into(),
328        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC renders its exact
329        // decimal string.
330        Value::NumericBig(b) => b.to_decimal_str(),
331        // v7.38 (read01, T9) — PG record_out: `(f1,f2,...)`, NULL fields empty,
332        // fields with special characters double-quoted (`\` and `"` escaped).
333        Value::Composite(fields) => {
334            let mut out = String::from("(");
335            for (i, (_, fv)) in fields.iter().enumerate() {
336                if i > 0 {
337                    out.push(',');
338                }
339                if matches!(fv, Value::Null) {
340                    continue;
341                }
342                let field = super::strings::value_to_format_text(fv);
343                let needs_quote = field.is_empty()
344                    || field
345                        .chars()
346                        .any(|c| matches!(c, ',' | '(' | ')' | '"' | '\\') || c.is_whitespace());
347                if needs_quote {
348                    out.push('"');
349                    for c in field.chars() {
350                        match c {
351                            '"' => out.push_str("\"\""),
352                            '\\' => out.push_str("\\\\"),
353                            other => out.push(other),
354                        }
355                    }
356                    out.push('"');
357                } else {
358                    out.push_str(&field);
359                }
360            }
361            out.push(')');
362            out
363        }
364        Value::Vector(v) => {
365            let cells: Vec<String> = v.iter().map(|x| format!("{x}")).collect();
366            format!("[{}]", cells.join(","))
367        }
368        // v6.0.1: render SQ8 cells dequantised, so SELECT output
369        // matches the pgvector wire shape clients expect. The
370        // recall envelope already absorbs the ≤ (max-min)/255/2
371        // dequantisation error.
372        Value::Sq8Vector(q) => {
373            let cells: Vec<String> = spg_storage::quantize::dequantize(q)
374                .iter()
375                .map(|x| format!("{x}"))
376                .collect();
377            format!("[{}]", cells.join(","))
378        }
379        // v6.0.3: HalfVector cells dequantise bit-exactly to f32
380        // for SELECT output.
381        Value::HalfVector(h) => {
382            let cells: Vec<String> = h.to_f32_vec().iter().map(|x| format!("{x}")).collect();
383            format!("[{}]", cells.join(","))
384        }
385        Value::Numeric {
386            scaled,
387            scale,
388            kind,
389        } => format_numeric_kind(*kind, *scaled, *scale),
390        Value::Date(d) => crate::eval::format_date_styled(*d, style),
391        Value::Timestamp(t) => crate::eval::format_timestamp_styled(*t, style),
392        Value::Interval {
393            months,
394            days,
395            micros,
396            kind,
397        } if kind.is_finite() => {
398            crate::eval::format_interval_styled(*months, *days, *micros, style)
399        }
400        Value::Interval { kind, .. } => crate::eval::format_interval_kinded(0, 0, 0, *kind),
401        Value::Null => "NULL".into(),
402        // v7.10.4 — BYTEA renders as PG hex form.
403        // v7.39 (round 524) — unless the session asked for `escape`.
404        Value::Bytes(b) => {
405            if style.bytea_escape {
406                crate::eval::format::format_bytea_escape(b)
407            } else {
408                format_bytea_hex(b)
409            }
410        }
411        // v7.10.9 — TEXT[] / INT[] / BIGINT[] render PG external form.
412        Value::TextArray(items) => format_text_array(items),
413        Value::IntArray(items) => format_int_array(items),
414        Value::BigIntArray(items) => format_bigint_array(items),
415        // v7.12.0 — tsvector / tsquery render PG external form.
416        Value::TsVector(lexs) => format_tsvector(lexs),
417        Value::TsQuery(ast) => format_tsquery(ast),
418        // v7.17.0 — UUID renders canonical lowercase 8-4-4-4-12
419        // hyphenated form (PG `uuid_out`).
420        Value::Uuid(b) => spg_storage::format_uuid(b),
421        // v7.17.0 Phase 3.P0-32 — TIME canonical text.
422        Value::Time(us) => format_time(*us),
423        // v7.17.0 Phase 3.P0-34 — TIMETZ canonical text.
424        Value::TimeTz { us, offset_secs } => format_timetz(*us, *offset_secs),
425        // v7.17.0 Phase 3.P0-33 — YEAR 4-digit zero-padded.
426        Value::Year(y) => format!("{y:04}"),
427        // v7.17.0 Phase 3.P0-35 — MONEY en_US locale.
428        Value::Money(c) => format_money(*c),
429        // v7.17.0 Phase 3.P0-38 — Range canonical form. Routes
430        // through the engine's format_range_text to share the
431        // single renderer with pgwire / sqllogictest.
432        Value::Range { .. } => crate::conversions::format_range_text(v),
433        // v7.17.0 Phase 3.P0-39 — Hstore canonical PG text form.
434        Value::Hstore(pairs) => crate::conversions::format_hstore_text(pairs),
435        // v7.17.0 Phase 3.P0-40 — 2D array canonical PG text form.
436        Value::IntArray2D(rows) => crate::conversions::format_int_2d_text_pub(rows),
437        Value::BigIntArray2D(rows) => crate::conversions::format_bigint_2d_text_pub(rows),
438        Value::TextArray2D(rows) => crate::conversions::format_text_2d_text_pub(rows),
439        Value::BoolArray2D(rows) => crate::conversions::format_bool_2d_text_pub(rows),
440        // v7.37.5 γ — complete array-family rendering for the
441        // ζ-A/γ/δ/ε first-class types.
442        Value::BoolArray(items) => crate::eval::format_bool_array(items),
443        Value::SmallIntArray(items) => crate::eval::format_smallint_array(items),
444        // v7.39.11 — PG's vector output function: elements separated by
445        // one space, no braces, no quoting. `1 2` where the array types
446        // print `{1,2}`.
447        Value::Int2Vector(items) => items
448            .iter()
449            .map(alloc::string::ToString::to_string)
450            .collect::<alloc::vec::Vec<_>>()
451            .join(" "),
452        Value::OidVector(items) => items
453            .iter()
454            .map(alloc::string::ToString::to_string)
455            .collect::<alloc::vec::Vec<_>>()
456            .join(" "),
457        Value::FloatArray(items) => crate::eval::format_float_array_styled(items, style),
458        Value::NumericArray(items) => crate::eval::format_numeric_array(items),
459        Value::DateArray(items) => crate::eval::format_date_array_styled(items, style),
460        Value::TimestampArray(items) => {
461            crate::eval::format_timestamp_array_styled(items, false, style)
462        }
463        Value::TimestamptzArray(items) => {
464            crate::eval::format_timestamp_array_styled(items, true, style)
465        }
466        Value::UuidArray(items) => crate::eval::format_uuid_array(items),
467        Value::JsonArray(items) | Value::JsonbArray(items) | Value::XmlArray(items) => {
468            crate::eval::format_text_array(items)
469        }
470        // v7.40.0 — bare element forms; see `format.rs`.
471        Value::RealArray(items) => crate::eval::format_real_array(items, style),
472        Value::TimeArray(items) => crate::eval::format_time_array(items),
473        Value::TimeTzArray(items) => crate::eval::format_timetz_array(items),
474        Value::InetArray(items) => crate::eval::format_inet_array(items),
475        Value::BytesArray(items) => crate::eval::format_bytea_array(items),
476        Value::IntervalArray(items) => crate::eval::format_interval_array_styled(items, style),
477        Value::MoneyArray(items) => crate::conversions::format_money_array(items),
478        // v7.37.5 ε — geometry canonical PG text.
479        Value::Point(p) => crate::conversions::format_point(*p),
480        Value::Lseg(a, b) => crate::conversions::format_lseg(*a, *b),
481        Value::Path { points, closed } => crate::conversions::format_path(points, *closed),
482        Value::PgBox(ur, ll) => crate::conversions::format_pg_box(*ur, *ll),
483        Value::Polygon(points) => crate::conversions::format_polygon(points),
484        Value::Line { a, b, c } => crate::conversions::format_line(*a, *b, *c),
485        Value::Circle { center, radius } => crate::conversions::format_circle(*center, *radius),
486        // v7.37.5 δ — multirange canonical PG text.
487        Value::Multirange { ranges, .. } => crate::conversions::format_multirange(ranges),
488        // v7.37.5 ζ-A — network/MAC/bit/XML/char1.
489        Value::Inet { family, bits, addr } => crate::conversions::format_inet(*family, *bits, addr),
490        // v7.39 (round 262) — a CIDR ALWAYS shows its mask length, where
491        // an inet omits a full-width one: `'192.168.1.5'::inet::cidr` is
492        // `192.168.1.5/32` and `'::1'::inet::cidr` is `::1/128` (probed).
493        // Both variants shared the inet renderer, so a full-width cidr
494        // printed without its `/32`.
495        Value::Cidr { family, bits, addr } => {
496            crate::conversions::format_inet_full(*family, *bits, addr)
497        }
498        Value::Macaddr(b) => crate::conversions::format_macaddr(b),
499        Value::Macaddr8(b) => crate::conversions::format_macaddr8(b),
500        Value::PgLsn(l) => crate::conversions::format_pg_lsn(*l),
501        Value::RegClass(_, name) | Value::RegProc(_, name) => name.to_string(),
502        Value::RegType(_, name) => name.to_string(),
503        // v7.39 (round 511) — PG renders a tid `(block,offset)`.
504        Value::Tid(b, o) => alloc::format!("({b},{o})"),
505        // A transaction / command id renders as its number.
506        Value::Xid(x) => alloc::format!("{x}"),
507        Value::Cid(c) => alloc::format!("{c}"),
508        Value::BitString { nbits, bytes } => crate::conversions::format_bit_string(*nbits, bytes),
509        Value::Xml(s) => s.to_string(),
510        Value::Char1(b) => format!("{}", *b as char),
511        // v7.5.0 — #[non_exhaustive] fallback for future Value variants.
512        _ => format!("{v:?}"),
513    }
514}
515
516/// Element count of a 1-D array value, or `None` when `v` is not a 1-D
517/// array. Element-type-agnostic — every PG array element type is covered
518/// so count-only callers (array_length / array_upper / array_lower /
519/// array_ndims / array_dims / cardinality) stay uniform.
520pub(crate) fn array_len(v: &Value) -> Option<usize> {
521    match v {
522        Value::TextArray(items)
523        | Value::VarcharArray(items)
524        | Value::CharArray(items)
525        | Value::JsonArray(items)
526        | Value::JsonbArray(items) => Some(items.len()),
527        Value::IntArray(items) => Some(items.len()),
528        Value::BigIntArray(items) => Some(items.len()),
529        Value::SmallIntArray(items) => Some(items.len()),
530        // v7.39.11 — the catalog vectors ARE arrays; only their I/O
531        // differs. Without these two arms `= ANY (i.indkey)` raised.
532        Value::Int2Vector(items) => Some(items.len()),
533        Value::OidVector(items) => Some(items.len()),
534        Value::BoolArray(items) => Some(items.len()),
535        Value::FloatArray(items) => Some(items.len()),
536        Value::NumericArray(items) => Some(items.len()),
537        Value::DateArray(items) => Some(items.len()),
538        Value::TimestampArray(items) | Value::TimestamptzArray(items) => Some(items.len()),
539        Value::MoneyArray(items) => Some(items.len()),
540        Value::IntervalArray(items) => Some(items.len()),
541        Value::UuidArray(items) => Some(items.len()),
542        Value::BytesArray(items) => Some(items.len()),
543        Value::RealArray(items) => Some(items.len()),
544        Value::TimeArray(items) => Some(items.len()),
545        Value::TimeTzArray(items) => Some(items.len()),
546        Value::InetArray(items) => Some(items.len()),
547        Value::XmlArray(items) => Some(items.len()),
548        _ => None,
549    }
550}
551
552/// v7.39 (read01 round 76) — every element of an array as an owned `Value`,
553/// or `None` when `v` is not an array at all. A 2-D matrix yields one 1-D
554/// array `Value` per row, so a caller that recurses (JSON encoding) gets the
555/// nesting for free.
556///
557/// This is the *iteration* half of the element menu whose *indexing* half is
558/// `array_element_at`: without it, every consumer that WALKS an array was
559/// written variant by variant, and the variants nobody had needed yet fell
560/// into a `_ =>` that quietly did the wrong thing — `to_jsonb(ARRAY[[1,2]])`
561/// rendered the *text* `"{{1,2}}"` as a JSON string instead of `[[1, 2]]`.
562pub(crate) fn array_elements(v: &Value) -> Option<alloc::vec::Vec<Value<'static>>> {
563    if let Some(n) = array_len(v) {
564        let mut out = alloc::vec::Vec::with_capacity(n);
565        for i in 0..n {
566            out.push(array_element_at(v, i)?);
567        }
568        return Some(out);
569    }
570    // 2-D: one 1-D array per row, same element type.
571    macro_rules! rows {
572        ($m:expr, $variant:ident) => {
573            Some($m.iter().map(|r| Value::$variant(r.clone())).collect())
574        };
575    }
576    match v {
577        Value::IntArray2D(m) => rows!(m, IntArray),
578        Value::BigIntArray2D(m) => rows!(m, BigIntArray),
579        Value::TextArray2D(m) => rows!(m, TextArray),
580        Value::BoolArray2D(m) => rows!(m, BoolArray),
581        _ => None,
582    }
583}
584
585/// v7.38 (read01, T10) — the (rows, cols) dimensions of a 2-D array, or None
586/// for anything that is not a 2-D matrix.
587pub(super) fn array_2d_dims(v: &Value) -> Option<(usize, usize)> {
588    match v {
589        Value::IntArray2D(m) => Some((m.len(), m.first().map_or(0, alloc::vec::Vec::len))),
590        Value::BigIntArray2D(m) => Some((m.len(), m.first().map_or(0, alloc::vec::Vec::len))),
591        Value::TextArray2D(m) => Some((m.len(), m.first().map_or(0, alloc::vec::Vec::len))),
592        Value::BoolArray2D(m) => Some((m.len(), m.first().map_or(0, alloc::vec::Vec::len))),
593        _ => None,
594    }
595}
596
597/// The `pos`-th (0-based) element of a 1-D array as an owned scalar
598/// `Value` (a NULL hole becomes `Value::Null`), or `None` when `pos` is
599/// out of range or `v` is not a 1-D array. O(1) per element. This is the
600/// single per-type element menu shared by array subscript and
601/// array_position / array_positions, which previously only matched
602/// Text/Int/BigInt arrays and errored on every other element type.
603pub(crate) fn array_element_at(v: &Value, pos: usize) -> Option<Value<'static>> {
604    use alloc::borrow::Cow;
605    macro_rules! nth {
606        ($items:expr, $map:expr) => {
607            $items
608                .get(pos)
609                .map(|e| e.as_ref().map_or(Value::Null, $map))
610        };
611    }
612    match v {
613        Value::TextArray(items) | Value::VarcharArray(items) | Value::CharArray(items) => {
614            nth!(items, |s| Value::Text(Cow::Owned(s.clone())))
615        }
616        Value::JsonArray(items) | Value::JsonbArray(items) => {
617            nth!(items, |s| Value::Json(Cow::Owned(s.clone())))
618        }
619        Value::IntArray(items) => nth!(items, |n| Value::Int(*n)),
620        Value::BigIntArray(items) => nth!(items, |n| Value::BigInt(*n)),
621        Value::SmallIntArray(items) => nth!(items, |n| Value::SmallInt(*n)),
622        // v7.39.11 — no null flags to unwrap; PG's vectors hold none.
623        Value::Int2Vector(items) => items.get(pos).map(|n| Value::SmallInt(*n)),
624        Value::OidVector(items) => items.get(pos).map(|n| Value::BigInt(i64::from(*n))),
625        Value::BoolArray(items) => nth!(items, |b| Value::Bool(*b)),
626        Value::FloatArray(items) => nth!(items, |f| Value::Float(*f)),
627        Value::NumericArray(items) => {
628            nth!(items, |t: &(i128, u16)| Value::Numeric {
629                scaled: t.0,
630                scale: t.1,
631                kind: spg_storage::NumericKind::Finite
632            })
633        }
634        Value::DateArray(items) => nth!(items, |d| Value::Date(*d)),
635        Value::TimestampArray(items) | Value::TimestamptzArray(items) => {
636            nth!(items, |t| Value::Timestamp(*t))
637        }
638        Value::MoneyArray(items) => nth!(items, |m| Value::Money(*m)),
639        Value::IntervalArray(items) => nth!(items, |s| Value::Interval {
640            months: s.months,
641            days: s.days,
642            micros: s.micros,
643            kind: s.kind,
644        }),
645        Value::UuidArray(items) => nth!(items, |u| Value::Uuid(*u)),
646        Value::BytesArray(items) => nth!(items, |b| Value::Bytes(Cow::Owned(b.clone()))),
647        Value::RealArray(items) => nth!(items, |x| Value::Real(*x)),
648        Value::TimeArray(items) => nth!(items, |us| Value::Time(*us)),
649        Value::TimeTzArray(items) => nth!(items, |(us, off)| Value::TimeTz {
650            us: *us,
651            offset_secs: *off,
652        }),
653        Value::InetArray(items) => nth!(items, |(family, bits, addr)| Value::Inet {
654            family: *family,
655            bits: *bits,
656            addr: *addr,
657        }),
658        Value::XmlArray(items) => nth!(items, |x| Value::Xml(Cow::Owned(x.clone()))),
659        _ => None,
660    }
661}
662
663/// v7.39 (read01 round 72) — rebuild an array of the SAME variant as `model`
664/// from a list of element values. The mirror of `array_element_at`, and the
665/// piece that was missing: without it, every array function that BUILDS a result
666/// was written variant by variant, and the variants nobody had needed yet were
667/// simply absent (`array_remove` had int arms only — see round 71).
668///
669/// A value that does not fit the model's element type is a caller error, and the
670/// caller phrases it; here it becomes `None`.
671pub(super) fn array_rebuild(model: &Value<'_>, elems: &[Value<'static>]) -> Option<Value<'static>> {
672    macro_rules! build {
673        ($variant:ident, $conv:expr) => {{
674            let mut out = alloc::vec::Vec::with_capacity(elems.len());
675            for e in elems {
676                if matches!(e, Value::Null) {
677                    out.push(None);
678                    continue;
679                }
680                out.push(Some(($conv)(e)?));
681            }
682            Some(Value::$variant(out))
683        }};
684    }
685    let as_i64 = |v: &Value<'_>| -> Option<i64> {
686        match v {
687            Value::SmallInt(n) => Some(i64::from(*n)),
688            Value::Int(n) => Some(i64::from(*n)),
689            Value::BigInt(n) => Some(*n),
690            _ => None,
691        }
692    };
693    match model {
694        Value::TextArray(_) => build!(TextArray, |e: &Value<'_>| match e {
695            Value::Text(s) => Some(s.as_ref().to_string()),
696            _ => None,
697        }),
698        Value::VarcharArray(_) => build!(VarcharArray, |e: &Value<'_>| match e {
699            Value::Text(s) => Some(s.as_ref().to_string()),
700            _ => None,
701        }),
702        Value::JsonArray(_) => build!(JsonArray, |e: &Value<'_>| match e {
703            Value::Json(s) | Value::Text(s) => Some(s.as_ref().to_string()),
704            _ => None,
705        }),
706        Value::JsonbArray(_) => build!(JsonbArray, |e: &Value<'_>| match e {
707            Value::Json(s) | Value::Text(s) => Some(s.as_ref().to_string()),
708            _ => None,
709        }),
710        Value::IntArray(_) => build!(IntArray, |e: &Value<'_>| as_i64(e)
711            .and_then(|n| i32::try_from(n).ok())),
712        Value::BigIntArray(_) => build!(BigIntArray, as_i64),
713        Value::SmallIntArray(_) => build!(SmallIntArray, |e: &Value<'_>| as_i64(e)
714            .and_then(|n| i16::try_from(n).ok())),
715        Value::BoolArray(_) => build!(BoolArray, |e: &Value<'_>| match e {
716            Value::Bool(b) => Some(*b),
717            _ => None,
718        }),
719        Value::FloatArray(_) => build!(FloatArray, |e: &Value<'_>| match e {
720            Value::Float(f) => Some(*f),
721            Value::Real(f) => Some(f64::from(*f)),
722            other => as_i64(other).map(|n| n as f64),
723        }),
724        Value::NumericArray(_) => build!(NumericArray, |e: &Value<'_>| match e {
725            Value::Numeric { scaled, scale, .. } => Some((*scaled, *scale)),
726            other => as_i64(other).map(|n| (i128::from(n), 0u16)),
727        }),
728        Value::DateArray(_) => build!(DateArray, |e: &Value<'_>| match e {
729            Value::Date(d) => Some(*d),
730            _ => None,
731        }),
732        Value::TimestampArray(_) => build!(TimestampArray, |e: &Value<'_>| match e {
733            Value::Timestamp(t) => Some(*t),
734            _ => None,
735        }),
736        Value::TimestamptzArray(_) => build!(TimestamptzArray, |e: &Value<'_>| match e {
737            Value::Timestamp(t) => Some(*t),
738            _ => None,
739        }),
740        Value::MoneyArray(_) => build!(MoneyArray, |e: &Value<'_>| match e {
741            Value::Money(m) => Some(*m),
742            _ => None,
743        }),
744        Value::UuidArray(_) => build!(UuidArray, |e: &Value<'_>| match e {
745            Value::Uuid(u) => Some(*u),
746            _ => None,
747        }),
748        Value::BytesArray(_) => build!(BytesArray, |e: &Value<'_>| match e {
749            Value::Bytes(b) => Some(b.as_ref().to_vec()),
750            _ => None,
751        }),
752        Value::RealArray(_) => build!(RealArray, |e: &Value<'_>| match e {
753            Value::Real(x) => Some(*x),
754            _ => None,
755        }),
756        Value::TimeArray(_) => build!(TimeArray, |e: &Value<'_>| match e {
757            Value::Time(us) => Some(*us),
758            _ => None,
759        }),
760        Value::TimeTzArray(_) => build!(TimeTzArray, |e: &Value<'_>| match e {
761            Value::TimeTz { us, offset_secs } => Some((*us, *offset_secs)),
762            _ => None,
763        }),
764        Value::InetArray(_) => build!(InetArray, |e: &Value<'_>| match e {
765            Value::Inet { family, bits, addr } => Some((*family, *bits, *addr)),
766            _ => None,
767        }),
768        Value::XmlArray(_) => build!(XmlArray, |e: &Value<'_>| match e {
769            Value::Xml(x) => Some(x.as_ref().into()),
770            _ => None,
771        }),
772        Value::IntervalArray(_) => build!(IntervalArray, |e: &Value<'_>| match e {
773            Value::Interval {
774                months,
775                days,
776                micros,
777                kind,
778            } => Some(spg_storage::IntervalSpan {
779                months: *months,
780                days: *days,
781                micros: *micros,
782                kind: *kind,
783            }),
784            _ => None,
785        }),
786        _ => None,
787    }
788}
789
790/// v7.39 (read01 round 73) — build an array Value from a list of element values,
791/// choosing the element type PG would choose. ONE place, used by the `ARRAY[…]`
792/// literal, by `array_agg`, and by the ordered-set aggregates.
793///
794/// This is the fifth site that had been written variant by variant with a text
795/// fallback for everything it did not know (rounds 71/72 killed the first four).
796/// The rule: a homogeneous non-numeric, non-text list keeps its type; the numeric
797/// ladder unifies (float > numeric > bigint > int); anything mixed or unknown is
798/// `text[]`, which is a DECISION. v7.39 (round 779 audit, I1) — PG does NOT
799/// make the same one for the EMPTY case: bare `ARRAY[]` refuses there
800/// (`cannot determine type of empty array`) while SPG answers `text[]`.
801/// A deliberate superset, §9-ledgered: `ARRAY[]::t[]`, `'{}'::t[]` and every
802/// non-empty list agree with PG exactly.
803pub(crate) fn build_array_from_values(vals: &[Value<'static>]) -> Value<'static> {
804    if let Some(v) = homogeneous_typed_array(vals) {
805        return v;
806    }
807    let mut has_text = false;
808    let mut has_float = false;
809    // v7.40.0 — PG 18.6, measured: `ARRAY[1.5::real, 2]` is `real[]` and
810    // `ARRAY[1.5::real, 2.5::numeric]` is `real[]`; only an actual
811    // `float8` in the list widens the whole array to `double precision[]`.
812    let mut has_real = false;
813    let mut has_numeric = false;
814    let mut has_bigint = false;
815    let mut has_int = false;
816    for v in vals {
817        match v {
818            Value::Null => {}
819            Value::Int(_) | Value::SmallInt(_) => has_int = true,
820            Value::BigInt(_) => has_bigint = true,
821            Value::Numeric { .. } | Value::NumericBig(_) => has_numeric = true,
822            Value::Float(_) => has_float = true,
823            Value::Real(_) => has_real = true,
824            _ => has_text = true,
825        }
826    }
827    let as_i64 = |v: &Value<'_>| -> Option<i64> {
828        match v {
829            Value::SmallInt(n) => Some(i64::from(*n)),
830            Value::Int(n) => Some(i64::from(*n)),
831            Value::BigInt(n) => Some(*n),
832            _ => None,
833        }
834    };
835    if !has_text {
836        if has_real && !has_float {
837            #[allow(clippy::cast_possible_truncation)]
838            return Value::RealArray(
839                vals.iter()
840                    .map(|v| match v {
841                        Value::Null => None,
842                        Value::Real(f) => Some(*f),
843                        #[allow(clippy::cast_precision_loss)]
844                        Value::Numeric { scaled, scale, .. } => {
845                            Some((*scaled as f64 / libm::pow(10.0, f64::from(*scale))) as f32)
846                        }
847                        other => as_i64(other).map(|n| n as f32),
848                    })
849                    .collect(),
850            );
851        }
852        if has_float || has_real {
853            return Value::FloatArray(
854                vals.iter()
855                    .map(|v| match v {
856                        Value::Null => None,
857                        Value::Float(f) => Some(*f),
858                        Value::Real(f) => Some(f64::from(*f)),
859                        #[allow(clippy::cast_precision_loss)]
860                        Value::Numeric { scaled, scale, .. } => {
861                            Some(*scaled as f64 / libm::pow(10.0, f64::from(*scale)))
862                        }
863                        other => as_i64(other).map(|n| n as f64),
864                    })
865                    .collect(),
866            );
867        }
868        if has_numeric {
869            // A NumericBig / non-finite value cannot live in a (i128, scale)
870            // cell, so it falls through to text[] rather than losing precision.
871            if vals.iter().all(|v| {
872                matches!(
873                    v,
874                    Value::Null
875                        | Value::SmallInt(_)
876                        | Value::Int(_)
877                        | Value::BigInt(_)
878                        | Value::Numeric {
879                            kind: spg_storage::NumericKind::Finite,
880                            ..
881                        }
882                )
883            }) {
884                return Value::NumericArray(
885                    vals.iter()
886                        .map(|v| match v {
887                            Value::Null => None,
888                            Value::Numeric { scaled, scale, .. } => Some((*scaled, *scale)),
889                            other => as_i64(other).map(|n| (i128::from(n), 0u16)),
890                        })
891                        .collect(),
892                );
893            }
894        } else if has_bigint {
895            return Value::BigIntArray(vals.iter().map(as_i64).collect());
896        } else if has_int {
897            return Value::IntArray(
898                vals.iter()
899                    .map(|v| as_i64(v).and_then(|n| i32::try_from(n).ok()))
900                    .collect(),
901            );
902        }
903    }
904    Value::TextArray(
905        vals.iter()
906            .map(|v| match v {
907                Value::Null => None,
908                Value::Text(s) | Value::Json(s) => Some(s.as_ref().to_string()),
909                other => Some(crate::eval::value_to_text(other)),
910            })
911            .collect(),
912    )
913}
914
915/// An `ARRAY[…]` / `array_agg` of ONE non-numeric, non-text type keeps that
916/// type. `None` for an empty list or a mix.
917pub(crate) fn homogeneous_typed_array(vals: &[Value<'static>]) -> Option<Value<'static>> {
918    let first = vals.iter().find(|v| !matches!(v, Value::Null))?;
919    macro_rules! collect {
920        ($variant:ident, $pat:pat => $val:expr) => {{
921            let mut out = alloc::vec::Vec::with_capacity(vals.len());
922            for v in vals {
923                match v {
924                    Value::Null => out.push(None),
925                    $pat => out.push(Some($val)),
926                    _ => return None,
927                }
928            }
929            Some(Value::$variant(out))
930        }};
931    }
932    match first {
933        Value::Bool(_) => collect!(BoolArray, Value::Bool(b) => *b),
934        Value::Date(_) => collect!(DateArray, Value::Date(d) => *d),
935        Value::Timestamp(_) => collect!(TimestampArray, Value::Timestamp(t) => *t),
936        Value::Uuid(_) => collect!(UuidArray, Value::Uuid(u) => *u),
937        Value::Money(_) => collect!(MoneyArray, Value::Money(m) => *m),
938        Value::Bytes(_) => collect!(BytesArray, Value::Bytes(b) => b.as_ref().to_vec()),
939        // v7.40.0 — the five that had no array variant to keep.
940        Value::Real(_) => collect!(RealArray, Value::Real(x) => *x),
941        Value::Time(_) => collect!(TimeArray, Value::Time(us) => *us),
942        Value::TimeTz { .. } => {
943            collect!(TimeTzArray, Value::TimeTz { us, offset_secs } => (*us, *offset_secs))
944        }
945        // PG unifies `inet` and `cidr` to `inet[]` (measured on 18.6),
946        // and the two share a body, so one arm takes both.
947        Value::Inet { .. } | Value::Cidr { .. } => {
948            let mut out = alloc::vec::Vec::with_capacity(vals.len());
949            for v in vals {
950                match v {
951                    Value::Null => out.push(None),
952                    Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
953                        out.push(Some((*family, *bits, *addr)));
954                    }
955                    _ => return None,
956                }
957            }
958            Some(Value::InetArray(out))
959        }
960        Value::Xml(_) => collect!(XmlArray, Value::Xml(x) => x.as_ref().into()),
961        Value::Interval { .. } => {
962            let mut out = alloc::vec::Vec::with_capacity(vals.len());
963            for v in vals {
964                match v {
965                    Value::Null => out.push(None),
966                    Value::Interval {
967                        months,
968                        days,
969                        micros,
970                        kind,
971                    } => out.push(Some(spg_storage::IntervalSpan {
972                        months: *months,
973                        days: *days,
974                        micros: *micros,
975                        kind: *kind,
976                    })),
977                    _ => return None,
978                }
979            }
980            Some(Value::IntervalArray(out))
981        }
982        _ => None,
983    }
984}
985
986/// v7.39 (read01 round 75) — build a 2-D array from rows that are themselves
987/// arrays. `None` when the list is not all-arrays (the caller then treats it as
988/// a 1-D list).
989///
990/// SEVENTH site of the per-variant pattern this campaign has been unpicking: the
991/// INSERT literal path had its OWN array builder, and it did not know 2-D at all
992/// — an `ARRAY[ARRAY[…]]` in a VALUES list collapsed to text[]. One builder now.
993/// v7.39 (read01 round 92) — a 2-D array literal like `{{1,2},{3,4}}`. If the
994/// brace-stripped inner opens with `{`, split it into the top-level `{…}` rows
995/// (depth-aware, respecting nesting and double-quoted strings), else return None
996/// so the caller runs its 1-D path. The `ARRAY[[…]]` constructor already made
997/// 2-D values (round 75); the text-literal cast — the form pg_dump emits —
998/// never learned nested braces and split the whole thing on the first comma.
999pub(crate) fn split_2d_rows(s: &str) -> Option<Vec<alloc::string::String>> {
1000    let trimmed = s.trim();
1001    let inner = trimmed
1002        .strip_prefix('{')
1003        .and_then(|x| x.strip_suffix('}'))?
1004        .trim();
1005    if !inner.starts_with('{') {
1006        return None;
1007    }
1008    let mut rows = alloc::vec::Vec::new();
1009    let bytes = inner.as_bytes();
1010    let mut depth = 0i32;
1011    let mut start = 0usize;
1012    let mut in_quote = false;
1013    let mut i = 0;
1014    while i < bytes.len() {
1015        let c = bytes[i];
1016        if in_quote {
1017            if c == b'\\' {
1018                i += 2;
1019                continue;
1020            }
1021            if c == b'"' {
1022                in_quote = false;
1023            }
1024        } else {
1025            match c {
1026                b'"' => in_quote = true,
1027                b'{' => depth += 1,
1028                b'}' => depth -= 1,
1029                b',' if depth == 0 => {
1030                    rows.push(inner[start..i].trim().to_string());
1031                    start = i + 1;
1032                }
1033                _ => {}
1034            }
1035        }
1036        i += 1;
1037    }
1038    rows.push(inner[start..].trim().to_string());
1039    Some(rows)
1040}
1041
1042pub(crate) fn build_2d_from_rows(rows: &[Value<'static>]) -> Option<Value<'static>> {
1043    if rows.is_empty() || !rows.iter().all(|v| array_len(v).is_some()) {
1044        return None;
1045    }
1046    let width = array_len(&rows[0])?;
1047    if !rows.iter().all(|v| array_len(v) == Some(width)) {
1048        return None;
1049    }
1050    if rows.iter().all(|v| matches!(v, Value::BoolArray(_))) {
1051        return Some(Value::BoolArray2D(
1052            rows.iter()
1053                .map(|v| match v {
1054                    Value::BoolArray(r) => r.clone(),
1055                    _ => unreachable!("checked"),
1056                })
1057                .collect(),
1058        ));
1059    }
1060    if rows.iter().all(|v| matches!(v, Value::IntArray(_))) {
1061        return Some(Value::IntArray2D(
1062            rows.iter()
1063                .map(|v| match v {
1064                    Value::IntArray(r) => r.clone(),
1065                    _ => unreachable!("checked"),
1066                })
1067                .collect(),
1068        ));
1069    }
1070    if rows
1071        .iter()
1072        .all(|v| matches!(v, Value::IntArray(_) | Value::BigIntArray(_)))
1073    {
1074        return Some(Value::BigIntArray2D(
1075            rows.iter()
1076                .map(|v| match v {
1077                    Value::BigIntArray(r) => r.clone(),
1078                    Value::IntArray(r) => r.iter().map(|c| c.map(i64::from)).collect(),
1079                    _ => unreachable!("checked"),
1080                })
1081                .collect(),
1082        ));
1083    }
1084    // Everything else renders into the text 2-D form, element by element, with
1085    // the SCALAR rendering (a cell pulled out with `[i][j]::text` must read like
1086    // a scalar).
1087    Some(Value::TextArray2D(
1088        rows.iter()
1089            .map(|v| {
1090                let n = array_len(v).unwrap_or(0);
1091                (0..n)
1092                    .map(|i| match array_element_at(v, i) {
1093                        None | Some(Value::Null) => None,
1094                        Some(x) => Some(crate::eval::value_to_text(&x)),
1095                    })
1096                    .collect()
1097            })
1098            .collect(),
1099    ))
1100}
1101
1102/// v7.39 (round 236) — PG's array functions treat a multidimensional array
1103/// as its elements in row-major order: `array_to_string(ARRAY[[1,2],[3,4]],
1104/// ',')` is `1,2,3,4` and `unnest` of it yields four rows. SPG stores 2-D
1105/// arrays as their own variants, and the generic `array_len` /
1106/// element-access helpers only knew the 1-D ones, so both functions
1107/// rejected the value outright. Flattening here keeps every caller generic.
1108pub(crate) fn flatten_2d(v: &Value<'_>) -> Option<Value<'static>> {
1109    Some(match v {
1110        Value::IntArray2D(rows) => Value::IntArray(rows.iter().flatten().copied().collect()),
1111        Value::BigIntArray2D(rows) => Value::BigIntArray(rows.iter().flatten().copied().collect()),
1112        Value::BoolArray2D(rows) => Value::BoolArray(rows.iter().flatten().copied().collect()),
1113        Value::TextArray2D(rows) => Value::TextArray(rows.iter().flatten().cloned().collect()),
1114        _ => return None,
1115    })
1116}