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/// v7.39 (GUC knife 3) — the canonical renderer under a session
269/// `RenderStyle` (DateStyle / IntervalStyle / extra_float_digits).
270/// `value_to_text` is the default-style shorthand.
271pub fn value_to_text_styled(v: &Value, style: &crate::eval::RenderStyle) -> String {
272    match v {
273        // v7.5.0 — Value is #[non_exhaustive]; any future variant
274        // without explicit text rendering hits the Debug fallback
275        // at the end.
276        Value::SmallInt(n) => format!("{n}"),
277        Value::Int(n) => format!("{n}"),
278        Value::BigInt(n) => format!("{n}"),
279        // PG `float8out`: shortest round-trip, scientific notation past
280        // the ±exponent thresholds, `Infinity` / `-Infinity` / `NaN` for
281        // the non-finite values.
282        Value::Float(x) => crate::eval::format_float_styled(*x, style),
283        // v7.38 (read01, T-float4) — PG float4out (f32 shortest round-trip).
284        Value::Real(x) => crate::eval::format_real_styled(*x, style),
285        // v7.38 (read01, T11) — bpchar renders blank-padded (the stored form,
286        // as PG's wire display). The ::text CAST strips (handled in cast.rs).
287        Value::BpChar(s) => s.to_string(),
288        // v4.9: JSON renders identically to Text — both are raw UTF-8.
289        Value::Text(s) | Value::Json(s) => s.to_string(),
290        Value::Bool(b) => (if *b { "true" } else { "false" }).into(),
291        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC renders its exact
292        // decimal string.
293        Value::NumericBig(b) => b.to_decimal_str(),
294        // v7.38 (read01, T9) — PG record_out: `(f1,f2,...)`, NULL fields empty,
295        // fields with special characters double-quoted (`\` and `"` escaped).
296        Value::Composite(fields) => {
297            let mut out = String::from("(");
298            for (i, (_, fv)) in fields.iter().enumerate() {
299                if i > 0 {
300                    out.push(',');
301                }
302                if matches!(fv, Value::Null) {
303                    continue;
304                }
305                let field = super::strings::value_to_format_text(fv);
306                let needs_quote = field.is_empty()
307                    || field
308                        .chars()
309                        .any(|c| matches!(c, ',' | '(' | ')' | '"' | '\\') || c.is_whitespace());
310                if needs_quote {
311                    out.push('"');
312                    for c in field.chars() {
313                        match c {
314                            '"' => out.push_str("\"\""),
315                            '\\' => out.push_str("\\\\"),
316                            other => out.push(other),
317                        }
318                    }
319                    out.push('"');
320                } else {
321                    out.push_str(&field);
322                }
323            }
324            out.push(')');
325            out
326        }
327        Value::Vector(v) => {
328            let cells: Vec<String> = v.iter().map(|x| format!("{x}")).collect();
329            format!("[{}]", cells.join(","))
330        }
331        // v6.0.1: render SQ8 cells dequantised, so SELECT output
332        // matches the pgvector wire shape clients expect. The
333        // recall envelope already absorbs the ≤ (max-min)/255/2
334        // dequantisation error.
335        Value::Sq8Vector(q) => {
336            let cells: Vec<String> = spg_storage::quantize::dequantize(q)
337                .iter()
338                .map(|x| format!("{x}"))
339                .collect();
340            format!("[{}]", cells.join(","))
341        }
342        // v6.0.3: HalfVector cells dequantise bit-exactly to f32
343        // for SELECT output.
344        Value::HalfVector(h) => {
345            let cells: Vec<String> = h.to_f32_vec().iter().map(|x| format!("{x}")).collect();
346            format!("[{}]", cells.join(","))
347        }
348        Value::Numeric {
349            scaled,
350            scale,
351            kind,
352        } => format_numeric_kind(*kind, *scaled, *scale),
353        Value::Date(d) => crate::eval::format_date_styled(*d, style),
354        Value::Timestamp(t) => crate::eval::format_timestamp_styled(*t, style),
355        Value::Interval {
356            months,
357            days,
358            micros,
359        } => crate::eval::format_interval_styled(*months, *days, *micros, style),
360        Value::Null => "NULL".into(),
361        // v7.10.4 — BYTEA renders as PG hex form.
362        // v7.39 (round 524) — unless the session asked for `escape`.
363        Value::Bytes(b) => {
364            if style.bytea_escape {
365                crate::eval::format::format_bytea_escape(b)
366            } else {
367                format_bytea_hex(b)
368            }
369        }
370        // v7.10.9 — TEXT[] / INT[] / BIGINT[] render PG external form.
371        Value::TextArray(items) => format_text_array(items),
372        Value::IntArray(items) => format_int_array(items),
373        Value::BigIntArray(items) => format_bigint_array(items),
374        // v7.12.0 — tsvector / tsquery render PG external form.
375        Value::TsVector(lexs) => format_tsvector(lexs),
376        Value::TsQuery(ast) => format_tsquery(ast),
377        // v7.17.0 — UUID renders canonical lowercase 8-4-4-4-12
378        // hyphenated form (PG `uuid_out`).
379        Value::Uuid(b) => spg_storage::format_uuid(b),
380        // v7.17.0 Phase 3.P0-32 — TIME canonical text.
381        Value::Time(us) => format_time(*us),
382        // v7.17.0 Phase 3.P0-34 — TIMETZ canonical text.
383        Value::TimeTz { us, offset_secs } => format_timetz(*us, *offset_secs),
384        // v7.17.0 Phase 3.P0-33 — YEAR 4-digit zero-padded.
385        Value::Year(y) => format!("{y:04}"),
386        // v7.17.0 Phase 3.P0-35 — MONEY en_US locale.
387        Value::Money(c) => format_money(*c),
388        // v7.17.0 Phase 3.P0-38 — Range canonical form. Routes
389        // through the engine's format_range_text to share the
390        // single renderer with pgwire / sqllogictest.
391        Value::Range { .. } => crate::conversions::format_range_text(v),
392        // v7.17.0 Phase 3.P0-39 — Hstore canonical PG text form.
393        Value::Hstore(pairs) => crate::conversions::format_hstore_text(pairs),
394        // v7.17.0 Phase 3.P0-40 — 2D array canonical PG text form.
395        Value::IntArray2D(rows) => crate::conversions::format_int_2d_text_pub(rows),
396        Value::BigIntArray2D(rows) => crate::conversions::format_bigint_2d_text_pub(rows),
397        Value::TextArray2D(rows) => crate::conversions::format_text_2d_text_pub(rows),
398        Value::BoolArray2D(rows) => crate::conversions::format_bool_2d_text_pub(rows),
399        // v7.37.5 γ — complete array-family rendering for the
400        // ζ-A/γ/δ/ε first-class types.
401        Value::BoolArray(items) => crate::eval::format_bool_array(items),
402        Value::SmallIntArray(items) => crate::eval::format_smallint_array(items),
403        Value::FloatArray(items) => crate::eval::format_float_array_styled(items, style),
404        Value::NumericArray(items) => crate::eval::format_numeric_array(items),
405        Value::DateArray(items) => crate::eval::format_date_array_styled(items, style),
406        Value::TimestampArray(items) => {
407            crate::eval::format_timestamp_array_styled(items, false, style)
408        }
409        Value::TimestamptzArray(items) => {
410            crate::eval::format_timestamp_array_styled(items, true, style)
411        }
412        Value::UuidArray(items) => crate::eval::format_uuid_array(items),
413        Value::JsonArray(items) | Value::JsonbArray(items) => crate::eval::format_text_array(items),
414        Value::BytesArray(items) => crate::eval::format_bytea_array(items),
415        Value::IntervalArray(items) => crate::eval::format_interval_array_styled(items, style),
416        Value::MoneyArray(items) => crate::conversions::format_money_array(items),
417        // v7.37.5 ε — geometry canonical PG text.
418        Value::Point(p) => crate::conversions::format_point(*p),
419        Value::Lseg(a, b) => crate::conversions::format_lseg(*a, *b),
420        Value::Path { points, closed } => crate::conversions::format_path(points, *closed),
421        Value::PgBox(ur, ll) => crate::conversions::format_pg_box(*ur, *ll),
422        Value::Polygon(points) => crate::conversions::format_polygon(points),
423        Value::Line { a, b, c } => crate::conversions::format_line(*a, *b, *c),
424        Value::Circle { center, radius } => crate::conversions::format_circle(*center, *radius),
425        // v7.37.5 δ — multirange canonical PG text.
426        Value::Multirange { ranges, .. } => crate::conversions::format_multirange(ranges),
427        // v7.37.5 ζ-A — network/MAC/bit/XML/char1.
428        Value::Inet { family, bits, addr } => crate::conversions::format_inet(*family, *bits, addr),
429        // v7.39 (round 262) — a CIDR ALWAYS shows its mask length, where
430        // an inet omits a full-width one: `'192.168.1.5'::inet::cidr` is
431        // `192.168.1.5/32` and `'::1'::inet::cidr` is `::1/128` (probed).
432        // Both variants shared the inet renderer, so a full-width cidr
433        // printed without its `/32`.
434        Value::Cidr { family, bits, addr } => {
435            crate::conversions::format_inet_full(*family, *bits, addr)
436        }
437        Value::Macaddr(b) => crate::conversions::format_macaddr(b),
438        Value::Macaddr8(b) => crate::conversions::format_macaddr8(b),
439        Value::PgLsn(l) => crate::conversions::format_pg_lsn(*l),
440        Value::RegClass(_, name) | Value::RegProc(_, name) => name.to_string(),
441        Value::RegType(_, name) => name.to_string(),
442        // v7.39 (round 511) — PG renders a tid `(block,offset)`.
443        Value::Tid(b, o) => alloc::format!("({b},{o})"),
444        // A transaction / command id renders as its number.
445        Value::Xid(x) => alloc::format!("{x}"),
446        Value::Cid(c) => alloc::format!("{c}"),
447        Value::BitString { nbits, bytes } => crate::conversions::format_bit_string(*nbits, bytes),
448        Value::Xml(s) => s.to_string(),
449        Value::Char1(b) => format!("{}", *b as char),
450        // v7.5.0 — #[non_exhaustive] fallback for future Value variants.
451        _ => format!("{v:?}"),
452    }
453}
454
455/// Element count of a 1-D array value, or `None` when `v` is not a 1-D
456/// array. Element-type-agnostic — every PG array element type is covered
457/// so count-only callers (array_length / array_upper / array_lower /
458/// array_ndims / array_dims / cardinality) stay uniform.
459pub(crate) fn array_len(v: &Value) -> Option<usize> {
460    match v {
461        Value::TextArray(items)
462        | Value::VarcharArray(items)
463        | Value::CharArray(items)
464        | Value::JsonArray(items)
465        | Value::JsonbArray(items) => Some(items.len()),
466        Value::IntArray(items) => Some(items.len()),
467        Value::BigIntArray(items) => Some(items.len()),
468        Value::SmallIntArray(items) => Some(items.len()),
469        Value::BoolArray(items) => Some(items.len()),
470        Value::FloatArray(items) => Some(items.len()),
471        Value::NumericArray(items) => Some(items.len()),
472        Value::DateArray(items) => Some(items.len()),
473        Value::TimestampArray(items) | Value::TimestamptzArray(items) => Some(items.len()),
474        Value::MoneyArray(items) => Some(items.len()),
475        Value::IntervalArray(items) => Some(items.len()),
476        Value::UuidArray(items) => Some(items.len()),
477        Value::BytesArray(items) => Some(items.len()),
478        _ => None,
479    }
480}
481
482/// v7.39 (read01 round 76) — every element of an array as an owned `Value`,
483/// or `None` when `v` is not an array at all. A 2-D matrix yields one 1-D
484/// array `Value` per row, so a caller that recurses (JSON encoding) gets the
485/// nesting for free.
486///
487/// This is the *iteration* half of the element menu whose *indexing* half is
488/// `array_element_at`: without it, every consumer that WALKS an array was
489/// written variant by variant, and the variants nobody had needed yet fell
490/// into a `_ =>` that quietly did the wrong thing — `to_jsonb(ARRAY[[1,2]])`
491/// rendered the *text* `"{{1,2}}"` as a JSON string instead of `[[1, 2]]`.
492pub(crate) fn array_elements(v: &Value) -> Option<alloc::vec::Vec<Value<'static>>> {
493    if let Some(n) = array_len(v) {
494        let mut out = alloc::vec::Vec::with_capacity(n);
495        for i in 0..n {
496            out.push(array_element_at(v, i)?);
497        }
498        return Some(out);
499    }
500    // 2-D: one 1-D array per row, same element type.
501    macro_rules! rows {
502        ($m:expr, $variant:ident) => {
503            Some($m.iter().map(|r| Value::$variant(r.clone())).collect())
504        };
505    }
506    match v {
507        Value::IntArray2D(m) => rows!(m, IntArray),
508        Value::BigIntArray2D(m) => rows!(m, BigIntArray),
509        Value::TextArray2D(m) => rows!(m, TextArray),
510        Value::BoolArray2D(m) => rows!(m, BoolArray),
511        _ => None,
512    }
513}
514
515/// v7.38 (read01, T10) — the (rows, cols) dimensions of a 2-D array, or None
516/// for anything that is not a 2-D matrix.
517pub(super) fn array_2d_dims(v: &Value) -> Option<(usize, usize)> {
518    match v {
519        Value::IntArray2D(m) => Some((m.len(), m.first().map_or(0, alloc::vec::Vec::len))),
520        Value::BigIntArray2D(m) => Some((m.len(), m.first().map_or(0, alloc::vec::Vec::len))),
521        Value::TextArray2D(m) => Some((m.len(), m.first().map_or(0, alloc::vec::Vec::len))),
522        Value::BoolArray2D(m) => Some((m.len(), m.first().map_or(0, alloc::vec::Vec::len))),
523        _ => None,
524    }
525}
526
527/// The `pos`-th (0-based) element of a 1-D array as an owned scalar
528/// `Value` (a NULL hole becomes `Value::Null`), or `None` when `pos` is
529/// out of range or `v` is not a 1-D array. O(1) per element. This is the
530/// single per-type element menu shared by array subscript and
531/// array_position / array_positions, which previously only matched
532/// Text/Int/BigInt arrays and errored on every other element type.
533pub(crate) fn array_element_at(v: &Value, pos: usize) -> Option<Value<'static>> {
534    use alloc::borrow::Cow;
535    macro_rules! nth {
536        ($items:expr, $map:expr) => {
537            $items
538                .get(pos)
539                .map(|e| e.as_ref().map_or(Value::Null, $map))
540        };
541    }
542    match v {
543        Value::TextArray(items) | Value::VarcharArray(items) | Value::CharArray(items) => {
544            nth!(items, |s| Value::Text(Cow::Owned(s.clone())))
545        }
546        Value::JsonArray(items) | Value::JsonbArray(items) => {
547            nth!(items, |s| Value::Json(Cow::Owned(s.clone())))
548        }
549        Value::IntArray(items) => nth!(items, |n| Value::Int(*n)),
550        Value::BigIntArray(items) => nth!(items, |n| Value::BigInt(*n)),
551        Value::SmallIntArray(items) => nth!(items, |n| Value::SmallInt(*n)),
552        Value::BoolArray(items) => nth!(items, |b| Value::Bool(*b)),
553        Value::FloatArray(items) => nth!(items, |f| Value::Float(*f)),
554        Value::NumericArray(items) => {
555            nth!(items, |t: &(i128, u16)| Value::Numeric {
556                scaled: t.0,
557                scale: t.1,
558                kind: spg_storage::NumericKind::Finite
559            })
560        }
561        Value::DateArray(items) => nth!(items, |d| Value::Date(*d)),
562        Value::TimestampArray(items) | Value::TimestamptzArray(items) => {
563            nth!(items, |t| Value::Timestamp(*t))
564        }
565        Value::MoneyArray(items) => nth!(items, |m| Value::Money(*m)),
566        Value::IntervalArray(items) => nth!(items, |s| Value::Interval {
567            months: s.months,
568            days: s.days,
569            micros: s.micros,
570        }),
571        Value::UuidArray(items) => nth!(items, |u| Value::Uuid(*u)),
572        Value::BytesArray(items) => nth!(items, |b| Value::Bytes(Cow::Owned(b.clone()))),
573        _ => None,
574    }
575}
576
577/// v7.39 (read01 round 72) — rebuild an array of the SAME variant as `model`
578/// from a list of element values. The mirror of `array_element_at`, and the
579/// piece that was missing: without it, every array function that BUILDS a result
580/// was written variant by variant, and the variants nobody had needed yet were
581/// simply absent (`array_remove` had int arms only — see round 71).
582///
583/// A value that does not fit the model's element type is a caller error, and the
584/// caller phrases it; here it becomes `None`.
585pub(super) fn array_rebuild(model: &Value<'_>, elems: &[Value<'static>]) -> Option<Value<'static>> {
586    macro_rules! build {
587        ($variant:ident, $conv:expr) => {{
588            let mut out = alloc::vec::Vec::with_capacity(elems.len());
589            for e in elems {
590                if matches!(e, Value::Null) {
591                    out.push(None);
592                    continue;
593                }
594                out.push(Some(($conv)(e)?));
595            }
596            Some(Value::$variant(out))
597        }};
598    }
599    let as_i64 = |v: &Value<'_>| -> Option<i64> {
600        match v {
601            Value::SmallInt(n) => Some(i64::from(*n)),
602            Value::Int(n) => Some(i64::from(*n)),
603            Value::BigInt(n) => Some(*n),
604            _ => None,
605        }
606    };
607    match model {
608        Value::TextArray(_) => build!(TextArray, |e: &Value<'_>| match e {
609            Value::Text(s) => Some(s.as_ref().to_string()),
610            _ => None,
611        }),
612        Value::VarcharArray(_) => build!(VarcharArray, |e: &Value<'_>| match e {
613            Value::Text(s) => Some(s.as_ref().to_string()),
614            _ => None,
615        }),
616        Value::JsonArray(_) => build!(JsonArray, |e: &Value<'_>| match e {
617            Value::Json(s) | Value::Text(s) => Some(s.as_ref().to_string()),
618            _ => None,
619        }),
620        Value::JsonbArray(_) => build!(JsonbArray, |e: &Value<'_>| match e {
621            Value::Json(s) | Value::Text(s) => Some(s.as_ref().to_string()),
622            _ => None,
623        }),
624        Value::IntArray(_) => build!(IntArray, |e: &Value<'_>| as_i64(e)
625            .and_then(|n| i32::try_from(n).ok())),
626        Value::BigIntArray(_) => build!(BigIntArray, as_i64),
627        Value::SmallIntArray(_) => build!(SmallIntArray, |e: &Value<'_>| as_i64(e)
628            .and_then(|n| i16::try_from(n).ok())),
629        Value::BoolArray(_) => build!(BoolArray, |e: &Value<'_>| match e {
630            Value::Bool(b) => Some(*b),
631            _ => None,
632        }),
633        Value::FloatArray(_) => build!(FloatArray, |e: &Value<'_>| match e {
634            Value::Float(f) => Some(*f),
635            Value::Real(f) => Some(f64::from(*f)),
636            other => as_i64(other).map(|n| n as f64),
637        }),
638        Value::NumericArray(_) => build!(NumericArray, |e: &Value<'_>| match e {
639            Value::Numeric { scaled, scale, .. } => Some((*scaled, *scale)),
640            other => as_i64(other).map(|n| (i128::from(n), 0u16)),
641        }),
642        Value::DateArray(_) => build!(DateArray, |e: &Value<'_>| match e {
643            Value::Date(d) => Some(*d),
644            _ => None,
645        }),
646        Value::TimestampArray(_) => build!(TimestampArray, |e: &Value<'_>| match e {
647            Value::Timestamp(t) => Some(*t),
648            _ => None,
649        }),
650        Value::TimestamptzArray(_) => build!(TimestamptzArray, |e: &Value<'_>| match e {
651            Value::Timestamp(t) => Some(*t),
652            _ => None,
653        }),
654        Value::MoneyArray(_) => build!(MoneyArray, |e: &Value<'_>| match e {
655            Value::Money(m) => Some(*m),
656            _ => None,
657        }),
658        Value::UuidArray(_) => build!(UuidArray, |e: &Value<'_>| match e {
659            Value::Uuid(u) => Some(*u),
660            _ => None,
661        }),
662        Value::BytesArray(_) => build!(BytesArray, |e: &Value<'_>| match e {
663            Value::Bytes(b) => Some(b.as_ref().to_vec()),
664            _ => None,
665        }),
666        Value::IntervalArray(_) => build!(IntervalArray, |e: &Value<'_>| match e {
667            Value::Interval {
668                months,
669                days,
670                micros,
671            } => Some(spg_storage::IntervalSpan {
672                months: *months,
673                days: *days,
674                micros: *micros,
675            }),
676            _ => None,
677        }),
678        _ => None,
679    }
680}
681
682/// v7.39 (read01 round 73) — build an array Value from a list of element values,
683/// choosing the element type PG would choose. ONE place, used by the `ARRAY[…]`
684/// literal, by `array_agg`, and by the ordered-set aggregates.
685///
686/// This is the fifth site that had been written variant by variant with a text
687/// fallback for everything it did not know (rounds 71/72 killed the first four).
688/// The rule: a homogeneous non-numeric, non-text list keeps its type; the numeric
689/// ladder unifies (float > numeric > bigint > int); anything mixed or unknown is
690/// `text[]`, which is a DECISION. v7.39 (round 779 audit, I1) — PG does NOT
691/// make the same one for the EMPTY case: bare `ARRAY[]` refuses there
692/// (`cannot determine type of empty array`) while SPG answers `text[]`.
693/// A deliberate superset, §9-ledgered: `ARRAY[]::t[]`, `'{}'::t[]` and every
694/// non-empty list agree with PG exactly.
695pub(crate) fn build_array_from_values(vals: &[Value<'static>]) -> Value<'static> {
696    if let Some(v) = homogeneous_typed_array(vals) {
697        return v;
698    }
699    let mut has_text = false;
700    let mut has_float = false;
701    let mut has_numeric = false;
702    let mut has_bigint = false;
703    let mut has_int = false;
704    for v in vals {
705        match v {
706            Value::Null => {}
707            Value::Int(_) | Value::SmallInt(_) => has_int = true,
708            Value::BigInt(_) => has_bigint = true,
709            Value::Numeric { .. } | Value::NumericBig(_) => has_numeric = true,
710            Value::Float(_) | Value::Real(_) => has_float = true,
711            _ => has_text = true,
712        }
713    }
714    let as_i64 = |v: &Value<'_>| -> Option<i64> {
715        match v {
716            Value::SmallInt(n) => Some(i64::from(*n)),
717            Value::Int(n) => Some(i64::from(*n)),
718            Value::BigInt(n) => Some(*n),
719            _ => None,
720        }
721    };
722    if !has_text {
723        if has_float {
724            return Value::FloatArray(
725                vals.iter()
726                    .map(|v| match v {
727                        Value::Null => None,
728                        Value::Float(f) => Some(*f),
729                        Value::Real(f) => Some(f64::from(*f)),
730                        #[allow(clippy::cast_precision_loss)]
731                        Value::Numeric { scaled, scale, .. } => {
732                            Some(*scaled as f64 / libm::pow(10.0, f64::from(*scale)))
733                        }
734                        other => as_i64(other).map(|n| n as f64),
735                    })
736                    .collect(),
737            );
738        }
739        if has_numeric {
740            // A NumericBig / non-finite value cannot live in a (i128, scale)
741            // cell, so it falls through to text[] rather than losing precision.
742            if vals.iter().all(|v| {
743                matches!(
744                    v,
745                    Value::Null
746                        | Value::SmallInt(_)
747                        | Value::Int(_)
748                        | Value::BigInt(_)
749                        | Value::Numeric {
750                            kind: spg_storage::NumericKind::Finite,
751                            ..
752                        }
753                )
754            }) {
755                return Value::NumericArray(
756                    vals.iter()
757                        .map(|v| match v {
758                            Value::Null => None,
759                            Value::Numeric { scaled, scale, .. } => Some((*scaled, *scale)),
760                            other => as_i64(other).map(|n| (i128::from(n), 0u16)),
761                        })
762                        .collect(),
763                );
764            }
765        } else if has_bigint {
766            return Value::BigIntArray(vals.iter().map(as_i64).collect());
767        } else if has_int {
768            return Value::IntArray(
769                vals.iter()
770                    .map(|v| as_i64(v).and_then(|n| i32::try_from(n).ok()))
771                    .collect(),
772            );
773        }
774    }
775    Value::TextArray(
776        vals.iter()
777            .map(|v| match v {
778                Value::Null => None,
779                Value::Text(s) | Value::Json(s) => Some(s.as_ref().to_string()),
780                other => Some(crate::eval::value_to_text(other)),
781            })
782            .collect(),
783    )
784}
785
786/// An `ARRAY[…]` / `array_agg` of ONE non-numeric, non-text type keeps that
787/// type. `None` for an empty list or a mix.
788pub(crate) fn homogeneous_typed_array(vals: &[Value<'static>]) -> Option<Value<'static>> {
789    let first = vals.iter().find(|v| !matches!(v, Value::Null))?;
790    macro_rules! collect {
791        ($variant:ident, $pat:pat => $val:expr) => {{
792            let mut out = alloc::vec::Vec::with_capacity(vals.len());
793            for v in vals {
794                match v {
795                    Value::Null => out.push(None),
796                    $pat => out.push(Some($val)),
797                    _ => return None,
798                }
799            }
800            Some(Value::$variant(out))
801        }};
802    }
803    match first {
804        Value::Bool(_) => collect!(BoolArray, Value::Bool(b) => *b),
805        Value::Date(_) => collect!(DateArray, Value::Date(d) => *d),
806        Value::Timestamp(_) => collect!(TimestampArray, Value::Timestamp(t) => *t),
807        Value::Uuid(_) => collect!(UuidArray, Value::Uuid(u) => *u),
808        Value::Money(_) => collect!(MoneyArray, Value::Money(m) => *m),
809        Value::Bytes(_) => collect!(BytesArray, Value::Bytes(b) => b.as_ref().to_vec()),
810        Value::Interval { .. } => {
811            let mut out = alloc::vec::Vec::with_capacity(vals.len());
812            for v in vals {
813                match v {
814                    Value::Null => out.push(None),
815                    Value::Interval {
816                        months,
817                        days,
818                        micros,
819                    } => out.push(Some(spg_storage::IntervalSpan {
820                        months: *months,
821                        days: *days,
822                        micros: *micros,
823                    })),
824                    _ => return None,
825                }
826            }
827            Some(Value::IntervalArray(out))
828        }
829        _ => None,
830    }
831}
832
833/// v7.39 (read01 round 75) — build a 2-D array from rows that are themselves
834/// arrays. `None` when the list is not all-arrays (the caller then treats it as
835/// a 1-D list).
836///
837/// SEVENTH site of the per-variant pattern this campaign has been unpicking: the
838/// INSERT literal path had its OWN array builder, and it did not know 2-D at all
839/// — an `ARRAY[ARRAY[…]]` in a VALUES list collapsed to text[]. One builder now.
840/// v7.39 (read01 round 92) — a 2-D array literal like `{{1,2},{3,4}}`. If the
841/// brace-stripped inner opens with `{`, split it into the top-level `{…}` rows
842/// (depth-aware, respecting nesting and double-quoted strings), else return None
843/// so the caller runs its 1-D path. The `ARRAY[[…]]` constructor already made
844/// 2-D values (round 75); the text-literal cast — the form pg_dump emits —
845/// never learned nested braces and split the whole thing on the first comma.
846pub(crate) fn split_2d_rows(s: &str) -> Option<Vec<alloc::string::String>> {
847    let trimmed = s.trim();
848    let inner = trimmed
849        .strip_prefix('{')
850        .and_then(|x| x.strip_suffix('}'))?
851        .trim();
852    if !inner.starts_with('{') {
853        return None;
854    }
855    let mut rows = alloc::vec::Vec::new();
856    let bytes = inner.as_bytes();
857    let mut depth = 0i32;
858    let mut start = 0usize;
859    let mut in_quote = false;
860    let mut i = 0;
861    while i < bytes.len() {
862        let c = bytes[i];
863        if in_quote {
864            if c == b'\\' {
865                i += 2;
866                continue;
867            }
868            if c == b'"' {
869                in_quote = false;
870            }
871        } else {
872            match c {
873                b'"' => in_quote = true,
874                b'{' => depth += 1,
875                b'}' => depth -= 1,
876                b',' if depth == 0 => {
877                    rows.push(inner[start..i].trim().to_string());
878                    start = i + 1;
879                }
880                _ => {}
881            }
882        }
883        i += 1;
884    }
885    rows.push(inner[start..].trim().to_string());
886    Some(rows)
887}
888
889pub(crate) fn build_2d_from_rows(rows: &[Value<'static>]) -> Option<Value<'static>> {
890    if rows.is_empty() || !rows.iter().all(|v| array_len(v).is_some()) {
891        return None;
892    }
893    let width = array_len(&rows[0])?;
894    if !rows.iter().all(|v| array_len(v) == Some(width)) {
895        return None;
896    }
897    if rows.iter().all(|v| matches!(v, Value::BoolArray(_))) {
898        return Some(Value::BoolArray2D(
899            rows.iter()
900                .map(|v| match v {
901                    Value::BoolArray(r) => r.clone(),
902                    _ => unreachable!("checked"),
903                })
904                .collect(),
905        ));
906    }
907    if rows.iter().all(|v| matches!(v, Value::IntArray(_))) {
908        return Some(Value::IntArray2D(
909            rows.iter()
910                .map(|v| match v {
911                    Value::IntArray(r) => r.clone(),
912                    _ => unreachable!("checked"),
913                })
914                .collect(),
915        ));
916    }
917    if rows
918        .iter()
919        .all(|v| matches!(v, Value::IntArray(_) | Value::BigIntArray(_)))
920    {
921        return Some(Value::BigIntArray2D(
922            rows.iter()
923                .map(|v| match v {
924                    Value::BigIntArray(r) => r.clone(),
925                    Value::IntArray(r) => r.iter().map(|c| c.map(i64::from)).collect(),
926                    _ => unreachable!("checked"),
927                })
928                .collect(),
929        ));
930    }
931    // Everything else renders into the text 2-D form, element by element, with
932    // the SCALAR rendering (a cell pulled out with `[i][j]::text` must read like
933    // a scalar).
934    Some(Value::TextArray2D(
935        rows.iter()
936            .map(|v| {
937                let n = array_len(v).unwrap_or(0);
938                (0..n)
939                    .map(|i| match array_element_at(v, i) {
940                        None | Some(Value::Null) => None,
941                        Some(x) => Some(crate::eval::value_to_text(&x)),
942                    })
943                    .collect()
944            })
945            .collect(),
946    ))
947}
948
949/// v7.39 (round 236) — PG's array functions treat a multidimensional array
950/// as its elements in row-major order: `array_to_string(ARRAY[[1,2],[3,4]],
951/// ',')` is `1,2,3,4` and `unnest` of it yields four rows. SPG stores 2-D
952/// arrays as their own variants, and the generic `array_len` /
953/// element-access helpers only knew the 1-D ones, so both functions
954/// rejected the value outright. Flattening here keeps every caller generic.
955pub(crate) fn flatten_2d(v: &Value<'_>) -> Option<Value<'static>> {
956    Some(match v {
957        Value::IntArray2D(rows) => Value::IntArray(rows.iter().flatten().copied().collect()),
958        Value::BigIntArray2D(rows) => Value::BigIntArray(rows.iter().flatten().copied().collect()),
959        Value::BoolArray2D(rows) => Value::BoolArray(rows.iter().flatten().copied().collect()),
960        Value::TextArray2D(rows) => Value::TextArray(rows.iter().flatten().cloned().collect()),
961        _ => return None,
962    })
963}