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