Skip to main content

spg_engine/
aggregate.rs

1//! Aggregate executor.
2//!
3//! Handles `SELECT … <aggs> … [GROUP BY …]` queries. The planning strategy
4//! is straightforward:
5//!
6//! 1. Walk the SELECT (and ORDER BY) expressions to find every aggregate
7//!    function call. Dedupe by AST equality and assign each `__agg_<i>`.
8//! 2. Same for every `GROUP BY` expression: assign `__grp_<j>`.
9//! 3. Stream the WHERE-filtered rows, group by the tuple of GROUP BY
10//!    values, and update per-group aggregate state.
11//! 4. Materialise a synthetic per-group row containing
12//!    `[__grp_0..__grp_K, __agg_0..__agg_N]` and rewrite the user's
13//!    SELECT / ORDER BY expressions to reference those synthetic columns
14//!    instead of the originals.
15//! 5. Evaluate the rewritten expressions against the synthetic schema and
16//!    emit results.
17//!
18//! v1.8 implements `count(*)`, `count(expr)`, `sum`, `min`, `max`, `avg`.
19//! NULL semantics follow PG: aggregates skip NULL inputs (except
20//! `count(*)`, which counts rows). `sum(int)` widens to `BigInt`;
21//! `avg(int|bigint)` returns `Float`.
22
23use alloc::borrow::Cow;
24use alloc::boxed::Box;
25use alloc::collections::BTreeSet;
26use alloc::format;
27use alloc::string::{String, ToString};
28use alloc::vec::Vec;
29
30use spg_sql::ast::{Expr, SelectItem, SelectStatement};
31use spg_storage::{ColumnSchema, DataType, Row, Value};
32
33use crate::eval::{self, EvalContext, EvalError};
34use crate::join::AggRows;
35
36impl crate::Engine {
37    /// v7.39 (round 763, F31-C1) — expand a `*` / `alias.*` SELECT item
38    /// into explicit column refs when the statement takes the aggregate
39    /// path and the FROM is one plain catalog table. Returns `None`
40    /// when nothing applies (the caller keeps the original statement).
41    /// Joined / derived / SRF sources keep the old refusal for now.
42    pub(crate) fn expand_aggregate_wildcard(
43        &self,
44        stmt: &SelectStatement,
45    ) -> Option<SelectStatement> {
46        use spg_sql::ast::SelectItem;
47        if !stmt
48            .items
49            .iter()
50            .any(|i| matches!(i, SelectItem::Wildcard | SelectItem::QualifiedWildcard(_)))
51        {
52            return None;
53        }
54        if !uses_aggregate(stmt) {
55            return None;
56        }
57        let from = stmt.from.as_ref()?;
58        if !from.joins.is_empty()
59            || from.primary.unnest_expr.is_some()
60            || from.primary.lateral_subquery.is_some()
61            || from.primary.generate_series_args.is_some()
62            || from.primary.table_fn_call.is_some()
63            || from.primary.json_table.is_some()
64            || from.primary.jsonb_each_text_arg.is_some()
65        {
66            return None;
67        }
68        let table = self.active_catalog().get(&from.primary.name)?;
69        let alias = from
70            .primary
71            .alias
72            .clone()
73            .unwrap_or_else(|| from.primary.name.clone());
74        let mut items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
75        for item in &stmt.items {
76            match item {
77                SelectItem::Wildcard => {
78                    for c in &table.schema().columns {
79                        items.push(SelectItem::Expr {
80                            expr: Expr::Column(spg_sql::ast::ColumnName {
81                                qualifier: None,
82                                name: c.name.clone(),
83                            }),
84                            alias: None,
85                        });
86                    }
87                }
88                SelectItem::QualifiedWildcard(q) => {
89                    if !q.eq_ignore_ascii_case(&alias) {
90                        return None; // unknown qualifier — keep the old path
91                    }
92                    // Bare names: the single-table qualifier is
93                    // redundant, and the group-expr matcher unifies
94                    // bare-to-bare (a qualified ref would miss a bare
95                    // GROUP BY id).
96                    for c in &table.schema().columns {
97                        items.push(SelectItem::Expr {
98                            expr: Expr::Column(spg_sql::ast::ColumnName {
99                                qualifier: None,
100                                name: c.name.clone(),
101                            }),
102                            alias: None,
103                        });
104                    }
105                }
106                other => items.push(other.clone()),
107            }
108        }
109        let mut out = stmt.clone();
110        out.items = items;
111        Some(out)
112    }
113}
114
115/// True if this statement should go through the aggregate path.
116pub fn uses_aggregate(stmt: &SelectStatement) -> bool {
117    if stmt.group_by.is_some() || stmt.having.is_some() {
118        return true;
119    }
120    uses_aggregate_ignoring_group_by(stmt)
121}
122
123/// v7.38.13 — the same question with the GROUP BY / HAVING short-circuit
124/// removed: does an aggregate CALL appear anywhere? `baregroup` needs
125/// this to tell a grouped aggregate from a GROUP BY that is a DISTINCT.
126pub(crate) fn uses_aggregate_ignoring_group_by(stmt: &SelectStatement) -> bool {
127    for item in &stmt.items {
128        if let SelectItem::Expr { expr, .. } = item
129            && contains_aggregate(expr)
130        {
131            return true;
132        }
133    }
134    for o in &stmt.order_by {
135        if contains_aggregate(&o.expr) {
136            return true;
137        }
138    }
139    if let Some(h) = &stmt.having
140        && contains_aggregate(h)
141    {
142        return true;
143    }
144    false
145}
146
147pub fn contains_aggregate(e: &Expr) -> bool {
148    match e {
149        Expr::FunctionCall { name, args } => {
150            is_aggregate_name(name) || args.iter().any(contains_aggregate)
151        }
152        Expr::NamedArg { expr, .. } => contains_aggregate(expr),
153        Expr::Variadic(expr) => contains_aggregate(expr),
154        Expr::AggregateOrdered { .. } => true,
155        Expr::Binary { lhs, rhs, .. } => contains_aggregate(lhs) || contains_aggregate(rhs),
156        Expr::Unary { expr, .. }
157        | Expr::Cast { expr, .. }
158        | Expr::IsNull { expr, .. }
159        | Expr::BoolTest { expr, .. }
160        | Expr::FieldAccess { base: expr, .. } => contains_aggregate(expr),
161        Expr::Like { expr, pattern, .. } => contains_aggregate(expr) || contains_aggregate(pattern),
162        Expr::Extract { source, .. } => contains_aggregate(source),
163        // v4.10 subqueries + v4.12 window functions / Literal /
164        // Column — all non-aggregate leaves from the regular
165        // aggregate planner's POV. Window-bearing projections are
166        // routed to exec_select_with_window before this runs.
167        Expr::ScalarSubquery(_)
168        | Expr::Exists { .. }
169        | Expr::InSubquery { .. }
170        | Expr::RowInSubquery { .. }
171        | Expr::RowCmpSubquery { .. }
172        | Expr::WindowFunction { .. }
173        | Expr::Literal(_)
174        | Expr::Placeholder(_)
175        | Expr::Column(_) => false,
176        // v7.10.10 — recurse into array constructor / subscript /
177        // ANY/ALL children. Aggregates inside `ARRAY[SUM(x)]` are
178        // valid PG and must be detected here.
179        Expr::Array(items) => items.iter().any(contains_aggregate),
180        Expr::ArraySubscript { target, index } => {
181            contains_aggregate(target) || contains_aggregate(index)
182        }
183        Expr::ArraySlice { target, lo, hi } => {
184            contains_aggregate(target)
185                || lo.as_deref().is_some_and(contains_aggregate)
186                || hi.as_deref().is_some_and(contains_aggregate)
187        }
188        Expr::AnyAll { expr, array, .. } => contains_aggregate(expr) || contains_aggregate(array),
189        Expr::InList { expr, list, .. } => {
190            contains_aggregate(expr) || list.iter().any(contains_aggregate)
191        }
192        // v7.13.0 — CASE WHEN … END. Recurse into operand,
193        // every (WHEN, THEN) pair, and the ELSE branch.
194        Expr::Case {
195            operand,
196            branches,
197            else_branch,
198        } => {
199            operand.as_deref().is_some_and(contains_aggregate)
200                || branches
201                    .iter()
202                    .any(|(w, t)| contains_aggregate(w) || contains_aggregate(t))
203                || else_branch.as_deref().is_some_and(contains_aggregate)
204        }
205    }
206}
207
208pub fn is_aggregate_name(name: &str) -> bool {
209    matches!(
210        name.to_ascii_lowercase().as_str(),
211        "count"
212            | "count_star"
213            | "sum"
214            | "min"
215            | "max"
216            | "avg"
217            // v7.17.0 — variadic / collection aggregates. ORM
218            // reports (Hibernate / Rails / Django) emit these in
219            // GROUP BY rollups; pre-7.17 SPG hit "unknown
220            // aggregate".
221            | "string_agg"
222            | "array_agg"
223            // PG 16+ — any_value: an arbitrary non-NULL value from
224            // the group (SPG: the first seen, deterministic for
225            // ordered input).
226            | "any_value"
227            // PG 14+ — range_agg: collect ranges into a multirange
228            // (insertion order, no coalescing — matches the
229            // multirange constructor contract).
230            | "range_agg"
231            // PG 14+ — range_intersect_agg: intersection fold.
232            | "range_intersect_agg"
233            // MySQL group_concat (string_agg with ',' default) +
234            // SQL/XML xmlagg (separator-less concatenation).
235            | "group_concat"
236            | "xmlagg"
237            // v7.17.0 — boolean aggregates. `every` is SQL-standard
238            // alias for `bool_and`.
239            | "bool_and"
240            | "bool_or"
241            | "every"
242            // v7.32 (round-29) — statistical aggregates (every BI /
243            // dashboard emits these in rollups).
244            | "stddev" | "stddev_samp" | "stddev_pop"
245            | "variance" | "var_samp" | "var_pop"
246            // v7.32 (round-29) — bitwise aggregates.
247            | "bit_and" | "bit_or" | "bit_xor"
248            // v7.32 (round-29) — ordered-set aggregates (used with
249            // `WITHIN GROUP (ORDER BY …)`).
250            | "percentile_cont" | "percentile_disc" | "mode"
251            // v7.32 (round-29) — hypothetical-set aggregates (also
252            // `WITHIN GROUP`): the rank the direct args WOULD have.
253            | "rank" | "dense_rank" | "percent_rank" | "cume_dist"
254            // v7.32 (round-29) — two-argument regression family.
255            | "covar_pop" | "covar_samp" | "corr"
256            | "regr_count" | "regr_avgx" | "regr_avgy" | "regr_slope"
257            | "regr_intercept" | "regr_r2" | "regr_sxx" | "regr_syy" | "regr_sxy"
258            // v7.32 (round-29) — JSON aggregates.
259            | "json_agg" | "jsonb_agg" | "json_object_agg" | "jsonb_object_agg"
260            | "json_agg_strict" | "jsonb_agg_strict"
261            | "json_object_agg_strict" | "jsonb_object_agg_strict"
262            | "json_object_agg_unique" | "jsonb_object_agg_unique"
263            | "json_object_agg_unique_strict" | "jsonb_object_agg_unique_strict"
264            // SQL:2016 standard spellings (PG 16+ accepts both).
265            | "json_arrayagg" | "json_objectagg"
266    )
267}
268
269/// v7.32 (round-29) — two-argument regression aggregates `f(Y, X)`.
270fn is_regression_name(name: &str) -> bool {
271    matches!(
272        name,
273        "covar_pop"
274            | "covar_samp"
275            | "corr"
276            | "regr_count"
277            | "regr_avgx"
278            | "regr_avgy"
279            | "regr_slope"
280            | "regr_intercept"
281            | "regr_r2"
282            | "regr_sxx"
283            | "regr_syy"
284            | "regr_sxy"
285    )
286}
287
288/// v7.32 (round-29) — aggregates that consume a second positional
289/// argument: `string_agg(v, sep)`, the regression family `f(Y, X)`, and
290/// `json_object_agg(key, value)`.
291fn agg_uses_second_arg(name: &str) -> bool {
292    // v7.39 (round 354, M12) — group_concat's SEPARATOR is lowered onto the
293    // same second argument string_agg takes; without this the separator was
294    // parsed and then dropped, so `SEPARATOR '|'` silently kept the default
295    // comma.
296    name == "group_concat"
297        || name == "string_agg"
298        || name.starts_with("json_object_agg")
299        || name.starts_with("jsonb_object_agg")
300        || name == "jsonb_object_agg"
301        || name == "json_objectagg"
302        || is_regression_name(name)
303}
304
305/// v7.32 (round-29) — ordered-set aggregates: the value to aggregate
306/// comes from the `WITHIN GROUP (ORDER BY …)` sort spec, and any
307/// in-parens arguments are *direct* arguments (the percentile fraction).
308/// `mode()` takes no direct argument.
309pub fn is_ordered_set_name(name: &str) -> bool {
310    // v7.32 — `eq_ignore_ascii_case` instead of `to_ascii_lowercase()`:
311    // these classifiers run in the aggregate row/group loop, where the
312    // old per-call `String` allocation showed up as ~16% of the inbox's
313    // aggregate path in a sampled profile (the names are constant).
314    ["percentile_cont", "percentile_disc", "mode"]
315        .iter()
316        .any(|k| name.eq_ignore_ascii_case(k))
317}
318
319/// v7.32 (round-29) — hypothetical-set aggregates: `rank(args) WITHIN
320/// GROUP (ORDER BY …)` and friends compute the rank the hypothetical
321/// row would have. Like ordered-set, the value stream comes from the
322/// sort spec and the in-parens args are direct (the hypothetical row).
323pub fn is_hypothetical_set_name(name: &str) -> bool {
324    ["rank", "dense_rank", "percent_rank", "cume_dist"]
325        .iter()
326        .any(|k| name.eq_ignore_ascii_case(k))
327}
328
329/// v7.32 (round-29) — every aggregate that takes its value stream from
330/// a `WITHIN GROUP (ORDER BY …)` clause (ordered-set + hypothetical-set).
331pub fn is_within_group_name(name: &str) -> bool {
332    is_ordered_set_name(name) || is_hypothetical_set_name(name)
333}
334
335/// v7.37.4 (R34) — pre-computed aggregate kind. Replaces per-row
336/// string matches in `update_state` with a single `match` on a
337/// `Copy` enum (compiles to a jump table). For the mailrs prod
338/// `/api/conversations` shape (14 aggregates × 100 k rows = 1.4 M
339/// inner-loop iterations) this is the dominant per-row cost.
340///
341/// Lowered from `AggSpec::name` at spec build time via
342/// [`classify_agg_name`]; populated by the three `AggSpec`
343/// construction sites (window+ORDER, plain, `first_ordered`
344/// `array_agg`).
345#[derive(Copy, Clone, Debug, PartialEq, Eq)]
346pub(crate) enum AggKind {
347    CountStar,
348    Count,
349    Sum,
350    Avg,
351    Min,
352    Max,
353    /// PG 16+ any_value — first non-NULL value seen.
354    AnyValue,
355    /// PG 14+ range_agg — collect ranges into a multirange.
356    RangeAgg,
357    /// PG 14+ range_intersect_agg — intersection fold over ranges.
358    RangeIntersectAgg,
359    StringAgg,
360    ArrayAgg,
361    BoolAnd,
362    BoolOr,
363    /// stddev / stddev_samp / stddev_pop / variance / var_samp / var_pop.
364    StddevFamily,
365    BitAnd,
366    BitOr,
367    BitXor,
368    /// ordered-set (`percentile_cont/disc`, `mode`) +
369    /// hypothetical-set (`rank`/`dense_rank`/etc.) aggregates that
370    /// share the WITHIN-GROUP collection path.
371    WithinGroup,
372    /// covar_samp / covar_pop / corr / regr_*.
373    Regression,
374    JsonAgg,
375    JsonObjectAgg,
376}
377
378/// v7.37.4 (R34) — name → kind, called once per spec at build time.
379/// Hot path (`update_state_kind`) only sees the enum; the canonical
380/// string still travels with the spec so `finalize` and errors can
381/// quote it.
382/// v7.39 (round 231) — the spelling `classify_agg_name` / `update_state` /
383/// `finalize` expect. PG's `every` is a standard-SQL alias for `bool_and`
384/// and every accumulator keys off the latter. The GROUP BY builder folded
385/// it at two of its own call sites; the window path (round 230) reached
386/// `classify_agg_name` without folding and hit its panic arm, so
387/// `every(x) OVER (…)` aborted the query. One entry point now, and
388/// `every_aggregate_name_classifies` keeps the two name lists in step.
389pub(crate) fn canonical_agg_name(name: &str) -> &str {
390    if name.eq_ignore_ascii_case("every") {
391        "bool_and"
392    } else {
393        name
394    }
395}
396
397pub(crate) fn classify_agg_name(name: &str) -> AggKind {
398    match name {
399        "count_star" => AggKind::CountStar,
400        "count" => AggKind::Count,
401        "sum" => AggKind::Sum,
402        "avg" => AggKind::Avg,
403        "min" => AggKind::Min,
404        "max" => AggKind::Max,
405        "any_value" => AggKind::AnyValue,
406        "range_agg" => AggKind::RangeAgg,
407        "range_intersect_agg" => AggKind::RangeIntersectAgg,
408        "string_agg" | "group_concat" | "xmlagg" => AggKind::StringAgg,
409        "array_agg" => AggKind::ArrayAgg,
410        "bool_and" => AggKind::BoolAnd,
411        "bool_or" => AggKind::BoolOr,
412        "stddev" | "stddev_samp" | "stddev_pop" | "variance" | "var_samp" | "var_pop" => {
413            AggKind::StddevFamily
414        }
415        "bit_and" => AggKind::BitAnd,
416        "bit_or" => AggKind::BitOr,
417        "bit_xor" => AggKind::BitXor,
418        "json_agg" | "jsonb_agg" | "json_arrayagg" | "json_agg_strict" | "jsonb_agg_strict" => {
419            AggKind::JsonAgg
420        }
421        "json_object_agg"
422        | "jsonb_object_agg"
423        | "json_objectagg"
424        | "json_object_agg_strict"
425        | "jsonb_object_agg_strict"
426        | "json_object_agg_unique"
427        | "jsonb_object_agg_unique"
428        | "json_object_agg_unique_strict"
429        | "jsonb_object_agg_unique_strict" => AggKind::JsonObjectAgg,
430        n if is_within_group_name(n) => AggKind::WithinGroup,
431        n if is_regression_name(n) => AggKind::Regression,
432        other => panic!("classify_agg_name: unknown aggregate {other}"),
433    }
434}
435
436/// Per-aggregate running state.
437///
438/// The four `use_*` flags are independent observations about which value
439/// shapes have flowed through this accumulator (a single `sum()` can see both
440/// numeric and float inputs), not a discriminant — collapsing them into one
441/// enum would change accumulation semantics, and a bitflags word would hide
442/// which gate each fast path reads.
443#[allow(clippy::struct_excessive_bools)]
444#[derive(Debug, Default, Clone)]
445pub(crate) struct AggState {
446    /// The shared sum/avg running state (see `NumAcc`).
447    num: NumAcc,
448    extreme: Option<Value<'static>>,
449    /// v7.17.0 — running collection for string_agg / array_agg.
450    /// Each entry is one row's contribution (NULL preserved as
451    /// `Value::Null`; string_agg's finalize step drops them, but
452    /// array_agg keeps them). Pushing in insertion order matches
453    /// PG behaviour when no `ORDER BY` is given inside the
454    /// aggregate call.
455    items: Vec<Value<'static>>,
456    /// v7.39 (round 762, F31-C2) — per-item separator, parallel to
457    /// `items`. PG evaluates string_agg's separator PER ROW: element
458    /// i is prefixed by ITS row's separator (`string_agg(v,
459    /// '<'||v||'>')` over a,b,c answers `a<b>b<c>c`; a NULL separator
460    /// renders empty; a skipped-NULL value row's separator is never
461    /// used). Populated only on the general path when the call has a
462    /// second argument; the fused lane is literal-separator only and
463    /// keeps the single `separator` snapshot below.
464    item_seps: Vec<Option<String>>,
465    /// v7.25 (round-17) — per-group dedupe set for DISTINCT
466    /// aggregates (encoded values; NULLs never reach it because
467    /// the caller's skip runs after the per-aggregate NULL rules).
468    /// v7.37.4 measured `hashbrown::HashSet` as worse at this
469    /// shape — the per-(group × distinct-spec) hash table alloc
470    /// overhead beats the lookup-speed gain when each set is
471    /// small. Sticking with `BTreeSet`; the dispatch-side enum
472    /// fix in `update_state` is the R34 win.
473    seen: BTreeSet<String>,
474    /// v7.37.x (docker-fair DISTA attack) — fast-path BigInt seen
475    /// set. The hot DISTINCT path used `encode_key_refs_into` to
476    /// turn `Value::BigInt(n)` into a string key like `"I<n>|"` then
477    /// inserted that into the String BTreeSet — ~100 ns of pure alloc
478    /// + format churn per row × 25 k rows × 1 BigInt DISTINCT spec
479    /// (the DISTA `COUNT(DISTINCT m.id)` shape) ≈ 2.5 ms of waste.
480    /// Direct `BTreeSet<i64>` skips encode entirely; lookups stay
481    /// O(log small) on the per-group set. Lazy-allocated — only the
482    /// BigInt-DISTINCT path constructs it.
483    seen_int: Option<BTreeSet<i64>>,
484    /// v7.24 (round-16 A) — per-item ORDER BY key tuples, parallel
485    /// to `items` (pushed under the same skip/keep conditions).
486    /// Empty when the aggregate carries no internal ordering.
487    /// v7.39 (round 723) — FLAT (SoA): `order_by.len()` key values per
488    /// item, back to back. The per-item `Vec<Vec<Value>>` form allocated
489    /// one heap Vec PER ROW just to hold (usually) one integer — ~20 ms
490    /// of pure allocator traffic on the panel's 500k `string_agg(s, ','
491    /// ORDER BY id)`. The key width is the spec's `order_by.len()`,
492    /// which every consumer already has.
493    item_keys: Vec<Value<'static>>,
494    /// v7.17.0 — captured separator for string_agg: the last
495    /// non-NULL text seen. v7.39 (round 762, F31-C2) — this is the
496    /// CONSTANT-separator snapshot only (fused lane, group_concat
497    /// default, DISTINCT fallback); the per-row truth lives in
498    /// `item_seps` (the old note claimed "use the latest row's
499    /// value" was PG's behaviour — measured false, PG is per-row).
500    separator: Option<String>,
501    /// v7.17.0 — running boolean accumulator for bool_and /
502    /// bool_or / every. `None` until the first non-NULL input;
503    /// at finalize None → SQL NULL.
504    bool_acc: Option<bool>,
505    /// v7.32 (round-29) — sum of squares for the variance / stddev
506    /// family (`sum_float` carries the running sum; `count` the n).
507    sum_sq: f64,
508    /// v7.38 (read01) — exact accumulators for the stddev/variance family.
509    /// PG computes those aggregates in NUMERIC over exact inputs (its float8
510    /// overload only serves float inputs), so an f64 accumulator loses PG's
511    /// exact division scale — `var_pop(1,2,3)` is `0.66666666666666666667`,
512    /// not the 16-digit double. `stddev_saw_float` flips on the first
513    /// float/real input and drops the family back to the f64 accumulators,
514    /// whose result is then double precision, matching PG's float8 overload.
515    stddev_saw_float: bool,
516    stddev_sum: Option<spg_storage::bignum::BigNumeric>,
517    stddev_sum_sq: Option<spg_storage::bignum::BigNumeric>,
518    /// v7.39 (round 615) — the same exact Σx / Σx², accumulated in `i128`
519    /// while every input is an integer and neither sum has overflowed.
520    ///
521    /// The `BigNumeric` pair above is exact and is what the finaliser wants,
522    /// but reaching it cost NINE allocations a row on a plain INTEGER column
523    /// — a boxed value per input, its square, and a fresh box for each of
524    /// the two running totals — where `sum` and `avg` over the same column
525    /// cost none. `i128` holds the same integers exactly: an `int4` squares
526    /// to at most 4.6e18, so the running Σx² has room for 3.7e19 rows before
527    /// it can overflow, and a `bigint` input that does overflow falls back
528    /// below with nothing lost — the pair is folded into the BigNumeric
529    /// accumulator first, so the total is the one it would have had.
530    stddev_i_sum: i128,
531    stddev_i_sum_sq: i128,
532    stddev_i_spent: bool,
533    /// v7.32 (round-29) — running accumulator for bit_and / bit_or /
534    /// bit_xor. `None` until the first non-NULL input → SQL NULL.
535    bit_acc: Option<i64>,
536    /// v7.38 (read01, T4.4) — true once a BIGINT input is seen, so
537    /// bit_and/or/xor finalize as bigint vs integer (PG input-typed).
538    bit_wide: bool,
539    /// v7.39 (round 254/255) — EVERY row fed to a WITHIN GROUP
540    /// aggregate, NULLs included. `items` (and `count`) hold only the
541    /// non-NULL values, which is right for `percentile_*` / `mode` —
542    /// but PG's hypothetical-set fractions divide by the full input
543    /// size: with one extra NULL row, `percent_rank(3)` moves from 2/6
544    /// to 2/7 (probed live). rank / dense_rank are unaffected either
545    /// way, since they only count values sorting before the
546    /// hypothetical row.
547    within_group_rows: usize,
548    /// v7.32 (round-29) — two-argument regression family
549    /// (`covar_*` / `corr` / `regr_*`), PG arg order `f(Y, X)`. Only
550    /// rows where BOTH inputs are non-NULL contribute (`count` is the
551    /// paired n, independent of the single-arg `sum_*`).
552    reg_n: i64,
553    reg_sx: f64,
554    reg_sy: f64,
555    reg_sxx: f64,
556    reg_syy: f64,
557    reg_sxy: f64,
558    /// v7.32 (round-29) — second value stream for `json_object_agg`
559    /// (`items` holds the keys, `aux_items` the values).
560    aux_items: Vec<Value<'static>>,
561    /// v7.33 (array_agg argmax) — for a `first_ordered` spec
562    /// (`(array_agg(x ORDER BY y))[1]`), the running first-by-order
563    /// (sort-key tuple, value). Replaced only when a new row's key sorts
564    /// strictly before the current best (ties keep the earliest row, =
565    /// the stable-sort `[1]`). No items/item_keys array is built.
566    first_best: Option<(Vec<Value<'static>>, Value<'static>)>,
567}
568
569#[derive(Debug, Clone)]
570struct AggSpec {
571    name: String, // lowercased
572    /// First argument (value expression) for every aggregate
573    /// except `count(*)`. `None` for `count_star`.
574    arg: Option<Expr>,
575    /// v7.17.0 — second argument. Only `string_agg(value, sep)`
576    /// uses it today. `None` for every other aggregate (or for
577    /// `array_agg`, which is single-arg). Carried in the spec so
578    /// per-row evaluation can re-use the same separator
579    /// expression across calls.
580    arg2: Option<Expr>,
581    /// v7.25 (round-17) — `COUNT(DISTINCT x)` & friends: dedupe
582    /// the input stream per group before accumulation.
583    distinct: bool,
584    /// v7.24 (round-16 A) — aggregate-internal ORDER BY keys
585    /// (`array_agg(x ORDER BY y DESC NULLS LAST)`). Empty for the
586    /// plain form. Only the collection aggregates honour it;
587    /// other aggregates are order-insensitive and ignore it (PG
588    /// accepts the syntax everywhere too).
589    order_by: Vec<spg_sql::ast::OrderBy>,
590    /// v7.32 (round-29) — `FILTER (WHERE cond)`: a per-row predicate
591    /// evaluated against the source row before accumulation. A row
592    /// whose `cond` is not TRUE (false or NULL) is excluded from this
593    /// aggregate only. `None` for the unfiltered form.
594    filter: Option<Expr>,
595    /// v7.32 (round-29) — ordered-set aggregates only: the *direct*
596    /// argument (the percentile fraction for `percentile_cont/disc`).
597    /// PG requires it constant, so it is evaluated once. `None` for
598    /// `mode()` and for every non-ordered-set aggregate.
599    direct_arg: Option<Expr>,
600    /// v7.39 (read01 orderedsetaggs.c) — the remaining direct arguments
601    /// of a multi-key hypothetical-set call (`rank(5, 'x') WITHIN GROUP
602    /// (ORDER BY a, b)`); one per sort key past the first. Empty
603    /// everywhere else.
604    direct_args_extra: Vec<Expr>,
605    /// v7.33 (array_agg argmax) — set when this spec came from
606    /// `(array_agg(x ORDER BY y))[1]`: accumulate only the first-by-order
607    /// element (a running argmax/argmin) and finalise to that scalar
608    /// value, instead of collecting + sorting + materialising the whole
609    /// per-group array just to take element 1. Returns the element type,
610    /// not the array type.
611    first_ordered: bool,
612    /// v7.37.4 (R34) — derived from `name` at spec build time so the
613    /// per-row inner loop dispatches via a `match` on `Copy` enum
614    /// instead of a string compare for every (row × aggregate)
615    /// iteration.
616    kind: AggKind,
617    /// v7.39 (enum order knife) — member labels when the aggregate's
618    /// argument is enum-typed and the aggregate orders its input
619    /// (min/max): extreme comparisons use member order, not label text.
620    /// Enriched once per query in `run` (spec collection is AST-only and
621    /// has no catalog).
622    enum_labels: Option<Vec<String>>,
623    /// v7.39 (round 690) — the argument column's declared collation, for
624    /// `min`/`max`. Resolved beside `enum_labels` and for the same reason:
625    /// both are facts about the ARGUMENT that the comparison needs and
626    /// cannot look up for itself.
627    arg_collation: Option<alloc::string::String>,
628    /// v7.39 (enum order knife) — per-ORDER-BY-key member labels for the
629    /// ordered collection aggregates (`array_agg(x ORDER BY enum_col)`).
630    /// Parallel to `order_by`; all-None when no key is enum-typed.
631    order_enum_labels: Vec<Option<Vec<String>>>,
632    /// v7.38.18 — per-ORDER-BY-key declared collation, for the ordered
633    /// collection aggregates. Parallel to `order_by`, resolved the same
634    /// way and for the same reason as `order_enum_labels` beside it.
635    ///
636    /// `min`/`max` have read the argument's collation since round 690
637    /// (`arg_collation`), and so does the statement's own ORDER BY, but
638    /// the sort INSIDE an aggregate did not: on a column declared
639    /// `COLLATE "en_US.utf8"`, `SELECT x FROM t ORDER BY x` answered
640    /// `apple, client, DateStyle, Zebra` while `string_agg(x, ' ' ORDER
641    /// BY x)` over the same column answered `DateStyle Zebra apple
642    /// client`. Two orderings of one column in one query.
643    order_collations: Vec<Option<alloc::string::String>>,
644}
645
646/// Output of running the aggregate path. Schema describes one row per
647/// group; rows are not yet ORDER BY-sorted (caller does it).
648#[derive(Debug)]
649pub struct AggResult {
650    pub columns: Vec<ColumnSchema>,
651    pub rows: Vec<Row<'static>>,
652    /// v7.31 (perf — PG lesson #1, post-LIMIT subquery projection):
653    /// select-list items whose rewritten expr carries a subquery and
654    /// is referenced by neither ORDER BY nor HAVING. Their output
655    /// cells hold NULL placeholders; the caller truncates to
656    /// LIMIT+OFFSET first and only then evaluates these for the
657    /// surviving rows (PG runs the same shape with SubPlan loops=50
658    /// instead of loops=24000). `(output_col, rewritten_expr)`.
659    pub deferred: Vec<(usize, Expr)>,
660    /// Synthetic group rows aligned 1:1 with `rows`; populated only
661    /// when `deferred` is non-empty.
662    pub synth_rows: Vec<Row<'static>>,
663    /// Schema the deferred exprs evaluate against.
664    pub synth_schema: Vec<ColumnSchema>,
665}
666
667/// Execute aggregate logic against an already-WHERE-filtered iterator of
668/// rows. `table_alias` is the alias accepted by column resolution.
669#[allow(clippy::too_many_lines)]
670/// v7.25.2 (round-19 A) — caller-injected evaluator for synth-row
671/// expressions that still carry subquery nodes after the rewrite
672/// (correlated subqueries in the select list / HAVING / aggregate
673/// ORDER BY of a GROUP BY query). The engine passes its
674/// correlated-aware evaluator; pure-library callers pass None and
675/// surviving subqueries keep erroring loudly.
676pub type CorrelatedEval<'a> =
677    &'a dyn Fn(&Expr, &Row<'static>, &EvalContext<'_>) -> Result<Value<'static>, EvalError>;
678
679/// Output of the per-group projection stage (`project_groups`): the
680/// output schema, the projected rows, the synth rows kept alongside
681/// them for post-LIMIT deferred evaluation, the deferred subquery
682/// items, and the rewritten ORDER BY exprs (shared with the sort).
683struct Projection {
684    columns: Vec<ColumnSchema>,
685    out_rows: Vec<Row<'static>>,
686    kept_synth: Vec<Row<'static>>,
687    deferred: Vec<(usize, Expr)>,
688    order_rewritten: Vec<Expr>,
689    /// v7.37.x — when `defer_projection` is requested, `out_rows`
690    /// carries empty placeholders and the caller runs the per-item
691    /// eval pass after sort+truncate over the surviving ≤ keep_n
692    /// rows. `None` when projection was performed inline.
693    deferred_project: Option<DeferredProject>,
694}
695
696struct DeferredProject {
697    items_rewritten: Vec<Option<Expr>>,
698    items_compiled: Vec<Option<eval::CompiledExpr>>,
699}
700
701/// v7.35.0 — detect the `SELECT COUNT(*) FROM … [WHERE …]` shape
702/// (single item, no GROUP BY / HAVING / ORDER BY / DISTINCT /
703/// LIMIT WITH TIES / FILTER / window). For this shape the answer
704/// is exactly `rows.len()` as `BigInt`, no group state needed.
705/// Returns `None` for any deviation so the caller's full pipeline
706/// runs verbatim.
707///
708/// v7.35.2 — also short-circuit `COUNT(<literal>)` (e.g.
709/// `COUNT(1)`) and `COUNT(<column>)` when the column is declared
710/// NOT NULL on the input schema. PG handles both cases as
711/// `COUNT(*)` (the non-null filter is a no-op), so doing the same
712/// here keeps every `count this thing` shape on the same fast path
713/// instead of routing the literal / non-null-col variants through
714/// the four-stage aggregate pipeline.
715fn try_pure_count_star_short_circuit(
716    stmt: &SelectStatement,
717    rows: AggRows<'_>,
718    schema_cols: &[ColumnSchema],
719    table_alias: Option<&str>,
720) -> Option<AggResult> {
721    if stmt.distinct
722        || stmt.limit_with_ties
723        || stmt.group_by.is_some()
724        || stmt.having.is_some()
725        || !stmt.order_by.is_empty()
726    {
727        return None;
728    }
729    if stmt.items.len() != 1 {
730        return None;
731    }
732    let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
733        return None;
734    };
735    let Expr::FunctionCall { name, args } = expr else {
736        return None;
737    };
738    if !name.eq_ignore_ascii_case("count") && !name.eq_ignore_ascii_case("count_star") {
739        return None;
740    }
741    let count_star_shape = match args.as_slice() {
742        // `COUNT(*)` parses to `count_star` with no args.
743        [] if name.eq_ignore_ascii_case("count_star") => true,
744        // `COUNT(<literal>)` — the per-row test is "is this literal
745        // non-null?" which is constant, so it's COUNT(*) when the
746        // literal is non-null.
747        [Expr::Literal(lit)] => !matches!(lit, spg_sql::ast::Literal::Null),
748        // `COUNT(<column>)` — same answer as COUNT(*) when the
749        // column is statically declared NOT NULL on the input
750        // schema. Resolve through the alias if one is set.
751        [Expr::Column(c)] => {
752            if let Some(q) = c.qualifier.as_deref()
753                && let Some(alias) = table_alias
754                && !q.eq_ignore_ascii_case(alias)
755            {
756                return None;
757            }
758            schema_cols
759                .iter()
760                .find(|s| s.name.eq_ignore_ascii_case(&c.name))
761                .is_some_and(|s| !s.nullable)
762        }
763        _ => return None,
764    };
765    if !count_star_shape {
766        return None;
767    }
768    let col_name = alias.clone().unwrap_or_else(|| "count".to_string());
769    let count = i64::try_from(rows.len()).unwrap_or(i64::MAX);
770    Some(AggResult {
771        columns: alloc::vec![ColumnSchema::new(col_name, DataType::BigInt, false)],
772        rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])],
773        deferred: Vec::new(),
774        synth_rows: Vec::new(),
775        synth_schema: Vec::new(),
776    })
777}
778
779/// v7.39 (round 528) — a GROUP BY name that names an output column.
780///
781/// `SELECT date_trunc('day', ts) AS d, count(*) FROM t GROUP BY d` is the
782/// canonical daily rollup, and it answered `column "d" does not exist`.
783/// Both PG and MySQL take a GROUP BY identifier that matches an output
784/// alias and group by the expression behind it; only grouping by a real
785/// column or an ordinal worked here.
786///
787/// Precedence is PG's, measured: an INPUT column of that name WINS.
788/// `SELECT v AS ts … GROUP BY ts` on a table that has a `ts` column
789/// groups by the column, which is why PG then rejects the ungrouped `v` —
790/// so the alias is consulted only when nothing else answers to the name.
791fn resolve_group_by_aliases(
792    keys: Vec<Expr>,
793    stmt: &SelectStatement,
794    schema_cols: &[ColumnSchema],
795) -> Result<Vec<Expr>, EvalError> {
796    let mut out = Vec::with_capacity(keys.len());
797    for key in keys {
798        let Expr::Column(c) = &key else {
799            out.push(key);
800            continue;
801        };
802        if c.qualifier.is_some()
803            || schema_cols
804                .iter()
805                .any(|sc| sc.name.eq_ignore_ascii_case(&c.name))
806        {
807            out.push(key);
808            continue;
809        }
810        let target = stmt.items.iter().find_map(|it| match it {
811            SelectItem::Expr {
812                expr,
813                alias: Some(a),
814            } if a.eq_ignore_ascii_case(&c.name) => Some(expr),
815            _ => None,
816        });
817        match target {
818            // PG's wording for the one alias that cannot be grouped by.
819            Some(e) if contains_aggregate(e) => {
820                return Err(EvalError::TypeMismatch {
821                    detail: alloc::string::String::from(
822                        "aggregate functions are not allowed in GROUP BY",
823                    ),
824                });
825            }
826            Some(e) => out.push(e.clone()),
827            // Not an alias either — leave it, so the resolver reports the
828            // missing column as it always did.
829            None => out.push(key),
830        }
831    }
832    Ok(out)
833}
834
835pub(crate) fn run(
836    stmt: &SelectStatement,
837    rows: AggRows<'_>,
838    schema_cols: &[ColumnSchema],
839    table_alias: Option<&str>,
840    correlated_eval: Option<CorrelatedEval<'_>>,
841    // v7.39 (parallel-agg P1) — host-injected executor; None = the
842    // single-threaded paths, byte-identical to pre-P1.
843    runner: Option<&dyn crate::ParallelRunner>,
844    // v7.39 (enum order knife) — catalog for enum member-order metadata
845    // (spec collection is AST-only). None keeps every ordering textual.
846    catalog: Option<&spg_storage::Catalog>,
847    // v7.39 (read01 round 63) — and the engine, so a user function whose body
848    // has its own FROM can run inside an aggregate's argument
849    // (`string_agg(lookup(id), ',')`). The catalog alone is not enough: the body
850    // is a QUERY and has to go through the real executor.
851    engine: Option<&crate::Engine>,
852) -> Result<AggResult, EvalError> {
853    // v7.38 P0 元机制 A — fires at the top of the aggregate
854    // executor with the number of input rows. Tests use this to
855    // block before a hypothetical spill decision; in release it
856    // expands to `let _ = (...);`.
857    let __spg_row_count = rows.len();
858    crate::injection_point!("aggregate_spill_trigger", &__spg_row_count);
859    // v7.35.0 — pure `SELECT COUNT(*) FROM … WHERE …` short-circuit.
860    // The caller already filtered rows by WHERE (we run on the
861    // post-WHERE survivor set), so for the canonical pure-COUNT(*)
862    // shape (no GROUP BY / HAVING / ORDER BY / DISTINCT / FILTER /
863    // window) the answer is simply `rows.len()`. The four-stage
864    // aggregate pipeline below (accumulate_groups → build_synth_schema
865    // → finalize_synth_rows → project_groups) collapses to a single
866    // BigInt cell when there's a single group, but each stage still
867    // pays its own allocation tax — group state map, synth schema
868    // vec, finalize loop. `exists_in_60` (mailrs prod #4 baseline)
869    // is exactly this shape on a 25 k-row JOIN.
870    if let Some(short) = try_pure_count_star_short_circuit(stmt, rows, schema_cols, table_alias) {
871        return Ok(short);
872    }
873    let group_exprs: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
874    // v7.39 (round 528) — a GROUP BY name that is only an output ALIAS.
875    let group_exprs = resolve_group_by_aliases(group_exprs, stmt, schema_cols)?;
876
877    // v7.39 (round 620) — PG's strict rule, checked BEFORE the pipeline so the
878    // diagnosis names what is actually wrong. Skipped under the MySQL dialect,
879    // which licenses exactly what this rejects (the loose rewrite below), and
880    // skipped when the grouping is by a primary key, which licenses every other
881    // column of that table.
882    // A GROUP BY name that resolves to nothing is reported as the missing
883    // column it is, ahead of this rule — measured against PG, which answers
884    // `column "nosuch" does not exist` for `SELECT v FROM t GROUP BY nosuch`
885    // rather than complaining that `v` is ungrouped.
886    let group_keys_all_resolve = group_exprs.iter().all(|g| match g {
887        Expr::Column(c) => {
888            c.qualifier.is_some()
889                || schema_cols
890                    .iter()
891                    .any(|sc| sc.name.eq_ignore_ascii_case(&c.name))
892        }
893        _ => true,
894    });
895    let licensed = qualifiers_grouped_by_primary_key(stmt, &group_exprs, schema_cols, catalog);
896    let fd_on_primary_key = !licensed.is_empty();
897    if group_keys_all_resolve && !engine.is_some_and(|e| e.backslash_escapes) {
898        let offender = stmt
899            .items
900            .iter()
901            .find_map(|it| match it {
902                SelectItem::Expr { expr, .. } => {
903                    first_ungrouped_column(expr, &group_exprs, schema_cols, &licensed)
904                }
905                _ => None,
906            })
907            .or_else(|| {
908                stmt.order_by.iter().find_map(|o| {
909                    first_ungrouped_column(&o.expr, &group_exprs, schema_cols, &licensed)
910                })
911            })
912            .or_else(|| {
913                stmt.having
914                    .as_ref()
915                    .and_then(|h| first_ungrouped_column(h, &group_exprs, schema_cols, &licensed))
916            });
917        if let Some(c) = offender {
918            // PG qualifies the column with the alias when there is one, and
919            // with the table name otherwise.
920            let qual = c
921                .qualifier
922                .as_deref()
923                .or(table_alias)
924                .or_else(|| stmt.from.as_ref().map(|f| f.primary.name.as_str()))
925                .unwrap_or("");
926            return Err(EvalError::TypeMismatch {
927                detail: alloc::format!(
928                    "column \"{qual}.{}\" must appear in the GROUP BY clause or be used in an aggregate function",
929                    c.name
930                ),
931            });
932        }
933    }
934
935    // v7.39 (round 405) — MySQL's loose GROUP BY: wrap each non-grouped,
936    // non-aggregated column in `any_value(col)` so the rest of the pipeline
937    // treats it as an aggregate (first-seen value per group). Only under the
938    // dialect and only when there is an explicit GROUP BY; PG keeps the
939    // strict "must appear in GROUP BY / be aggregated" rule.
940    //
941    // v7.39 (round 620) — the same rewrite serves PG's functional dependency.
942    // Letting the ungrouped column PAST the check above is not enough: the
943    // grouped row carries only the keys and the aggregates, so `s` still has
944    // nowhere to be read from and the query failed on `column "s" does not
945    // exist`. Grouping by a primary key means one input row per group, so
946    // "any value in the group" IS the value — the identical rewrite, reached
947    // for a different and much narrower reason.
948    let mysql_loose = engine.is_some_and(|e| e.backslash_escapes);
949    let loose_stmt;
950    let stmt = if (mysql_loose || fd_on_primary_key) && !group_exprs.is_empty() {
951        // The dialect claims every ungrouped column; the functional dependency
952        // claims only what a grouped primary key determines.
953        let claim: Option<&[alloc::string::String]> =
954            if mysql_loose { None } else { Some(&licensed) };
955        let mut s = stmt.clone();
956        for item in &mut s.items {
957            if let SelectItem::Expr { expr, .. } = item {
958                let taken = core::mem::replace(expr, Expr::Literal(spg_sql::ast::Literal::Null));
959                *expr = wrap_loose_group_columns(taken, &group_exprs, schema_cols, claim);
960            }
961        }
962        for o in &mut s.order_by {
963            let taken = core::mem::replace(&mut o.expr, Expr::Literal(spg_sql::ast::Literal::Null));
964            o.expr = wrap_loose_group_columns(taken, &group_exprs, schema_cols, claim);
965        }
966        if let Some(h) = s.having.take() {
967            s.having = Some(wrap_loose_group_columns(
968                h,
969                &group_exprs,
970                schema_cols,
971                claim,
972            ));
973        }
974        loose_stmt = s;
975        &loose_stmt
976    } else {
977        stmt
978    };
979
980    // Collect aggregate sub-expressions across items + order_by.
981    let mut agg_specs: Vec<AggSpec> = Vec::new();
982    for item in &stmt.items {
983        if let SelectItem::Expr { expr, .. } = item {
984            collect_aggregates(expr, &mut agg_specs);
985        }
986    }
987    for o in &stmt.order_by {
988        collect_aggregates(&o.expr, &mut agg_specs);
989    }
990    if let Some(h) = &stmt.having {
991        collect_aggregates(h, &mut agg_specs);
992    }
993    // v7.17.0 — arity validation. The collector tolerates an
994    // arbitrary positional-arg count; here we enforce the
995    // per-aggregate contract so a malformed call (e.g.
996    // `array_agg()` or `string_agg(x)`) surfaces as a SQL error
997    // rather than silently coercing to a degenerate aggregate.
998    validate_agg_arities(stmt, &agg_specs)?;
999    validate_within_group(&agg_specs, schema_cols, stmt.group_by.as_deref())?;
1000
1001    // v7.38.18 (S2) — the database's collation, for the columns that
1002    // declare none. `None` when it is byte order, which is every
1003    // database written before this existed.
1004    let db_collation: Option<&str> = catalog
1005        .map(spg_storage::Catalog::db_collation)
1006        .filter(|d| !crate::collate::is_byte_wise(d));
1007    // v7.39 (round 690) — resolve the argument's declared collation for
1008    // `min`/`max`. This rides beside `enum_labels` in `AggSpec` but NOT
1009    // inside its resolver loop: that loop only runs when the catalog holds
1010    // at least one enum type, and a collation has nothing to do with enums.
1011    for spec in &mut agg_specs {
1012        if matches!(spec.kind, AggKind::Min | AggKind::Max)
1013            && let Some(Expr::Column(c)) = &spec.arg
1014        {
1015            // A bare column argument carries its collation; an expression
1016            // produces a new value and has none (derivation is unbuilt).
1017            spec.arg_collation = schema_cols
1018                .iter()
1019                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
1020                .and_then(|sc| sc.collation_name.clone())
1021                // v7.38.18 (S2) — the database's when the column
1022                // declares none. `C` filters out below, so nothing moves
1023                // for a database that has not asked for a locale.
1024                .or_else(|| db_collation.map(alloc::string::String::from))
1025                .filter(|n| crate::collate::is_supported(n));
1026        }
1027    }
1028
1029    // v7.38.18 — the same fact for each ORDER BY key of an ordered
1030    // collection aggregate. Outside the enum resolver below for the
1031    // reason the loop above is: that one only runs when the catalog
1032    // holds an enum type, and a collation has nothing to do with enums.
1033    for spec in &mut agg_specs {
1034        if spec.order_by.is_empty() {
1035            continue;
1036        }
1037        spec.order_collations = spec
1038            .order_by
1039            .iter()
1040            .map(|o| {
1041                // A bare column key carries its collation; an expression
1042                // produces a new value and has none, the same limit
1043                // `min`/`max` has over an expression argument.
1044                let Expr::Column(c) = &o.expr else {
1045                    return None;
1046                };
1047                schema_cols
1048                    .iter()
1049                    .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
1050                    .and_then(|sc| sc.collation_name.clone())
1051                    .or_else(|| db_collation.map(alloc::string::String::from))
1052                    .filter(|n| crate::collate::is_supported(n))
1053            })
1054            .collect();
1055    }
1056
1057    // v7.39 (enum order knife) — resolve enum member-order metadata once
1058    // per query: min/max extremes and ordered-collection sort keys over
1059    // enum-typed expressions compare by member order (PG enumsortorder).
1060    if let Some(cat) = catalog
1061        && !cat.enum_types().is_empty()
1062    {
1063        for spec in &mut agg_specs {
1064            // v7.39 (round 258) — min/max have always needed the argument's
1065            // enum labels; a DISTINCT aggregate now does too, because its
1066            // dedup sort must follow MEMBER order (round 257 added the sort
1067            // and, deriving labels only here, sorted enum columns by text).
1068            if (matches!(spec.kind, AggKind::Min | AggKind::Max) || spec.distinct)
1069                && let Some(arg) = &spec.arg
1070            {
1071                spec.enum_labels = crate::eval::expr_enum_labels(arg, schema_cols, catalog)
1072                    .map(<[String]>::to_vec);
1073            }
1074            if !spec.order_by.is_empty() {
1075                spec.order_enum_labels = spec
1076                    .order_by
1077                    .iter()
1078                    .map(|o| {
1079                        crate::eval::expr_enum_labels(&o.expr, schema_cols, catalog)
1080                            .map(<[String]>::to_vec)
1081                    })
1082                    .collect();
1083            }
1084        }
1085    }
1086
1087    // (1) Stream the WHERE-filtered rows into insertion-ordered group state.
1088    let order = accumulate_groups(
1089        rows,
1090        &group_exprs,
1091        &agg_specs,
1092        schema_cols,
1093        table_alias,
1094        correlated_eval,
1095        runner,
1096        catalog,
1097        engine,
1098    )?;
1099
1100    // (2) Build the synthetic per-group schema and finalise each group's row.
1101    let synth_schema = build_synth_schema(
1102        rows,
1103        &group_exprs,
1104        &agg_specs,
1105        schema_cols,
1106        table_alias,
1107        catalog,
1108        engine,
1109    )?;
1110    let synth_rows = finalize_synth_rows(
1111        &order,
1112        &agg_specs,
1113        &synth_schema,
1114        rows,
1115        schema_cols,
1116        table_alias,
1117        catalog,
1118        engine,
1119        runner,
1120    )?;
1121
1122    // v7.37.x (mailrs Track A 100k attack) — defer the bound
1123    // per-item SELECT projection on the synth rows until AFTER
1124    // sort + LIMIT truncation. On a `GROUP BY t ORDER BY agg DESC
1125    // LIMIT 50` with 20 000 groups (the mailrs minimal 100k shape)
1126    // pre-defer ran 20 000 × N_items compiled-VM evals + Row
1127    // allocations before discarding 99.75 % at the sort truncation
1128    // step. HAVING still runs inline on every group because it
1129    // filters BEFORE the LIMIT; we only skip the SELECT-list eval.
1130    //
1131    // v7.37 (round 998) — and so a HAVING no longer stands the deferral
1132    // down. It used to, which cost the mailrs Track A query 11.9 ms of
1133    // 83. Neither clause is expensive alone: HAVING costs 5.0 ms without
1134    // an ORDER BY and 16.9 with one, and an ORDER BY costs MINUS 8.6 ms
1135    // without a HAVING, because ORDER BY + LIMIT is what switches this
1136    // deferral on. The residue of 11.9 ms belonged to neither and
1137    // appeared only together.
1138    //
1139    // What named it: the interaction tracks what the aggregates COST
1140    // rather than how many there are — one expensive aggregate
1141    // reproduces it as fully as twelve cheap ones — and it does not move
1142    // when the LIMIT changes. Both follow from projecting all 20 000
1143    // groups instead of the 50 that survive truncation.
1144    //
1145    // Safe because the clause above runs first: HAVING filters into
1146    // `kept_synth` BEFORE this branch, the sort truncates that survivor
1147    // list, and the completion projects from it. HAVING is rewritten
1148    // against the synthetic group schema, so it never reads a projected
1149    // item.
1150    //
1151    // v7.37 (round 997) — a set-returning item must NOT defer. The
1152    // deferred completion at the end of this function evaluates each item
1153    // scalarly; the expansion that turns one group into one row per
1154    // element lives in the branch the deferral skips. So a deferred
1155    // `unnest(...)` in the select list came back as
1156    // `function unnest(integer[]) does not exist` — the exact error round
1157    // 621 had fixed, reintroduced for the shapes that qualify to defer.
1158    // Differential against PG18.4: the same query answered correctly
1159    // without LIMIT, with LIMIT >= the group count, and — at the time —
1160    // with a HAVING, those being the cases where the deferral was off.
1161    // Round 998 removed the HAVING one from that list, which is why this
1162    // guard carries the SRF rule on its own now.
1163    let any_srf_item = stmt.items.iter().any(|i| match i {
1164        SelectItem::Expr { expr, .. } => crate::select::top_level_srf_kind(expr).is_some(),
1165        _ => false,
1166    });
1167    let defer_projection = !stmt.order_by.is_empty()
1168        && !stmt.distinct
1169        && !stmt.limit_with_ties
1170        && !any_srf_item
1171        && stmt.limit_literal().is_some_and(|l| {
1172            let off = stmt.offset_literal().unwrap_or(0) as usize;
1173            let k = (l as usize).saturating_add(off);
1174            k > 0 && k < synth_rows.len()
1175        });
1176
1177    // (3) Rewrite the user's expressions, filter groups by HAVING and project.
1178    let Projection {
1179        columns,
1180        mut out_rows,
1181        mut kept_synth,
1182        deferred,
1183        order_rewritten,
1184        deferred_project,
1185    } = project_groups(
1186        synth_rows,
1187        stmt,
1188        &group_exprs,
1189        &agg_specs,
1190        &synth_schema,
1191        correlated_eval,
1192        defer_projection,
1193        catalog,
1194        engine.is_some_and(|e| e.backslash_escapes),
1195    )?;
1196
1197    // (4) ORDER BY on the aggregated output (the caller applies LIMIT).
1198    //
1199    // v7.37.3 (mailrs prod /api/contacts 3.21× regression — and the
1200    // general inbox-listing-shape SPG-vs-PG gap) — top-K sink for
1201    // `ORDER BY <agg> [DESC] LIMIT k`. Pre-7.37.3 this stage ran a
1202    // full O(N log N) sort over every surviving group, then the
1203    // caller truncated to `k`. With high-cardinality GROUP BY (a
1204    // sender column with hundreds-thousands of distinct values) the
1205    // truncated set is a tiny fraction of `N` — keep an O(k) top-K
1206    // sink and never sort the discarded majority. Matches PG /
1207    // MySQL / MariaDB's standard "LIMIT k under ORDER BY agg"
1208    // optimisation; SPG previously implemented it only on the
1209    // streamed inner-join path (`try_streamed_inner_join_topn`)
1210    // and not on the aggregate output.
1211    //
1212    // Gate: needs a literal LIMIT (placeholder LIMIT we can't bound
1213    // statically here), no DISTINCT (would need post-dedup, can't
1214    // truncate during sort), no LIMIT WITH TIES (which extends past
1215    // the literal k by run-time tie-key comparison).
1216    let keep_n: Option<usize> =
1217        if !stmt.order_by.is_empty() && !stmt.distinct && !stmt.limit_with_ties {
1218            stmt.limit_literal().map(|l| {
1219                let off = stmt.offset_literal().unwrap_or(0) as usize;
1220                (l as usize).saturating_add(off)
1221            })
1222        } else {
1223            None
1224        };
1225    if !stmt.order_by.is_empty() {
1226        let (sorted_synth, sorted_out) = sort_synth_by_order_by(
1227            &synth_schema,
1228            &columns,
1229            &stmt.order_by,
1230            &order_rewritten,
1231            kept_synth,
1232            out_rows,
1233            correlated_eval,
1234            keep_n,
1235            catalog,
1236            engine.is_some_and(|e| e.backslash_escapes),
1237        )?;
1238        kept_synth = sorted_synth;
1239        out_rows = sorted_out;
1240    }
1241
1242    // v7.37.x — run deferred SELECT-list projection on the truncated
1243    // top-K survivors. For `GROUP BY thread_id ORDER BY MAX(date) DESC
1244    // LIMIT 50` against 20 000 groups, this turns ~40 000 compiled-VM
1245    // evals + Row allocations into 100, saving ~2-3 ms on the mailrs
1246    // minimal 100k shape.
1247    if let Some(DeferredProject {
1248        items_rewritten,
1249        items_compiled,
1250    }) = deferred_project
1251    {
1252        let mut synth_ctx = EvalContext::new(&synth_schema, None);
1253        if let Some(cat) = catalog {
1254            synth_ctx = synth_ctx.with_catalog(cat);
1255        }
1256        let mut stack: Vec<Value<'static>> = Vec::new();
1257        for (idx, srow) in kept_synth.iter().enumerate() {
1258            let mut values: Vec<Value<'static>> = Vec::with_capacity(columns.len());
1259            for (i, rewritten) in items_rewritten.iter().enumerate() {
1260                let Some(rewritten) = rewritten else { continue };
1261                if deferred.iter().any(|(c, _)| *c == i) {
1262                    values.push(Value::Null);
1263                    continue;
1264                }
1265                values.push(if let Some(cc) = &items_compiled[i] {
1266                    eval::eval_compiled(cc, srow, &synth_ctx, &mut stack)?
1267                } else {
1268                    match correlated_eval {
1269                        Some(f) if crate::expr_has_subquery(rewritten) => {
1270                            f(rewritten, srow, &synth_ctx)?
1271                        }
1272                        _ => eval::eval_expr(rewritten, srow, &synth_ctx)?,
1273                    }
1274                });
1275            }
1276            out_rows[idx] = Row::new(values);
1277        }
1278    }
1279
1280    // v7.37 (round 999) — SELECT DISTINCT over a GROUP BY query.
1281    //
1282    // Every other path deduplicates: the scan paths, the window path and
1283    // the set operations all call `dedup_rows`. This one never did, so
1284    // `SELECT DISTINCT count(*) FROM t GROUP BY g` returned one row per
1285    // GROUP — 200 where PG18.4 returns 1, all of them the same value.
1286    // Not an error, not a missing column: 199 extra rows, silently.
1287    //
1288    // The gate on the top-K sink above says it in as many words — "no
1289    // DISTINCT (would need post-dedup, can't truncate during sort)" — so
1290    // the sink correctly declines to truncate, and the post-dedup it
1291    // names was never written. This is it.
1292    //
1293    // After the ORDER BY, like the window path: duplicate rows carry
1294    // identical sort keys, so removing them cannot disturb the order.
1295    // Before the LIMIT, which the caller applies, because PG deduplicates
1296    // and then counts.
1297    //
1298    // Only `out_rows` needs it: `deferred` is empty whenever DISTINCT is
1299    // set (`defer_enabled` requires `!stmt.distinct`), so nothing indexes
1300    // into `kept_synth` alongside these rows.
1301    if stmt.distinct {
1302        // v7.38.14 — masked, not dialect-only. `SELECT DISTINCT` over a
1303        // GROUP BY result folded every text position regardless of what
1304        // the column declared, which is the defect 3b494b6e closed on the
1305        // main scan path. The output schema is in scope here and carries
1306        // the collation, so the mask needs no new plumbing.
1307        out_rows = crate::select::dedup_rows(
1308            out_rows,
1309            crate::select::FoldSpec::of_masks(
1310                engine.is_some_and(|e| e.backslash_escapes),
1311                &crate::select::fold_mask_of_columns(&columns),
1312                &crate::select::pad_mask_of_columns(&columns),
1313            ),
1314        );
1315    }
1316
1317    let (synth_rows_out, synth_schema_out) = if deferred.is_empty() {
1318        (Vec::new(), Vec::new())
1319    } else {
1320        (kept_synth, synth_schema.clone())
1321    };
1322    Ok(AggResult {
1323        columns,
1324        rows: out_rows,
1325        deferred,
1326        synth_rows: synth_rows_out,
1327        synth_schema: synth_schema_out,
1328    })
1329}
1330
1331/// v7.32 (round-29) — validate the structural requirements of WITHIN
1332/// GROUP (ordered-set / hypothetical-set) aggregates up front, so a
1333/// malformed call surfaces as a SQL error rather than a silently
1334/// degenerate aggregate.
1335/// v7.39 (round 255) — PG's name for an expression's type in an
1336/// ordered-set signature error. Only a CAST / COLUMN is trusted (the
1337/// round-237 lesson: `describe_expr` reports a binary operator as its
1338/// left operand's type); an untyped literal is PG's own `unknown`, and
1339/// anything else falls back to `unknown` rather than guessing.
1340fn ordered_set_arg_type_name(e: &Expr, columns: &[ColumnSchema]) -> String {
1341    if matches!(
1342        e,
1343        Expr::Literal(spg_sql::ast::Literal::String(_))
1344            | Expr::Literal(spg_sql::ast::Literal::Null)
1345    ) {
1346        return String::from("unknown");
1347    }
1348    match e {
1349        Expr::Cast { .. } | Expr::Column(_) | Expr::Literal(_) => {
1350            crate::describe::describe_expr(e, columns).map_or_else(
1351                || String::from("unknown"),
1352                |s| crate::conversions::pg_type_name_for_error(s.ty),
1353            )
1354        }
1355        _ => String::from("unknown"),
1356    }
1357}
1358
1359/// v7.39 (round 255) — PG resolves an ordered-set / hypothetical-set
1360/// call as ONE function whose signature is `(direct args…, WITHIN GROUP
1361/// args…)`; anything that does not match a declared overload is a plain
1362/// `function f(…) does not exist` (42883), not a bespoke message. Probed
1363/// live: `percentile_cont(numeric, text)`, `rank(integer, integer,
1364/// text)`, `mode(integer, integer)`.
1365fn ordered_set_signature_error(name: &str, spec: &AggSpec, columns: &[ColumnSchema]) -> EvalError {
1366    let mut parts: Vec<String> = Vec::new();
1367    if let Some(d) = &spec.direct_arg {
1368        parts.push(ordered_set_arg_type_name(d, columns));
1369    }
1370    for d in &spec.direct_args_extra {
1371        parts.push(ordered_set_arg_type_name(d, columns));
1372    }
1373    for o in &spec.order_by {
1374        parts.push(ordered_set_arg_type_name(&o.expr, columns));
1375    }
1376    EvalError::TypeMismatch {
1377        detail: format!("function {name}({}) does not exist", parts.join(", ")),
1378    }
1379}
1380
1381fn validate_within_group(
1382    agg_specs: &[AggSpec],
1383    columns: &[ColumnSchema],
1384    group_by: Option<&[Expr]>,
1385) -> Result<(), EvalError> {
1386    // v7.39 (round 765, F31-D2) — PG requires an ordered-set
1387    // aggregate's DIRECT arguments to use only grouped columns
1388    // (`percentile_cont(x) WITHIN GROUP (ORDER BY x)` refuses with
1389    // "column … must appear in the GROUP BY clause", DETAIL "Direct
1390    // arguments of an ordered-set aggregate must use only grouped
1391    // columns", PG18-measured); SPG evaluated the first row's value
1392    // and answered.
1393    fn first_ungrouped(e: &Expr, group_by: Option<&[Expr]>) -> Option<String> {
1394        let mut found: Option<String> = None;
1395        let mut subs: Vec<&SelectStatement> = Vec::new();
1396        crate::visit_expr_columns_and_subqueries(
1397            e,
1398            &mut |c| {
1399                if found.is_some() {
1400                    return;
1401                }
1402                let grouped = group_by.is_some_and(|gs| {
1403                    gs.iter().any(|g| match g {
1404                        Expr::Column(gc) => gc.name.eq_ignore_ascii_case(&c.name),
1405                        _ => false,
1406                    })
1407                });
1408                // The visitor's exotic-node BAIL marker is an empty
1409                // name — not a real column; skip it (refusing on it
1410                // would reject constant shapes like ARRAY[…] casts).
1411                if !grouped && !c.name.is_empty() {
1412                    found = Some(match &c.qualifier {
1413                        Some(q) => format!("{q}.{}", c.name),
1414                        None => c.name.clone(),
1415                    });
1416                }
1417            },
1418            &mut |s| subs.push(s),
1419        );
1420        found
1421    }
1422    for spec in agg_specs {
1423        if !is_within_group_name(&spec.name) {
1424            continue;
1425        }
1426        for d in spec.direct_arg.iter().chain(spec.direct_args_extra.iter()) {
1427            if let Some(col) = first_ungrouped(d, group_by) {
1428                return Err(EvalError::TypeMismatch {
1429                    detail: format!(
1430                        "column \"{col}\" must appear in the GROUP BY clause or be used in an aggregate function"
1431                    ),
1432                });
1433            }
1434        }
1435    }
1436    // v7.32 (round-29) — WITHIN GROUP aggregates require the clause (PG
1437    // raises a hard error otherwise rather than silently degrading), and
1438    // SPG supports the single-sort-key form only.
1439    for spec in agg_specs {
1440        if is_within_group_name(&spec.name) {
1441            if spec.order_by.is_empty() {
1442                // v7.39 (round 704) — the hypothetical-set names double as
1443                // WINDOW functions, and PG resolves the bare zero-argument
1444                // spelling to the window reading: `SELECT rank() FROM t` is
1445                // `window function rank requires an OVER clause` there, not
1446                // a WITHIN GROUP complaint. With a direct argument the
1447                // ordered-set reading is the one the caller meant, and the
1448                // WITHIN GROUP wording stands.
1449                if spec.direct_arg.is_none() && is_hypothetical_set_name(&spec.name) {
1450                    return Err(EvalError::TypeMismatch {
1451                        detail: format!("window function {} requires an OVER clause", spec.name),
1452                    });
1453                }
1454                return Err(EvalError::TypeMismatch {
1455                    detail: format!("{}() requires WITHIN GROUP (ORDER BY …)", spec.name),
1456                });
1457            }
1458            // mode() is the only WITHIN GROUP aggregate with no direct
1459            // argument; the rest carry one (percentile fraction /
1460            // hypothetical value).
1461            if spec.name != "mode" && spec.direct_arg.is_none() {
1462                return Err(EvalError::TypeMismatch {
1463                    detail: format!("{}() requires a direct argument", spec.name),
1464                });
1465            }
1466            // …and mode() takes NONE: `mode(1)` used to be accepted with
1467            // the argument silently dropped.
1468            if spec.name == "mode" && spec.direct_arg.is_some() {
1469                return Err(ordered_set_signature_error(&spec.name, spec, columns));
1470            }
1471            // v7.39 (read01 orderedsetaggs.c) — the hypothetical-set
1472            // family supports the multi-key form: one direct argument
1473            // per sort key (PG resolves a mismatch as a missing
1474            // function overload; its HINT carries the real rule).
1475            let hypothetical = matches!(
1476                spec.name.as_str(),
1477                "rank" | "dense_rank" | "percent_rank" | "cume_dist"
1478            );
1479            // Only the hypothetical-set family takes a multi-key sort
1480            // spec, and then it needs exactly one direct argument per
1481            // key. PG reports every mismatch as a missing overload.
1482            if hypothetical {
1483                if 1 + spec.direct_args_extra.len() != spec.order_by.len() {
1484                    return Err(ordered_set_signature_error(&spec.name, spec, columns));
1485                }
1486            } else if spec.order_by.len() > 1 || !spec.direct_args_extra.is_empty() {
1487                // `percentile_cont(0.5, 0.6)` and `mode(1)` used to be
1488                // silently accepted (the extra arguments were dropped and
1489                // the aggregate answered anyway).
1490                return Err(ordered_set_signature_error(&spec.name, spec, columns));
1491            }
1492            // v7.39 (round 255) — `percentile_cont` interpolates, so PG
1493            // declares it only over the numeric tower and interval
1494            // (probed: text / date / timestamp / bool are refused, while
1495            // `percentile_disc` and `mode` take any sortable type). SPG
1496            // answered NULL for the refused types. Judged from the
1497            // STATICALLY known type only — an unknown one is let through
1498            // (round 237: refusing a legal query is worse than missing an
1499            // illegal one).
1500            if spec.name == "percentile_cont"
1501                && let Some(o) = spec.order_by.first()
1502                && matches!(o.expr, Expr::Cast { .. } | Expr::Column(_))
1503                && let Some(sch) = crate::describe::describe_expr(&o.expr, columns)
1504                && !matches!(
1505                    sch.ty,
1506                    spg_storage::DataType::SmallInt
1507                        | spg_storage::DataType::Int
1508                        | spg_storage::DataType::BigInt
1509                        | spg_storage::DataType::Float
1510                        | spg_storage::DataType::Real
1511                        | spg_storage::DataType::Numeric { .. }
1512                        | spg_storage::DataType::Interval
1513                )
1514            {
1515                return Err(ordered_set_signature_error(&spec.name, spec, columns));
1516            }
1517        }
1518    }
1519    Ok(())
1520}
1521
1522/// (1) Stream the WHERE-filtered rows, group by the GROUP BY value
1523/// tuple, and update per-group aggregate state. Returns the groups in
1524/// insertion order. See `run` for the bind-once fast path rationale.
1525/// v7.39 (round 665) — the running numeric state a sum/avg keeps, in ONE
1526/// place.
1527///
1528/// It used to live in four independently written copies: `FusedAcc`'s own
1529/// fields, `AggState`'s own fields, and twice more as loose locals inside
1530/// `accumulate_groups`. `FusedAcc`'s doc comment described that openly —
1531/// "field-for-field the same running state the single-spec sum/avg fast
1532/// path keeps in locals" — so the duplication was deliberate manual
1533/// inlining, not drift.
1534///
1535/// The cost was not abstract. Round 664 measured it: adding one guard to
1536/// the sum/avg family meant editing FOUR sites, and three of the four were
1537/// found only by running a different SQL shape and watching the wrong
1538/// answer come back. Reading the code did not reveal them, because the
1539/// three parallel loops in the fused block are not symmetric — the middle
1540/// one is a `length()` shortcut that accumulates nothing numeric.
1541///
1542/// `count` deliberately stays outside: `count(*)` keeps it too, and it is
1543/// not part of the numeric running state.
1544#[derive(Debug, Default, Clone)]
1545struct NumAcc {
1546    sum_int: i64,
1547    sum_float: f64,
1548    use_float: bool,
1549    float_not_real: bool,
1550    sum_num_scaled: i128,
1551    sum_num_kind: spg_storage::NumericKind,
1552    sum_num_scale: u16,
1553    /// v7.39 (read01 numeric.c) — bignum spill; see `SumBig`.
1554    sum_big: SumBig,
1555    use_numeric: bool,
1556    sum_iv_months: i64,
1557    sum_iv_days: i64,
1558    sum_iv_micros: i128,
1559    use_interval: bool,
1560    sum_money: i128,
1561    use_money: bool,
1562    /// Inside the struct, not beside it. Measured: splitting it out gave
1563    /// `acc_cell` two base pointers where the copy it replaced had one,
1564    /// and `sum(int)` over 500k rows lost ~8% (paired, n=12, p=0.04).
1565    /// `count(*)` reading `st.num.count` is a small price for that.
1566    count: i64,
1567}
1568
1569#[allow(clippy::too_many_lines, clippy::type_complexity)]
1570/// v7.37.16 — per-spec accumulator for the fused multi-spec fast path.
1571/// Field-for-field the same running state the single-spec sum/avg fast
1572/// path keeps in locals; finalized into `AggState` identically.
1573#[derive(Default, Clone)]
1574struct FusedAcc {
1575    /// The shared sum/avg running state (see `NumAcc`).
1576    num: NumAcc,
1577    /// v7.39 (round 568/569) — the min/max lane. `min` and `max` were
1578    /// the only ordinary aggregates the fused layout did not accept, so
1579    /// they fell to the generic per-spec machinery and cost DOUBLE a
1580    /// `sum` over the same scan (500k INTs: sum 13.4 ms, min 26.5,
1581    /// max 27.6, while PG18 is flat at 8.2 for all three). They also
1582    /// missed the shard-parallel scan the fused path runs.
1583    extreme: Option<Value<'static>>,
1584    /// Which way this accumulator's comparison goes, so a shard merge
1585    /// does not need to be told.
1586    extreme_max: bool,
1587    extreme_mysql: bool,
1588    /// v7.39 (round 690) — the argument's declared collation, so a
1589    /// shard merge compares the two extremes the same way the scan did.
1590    extreme_coll: Option<alloc::string::String>,
1591    /// v7.39 (round 724) — the collection lanes: string_agg / array_agg
1592    /// items in ROW order (shard merge concatenates in shard order,
1593    /// which IS row order), plus the flat ORDER BY keys (round 723's
1594    /// layout). The finalize sort/join is the existing AggState path.
1595    items: Vec<Value<'static>>,
1596    item_keys: Vec<Value<'static>>,
1597}
1598
1599/// v7.39 (round 569) — a fresh accumulator per op, carrying each one's
1600/// comparison direction so `merge_fused` stays a two-argument fold.
1601fn fused_accs(ops: &[FusedOp], mysql: bool) -> Vec<FusedAcc> {
1602    ops.iter()
1603        .map(|op| {
1604            let mut a = FusedAcc::default();
1605            if let FusedOp::Extreme { max, coll, .. } | FusedOp::ExtremeExpr { max, coll, .. } = op
1606            {
1607                a.extreme_max = *max;
1608                a.extreme_mysql = mysql;
1609                a.extreme_coll = coll.clone();
1610            }
1611            a
1612        })
1613        .collect()
1614}
1615
1616/// v7.39 (parallel-agg P3) — the fused-op layout shared by the
1617/// single-group fast path and the parallel GROUP BY fast path.
1618/// `spec_src[i]`: None = count(*) (finalize from the group row
1619/// count); Some(slot) = unique_ops[slot]'s accumulator.
1620enum FusedOp {
1621    CountCol(usize),
1622    AccCol(usize),
1623    /// v7.39 (round 569) — min/max over a bound column.
1624    /// v7.39 (round 690) — `coll` is the column's declared collation.
1625    /// Unlike an enum's member order (which sends the spec to the
1626    /// generic path), a collation rides along, so a collated column
1627    /// keeps the fused lane's shard-parallel scan.
1628    Extreme {
1629        pos: usize,
1630        max: bool,
1631        coll: Option<alloc::string::String>,
1632    },
1633    /// v7.39 (round 716, S07) — the same three shapes over a COMPILED
1634    /// argument expression. `count(least(id, 0))` used to fall off this
1635    /// lane entirely — `fused_layout` only accepted bound columns — and
1636    /// landed in the SERIAL generic loop, which is where the whole 7.6×
1637    /// against PG lived: PG runs the identical cell as a parallel seq
1638    /// scan. The payload is the SPEC INDEX whose `arg_compiled` program
1639    /// to run; the accumulator lanes are the ones the column ops use.
1640    CountExpr(usize),
1641    AccExpr(usize),
1642    ExtremeExpr {
1643        spec: usize,
1644        max: bool,
1645        coll: Option<alloc::string::String>,
1646    },
1647    /// v7.39 (round 724) — string_agg / array_agg over a bound column,
1648    /// optional bound ORDER BY keys. The payload is the spec index; the
1649    /// scan reads arg_pos / order_pos through it. Collection was the
1650    /// last per-row aggregate stuck on the serial generic loop — 32 ms
1651    /// single-threaded on the panel's 500k string_agg where PG runs a
1652    /// parallel plan.
1653    Collect {
1654        spec: usize,
1655        string_kind: bool,
1656    },
1657}
1658
1659/// Returns the (spec_src, unique_ops) layout when EVERY aggregate
1660/// spec is fused-eligible (count*/count/sum/avg over bound columns,
1661/// no FILTER/DISTINCT/arg2/ORDER), else None.
1662fn fused_layout(
1663    agg_specs: &[AggSpec],
1664    arg_pos: &[Option<usize>],
1665    // v7.39 (round 716) — a compiled argument keeps a spec on the fused
1666    // lane now; a bound column still takes the (cheaper) column op.
1667    arg_compiled: &[Option<eval::CompiledExpr>],
1668    // v7.39 (round 724) — bound ORDER BY key positions, for Collect.
1669    order_pos: &[Vec<Option<usize>>],
1670    arg2_literal_val: &[Option<Value<'static>>],
1671) -> Option<(Vec<Option<usize>>, Vec<FusedOp>)> {
1672    if agg_specs.is_empty() {
1673        return None;
1674    }
1675    let has_arg = |i: usize| arg_pos[i].is_some() || arg_compiled[i].is_some();
1676    // v7.39 (round 724) — a collection spec: bound argument, literal
1677    // separator (string_agg), every ORDER BY key a bound column. The
1678    // finalize path (sort + join) is the ordinary AggState one, so
1679    // multi-key and DESC orders are the finalizer's business, not ours.
1680    let collectible = |i: usize, s: &AggSpec| -> bool {
1681        !s.distinct
1682            && s.filter.is_none()
1683            && !s.first_ordered
1684            && arg_pos[i].is_some()
1685            && s.order_by
1686                .iter()
1687                .enumerate()
1688                .all(|(k, _)| order_pos[i].get(k).copied().flatten().is_some())
1689            && match s.name.as_str() {
1690                "string_agg" => matches!(&arg2_literal_val[i], Some(Value::Text(_))),
1691                "array_agg" => s.arg2.is_none() && s.enum_labels.is_none(),
1692                _ => false,
1693            }
1694    };
1695    let eligible = agg_specs.iter().enumerate().all(|(i, s)| {
1696        collectible(i, s)
1697            || (s.filter.is_none()
1698                && s.arg2.is_none()
1699                && s.order_by.is_empty()
1700                && !s.distinct
1701                && !s.first_ordered
1702                && match s.name.as_str() {
1703                    "count_star" => s.arg.is_none(),
1704                    "count" | "sum" | "avg" => has_arg(i),
1705                    // v7.39 (round 569) — an enum argument compares by
1706                    // catalog member order, which the fused lane does not
1707                    // carry; those keep the generic path.
1708                    "min" | "max" => has_arg(i) && s.enum_labels.is_none(),
1709                    _ => false,
1710                })
1711    });
1712    if !eligible {
1713        return None;
1714    }
1715    let mut unique_ops: Vec<FusedOp> = Vec::new();
1716    // Compiled dedupe key = the source Expr (same rule the executor-time
1717    // CSE uses): two specs share a slot only when their argument TREES
1718    // are equal, which `fully_compilable`'s purity makes sufficient.
1719    let same_arg = |j: usize, i: usize| agg_specs[j].arg == agg_specs[i].arg;
1720    let spec_src: Vec<Option<usize>> = agg_specs
1721        .iter()
1722        .enumerate()
1723        .map(|(i, s)| match s.name.as_str() {
1724            "count_star" => None,
1725            // Collection ops never share slots (each keeps its own
1726            // items), so no dedupe probe.
1727            "string_agg" | "array_agg" => {
1728                unique_ops.push(FusedOp::Collect {
1729                    spec: i,
1730                    string_kind: s.name.as_str() == "string_agg",
1731                });
1732                Some(unique_ops.len() - 1)
1733            }
1734            "min" | "max" => {
1735                let max = s.name.as_str() == "max";
1736                let slot = if let Some(p) = arg_pos[i] {
1737                    unique_ops
1738                        .iter()
1739                        .position(|o| {
1740                            matches!(o, FusedOp::Extreme { pos, max: m, coll }
1741                                if *pos == p && *m == max && *coll == s.arg_collation)
1742                        })
1743                        .unwrap_or_else(|| {
1744                            unique_ops.push(FusedOp::Extreme {
1745                                pos: p,
1746                                max,
1747                                coll: s.arg_collation.clone(),
1748                            });
1749                            unique_ops.len() - 1
1750                        })
1751                } else {
1752                    unique_ops
1753                        .iter()
1754                        .position(|o| {
1755                            matches!(o, FusedOp::ExtremeExpr { spec, max: m, coll }
1756                                if same_arg(*spec, i) && *m == max && *coll == s.arg_collation)
1757                        })
1758                        .unwrap_or_else(|| {
1759                            unique_ops.push(FusedOp::ExtremeExpr {
1760                                spec: i,
1761                                max,
1762                                coll: s.arg_collation.clone(),
1763                            });
1764                            unique_ops.len() - 1
1765                        })
1766                };
1767                Some(slot)
1768            }
1769            "count" => {
1770                let slot = if let Some(p) = arg_pos[i] {
1771                    unique_ops
1772                        .iter()
1773                        .position(|o| matches!(o, FusedOp::CountCol(q) if *q == p))
1774                        .unwrap_or_else(|| {
1775                            unique_ops.push(FusedOp::CountCol(p));
1776                            unique_ops.len() - 1
1777                        })
1778                } else {
1779                    unique_ops
1780                        .iter()
1781                        .position(|o| matches!(o, FusedOp::CountExpr(j) if same_arg(*j, i)))
1782                        .unwrap_or_else(|| {
1783                            unique_ops.push(FusedOp::CountExpr(i));
1784                            unique_ops.len() - 1
1785                        })
1786                };
1787                Some(slot)
1788            }
1789            _ => {
1790                let slot = if let Some(p) = arg_pos[i] {
1791                    unique_ops
1792                        .iter()
1793                        .position(|o| matches!(o, FusedOp::AccCol(q) if *q == p))
1794                        .unwrap_or_else(|| {
1795                            unique_ops.push(FusedOp::AccCol(p));
1796                            unique_ops.len() - 1
1797                        })
1798                } else {
1799                    unique_ops
1800                        .iter()
1801                        .position(|o| matches!(o, FusedOp::AccExpr(j) if same_arg(*j, i)))
1802                        .unwrap_or_else(|| {
1803                            unique_ops.push(FusedOp::AccExpr(i));
1804                            unique_ops.len() - 1
1805                        })
1806                };
1807                Some(slot)
1808            }
1809        })
1810        .collect();
1811    Some((spec_src, unique_ops))
1812}
1813
1814/// v7.39 (parallel-agg P1) — fold shard accumulator `b` into `a`.
1815/// Every FusedAcc field is a running sum plus a type-witness flag, so
1816/// the merge is field-wise addition with `numeric_add` aligning the
1817/// decimal scales. Merging in shard order keeps float summation
1818/// deterministic for a given shard count (PG's parallel aggregate
1819/// makes the same no-serial-equivalence tradeoff for floats).
1820fn merge_fused(a: &mut FusedAcc, b: &mut FusedAcc) {
1821    // v7.39 (round 569) — fold the shard's extreme in the direction this
1822    // accumulator was built for.
1823    if let Some(be) = &b.extreme {
1824        let take = match &a.extreme {
1825            None => true,
1826            Some(ae) => {
1827                let ord = extreme_cmp_in(None, a.extreme_coll.as_deref(), be, ae, a.extreme_mysql);
1828                if a.extreme_max {
1829                    ord == core::cmp::Ordering::Greater
1830                } else {
1831                    ord == core::cmp::Ordering::Less
1832                }
1833            }
1834        };
1835        if take {
1836            a.extreme = Some(be.clone());
1837        }
1838    }
1839    a.num.count += b.num.count;
1840    a.num.sum_int += b.num.sum_int;
1841    a.num.sum_float += b.num.sum_float;
1842    a.num.use_float |= b.num.use_float;
1843    a.num.float_not_real |= b.num.float_not_real;
1844    if b.num.use_numeric {
1845        // v7.39 (read01 numeric.c) — fold the shard's bignum spill first,
1846        // then its i128 lane (zero if the shard promoted).
1847        if let Some(bb) = &b.num.sum_big {
1848            sum_add_bignum(
1849                &mut a.num.sum_num_scaled,
1850                &mut a.num.sum_num_scale,
1851                &mut a.num.sum_big,
1852                bb,
1853            );
1854        }
1855        sum_add_exact(
1856            &mut a.num.sum_num_scaled,
1857            &mut a.num.sum_num_scale,
1858            &mut a.num.sum_big,
1859            b.num.sum_num_scaled,
1860            b.num.sum_num_scale,
1861        );
1862        a.num.sum_num_kind = fold_sum_kind(a.num.sum_num_kind, b.num.sum_num_kind);
1863        a.num.use_numeric = true;
1864    }
1865    a.num.sum_iv_months += b.num.sum_iv_months;
1866    a.num.sum_iv_days += b.num.sum_iv_days;
1867    a.num.sum_iv_micros += b.num.sum_iv_micros;
1868    a.num.use_interval |= b.num.use_interval;
1869    a.num.sum_money += b.num.sum_money;
1870    a.num.use_money |= b.num.use_money;
1871    // v7.39 (round 724) — collection lanes concatenate; shard order is
1872    // row order. The merge takes `b` by reference (both call sites), so
1873    // this clones — the per-shard vectors are moved into place only at
1874    // fill time.
1875    a.items.extend(core::mem::take(&mut b.items));
1876    a.item_keys.extend(core::mem::take(&mut b.item_keys));
1877}
1878
1879/// v7.39 — write fused accumulators into the per-spec AggStates
1880/// (shared by the single-group and parallel-GROUP-BY fast paths).
1881/// `group_rows` finalizes count(*) specs.
1882/// v7.39 (round 724) — one row's contribution to a fused Collect op.
1883/// Mirrors `update_state`'s StringAgg / ArrayAgg arms: string_agg skips
1884/// NULL and renders through the shared helper (a non-renderable type
1885/// errors with the same sentence); array_agg keeps NULL elements.
1886fn collect_cell(
1887    a: &mut FusedAcc,
1888    row: &crate::join::RowRef<'_>,
1889    pos: usize,
1890    key_pos: &[Option<usize>],
1891    string_kind: bool,
1892) -> Result<(), EvalError> {
1893    let v = row.get(pos).unwrap_or(&Value::Null);
1894    if string_kind {
1895        if matches!(v, Value::Null) {
1896            return Ok(());
1897        }
1898        let Some(item) = render_string_agg_item(v) else {
1899            return Err(EvalError::TypeMismatch {
1900                detail: format!(
1901                    "string_agg requires text value, got {}",
1902                    crate::conversions::pg_type_name_for_error_opt(v.data_type())
1903                ),
1904            });
1905        };
1906        a.items.push(item);
1907    } else {
1908        a.items.push(v.clone().into_owned());
1909    }
1910    a.num.count += 1;
1911    for kp in key_pos {
1912        let kv = row
1913            .get(kp.expect("layout-gated bound key"))
1914            .cloned()
1915            .map(Value::into_owned)
1916            .unwrap_or(Value::Null);
1917        a.item_keys.push(kv);
1918    }
1919    Ok(())
1920}
1921
1922/// The string_agg item rendering, shared by `update_state` and the
1923/// round-724 fused Collect op — one place, so the two paths cannot
1924/// drift. Text collects as-is; other scalars coerce to their text
1925/// rendering (MySQL group_concat semantics — also matches PG's
1926/// cast-then-aggregate idiom for `string_agg(v::text, sep)`).
1927fn render_string_agg_item(v: &Value<'_>) -> Option<Value<'static>> {
1928    match v {
1929        Value::Text(s) => Some(Value::text(s.clone())),
1930        // v7.39 (round 626, S05b/F29) — CHAR(n). PG aggregates a
1931        // bpchar column (`string_agg(c, ',')` -> text) and SPG said
1932        // "string_agg requires text value, got character". The text
1933        // form of a bpchar drops its padding, which is what PG's
1934        // own bpchar->text cast does.
1935        Value::BpChar(s) => Some(Value::text(s.trim_end_matches(' ').to_string())),
1936        // v7.39 (read01 round 111) — xmlagg feeds xml values through this
1937        // shared StringAgg path; render the fragment's text (it joins
1938        // separator-less into the concatenated document).
1939        Value::Xml(s) => Some(Value::text(s.to_string())),
1940        Value::Int(n) => Some(Value::text(n.to_string())),
1941        Value::BigInt(n) => Some(Value::text(n.to_string())),
1942        Value::SmallInt(n) => Some(Value::text(n.to_string())),
1943        Value::Float(f) => Some(Value::text(f.to_string())),
1944        Value::Bool(b) => Some(Value::text(if *b { "1" } else { "0" })),
1945        _ => None,
1946    }
1947}
1948
1949fn fill_states_from_fused(
1950    states: &mut [AggState],
1951    spec_src: &[Option<usize>],
1952    accs: &mut [FusedAcc],
1953    group_rows: i64,
1954    // v7.39 (round 724) — string_agg's literal separator, per spec.
1955    arg2_literal_val: &[Option<Value<'static>>],
1956) {
1957    for (i, src) in spec_src.iter().enumerate() {
1958        let state = &mut states[i];
1959        match src {
1960            None => state.num.count = group_rows,
1961            Some(slot) => {
1962                // Collection lanes MOVE (they are per-spec, never
1963                // shared; see the layout's no-dedupe rule).
1964                {
1965                    let a = &mut accs[*slot];
1966                    if !a.items.is_empty() {
1967                        state.items = core::mem::take(&mut a.items);
1968                        state.item_keys = core::mem::take(&mut a.item_keys);
1969                    }
1970                }
1971                if let Some(Value::Text(sep)) = &arg2_literal_val[i] {
1972                    state.separator = Some(sep.to_string());
1973                }
1974                let a = &accs[*slot];
1975                state.num.count = a.num.count;
1976                state.num.sum_int = a.num.sum_int;
1977                state.num.sum_float = a.num.sum_float;
1978                state.num.use_float = a.num.use_float;
1979                state.num.float_not_real = a.num.float_not_real;
1980                state.num.sum_num_scaled = a.num.sum_num_scaled;
1981                state.num.sum_num_kind = a.num.sum_num_kind;
1982                state.num.sum_num_scale = a.num.sum_num_scale;
1983                state.num.sum_big = a.num.sum_big.clone();
1984                state.num.use_numeric = a.num.use_numeric;
1985                state.num.sum_iv_months = a.num.sum_iv_months;
1986                state.num.sum_iv_days = a.num.sum_iv_days;
1987                state.num.sum_iv_micros = a.num.sum_iv_micros;
1988                state.num.use_interval = a.num.use_interval;
1989                state.num.sum_money = a.num.sum_money;
1990                state.num.use_money = a.num.use_money;
1991                if a.extreme.is_some() {
1992                    state.extreme = a.extreme.clone();
1993                }
1994            }
1995        }
1996    }
1997}
1998
1999/// v7.39 (read01 numeric.c) — the bignum spill lane of the NUMERIC sum
2000/// tri-state (i128 mantissa + scale + optional BigNumeric). `None` until the
2001/// i128 lane would overflow; from then on the sum lives in the spill and the
2002/// i128 lane stays frozen at zero (PG's sum(numeric) never saturates).
2003type SumBig = Option<alloc::boxed::Box<spg_storage::bignum::BigNumeric>>;
2004
2005/// Add an exact NUMERIC (mantissa × 10^-scale) into the sum tri-state.
2006fn sum_add_exact(
2007    scaled: &mut i128,
2008    scale: &mut u16,
2009    big: &mut SumBig,
2010    add_scaled: i128,
2011    add_scale: u16,
2012) {
2013    use spg_storage::bignum::BigNumeric;
2014    if let Some(b) = big {
2015        **b = b.add(&BigNumeric::from_i128(add_scaled, add_scale));
2016        return;
2017    }
2018    match crate::numeric::numeric_add_checked(*scaled, *scale, add_scaled, add_scale) {
2019        Some((s, sc)) => {
2020            *scaled = s;
2021            *scale = sc;
2022        }
2023        None => {
2024            *big = Some(alloc::boxed::Box::new(
2025                BigNumeric::from_i128(*scaled, *scale)
2026                    .add(&BigNumeric::from_i128(add_scaled, add_scale)),
2027            ));
2028            *scaled = 0;
2029            *scale = 0;
2030        }
2031    }
2032}
2033
2034/// Add a BigNumeric input into the sum tri-state (promotes immediately).
2035fn sum_add_bignum(
2036    scaled: &mut i128,
2037    scale: &mut u16,
2038    big: &mut SumBig,
2039    b_in: &spg_storage::bignum::BigNumeric,
2040) {
2041    use spg_storage::bignum::BigNumeric;
2042    let cur = match big.take() {
2043        Some(b) => *b,
2044        None => {
2045            let c = BigNumeric::from_i128(*scaled, *scale);
2046            *scaled = 0;
2047            *scale = 0;
2048            c
2049        }
2050    };
2051    *big = Some(alloc::boxed::Box::new(cur.add(b_in)));
2052}
2053
2054/// One sum/avg accumulation step — the same variant arms (and the same
2055/// error text) as the single-spec fast path's inline match.
2056#[inline]
2057/// v7.39 (round 569) — one row's contribution to a min/max lane.
2058///
2059/// The same question `accumulate_groups` asks per spec per row, with
2060/// none of the per-spec indexing around it. NULL contributes nothing,
2061/// which is PG's rule and the generic path's.
2062fn fused_extreme_cell(a: &mut FusedAcc, v: &Value<'_>, max: bool) -> Result<(), EvalError> {
2063    if matches!(v, Value::Null) {
2064        return Ok(());
2065    }
2066    // v7.39 (round 626) — the FOURTH place this comparison is made. The
2067    // deny list went onto the dispatched arm and the two inlined grouped
2068    // copies first, and `SELECT min(bool_col) FROM t` — no GROUP BY — still
2069    // answered, because it lands here.
2070    if !a.extreme_mysql && min_max_unsupported_type(v) {
2071        return Err(EvalError::TypeMismatch {
2072            detail: format!(
2073                "function {}({}) does not exist",
2074                if max { "max" } else { "min" },
2075                crate::conversions::pg_type_name_for_error_opt(v.data_type())
2076            ),
2077        });
2078    }
2079    let take = match &a.extreme {
2080        None => true,
2081        Some(prev) => {
2082            let ord = extreme_cmp_in(None, a.extreme_coll.as_deref(), v, prev, a.extreme_mysql);
2083            if max {
2084                ord == core::cmp::Ordering::Greater
2085            } else {
2086                ord == core::cmp::Ordering::Less
2087            }
2088        }
2089    };
2090    if take {
2091        a.extreme = Some(v.clone().into_owned());
2092    }
2093    Ok(())
2094}
2095
2096/// v7.39 (round 626, S05b/F29) — the types PG has no `min`/`max` for.
2097///
2098/// A DENY list, not an allow list, and every entry measured: PG accepts
2099/// min/max over int2 int4 int8 numeric float4 float8 money text varchar
2100/// bpchar name date time timetz timestamp timestamptz interval bytea inet
2101/// cidr and the array types, and refuses exactly these. Writing the allow
2102/// list instead is how round 625's first cut of the string guard managed to
2103/// refuse five overloads PG actually has; a deny list of measured
2104/// rejections cannot over-refuse.
2105fn min_max_unsupported_type(v: &Value<'_>) -> bool {
2106    matches!(
2107        v.data_type(),
2108        Some(
2109            spg_storage::DataType::Bool
2110                | spg_storage::DataType::Uuid
2111                | spg_storage::DataType::Macaddr
2112                | spg_storage::DataType::Macaddr8
2113                | spg_storage::DataType::Json
2114                | spg_storage::DataType::Jsonb
2115                | spg_storage::DataType::Bit(_)
2116                | spg_storage::DataType::BitVarying(_)
2117                | spg_storage::DataType::Xml
2118                | spg_storage::DataType::TsVector
2119                | spg_storage::DataType::TsQuery
2120                // v7.39 (round 641) — a transaction id has no ordering
2121                // operator, so PG has no `min(xid)` / `max(xid)` either:
2122                // "function min(xid) does not exist", measured. SPG
2123                // answered, because a Value::Xid carries a u32 that
2124                // compares perfectly well — which is exactly the trap
2125                // the type exists to avoid.
2126                | spg_storage::DataType::Xid
2127        )
2128    )
2129}
2130
2131/// Fold one value into a running sum/avg. THE accumulator — there is no
2132/// second copy, by design; see `NumAcc` for what four copies cost.
2133///
2134/// No `inline(always)` here, and the reason is measured rather than
2135/// stylistic. The four copies were hand-inlining, so the obvious guess was
2136/// that the collapse would cost a call per row and the attribute would buy
2137/// it back. It did not: with `count` split out of `NumAcc`, `sum(int)`
2138/// over 500k rows lost ~8% WITH the attribute applied. What actually
2139/// mattered was the pointer count — the copy this replaces took one
2140/// `&mut FusedAcc`, and passing `&mut NumAcc` plus a separate `&mut i64`
2141/// made two base pointers. Folding `count` back into the struct closed the
2142/// gap; the attribute never did, so it is not here.
2143fn acc_cell(a: &mut NumAcc, v: &Value<'_>) -> Result<(), EvalError> {
2144    match v {
2145        Value::Null => {}
2146        Value::SmallInt(n) => {
2147            a.sum_int += i64::from(*n);
2148            a.count += 1;
2149        }
2150        Value::Int(n) => {
2151            a.sum_int += i64::from(*n);
2152            a.count += 1;
2153        }
2154        // v7.38 (read01, T4) — BIGINT sums as exact NUMERIC (PG).
2155        Value::BigInt(n) => {
2156            sum_add_exact(
2157                &mut a.sum_num_scaled,
2158                &mut a.sum_num_scale,
2159                &mut a.sum_big,
2160                i128::from(*n),
2161                0,
2162            );
2163            a.use_numeric = true;
2164            a.count += 1;
2165        }
2166        Value::Float(x) => {
2167            a.sum_float += *x;
2168            a.use_float = true;
2169            a.float_not_real = true;
2170            a.count += 1;
2171        }
2172        Value::Real(x) => {
2173            a.sum_float += f64::from(*x);
2174            a.use_float = true;
2175            a.count += 1;
2176        }
2177        Value::Numeric {
2178            scaled,
2179            scale,
2180            kind,
2181        } => {
2182            sum_add_exact(
2183                &mut a.sum_num_scaled,
2184                &mut a.sum_num_scale,
2185                &mut a.sum_big,
2186                *scaled,
2187                *scale,
2188            );
2189            a.sum_num_kind = fold_sum_kind(a.sum_num_kind, *kind);
2190            a.use_numeric = true;
2191            a.count += 1;
2192        }
2193        // v7.39 (read01 numeric.c) — a NumericBig input promotes to the spill.
2194        Value::NumericBig(b) => {
2195            sum_add_bignum(
2196                &mut a.sum_num_scaled,
2197                &mut a.sum_num_scale,
2198                &mut a.sum_big,
2199                b,
2200            );
2201            a.use_numeric = true;
2202            a.count += 1;
2203        }
2204        Value::Interval {
2205            months,
2206            days,
2207            micros,
2208            kind,
2209        } => {
2210            a.sum_iv_months += i64::from(*months);
2211            a.sum_iv_days += i64::from(*days);
2212            a.sum_iv_micros += i128::from(*micros);
2213            a.use_interval = true;
2214            a.count += 1;
2215        }
2216        Value::Money(c) => {
2217            a.sum_money += i128::from(*c);
2218            a.use_money = true;
2219            a.count += 1;
2220        }
2221        other => {
2222            return Err(EvalError::TypeMismatch {
2223                detail: format!(
2224                    "sum/avg need numeric, got {}",
2225                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
2226                ),
2227            });
2228        }
2229    }
2230    Ok(())
2231}
2232
2233/// v7.39 (read01 round 61) — thread the catalog into a stage's context when the
2234/// caller has one. `EvalContext::with_catalog` takes a reference, so this keeps
2235/// the Option handling in one place rather than at four call sites.
2236fn with_catalog<'a>(
2237    ctx: EvalContext<'a>,
2238    catalog: Option<&'a spg_storage::Catalog>,
2239    engine: Option<&'a crate::Engine>,
2240) -> EvalContext<'a> {
2241    let ctx = match catalog {
2242        Some(c) => ctx.with_catalog(c),
2243        None => ctx,
2244    };
2245    match engine {
2246        Some(e) => ctx.with_engine(e),
2247        None => ctx,
2248    }
2249}
2250
2251fn accumulate_groups(
2252    rows: AggRows<'_>,
2253    group_exprs: &[Expr],
2254    agg_specs: &[AggSpec],
2255    schema_cols: &[ColumnSchema],
2256    table_alias: Option<&str>,
2257    correlated_eval: Option<CorrelatedEval<'_>>,
2258    runner: Option<&dyn crate::ParallelRunner>,
2259    // v7.39 (read01 round 61) — the catalog. `run` has carried it since the
2260    // enum-order knife, but the four stages below each built a BARE context and
2261    // dropped it — so a catalog-dependent expression inside an aggregate's
2262    // argument (`string_agg(f1(id), ',')`, a user function) answered "unknown
2263    // function". Same family as rounds 49/53/54/55/56.
2264    catalog: Option<&spg_storage::Catalog>,
2265    engine: Option<&crate::Engine>,
2266) -> Result<Vec<(Vec<Value<'static>>, Vec<AggState>)>, EvalError> {
2267    let ctx = with_catalog(EvalContext::new(schema_cols, table_alias), catalog, engine);
2268    // Map group key (vec of values, encoded as canonical string) -> group state.
2269    // v7.32 (architecture v2, P2b) — insertion-ordered group state in
2270    // a Vec; the hash map only maps key → index. Removes the parallel
2271    // `key_order: Vec<String>` (a second per-group key clone) and the
2272    // per-group re-probe `groups[k]` at finalize (24k hash lookups for
2273    // the inbox shape). The map owns its key once on vacant insert.
2274    let mut order: Vec<(Vec<Value<'static>>, Vec<AggState>)> = Vec::new();
2275    let mut groups: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2276    // v7.37.x (mailrs Track A perf — SPGE ≫ PG18) — single-Text GROUP
2277    // BY column fast path. The canonical-string encode (`S<text>|`)
2278    // + `encode_key_refs_into` reuse-buffer churn dominated the 30 k-
2279    // row mailrs minimal probe (~3-4 ms / 30 k). For `GROUP BY t` on
2280    // a TEXT column (the inbox-listing / conversation-grouping shape)
2281    // the column text IS the canonical key — no encoder, no prefix
2282    // byte, no `refs` Vec rebuild per row. The fallback `groups` map
2283    // above is retained for multi-col / non-Text / collation paths;
2284    // this map only fires when the schema and value structurally
2285    // permit it. `null_group_idx` collects NULL group rows (SQL groups
2286    // all NULLs into one bucket).
2287    let mut groups_text: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2288    // v7.37.16 — raw-i64 group map for the single-INT GROUP BY fast path.
2289    let mut groups_int: hashbrown::HashMap<i64, usize> = hashbrown::HashMap::new();
2290    let mut null_group_idx: Option<usize> = None;
2291    // When there are no GROUP BY exprs *and* there is at least one aggregate,
2292    // every row collapses into a single anonymous group keyed by "".
2293    if rows.is_empty() && group_exprs.is_empty() {
2294        // Single empty-aggregate group: count=0, sum=0, max=NULL, etc.
2295        // No rows follow, so the map is never probed — seed `order` only.
2296        let init: Vec<AggState> = (0..agg_specs.len()).map(|_| AggState::default()).collect();
2297        order.push((Vec::new(), init));
2298    }
2299
2300    // v7.30 (perf campaign) - hoist the per-row work that doesn't
2301    // depend on the row: which group exprs need collation folding
2302    // (none, for most queries - the old code cloned the whole
2303    // group_vals vec per row just in case).
2304    // v7.30 (perf campaign) - the no-tax row loop. When a group
2305    // expr or an aggregate argument is a bare column reference
2306    // (the overwhelmingly common shape), bind its position ONCE
2307    // and read row cells by offset in the loop - no per-row tree
2308    // walk, no owned-Value clone out of resolve_column. Anything
2309    // more complex keeps the eval path.
2310    let col_pos = |e: &Expr| -> Option<usize> {
2311        // v7.37.16 — bind bare names too, via the compiled-WHERE
2312        // resolver: `compile_column_pos` mirrors resolve_column's
2313        // happy layers exactly (composite → prefix/alias gate → bare
2314        // exact → unique suffix) and returns None on anything that
2315        // would reach an ambiguity / whole-row / error path, so the
2316        // eval fallback keeps identical semantics. Previously only
2317        // qualified refs bound (via the looser find_column_pos), so
2318        // single-table `GROUP BY g` / `avg(v)` ran the per-row
2319        // eval_expr tree-walk + Vec + encode_key String alloc — the
2320        // heavy.rs group_by / filter_agg residual loss vs PG18.
2321        if let Expr::Column(c) = e {
2322            eval::compile_column_pos(c, &ctx)
2323        } else {
2324            None
2325        }
2326    };
2327    let group_pos: Vec<Option<usize>> = group_exprs.iter().map(col_pos).collect();
2328    let all_groups_bound = group_pos.iter().all(Option::is_some);
2329    // v7.37.x — single-col GROUP BY on a TEXT-typed column lets the
2330    // hot loop key the hash map by the column text directly. Resolved
2331    // once from the bound position against `schema_cols`.
2332    // v7.39 (round 364, M4 P2) — the raw-text GROUP BY fast path keys
2333    // by the column's bytes, which cannot fold; a MySQL session takes
2334    // the general encoder path (which folds) instead.
2335    let single_text_group_col: bool = !ctx.mysql_dialect
2336        && group_pos.len() == 1
2337        && group_pos[0].is_some_and(|p| {
2338            schema_cols
2339                .get(p)
2340                .is_some_and(|c| matches!(c.ty, spg_storage::DataType::Text))
2341        });
2342    // v7.37.16 (heavy.rs group_500k 1.12× loss) — single-col GROUP BY on
2343    // an INTEGER-typed column keys the map by the raw i64 instead of the
2344    // canonical-string encode ("I{n}|" write! + String-keyed hash probe
2345    // was ~25-40 ns of the 42 ns/row 500k GROUP BY budget). Mirrors the
2346    // single-Text fast path; NULLs share `null_group_idx`; a non-integer
2347    // cell (coercion edge) falls back to the encoded path.
2348    let single_int_group_col: bool = group_pos.len() == 1
2349        && group_pos[0].is_some_and(|p| {
2350            schema_cols.get(p).is_some_and(|c| {
2351                matches!(
2352                    c.ty,
2353                    spg_storage::DataType::SmallInt
2354                        | spg_storage::DataType::Int
2355                        | spg_storage::DataType::BigInt
2356                )
2357            })
2358        });
2359    let arg_pos: Vec<Option<usize>> = agg_specs
2360        .iter()
2361        .map(|spec| spec.arg.as_ref().and_then(|e| col_pos(e)))
2362        .collect();
2363    // v7.39 (round 370, M4 P4a) — the MySQL dialect folds GROUP BY /
2364    // DISTINCT text keys (M4 P2), EXCEPT over a column with an explicit
2365    // `COLLATE utf8mb4_bin` (stored `Binary`), which de-dups byte-wise.
2366    // A folding default column stores `CaseInsensitive`, so only an
2367    // explicit binary column suppresses the fold. Multi-column GROUP BY
2368    // mixing a binary and a folding column is treated byte-wise as a whole
2369    // (rare; residual).
2370    let is_binary_key_col = |p: Option<usize>| -> bool {
2371        p.and_then(|i| schema_cols.get(i))
2372            .is_some_and(|c| matches!(c.collation, spg_storage::Collation::Binary))
2373    };
2374    // v7.39 (round 371, M4 P4b) — a per-expression `… COLLATE utf8mb4_bin`
2375    // / `BINARY …` key is byte-wise too, so its GROUP BY / DISTINCT does
2376    // not fold. The clause lowers to a `binary` cast the parser emits.
2377    let mysql_fold_groups: bool = ctx.mysql_dialect
2378        && !group_pos.iter().any(|&p| is_binary_key_col(p))
2379        && !group_exprs
2380            .iter()
2381            .any(|e| crate::eval::is_binary_coerced(e));
2382    // v7.38.18 — the padding mask, built beside the fold mask off the
2383    // same argument column so the two cannot come from different places.
2384    let distinct_pads: Vec<bool> = arg_pos
2385        .iter()
2386        .map(|&p| {
2387            p.and_then(|i| schema_cols.get(i))
2388                .is_some_and(|c| crate::collate::pads_space(c.collation_name.as_deref()))
2389        })
2390        .collect();
2391    let distinct_fold_case: Vec<bool> = arg_pos.iter().map(|&p| !is_binary_key_col(p)).collect();
2392    let distinct_fold: Vec<bool> = agg_specs
2393        .iter()
2394        .enumerate()
2395        .map(|(i, spec)| {
2396            // v7.38.18 — a byte-wise column still needs this step when
2397            // its collation PADS. `utf8mb4_bin` folds no case and
2398            // ignores trailing spaces, which is one flag short of what
2399            // a single boolean can say; the pad mask beside this one
2400            // carries the second half and the fold is skipped by
2401            // `distinct_fold_case` below.
2402            ctx.mysql_dialect
2403                && (!is_binary_key_col(arg_pos[i]) || distinct_pads[i])
2404                && !spec
2405                    .arg
2406                    .as_ref()
2407                    .is_some_and(|e| crate::eval::is_binary_coerced(e))
2408        })
2409        .collect();
2410    // v7.37.x (mailrs Track A 100k attack) — dedicated tight loop
2411    // for the "single-Text GROUP BY + single MAX(bound numeric arg)"
2412    // shape. This is the mailrs `/api/conversations` minimal shape
2413    // (`GROUP BY thread_id, MAX(internal_date)`) and an inbox-listing
2414    // staple across the SPG customer set. Skipping the per-row spec
2415    // loop, FILTER / arg2 / order_keys checks, and the union-typed
2416    // `update_state` enum jump saves ~80-100 ns/row at 100 k input
2417    // — the gap closing the SPGE vs PG18 ratio at this scale.
2418    let dedicated_max_loop: bool = single_text_group_col
2419        && agg_specs.len() == 1
2420        && matches!(agg_specs[0].kind, AggKind::Max)
2421        && agg_specs[0].filter.is_none()
2422        && agg_specs[0].arg2.is_none()
2423        && agg_specs[0].order_by.is_empty()
2424        && !agg_specs[0].distinct
2425        && !agg_specs[0].first_ordered
2426        && arg_pos[0].is_some();
2427    // v7.36 (perf — mailrs Ask 1 SUM(LENGTH(text_body)) 18ms → ?) —
2428    // pre-compile every aggregate arg that's a `fully_compilable`
2429    // PURE expression over bound columns. Without this, `LENGTH(col)`
2430    // / `COALESCE(col, '')` / `CAST(col AS BIGINT)` etc. ALL fell
2431    // through to the `(None, Some(e)) => eval_arg(e, mat, ...)` slow
2432    // path that materialises a Cow<Row> per input row — for a 25k-row
2433    // JOIN that's 25k full-row clones for one column read. The Step
2434    // VM (`eval_compiled_ref`) reads columns by RowRef::get and runs
2435    // the same `apply_function` dispatcher with zero materialisation.
2436    let arg_compiled: Vec<Option<eval::CompiledExpr>> = agg_specs
2437        .iter()
2438        .enumerate()
2439        .map(|(i, spec)| match (&arg_pos[i], &spec.arg) {
2440            (Some(_), _) => None,
2441            (None, Some(e)) if eval::fully_compilable(e) => Some(eval::compile_expr(e, &ctx)),
2442            _ => None,
2443        })
2444        .collect();
2445    // v7.37.4 (L1 — executor-time CSE / mailrs P0) — dedupe
2446    // compiled aggregate-arg expressions across specs. mailrs's
2447    // `/api/conversations` SQL has 14 aggregates whose compiled
2448    // CASE/CAST arg expressions overlap heavily (`m.message_id != ''`
2449    // re-appears 4×, the inner `CASE WHEN m.message_id != '' THEN
2450    // m.message_id ELSE CAST(m.id AS TEXT) END` re-appears 3×). Each
2451    // dup currently costs one Step-VM walk per row — 100k rows ×
2452    // ~3-4 redundant evals = ~300-400k wasted Step-VM runs.
2453    //
2454    // Dedupe key = source `Expr` (PartialEq). `CompiledExpr` itself
2455    // is not `Hash` / `Eq`, but n_specs is small (≤ ~20 in practice);
2456    // O(n²) PartialEq probe cost = ~196 cmp per query, vs millions
2457    // of saved per-row evals. `fully_compilable` requires PURE
2458    // scalars (no NOW / RANDOM / sequence accessors), so an earlier
2459    // eval has identical observable semantics to the original.
2460    //
2461    // `arg_slot[i] = Some(s)` means spec `i`'s compiled arg lives in
2462    // slot `s` of `arg_unique_idx` (which points back into
2463    // `arg_compiled` for the canonical owner). Per-row cache fills
2464    // LAZILY — preserves the current FILTER semantics where an arg
2465    // whose spec is filtered out is never evaluated (and never
2466    // surfaces a type error). Reset to `None` at the top of each row.
2467    let mut arg_unique_idx: Vec<usize> = Vec::new();
2468    let mut arg_slot: Vec<Option<usize>> = Vec::with_capacity(agg_specs.len());
2469    arg_slot.resize(agg_specs.len(), None);
2470    for (i, spec) in agg_specs.iter().enumerate() {
2471        if arg_pos[i].is_some() || arg_compiled[i].is_none() {
2472            continue;
2473        }
2474        let src = spec.arg.as_ref().expect("arg_compiled => spec.arg is Some");
2475        let pos = arg_unique_idx
2476            .iter()
2477            .position(|&j| agg_specs[j].arg.as_ref().is_some_and(|other| other == src));
2478        arg_slot[i] = Some(match pos {
2479            Some(p) => p,
2480            None => {
2481                arg_unique_idx.push(i);
2482                arg_unique_idx.len() - 1
2483            }
2484        });
2485    }
2486    let mut row_eval_cache: Vec<Option<Value>> = Vec::with_capacity(arg_unique_idx.len());
2487    row_eval_cache.resize(arg_unique_idx.len(), None);
2488    // v7.33 (array_agg perf) — bound positions for each spec's internal
2489    // ORDER BY keys, so an ordered aggregate (`array_agg(x ORDER BY y)`)
2490    // reads the sort key by reference (RowRef::get) instead of
2491    // materialising the whole combined join row per input row just to
2492    // eval one bound column. Mirrors arg_pos. On the inbox shape this
2493    // turned 24k full-row (~1 KB each) clones into 24k single-cell reads.
2494    let order_pos: Vec<Vec<Option<usize>>> = agg_specs
2495        .iter()
2496        .map(|spec| spec.order_by.iter().map(|o| col_pos(&o.expr)).collect())
2497        .collect();
2498    // v7.37.43 (DISTA A-3) — precompute the per-spec arg2 when it is a
2499    // bare literal. `string_agg(DISTINCT col, ',')` and every other
2500    // call with a constant separator goes through this path; PG evaluates
2501    // arg2 as a Const once at plan time. SPG was paying a Cow row
2502    // materialisation per input row purely so `eval_arg(literal, &row)`
2503    // could run — but a literal doesn't read the row at all. Hoist the
2504    // literal value into a per-query table; per-row arg2 just clones it.
2505    //
2506    // Sentinel: when arg2 is present but NOT a literal, the entry stays
2507    // `None` and the per-row path still falls into the eval branch
2508    // (which forces `needs_mat`).
2509    let arg2_literal_val: Vec<Option<Value<'static>>> = agg_specs
2510        .iter()
2511        .map(|s| match &s.arg2 {
2512            Some(Expr::Literal(l)) => Some(eval::literal_to_value(l)),
2513            _ => None,
2514        })
2515        .collect();
2516    // Does any spec need the fully-materialised row in the bound fast
2517    // path — a FILTER, a non-bound value arg, a NON-LITERAL second arg,
2518    // or a non-bound ORDER key? When false (every aggregate arg/key is a
2519    // bound column — the inbox shape, and the DISTA shape after A-3)
2520    // the bound fast path never materialises a row.
2521    let needs_mat = agg_specs.iter().enumerate().any(|(i, s)| {
2522        s.filter.is_some()
2523            || (s.arg.is_some() && arg_pos[i].is_none() && arg_compiled[i].is_none())
2524            || (s.arg2.is_some() && arg2_literal_val[i].is_none())
2525            || order_pos[i].iter().any(Option::is_none)
2526    });
2527    let ci_positions: Vec<usize> = group_exprs
2528        .iter()
2529        .enumerate()
2530        .filter(|(_, g)| {
2531            matches!(
2532                eval::column_collation(g, &ctx),
2533                Some(spg_storage::Collation::CaseInsensitive)
2534            )
2535        })
2536        .map(|(i, _)| i)
2537        .collect();
2538    // v7.31 (perf 3e) — per-row scratch buffers. The fast path used
2539    // to allocate a key String (and a refs Vec) for EVERY row just
2540    // to probe the group map; hits — the overwhelming case — now
2541    // touch the allocator zero times.
2542    let mut keybuf_s = String::new();
2543    // v7.36 — reused Step VM eval stack for compiled aggregate args.
2544    // v7.37.9 T3 S2 — elided lifetime so the Vec's `'val` binds to the
2545    // row-borrow lifetime per call (`eval_compiled_ref<'row, 'val>` now
2546    // requires `'row: 'val`). Caller-side Vec<Value<'_>> lets compiler
2547    // infer the shortest lifetime that covers all calls.
2548    let mut eval_stack: Vec<Value<'_>> = Vec::new();
2549    let mut dkeybuf = String::new();
2550    let mut refs: Vec<&Value> = Vec::with_capacity(group_pos.len());
2551    // v7.32 (round-31) — an aggregate's argument / FILTER / second arg /
2552    // ORDER key may itself be a *correlated* subquery, e.g.
2553    // `MAX((SELECT i.v FROM inner i WHERE i.fk = o.id))`. A non-correlated
2554    // subquery is pre-resolved to a literal before this loop, but a
2555    // correlated one survives as a subquery node and must be evaluated per
2556    // outer row through the correlated evaluator — the same hook the
2557    // select-list / HAVING / ORDER finalisers already use below. Plain
2558    // `eval_expr` would hit "subquery reached row eval".
2559    //
2560    // The `any_agg_subquery` gate is computed once here so the common case
2561    // (no subquery anywhere in the aggregate args — including every hot
2562    // scan/group aggregate) short-circuits before the per-row
2563    // `expr_has_subquery` walk: `eval_arg` is then exactly `eval_expr`.
2564    let any_agg_subquery = correlated_eval.is_some()
2565        && agg_specs.iter().any(|s| {
2566            s.filter
2567                .as_ref()
2568                .is_some_and(|e| crate::expr_has_subquery(e))
2569                || s.arg.as_ref().is_some_and(|e| crate::expr_has_subquery(e))
2570                || s.arg2.as_ref().is_some_and(|e| crate::expr_has_subquery(e))
2571                || s.order_by.iter().any(|o| crate::expr_has_subquery(&o.expr))
2572        });
2573    let eval_arg =
2574        |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| -> Result<Value<'static>, EvalError> {
2575            match correlated_eval {
2576                Some(f) if any_agg_subquery && crate::expr_has_subquery(e) => f(e, r, c),
2577                _ => eval::eval_expr(e, r, c),
2578            }
2579        };
2580    // v7.36 (perf — mailrs Phase 1, post u64-hash) — single
2581    // anonymous group fast path. When the query has no GROUP BY
2582    // (`SELECT SUM(LENGTH(col)) FROM ...`, COUNT, AVG, etc.) the
2583    // whole input collapses into one group. The fast path below
2584    // still pays one `groups.get("")` hash probe per row plus
2585    // `entry = &mut order[0]` reindex even when the empty-key
2586    // path encodes nothing — measured ~50 ns/row across 25 k rows
2587    // = ~1.25 ms of pure bookkeeping on the user_storage_usage
2588    // baseline.
2589    //
2590    // Bypass: lift `entry` outside the loop and feed every row
2591    // straight into it. Same `update_state` machinery, zero
2592    // per-row hash work, zero per-row index lookup.
2593    let single_anon_group = group_exprs.is_empty() && !rows.is_empty();
2594    if single_anon_group {
2595        // Seed the single group at idx 0 once.
2596        let init: Vec<AggState> = (0..agg_specs.len()).map(|_| AggState::default()).collect();
2597        order.clear();
2598        order.push((Vec::new(), init));
2599    }
2600    // v7.36 (perf — mailrs Phase 1, count_messages 2.58 → ?) —
2601    // `COUNT(*)` short-circuit. For a single-anon-group `COUNT(*)`
2602    // with no FILTER / DISTINCT, every survivor counts once — the
2603    // answer IS `rows.len()`. Skips the 25 k iterations of
2604    // `update_state("count_star", …)` on the mailrs count_messages
2605    // shape; the JOIN already produced exactly the set of rows
2606    // that must be counted.
2607    if single_anon_group
2608        && agg_specs.len() == 1
2609        && agg_specs[0].name == "count_star"
2610        && agg_specs[0].filter.is_none()
2611        && agg_specs[0].arg.is_none()
2612        && agg_specs[0].arg2.is_none()
2613        && agg_specs[0].order_by.is_empty()
2614        && !agg_specs[0].distinct
2615    {
2616        let state = &mut order[0].1[0];
2617        state.num.count = rows.len() as i64;
2618        return Ok(order);
2619    }
2620    // v7.37.16 (heavy.rs agg_500k 1.6× loss) — fused streaming accumulator
2621    // for ANY number of count(*)/count(col)/sum(col)/avg(col) specs over
2622    // BOUND columns (no FILTER/DISTINCT/arg2/ORDER). The generic per-row
2623    // spec loop paid arg dispatch + union-typed update_state per spec per
2624    // row (~10 ns/spec/row); PG's parallel agg runs the 500k 3-spec shape
2625    // at ~18 ns/row effective. Three cuts:
2626    // - count(*) never enters the row loop — it IS rows.len();
2627    // - sum/avg over the SAME column share one accumulator (identical
2628    //   running state), so `count(*), sum(v), avg(v)` does ONE cell read
2629    //   and one accumulate per row;
2630    // - remaining ops run in one tight pass, no update_state.
2631    // Finalize writes the same AggState fields as the single-spec path.
2632    if single_anon_group
2633        && let Some((spec_src, unique_ops)) = fused_layout(
2634            agg_specs,
2635            &arg_pos,
2636            &arg_compiled,
2637            &order_pos,
2638            &arg2_literal_val,
2639        )
2640    {
2641        let mut accs: Vec<FusedAcc> = fused_accs(&unique_ops, ctx.mysql_dialect);
2642        // v7.39 (parallel-agg P1) — shard the row scan across the
2643        // host-injected executor when the input is large enough.
2644        // Each shard runs the same tight loop over its row range and
2645        // returns its own Vec<FusedAcc>; the merge is field-wise
2646        // (see merge_fused). Errors inside a shard surface as the
2647        // shard result and re-raise after join.
2648        // v7.39 (round 716) — the scan takes its EvalContext as a
2649        // parameter: `EvalContext` is not Sync (per-eval memo Cells, the
2650        // sequence resolver's plain `&dyn Fn`), so the parallel branch
2651        // hands each shard a locally-built minimal context instead of
2652        // capturing the outer one. The compiled ops only reach the parts
2653        // a shard context carries — columns, alias, dialect, catalog —
2654        // because `fully_compilable` excludes everything else (params,
2655        // sequences, user functions, FTS).
2656        let fused_scan = |range: core::ops::Range<usize>,
2657                          accs: &mut Vec<FusedAcc>,
2658                          fctx: &EvalContext<'_>|
2659         -> Result<(), EvalError> {
2660            // One Step-VM stack per shard call, reused across every
2661            // row and every compiled op.
2662            let mut stack: Vec<Value<'_>> = Vec::new();
2663            for row in rows.range(range.start, range.end).iter() {
2664                for (si, op) in unique_ops.iter().enumerate() {
2665                    match op {
2666                        FusedOp::CountCol(p) => {
2667                            if !matches!(row.get(*p), Some(Value::Null) | None) {
2668                                accs[si].num.count += 1;
2669                            }
2670                        }
2671                        FusedOp::AccCol(p) => {
2672                            {
2673                                let a = &mut accs[si];
2674                                acc_cell(&mut a.num, row.get(*p).unwrap_or(&Value::Null))
2675                            }?;
2676                        }
2677                        FusedOp::Extreme { pos, max, .. } => {
2678                            fused_extreme_cell(
2679                                &mut accs[si],
2680                                row.get(*pos).unwrap_or(&Value::Null),
2681                                *max,
2682                            )?;
2683                        }
2684                        FusedOp::CountExpr(sp) => {
2685                            let c = arg_compiled[*sp].as_ref().expect("gated compiled");
2686                            let v = eval::eval_compiled_ref(c, row, fctx, &mut stack)?;
2687                            if !matches!(v, Value::Null) {
2688                                accs[si].num.count += 1;
2689                            }
2690                        }
2691                        FusedOp::AccExpr(sp) => {
2692                            let c = arg_compiled[*sp].as_ref().expect("gated compiled");
2693                            let v = eval::eval_compiled_ref(c, row, fctx, &mut stack)?;
2694                            acc_cell(&mut accs[si].num, &v)?;
2695                        }
2696                        FusedOp::ExtremeExpr { spec, max, .. } => {
2697                            let c = arg_compiled[*spec].as_ref().expect("gated compiled");
2698                            let v = eval::eval_compiled_ref(c, row, fctx, &mut stack)?;
2699                            fused_extreme_cell(&mut accs[si], &v, *max)?;
2700                        }
2701                        FusedOp::Collect { spec, string_kind } => {
2702                            collect_cell(
2703                                &mut accs[si],
2704                                &row,
2705                                arg_pos[*spec].expect("gated bound"),
2706                                &order_pos[*spec],
2707                                *string_kind,
2708                            )?;
2709                        }
2710                    }
2711                }
2712            }
2713            Ok(())
2714        };
2715        if !unique_ops.is_empty() {
2716            let par = runner.filter(|_| rows.len() >= crate::PARALLEL_MIN_ROWS);
2717            if let Some(r) = par {
2718                crate::PARALLEL_AGG_FIRED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2719                let n_shards = (rows.len() / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
2720                let chunk = rows.len().div_ceil(n_shards);
2721                type ShardOut = Result<Vec<FusedAcc>, EvalError>;
2722                let ops = &unique_ops;
2723                let mysql_for_accs = ctx.mysql_dialect;
2724                // v7.39 (round 716) — the whitelisted concat family
2725                // renders through the SESSION's style; a shard context
2726                // built from defaults would silently re-render dates and
2727                // floats the default way. RenderStyle is Copy.
2728                let outer_style = ctx.render_style;
2729                let results = r.run_shards(n_shards, &|i| {
2730                    let lo = i * chunk;
2731                    let hi = ((i + 1) * chunk).min(rows.len());
2732                    let mut local: Vec<FusedAcc> = fused_accs(ops, mysql_for_accs);
2733                    // Shard-local minimal context (the outer one is not
2734                    // Sync); see the fused_scan comment.
2735                    let mut sctx = EvalContext::new(schema_cols, table_alias);
2736                    sctx.mysql_dialect = mysql_for_accs;
2737                    sctx.render_style = outer_style;
2738                    let sctx = match catalog {
2739                        Some(c) => sctx.with_catalog(c),
2740                        None => sctx,
2741                    };
2742                    let out: ShardOut = fused_scan(lo..hi, &mut local, &sctx).map(|()| local);
2743                    alloc::boxed::Box::new(out)
2744                });
2745                for boxed in results {
2746                    let shard = boxed
2747                        .downcast::<ShardOut>()
2748                        .expect("runner echoes the closure's box");
2749                    let mut shard_accs = (*shard)?;
2750                    for (si, b) in shard_accs.iter_mut().enumerate() {
2751                        merge_fused(&mut accs[si], b);
2752                    }
2753                }
2754            } else {
2755                fused_scan(0..rows.len(), &mut accs, &ctx)?;
2756            }
2757        }
2758        fill_states_from_fused(
2759            &mut order[0].1,
2760            &spec_src,
2761            &mut accs,
2762            rows.len() as i64,
2763            &arg2_literal_val,
2764        );
2765        return Ok(order);
2766    }
2767    // v7.39 (parallel-agg P3) — parallel GROUP BY fast path: a single
2768    // bound INT group column with every spec fused-eligible (the
2769    // `GROUP BY g` + count/sum/avg panel shape). Shards build local
2770    // i64-keyed maps of FusedAcc slots; the merge folds maps in shard
2771    // order (first-seen group order across shards — SQL leaves GROUP
2772    // BY output order unspecified). Any non-integer cell under the
2773    // integer schema (coercion edge) aborts the shard and the whole
2774    // scan falls back to the serial path below.
2775    if single_int_group_col
2776        && group_exprs.len() == 1
2777        && rows.len() >= crate::PARALLEL_MIN_ROWS
2778        && let Some(r) = runner
2779        && let Some((spec_src, unique_ops)) = fused_layout(
2780            agg_specs,
2781            &arg_pos,
2782            &arg_compiled,
2783            &order_pos,
2784            &arg2_literal_val,
2785        )
2786        && !unique_ops.is_empty()
2787    {
2788        crate::PARALLEL_AGG_FIRED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2789        let gp = group_pos[0].expect("single_int_group_col implies bound");
2790        struct ShardMap {
2791            // first-seen order of keys within the shard.
2792            keys: Vec<(i64, Value<'static>)>,
2793            slots: hashbrown::HashMap<i64, Vec<FusedAcc>>,
2794            null_slot: Option<Vec<FusedAcc>>,
2795            null_rows: i64,
2796            key_rows: hashbrown::HashMap<i64, i64>,
2797        }
2798        // Err(None) = coercion edge -> serial fallback; Err(Some(e)) = real error.
2799        type ShardOut = Result<ShardMap, Option<EvalError>>;
2800        let n_shards = (rows.len() / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
2801        let chunk = rows.len().div_ceil(n_shards);
2802        let ops = &unique_ops;
2803        let mysql_for_accs = ctx.mysql_dialect;
2804        // Same session-style carry as the anonymous-group lane.
2805        let outer_style = ctx.render_style;
2806        let results = r.run_shards(n_shards, &|si| {
2807            let lo = si * chunk;
2808            let hi = ((si + 1) * chunk).min(rows.len());
2809            let mut m = ShardMap {
2810                keys: Vec::new(),
2811                slots: hashbrown::HashMap::new(),
2812                null_slot: None,
2813                null_rows: 0,
2814                key_rows: hashbrown::HashMap::new(),
2815            };
2816            let out: ShardOut = (|| {
2817                // v7.39 (round 716) — per-shard Step-VM stack for the
2818                // compiled-argument ops, reused across rows, plus a
2819                // shard-local minimal context (the outer one is not
2820                // Sync); see the anonymous-group fused_scan comment.
2821                let mut stack: Vec<Value<'_>> = Vec::new();
2822                let mut sctx = EvalContext::new(schema_cols, table_alias);
2823                sctx.mysql_dialect = mysql_for_accs;
2824                sctx.render_style = outer_style;
2825                let sctx = match catalog {
2826                    Some(c) => sctx.with_catalog(c),
2827                    None => sctx,
2828                };
2829                for row in rows.range(lo, hi).iter() {
2830                    let v = row.get(gp).unwrap_or(&Value::Null);
2831                    let key: Option<i64> = match v {
2832                        Value::SmallInt(n) => Some(i64::from(*n)),
2833                        Value::Int(n) => Some(i64::from(*n)),
2834                        Value::BigInt(n) => Some(*n),
2835                        Value::Null => None,
2836                        _ => return Err(None), // coercion edge -> serial
2837                    };
2838                    let slots = match key {
2839                        Some(k) => {
2840                            *m.key_rows.entry(k).or_insert(0) += 1;
2841                            m.slots.entry(k).or_insert_with(|| {
2842                                m.keys.push((k, v.clone().into_owned()));
2843                                fused_accs(ops, mysql_for_accs)
2844                            })
2845                        }
2846                        None => {
2847                            m.null_rows += 1;
2848                            m.null_slot
2849                                .get_or_insert_with(|| fused_accs(ops, mysql_for_accs))
2850                        }
2851                    };
2852                    for (oi, op) in ops.iter().enumerate() {
2853                        match op {
2854                            FusedOp::CountCol(p) => {
2855                                if !matches!(row.get(*p), Some(Value::Null) | None) {
2856                                    slots[oi].num.count += 1;
2857                                }
2858                            }
2859                            FusedOp::AccCol(p) => {
2860                                {
2861                                    let a = &mut slots[oi];
2862                                    acc_cell(&mut a.num, row.get(*p).unwrap_or(&Value::Null))
2863                                }
2864                                .map_err(Some)?;
2865                            }
2866                            FusedOp::Extreme { pos, max, .. } => {
2867                                fused_extreme_cell(
2868                                    &mut slots[oi],
2869                                    row.get(*pos).unwrap_or(&Value::Null),
2870                                    *max,
2871                                )
2872                                .map_err(Some)?;
2873                            }
2874                            FusedOp::CountExpr(sp) => {
2875                                let c = arg_compiled[*sp].as_ref().expect("gated compiled");
2876                                let v = eval::eval_compiled_ref(c, row, &sctx, &mut stack)
2877                                    .map_err(Some)?;
2878                                if !matches!(v, Value::Null) {
2879                                    slots[oi].num.count += 1;
2880                                }
2881                            }
2882                            FusedOp::AccExpr(sp) => {
2883                                let c = arg_compiled[*sp].as_ref().expect("gated compiled");
2884                                let v = eval::eval_compiled_ref(c, row, &sctx, &mut stack)
2885                                    .map_err(Some)?;
2886                                acc_cell(&mut slots[oi].num, &v).map_err(Some)?;
2887                            }
2888                            FusedOp::ExtremeExpr { spec, max, .. } => {
2889                                let c = arg_compiled[*spec].as_ref().expect("gated compiled");
2890                                let v = eval::eval_compiled_ref(c, row, &sctx, &mut stack)
2891                                    .map_err(Some)?;
2892                                fused_extreme_cell(&mut slots[oi], &v, *max).map_err(Some)?;
2893                            }
2894                            FusedOp::Collect { spec, string_kind } => {
2895                                collect_cell(
2896                                    &mut slots[oi],
2897                                    &row,
2898                                    arg_pos[*spec].expect("gated bound"),
2899                                    &order_pos[*spec],
2900                                    *string_kind,
2901                                )
2902                                .map_err(Some)?;
2903                            }
2904                        }
2905                    }
2906                }
2907                Ok(m)
2908            })();
2909            alloc::boxed::Box::new(out)
2910        });
2911        // Merge in shard order; a fallback sentinel drops to serial.
2912        let mut merged_keys: Vec<(i64, Value<'static>)> = Vec::new();
2913        let mut merged: hashbrown::HashMap<i64, (Vec<FusedAcc>, i64)> = hashbrown::HashMap::new();
2914        let mut merged_null: Option<(Vec<FusedAcc>, i64)> = None;
2915        let mut fallback = false;
2916        let mut shard_err: Option<EvalError> = None;
2917        for boxed in results {
2918            let shard = boxed
2919                .downcast::<ShardOut>()
2920                .expect("runner echoes the closure's box");
2921            match *shard {
2922                Ok(mut m) => {
2923                    for (k, kv) in m.keys {
2924                        // Removed (not borrowed): the slot MOVES into the
2925                        // merged map on first sight, and the round-724
2926                        // collection lanes move out of it on merge.
2927                        let mut accs = m.slots.remove(&k).expect("keyed slot");
2928                        let rows_k = m.key_rows[&k];
2929                        match merged.get_mut(&k) {
2930                            Some((dst, cnt)) => {
2931                                for (i, b) in accs.iter_mut().enumerate() {
2932                                    merge_fused(&mut dst[i], b);
2933                                }
2934                                *cnt += rows_k;
2935                            }
2936                            None => {
2937                                merged_keys.push((k, kv));
2938                                merged.insert(k, (accs, rows_k));
2939                            }
2940                        }
2941                    }
2942                    if let Some(mut nb) = m.null_slot.take() {
2943                        match &mut merged_null {
2944                            Some((dst, cnt)) => {
2945                                for (i, b) in nb.iter_mut().enumerate() {
2946                                    merge_fused(&mut dst[i], b);
2947                                }
2948                                *cnt += m.null_rows;
2949                            }
2950                            None => merged_null = Some((nb, m.null_rows)),
2951                        }
2952                    }
2953                }
2954                Err(None) => fallback = true,
2955                Err(Some(e)) => shard_err = Some(e),
2956            }
2957        }
2958        if let Some(e) = shard_err {
2959            return Err(e);
2960        }
2961        if !fallback {
2962            for (k, kv) in merged_keys {
2963                let (mut accs, group_rows) = merged.remove(&k).expect("key recorded");
2964                let mut states: Vec<AggState> =
2965                    (0..agg_specs.len()).map(|_| AggState::default()).collect();
2966                fill_states_from_fused(
2967                    &mut states,
2968                    &spec_src,
2969                    &mut accs,
2970                    group_rows,
2971                    &arg2_literal_val,
2972                );
2973                order.push((alloc::vec![kv], states));
2974            }
2975            if let Some((mut accs, group_rows)) = merged_null {
2976                let mut states: Vec<AggState> =
2977                    (0..agg_specs.len()).map(|_| AggState::default()).collect();
2978                fill_states_from_fused(
2979                    &mut states,
2980                    &spec_src,
2981                    &mut accs,
2982                    group_rows,
2983                    &arg2_literal_val,
2984                );
2985                order.push((alloc::vec![Value::Null], states));
2986            }
2987            return Ok(order);
2988        }
2989        // fallthrough: serial paths below handle the coercion edge.
2990    }
2991
2992    // v7.36 (perf — mailrs Phase 1) — `COUNT(<bound col>)` (non-`*`)
2993    // collapses to: read the cell, increment when not NULL. Skips
2994    // the per-row spec dispatch + `update_state("count", …)`.
2995    if single_anon_group
2996        && agg_specs.len() == 1
2997        && agg_specs[0].name == "count"
2998        && agg_specs[0].filter.is_none()
2999        && agg_specs[0].arg2.is_none()
3000        && agg_specs[0].order_by.is_empty()
3001        && !agg_specs[0].distinct
3002        && arg_pos[0].is_some()
3003    {
3004        let p = arg_pos[0].unwrap();
3005        let mut count: i64 = 0;
3006        for row in rows.iter() {
3007            if !matches!(row.get(p), Some(Value::Null) | None) {
3008                count += 1;
3009            }
3010        }
3011        let state = &mut order[0].1[0];
3012        state.num.count = count;
3013        return Ok(order);
3014    }
3015    // v7.36 (perf — mailrs Phase 1, user_storage_usage 7.5 → ?) —
3016    // single-aggregate streaming accumulator. For
3017    // `SUM(<compiled-expr>)` / `SUM(<bound col>)` with no GROUP BY,
3018    // no FILTER, no arg2, no ORDER BY, no DISTINCT, the whole
3019    // per-row work collapses to: eval the arg, match the Value
3020    // variant, accumulate. Skips the spec-dispatch loop +
3021    // `update_state` per-row name match. On a 25 k-row JOIN
3022    // (user_storage_usage `SUM(LENGTH(text_body))`) that's
3023    // ~50-100 ns/row of pure spec-dispatch overhead removed.
3024    if single_anon_group
3025        && agg_specs.len() == 1
3026        && agg_specs[0].filter.is_none()
3027        && agg_specs[0].arg2.is_none()
3028        && agg_specs[0].order_by.is_empty()
3029        && !agg_specs[0].distinct
3030        && (agg_specs[0].name == "sum" || agg_specs[0].name == "avg")
3031        && (arg_pos[0].is_some() || arg_compiled[0].is_some())
3032    {
3033        let arg_pos0 = arg_pos[0];
3034        let arg_c0 = &arg_compiled[0];
3035        // v7.39 (round 665) — was fifteen loose locals mirroring
3036        // `NumAcc` field for field; `FusedAcc`'s doc comment even
3037        // said so. One struct now, folded by the one `acc_cell`.
3038        let mut na = NumAcc::default();
3039        // Borrow-aware fast inner: avoid the per-row clone when arg
3040        // is a bound column position.
3041        if let Some(p) = arg_pos0 {
3042            for row in rows.iter() {
3043                let v_ref = row.get(p).unwrap_or(&Value::Null);
3044                acc_cell(&mut na, v_ref)?;
3045            }
3046        } else if let Some(p) = arg_c0.as_ref().and_then(|c| c.as_single_column_length()) {
3047            // v7.36 (perf — mailrs Phase 1, user_storage_usage hot
3048            // inner) — `SUM(LENGTH(<text col>))` collapses to a
3049            // straight scan: read the cell by ref, branch on the
3050            // variant, do an ASCII probe + `len()` (or
3051            // `chars().count()` on non-ASCII), accumulate. No Step
3052            // VM, no stack push/pop, no `BigInt` boxing on the way
3053            // out — pure i64 sum. The original Step VM path keeps
3054            // running for everything outside this shape (`SUM(col)`,
3055            // `SUM(expr)`, multi-step compiled args).
3056            for row in rows.iter() {
3057                let Some(v_ref) = row.get(p) else {
3058                    continue;
3059                };
3060                let n = match v_ref {
3061                    Value::Null => continue,
3062                    Value::Text(s) => {
3063                        if s.is_ascii() {
3064                            s.len() as i64
3065                        } else {
3066                            s.chars().count() as i64
3067                        }
3068                    }
3069                    other => {
3070                        return Err(EvalError::TypeMismatch {
3071                            detail: format!(
3072                                "length() needs text, got {}",
3073                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3074                            ),
3075                        });
3076                    }
3077                };
3078                na.sum_int += n;
3079                na.count += 1;
3080            }
3081        } else {
3082            let c = arg_c0.as_ref().unwrap();
3083            for row in rows.iter() {
3084                let v = eval::eval_compiled_ref(c, row, &ctx, &mut eval_stack)?;
3085                acc_cell(&mut na, &v)?;
3086            }
3087        }
3088        let state = &mut order[0].1[0];
3089        state.num = na;
3090        return Ok(order);
3091    }
3092    // v7.37.x (mailrs Track A 100k attack) — tight inlined loop for
3093    // the "single-Text GROUP BY + single MAX(bound numeric arg)"
3094    // shape. See `dedicated_max_loop` above for the gate. Returns
3095    // straight to the caller; the rest of the function (single-anon,
3096    // bound-fast, eval-slow paths) is skipped.
3097    if dedicated_max_loop && !single_anon_group {
3098        let gpos = group_pos[0].expect("dedicated_max_loop gates on Some");
3099        let apos = arg_pos[0].expect("dedicated_max_loop gates on Some");
3100        for row in rows.iter() {
3101            let kv = row.get(gpos).unwrap_or(&Value::Null);
3102            let idx = match kv {
3103                Value::Text(s) => match groups_text.get(s.as_ref()) {
3104                    Some(&i) => i,
3105                    None => {
3106                        let i = order.len();
3107                        order.push((
3108                            alloc::vec![Value::text(s.clone())],
3109                            alloc::vec![AggState::default()],
3110                        ));
3111                        groups_text.insert(s.to_string(), i);
3112                        i
3113                    }
3114                },
3115                Value::Null => match null_group_idx {
3116                    Some(i) => i,
3117                    None => {
3118                        let i = order.len();
3119                        order.push((alloc::vec![Value::Null], alloc::vec![AggState::default()]));
3120                        null_group_idx = Some(i);
3121                        i
3122                    }
3123                },
3124                _ => {
3125                    // Schema said Text but value isn't — fall back to
3126                    // the generic encoded path for correctness.
3127                    refs.clear();
3128                    refs.push(kv);
3129                    encode_key_refs_into_in(&refs, &mut keybuf_s, mysql_fold_groups);
3130                    match groups.get(keybuf_s.as_str()) {
3131                        Some(&i) => i,
3132                        None => {
3133                            let i = order.len();
3134                            order.push((
3135                                alloc::vec![kv.clone().into_owned()],
3136                                alloc::vec![AggState::default()],
3137                            ));
3138                            groups.insert(keybuf_s.clone(), i);
3139                            i
3140                        }
3141                    }
3142                }
3143            };
3144            // Inline MAX accumulator — skip the union-typed
3145            // `update_state` enum jump and per-spec arg dispatch.
3146            let av = row.get(apos).unwrap_or(&Value::Null);
3147            if !matches!(av, Value::Null) {
3148                let st = &mut order[idx].1[0];
3149                let upd = match &st.extreme {
3150                    None => true,
3151                    Some(prev) => {
3152                        extreme_cmp_in(
3153                            agg_specs[0].enum_labels.as_deref(),
3154                            agg_specs[0].arg_collation.as_deref(),
3155                            av,
3156                            prev,
3157                            ctx.mysql_dialect,
3158                        ) == core::cmp::Ordering::Greater
3159                    }
3160                };
3161                if upd {
3162                    st.extreme = Some(av.clone().into_owned());
3163                }
3164            }
3165        }
3166        return Ok(order);
3167    }
3168
3169    for row in rows.iter() {
3170        // v7.37.4 (L1 CSE) — reset per-row cache for shared compiled
3171        // aggregate-arg evals. No-op when no dedupe (empty vec).
3172        for slot in row_eval_cache.iter_mut() {
3173            *slot = None;
3174        }
3175        if single_anon_group {
3176            let entry = &mut order[0];
3177            let mat: Option<Cow<'_, Row>> = if needs_mat { Some(row.as_row()) } else { None };
3178            for (i, spec) in agg_specs.iter().enumerate() {
3179                if let Some(f) = &spec.filter
3180                    && !matches!(
3181                        eval_arg(f, mat.as_deref().expect("needs_mat for FILTER"), &ctx)?,
3182                        Value::Bool(true)
3183                    )
3184                {
3185                    continue;
3186                }
3187                let arg_owned: Value;
3188                let arg_ref: &Value = match (&arg_pos[i], arg_slot[i], &spec.arg) {
3189                    (Some(p), _, _) => {
3190                        // v7.37.9 Phase 1A-ext counter — fast position-bound arg.
3191                        crate::bump_counter!(AGG_PER_ROW_FAST_POS);
3192                        row.get(*p).unwrap_or(&Value::Null)
3193                    }
3194                    (None, None, None) => {
3195                        // COUNT(*) sentinel
3196                        crate::bump_counter!(AGG_PER_ROW_COUNT_STAR_SENTINEL);
3197                        arg_owned = Value::Bool(true);
3198                        &arg_owned
3199                    }
3200                    (None, Some(s), _) => {
3201                        if row_eval_cache[s].is_none() {
3202                            // v7.37.9 Phase 1A-ext counter — Step-VM ran (cache miss).
3203                            crate::bump_counter!(AGG_PER_ROW_COMPILED_MISS);
3204                            let c = arg_compiled[arg_unique_idx[s]]
3205                                .as_ref()
3206                                .expect("arg_unique_idx points at a compiled spec");
3207                            let v = eval::eval_compiled_ref(c, row, &ctx, &mut eval_stack)?;
3208                            row_eval_cache[s] = Some(v);
3209                        } else {
3210                            // v7.37.9 Phase 1A-ext counter — CSE cache hit
3211                            // (compiled arg deduped across specs in same row).
3212                            crate::bump_counter!(AGG_PER_ROW_COMPILED_HIT);
3213                        }
3214                        row_eval_cache[s].as_ref().expect("just filled above")
3215                    }
3216                    (None, None, Some(e)) => {
3217                        // v7.37.9 Phase 1A-ext counter — eval_expr fallback
3218                        // (uncompilable spec — Cow row materialise per row).
3219                        crate::bump_counter!(AGG_PER_ROW_EVAL_FALLBACK);
3220                        arg_owned = eval_arg(
3221                            e,
3222                            mat.as_deref().expect("needs_mat for non-bound arg"),
3223                            &ctx,
3224                        )?;
3225                        &arg_owned
3226                    }
3227                };
3228                let arg2_val = match (&spec.arg2, &arg2_literal_val[i]) {
3229                    (None, _) => None,
3230                    // v7.37.43 (DISTA A-3) — literal arg2: clone the
3231                    // precomputed value, skip per-row eval & row mat.
3232                    (Some(_), Some(lit)) => {
3233                        // v7.37.9 Phase 0 diagnostic — count per-row
3234                        // hits of the DISTA A-3 fast path.
3235                        crate::bump_counter!(DISTA_LITERAL_ARG2_CACHE_FIRE);
3236                        Some(lit.clone())
3237                    }
3238                    (Some(e), None) => Some(eval_arg(
3239                        e,
3240                        mat.as_deref().expect("needs_mat for arg2"),
3241                        &ctx,
3242                    )?),
3243                };
3244                let order_keys: Option<Vec<Value<'static>>> = if spec.order_by.is_empty() {
3245                    None
3246                } else {
3247                    crate::bump_counter!(AGGREGATE_ARRAY_AGG_ORDER_BY_FIRE);
3248                    let mut keys: Vec<Value<'static>> = Vec::with_capacity(spec.order_by.len());
3249                    for (k, o) in spec.order_by.iter().enumerate() {
3250                        let v: Value<'static> = if let Some(p) = order_pos[i][k] {
3251                            row.get(p)
3252                                .cloned()
3253                                .map(Value::into_owned)
3254                                .unwrap_or(Value::Null)
3255                        } else {
3256                            eval_arg(
3257                                &o.expr,
3258                                mat.as_deref().expect("needs_mat for ORDER key"),
3259                                &ctx,
3260                            )?
3261                        };
3262                        keys.push(v);
3263                    }
3264                    Some(keys)
3265                };
3266                // v7.36 (perf — bugfix v7.36.1 candidate) — first_ordered
3267                // was missing from the single_anon_group fast path,
3268                // sending `(array_agg(x ORDER BY y))[1]` values into
3269                // `update_state(array_agg, …)` whose finalize ignored
3270                // the absent `first_best` and returned `[]`. The slow
3271                // path below has the same branch — keep them aligned.
3272                if spec.first_ordered {
3273                    if let Some(keys) = order_keys {
3274                        let st = &mut entry.1[i];
3275                        let better = match &st.first_best {
3276                            None => true,
3277                            Some((bk, _)) => {
3278                                cmp_order_keys(
3279                                    &spec.order_by,
3280                                    &spec.order_enum_labels,
3281                                    &spec.order_collations,
3282                                    &keys,
3283                                    bk,
3284                                    ctx.mysql_dialect,
3285                                ) == core::cmp::Ordering::Less
3286                            }
3287                        };
3288                        if better {
3289                            st.first_best = Some((keys, arg_ref.clone().into_owned()));
3290                        }
3291                    }
3292                    continue;
3293                }
3294                if spec.distinct {
3295                    // v7.37.x (mailrs Track A 100k distinct_aggs attack)
3296                    // — single-Text DISTINCT fast path. Within a single
3297                    // distinct spec all input values come from one
3298                    // expression and share one type, so the encode-
3299                    // prefix (`S<text>|`) is redundant: the column
3300                    // text alone is collision-free within this spec's
3301                    // `seen` set. Skips encode_one + 2-walk
3302                    // contains+insert; only Text arms apply, others
3303                    // ride the encoded path unchanged.
3304                    //
3305                    // v7.37.x (docker-fair DISTA attack) — extend the
3306                    // single-family fast path to BigInt via a parallel
3307                    // `seen_int: Option<BTreeSet<i64>>`. The DISTA
3308                    // `COUNT(DISTINCT m.id)` shape pumps 25 k BigInt
3309                    // probes; skipping `encode_key_refs_into` saves
3310                    // ~100 ns of alloc + format churn per row.
3311                    if let Value::Text(s) = arg_ref {
3312                        // v7.39 (round 364, M4 P2) — a MySQL session folds
3313                        // the distinct key (case/accent) so `Foo`/`foo`
3314                        // count once. The `seen` set stays internally
3315                        // consistent: both probe and insert fold.
3316                        // v7.39 (round 370, M4 P4a) — but an explicit
3317                        // `COLLATE utf8mb4_bin` column de-dups byte-wise.
3318                        if distinct_fold[i] {
3319                            // v7.38.18 — and pad when the argument's
3320                            // collation says trailing spaces do not
3321                            // count. `utf8mb4_general_ci` folds AND
3322                            // pads; `utf8mb4_0900_ai_ci` only folds.
3323                            let base = if distinct_pads[i] {
3324                                s.trim_end_matches(' ')
3325                            } else {
3326                                s.as_ref()
3327                            };
3328                            let k = if distinct_fold_case[i] {
3329                                spg_storage::mysql_ci_fold(base)
3330                            } else {
3331                                alloc::string::ToString::to_string(base)
3332                            };
3333                            if entry.1[i].seen.contains(k.as_str()) {
3334                                continue;
3335                            }
3336                            entry.1[i].seen.insert(k);
3337                        } else {
3338                            if entry.1[i].seen.contains(s.as_ref()) {
3339                                continue;
3340                            }
3341                            entry.1[i].seen.insert(s.to_string());
3342                        }
3343                    } else if let Value::BigInt(n) = arg_ref {
3344                        let set = entry.1[i].seen_int.get_or_insert_with(BTreeSet::new);
3345                        if !set.insert(*n) {
3346                            continue;
3347                        }
3348                    } else if let Value::Int(n) = arg_ref {
3349                        let set = entry.1[i].seen_int.get_or_insert_with(BTreeSet::new);
3350                        if !set.insert(i64::from(*n)) {
3351                            continue;
3352                        }
3353                    } else {
3354                        encode_key_refs_into_in(
3355                            core::slice::from_ref(&arg_ref),
3356                            &mut dkeybuf,
3357                            distinct_fold[i],
3358                        );
3359                        if entry.1[i].seen.contains(dkeybuf.as_str()) {
3360                            continue;
3361                        }
3362                        entry.1[i].seen.insert(dkeybuf.clone());
3363                    }
3364                }
3365                // v7.37.x (mailrs Track A 100k attack) — inline the
3366                // common aggregate kinds (MAX / MIN / Count / CountStar
3367                // / BoolOr / BoolAnd) here instead of dispatching
3368                // through `update_state`'s enum jump + per-kind branch.
3369                // Skipping the function-call overhead saves ~20-30 ns
3370                // per spec per row at 100 k; the slow kinds keep the
3371                // dispatched call.
3372                match spec.kind {
3373                    AggKind::Max => {
3374                        if !matches!(arg_ref, Value::Null) {
3375                            // v7.39 (round 626) — the same deny list the
3376                            // dispatched path applies. These inlined copies
3377                            // exist for speed and are where `min(TRUE)`
3378                            // actually lands, so a guard placed only on the
3379                            // dispatched arm never fires.
3380                            if !ctx.mysql_dialect && min_max_unsupported_type(arg_ref) {
3381                                return Err(EvalError::TypeMismatch {
3382                                    detail: format!(
3383                                        "function max({}) does not exist",
3384                                        crate::conversions::pg_type_name_for_error_opt(
3385                                            arg_ref.data_type()
3386                                        )
3387                                    ),
3388                                });
3389                            }
3390                            let st = &mut entry.1[i];
3391                            let upd = match &st.extreme {
3392                                None => true,
3393                                Some(prev) => {
3394                                    extreme_cmp_in(
3395                                        spec.enum_labels.as_deref(),
3396                                        spec.arg_collation.as_deref(),
3397                                        arg_ref,
3398                                        prev,
3399                                        ctx.mysql_dialect,
3400                                    ) == core::cmp::Ordering::Greater
3401                                }
3402                            };
3403                            if upd {
3404                                st.extreme = Some(arg_ref.clone().into_owned());
3405                            }
3406                        }
3407                    }
3408                    AggKind::Min => {
3409                        if !matches!(arg_ref, Value::Null) {
3410                            // v7.39 (round 626) — see the Max arm above.
3411                            if !ctx.mysql_dialect && min_max_unsupported_type(arg_ref) {
3412                                return Err(EvalError::TypeMismatch {
3413                                    detail: format!(
3414                                        "function min({}) does not exist",
3415                                        crate::conversions::pg_type_name_for_error_opt(
3416                                            arg_ref.data_type()
3417                                        )
3418                                    ),
3419                                });
3420                            }
3421                            let st = &mut entry.1[i];
3422                            let upd = match &st.extreme {
3423                                None => true,
3424                                Some(prev) => {
3425                                    extreme_cmp_in(
3426                                        spec.enum_labels.as_deref(),
3427                                        spec.arg_collation.as_deref(),
3428                                        arg_ref,
3429                                        prev,
3430                                        ctx.mysql_dialect,
3431                                    ) == core::cmp::Ordering::Less
3432                                }
3433                            };
3434                            if upd {
3435                                st.extreme = Some(arg_ref.clone().into_owned());
3436                            }
3437                        }
3438                    }
3439                    AggKind::AnyValue => {
3440                        if !matches!(arg_ref, Value::Null) {
3441                            let st = &mut entry.1[i];
3442                            if st.extreme.is_none() {
3443                                st.extreme = Some(arg_ref.clone().into_owned());
3444                            }
3445                        }
3446                    }
3447                    AggKind::CountStar => {
3448                        entry.1[i].num.count += 1;
3449                    }
3450                    AggKind::Count => {
3451                        if !matches!(arg_ref, Value::Null) {
3452                            entry.1[i].num.count += 1;
3453                        }
3454                    }
3455                    AggKind::BoolOr => match arg_ref {
3456                        Value::Bool(b) => {
3457                            let st = &mut entry.1[i];
3458                            st.bool_acc = Some(st.bool_acc.unwrap_or(false) || *b);
3459                        }
3460                        Value::Null => {}
3461                        _ => update_state(
3462                            &mut entry.1[i],
3463                            spec.kind,
3464                            &spec.name,
3465                            arg_ref,
3466                            arg2_val.as_ref(),
3467                            order_keys,
3468                            spec.enum_labels.as_deref(),
3469                            spec.arg_collation.as_deref(),
3470                            ctx.mysql_dialect,
3471                        )?,
3472                    },
3473                    AggKind::BoolAnd => match arg_ref {
3474                        Value::Bool(b) => {
3475                            let st = &mut entry.1[i];
3476                            st.bool_acc = Some(st.bool_acc.unwrap_or(true) && *b);
3477                        }
3478                        Value::Null => {}
3479                        _ => update_state(
3480                            &mut entry.1[i],
3481                            spec.kind,
3482                            &spec.name,
3483                            arg_ref,
3484                            arg2_val.as_ref(),
3485                            order_keys,
3486                            spec.enum_labels.as_deref(),
3487                            spec.arg_collation.as_deref(),
3488                            ctx.mysql_dialect,
3489                        )?,
3490                    },
3491                    _ => {
3492                        update_state(
3493                            &mut entry.1[i],
3494                            spec.kind,
3495                            &spec.name,
3496                            arg_ref,
3497                            arg2_val.as_ref(),
3498                            order_keys,
3499                            spec.enum_labels.as_deref(),
3500                            spec.arg_collation.as_deref(),
3501                            ctx.mysql_dialect,
3502                        )?;
3503                    }
3504                }
3505            }
3506            continue;
3507        }
3508        // Fast key: bound positions + no ci folding -> encode
3509        // straight from borrowed cells; group_vals materialise
3510        // only when the group is NEW.
3511        if all_groups_bound && ci_positions.is_empty() {
3512            // v7.37.x — single-Text fast path uses the raw text as the
3513            // map key (no encode_one's `S<text>|` prefix/suffix push,
3514            // no refs Vec rebuild). NULL values land in a dedicated
3515            // slot so SQL's "all NULLs share one group" semantics hold.
3516            let idx = if single_text_group_col {
3517                let v = row.get(group_pos[0].unwrap()).unwrap_or(&Value::Null);
3518                match v {
3519                    Value::Text(s) => match groups_text.get(s.as_ref()) {
3520                        Some(&i) => i,
3521                        None => {
3522                            let i = order.len();
3523                            let init: Vec<AggState> =
3524                                (0..agg_specs.len()).map(|_| AggState::default()).collect();
3525                            order.push((alloc::vec![Value::text(s.clone())], init));
3526                            groups_text.insert(s.to_string(), i);
3527                            i
3528                        }
3529                    },
3530                    Value::Null => match null_group_idx {
3531                        Some(i) => i,
3532                        None => {
3533                            let i = order.len();
3534                            let init: Vec<AggState> =
3535                                (0..agg_specs.len()).map(|_| AggState::default()).collect();
3536                            order.push((alloc::vec![Value::Null], init));
3537                            null_group_idx = Some(i);
3538                            i
3539                        }
3540                    },
3541                    _ => {
3542                        // Schema says Text but value is something else
3543                        // (coercion edge case). Fall back to the encoded
3544                        // path for correctness — same logic as the
3545                        // non-single-Text branch below.
3546                        refs.clear();
3547                        refs.push(v);
3548                        encode_key_refs_into_in(&refs, &mut keybuf_s, mysql_fold_groups);
3549                        match groups.get(keybuf_s.as_str()) {
3550                            Some(&i) => i,
3551                            None => {
3552                                let i = order.len();
3553                                let init: Vec<AggState> =
3554                                    (0..agg_specs.len()).map(|_| AggState::default()).collect();
3555                                order.push((alloc::vec![v.clone().into_owned()], init));
3556                                groups.insert(keybuf_s.clone(), i);
3557                                i
3558                            }
3559                        }
3560                    }
3561                }
3562            } else if single_int_group_col {
3563                // v7.37.16 — raw-i64 keying (see single_int_group_col).
3564                let v = row.get(group_pos[0].unwrap()).unwrap_or(&Value::Null);
3565                let key: Option<i64> = match v {
3566                    Value::SmallInt(n) => Some(i64::from(*n)),
3567                    Value::Int(n) => Some(i64::from(*n)),
3568                    Value::BigInt(n) => Some(*n),
3569                    _ => None,
3570                };
3571                match (key, v) {
3572                    (Some(k), _) => match groups_int.get(&k) {
3573                        Some(&i) => i,
3574                        None => {
3575                            let i = order.len();
3576                            let init: Vec<AggState> =
3577                                (0..agg_specs.len()).map(|_| AggState::default()).collect();
3578                            order.push((alloc::vec![v.clone().into_owned()], init));
3579                            groups_int.insert(k, i);
3580                            i
3581                        }
3582                    },
3583                    (None, Value::Null) => match null_group_idx {
3584                        Some(i) => i,
3585                        None => {
3586                            let i = order.len();
3587                            let init: Vec<AggState> =
3588                                (0..agg_specs.len()).map(|_| AggState::default()).collect();
3589                            order.push((alloc::vec![Value::Null], init));
3590                            null_group_idx = Some(i);
3591                            i
3592                        }
3593                    },
3594                    (None, _) => {
3595                        // Non-integer cell under an integer schema
3596                        // (coercion edge) — encoded-path fallback.
3597                        refs.clear();
3598                        refs.push(v);
3599                        encode_key_refs_into_in(&refs, &mut keybuf_s, mysql_fold_groups);
3600                        match groups.get(keybuf_s.as_str()) {
3601                            Some(&i) => i,
3602                            None => {
3603                                let i = order.len();
3604                                let init: Vec<AggState> =
3605                                    (0..agg_specs.len()).map(|_| AggState::default()).collect();
3606                                order.push((alloc::vec![v.clone().into_owned()], init));
3607                                groups.insert(keybuf_s.clone(), i);
3608                                i
3609                            }
3610                        }
3611                    }
3612                }
3613            } else {
3614                refs.clear();
3615                refs.extend(
3616                    group_pos
3617                        .iter()
3618                        .map(|p| row.get(p.unwrap()).unwrap_or(&Value::Null)),
3619                );
3620                encode_key_refs_into_in(&refs, &mut keybuf_s, mysql_fold_groups);
3621                match groups.get(keybuf_s.as_str()) {
3622                    Some(&i) => i,
3623                    None => {
3624                        let i = order.len();
3625                        let init: Vec<AggState> =
3626                            (0..agg_specs.len()).map(|_| AggState::default()).collect();
3627                        let owned: Vec<Value<'static>> =
3628                            refs.iter().map(|v| (*v).clone().into_owned()).collect();
3629                        order.push((owned, init));
3630                        groups.insert(keybuf_s.clone(), i);
3631                        i
3632                    }
3633                }
3634            };
3635            let entry = &mut order[idx];
3636            // v7.33 (array_agg perf) — materialise the combined row AT
3637            // MOST once per input row, and only when a spec actually
3638            // needs the eval path (FILTER / non-bound arg / arg2 / non-
3639            // bound ORDER key). Bound args and bound ORDER keys read
3640            // cells by reference below, so the inbox shape (all bound)
3641            // never materialises — killing the per-row ~1 KB clone that
3642            // dominated the ordered-aggregate cost.
3643            let mat: Option<Cow<'_, Row>> = if needs_mat { Some(row.as_row()) } else { None };
3644            for (i, spec) in agg_specs.iter().enumerate() {
3645                // v7.32 (round-29) — FILTER (WHERE cond): exclude rows
3646                // where cond is not TRUE before they reach this
3647                // aggregate's accumulator (and before DISTINCT dedup).
3648                if let Some(f) = &spec.filter
3649                    && !matches!(
3650                        eval_arg(f, mat.as_deref().expect("needs_mat for FILTER"), &ctx)?,
3651                        Value::Bool(true)
3652                    )
3653                {
3654                    continue;
3655                }
3656                let arg_owned: Value;
3657                let arg_ref: &Value = match (&arg_pos[i], arg_slot[i], &spec.arg) {
3658                    (Some(p), _, _) => {
3659                        crate::bump_counter!(AGG_PER_ROW_FAST_POS);
3660                        row.get(*p).unwrap_or(&Value::Null)
3661                    }
3662                    (None, None, None) => {
3663                        crate::bump_counter!(AGG_PER_ROW_COUNT_STAR_SENTINEL);
3664                        arg_owned = Value::Bool(true);
3665                        &arg_owned
3666                    }
3667                    (None, Some(s), _) => {
3668                        // v7.37.4 (L1 CSE) — shared compiled-arg slot.
3669                        // First spec that needs slot `s` this row pays
3670                        // the Step-VM eval; siblings reading the same
3671                        // slot get the cached Value for free. Preserves
3672                        // FILTER semantics: a spec filtered out above
3673                        // never reaches here, so its arg stays unevaled.
3674                        if row_eval_cache[s].is_none() {
3675                            crate::bump_counter!(AGG_PER_ROW_COMPILED_MISS);
3676                            let c = arg_compiled[arg_unique_idx[s]]
3677                                .as_ref()
3678                                .expect("arg_unique_idx points at a compiled spec");
3679                            let v = eval::eval_compiled_ref(c, row, &ctx, &mut eval_stack)?;
3680                            row_eval_cache[s] = Some(v);
3681                        } else {
3682                            crate::bump_counter!(AGG_PER_ROW_COMPILED_HIT);
3683                        }
3684                        row_eval_cache[s].as_ref().expect("just filled above")
3685                    }
3686                    (None, None, Some(e)) => {
3687                        crate::bump_counter!(AGG_PER_ROW_EVAL_FALLBACK);
3688                        arg_owned = eval_arg(
3689                            e,
3690                            mat.as_deref().expect("needs_mat for non-bound arg"),
3691                            &ctx,
3692                        )?;
3693                        &arg_owned
3694                    }
3695                };
3696                let arg2_val = match (&spec.arg2, &arg2_literal_val[i]) {
3697                    (None, _) => None,
3698                    // v7.37.43 (DISTA A-3) — literal arg2: clone the
3699                    // precomputed value, skip per-row eval & row mat.
3700                    (Some(_), Some(lit)) => {
3701                        // v7.37.9 Phase 0 diagnostic — count per-row
3702                        // hits of the DISTA A-3 fast path.
3703                        crate::bump_counter!(DISTA_LITERAL_ARG2_CACHE_FIRE);
3704                        Some(lit.clone())
3705                    }
3706                    (Some(e), None) => Some(eval_arg(
3707                        e,
3708                        mat.as_deref().expect("needs_mat for arg2"),
3709                        &ctx,
3710                    )?),
3711                };
3712                let order_keys: Option<Vec<Value<'static>>> = if spec.order_by.is_empty() {
3713                    None
3714                } else {
3715                    crate::bump_counter!(AGGREGATE_ARRAY_AGG_ORDER_BY_FIRE);
3716                    let mut keys: Vec<Value<'static>> = Vec::with_capacity(spec.order_by.len());
3717                    for (k, o) in spec.order_by.iter().enumerate() {
3718                        // Bound ORDER key → read the cell by reference; only
3719                        // a non-bound key falls to the materialised eval path.
3720                        keys.push(match order_pos[i][k] {
3721                            Some(p) => row
3722                                .get(p)
3723                                .cloned()
3724                                .map(Value::into_owned)
3725                                .unwrap_or(Value::Null),
3726                            None => eval_arg(
3727                                &o.expr,
3728                                mat.as_deref().expect("needs_mat for non-bound ORDER key"),
3729                                &ctx,
3730                            )?,
3731                        });
3732                    }
3733                    Some(keys)
3734                };
3735                // v7.33 (array_agg argmax) — first_ordered: keep only the
3736                // running first-by-order element (strict-less replacement
3737                // = ties keep the earliest row, matching the stable-sort
3738                // `[1]`), no array build.
3739                if spec.first_ordered {
3740                    if let Some(keys) = order_keys {
3741                        let st = &mut entry.1[i];
3742                        let better = match &st.first_best {
3743                            None => true,
3744                            Some((bk, _)) => {
3745                                cmp_order_keys(
3746                                    &spec.order_by,
3747                                    &spec.order_enum_labels,
3748                                    &spec.order_collations,
3749                                    &keys,
3750                                    bk,
3751                                    ctx.mysql_dialect,
3752                                ) == core::cmp::Ordering::Less
3753                            }
3754                        };
3755                        if better {
3756                            st.first_best = Some((keys, arg_ref.clone().into_owned()));
3757                        }
3758                    }
3759                    continue;
3760                }
3761                if spec.distinct {
3762                    // v7.37.x — single-Text DISTINCT fast path (see
3763                    // bound fast path counterpart above). Per-spec
3764                    // type invariance lets us use the column text as
3765                    // the `seen` key directly, no `S<text>|` prefix.
3766                    // v7.37.x (docker-fair DISTA) — BigInt parallel
3767                    // path skips encode_key_refs_into entirely.
3768                    if let Value::Text(s) = arg_ref {
3769                        if entry.1[i].seen.contains(s.as_ref()) {
3770                            continue;
3771                        }
3772                        entry.1[i].seen.insert(s.to_string());
3773                    } else if let Value::BigInt(n) = arg_ref {
3774                        let set = entry.1[i].seen_int.get_or_insert_with(BTreeSet::new);
3775                        if !set.insert(*n) {
3776                            continue;
3777                        }
3778                    } else if let Value::Int(n) = arg_ref {
3779                        let set = entry.1[i].seen_int.get_or_insert_with(BTreeSet::new);
3780                        if !set.insert(i64::from(*n)) {
3781                            continue;
3782                        }
3783                    } else {
3784                        encode_key_refs_into_in(
3785                            core::slice::from_ref(&arg_ref),
3786                            &mut dkeybuf,
3787                            distinct_fold[i],
3788                        );
3789                        if entry.1[i].seen.contains(dkeybuf.as_str()) {
3790                            continue;
3791                        }
3792                        entry.1[i].seen.insert(dkeybuf.clone());
3793                    }
3794                }
3795                // v7.37.x (mailrs Track A 100k attack) — inline the
3796                // common aggregate kinds (MAX / MIN / Count / CountStar
3797                // / BoolOr / BoolAnd) here instead of dispatching
3798                // through `update_state`'s enum jump + per-kind branch.
3799                // Skipping the function-call overhead saves ~20-30 ns
3800                // per spec per row at 100 k; the slow kinds keep the
3801                // dispatched call.
3802                match spec.kind {
3803                    AggKind::Max => {
3804                        if !matches!(arg_ref, Value::Null) {
3805                            // v7.39 (round 626) — the same deny list the
3806                            // dispatched path applies. These inlined copies
3807                            // exist for speed and are where `min(TRUE)`
3808                            // actually lands, so a guard placed only on the
3809                            // dispatched arm never fires.
3810                            if !ctx.mysql_dialect && min_max_unsupported_type(arg_ref) {
3811                                return Err(EvalError::TypeMismatch {
3812                                    detail: format!(
3813                                        "function max({}) does not exist",
3814                                        crate::conversions::pg_type_name_for_error_opt(
3815                                            arg_ref.data_type()
3816                                        )
3817                                    ),
3818                                });
3819                            }
3820                            let st = &mut entry.1[i];
3821                            let upd = match &st.extreme {
3822                                None => true,
3823                                Some(prev) => {
3824                                    extreme_cmp_in(
3825                                        spec.enum_labels.as_deref(),
3826                                        spec.arg_collation.as_deref(),
3827                                        arg_ref,
3828                                        prev,
3829                                        ctx.mysql_dialect,
3830                                    ) == core::cmp::Ordering::Greater
3831                                }
3832                            };
3833                            if upd {
3834                                st.extreme = Some(arg_ref.clone().into_owned());
3835                            }
3836                        }
3837                    }
3838                    AggKind::Min => {
3839                        if !matches!(arg_ref, Value::Null) {
3840                            // v7.39 (round 626) — see the Max arm above.
3841                            if !ctx.mysql_dialect && min_max_unsupported_type(arg_ref) {
3842                                return Err(EvalError::TypeMismatch {
3843                                    detail: format!(
3844                                        "function min({}) does not exist",
3845                                        crate::conversions::pg_type_name_for_error_opt(
3846                                            arg_ref.data_type()
3847                                        )
3848                                    ),
3849                                });
3850                            }
3851                            let st = &mut entry.1[i];
3852                            let upd = match &st.extreme {
3853                                None => true,
3854                                Some(prev) => {
3855                                    extreme_cmp_in(
3856                                        spec.enum_labels.as_deref(),
3857                                        spec.arg_collation.as_deref(),
3858                                        arg_ref,
3859                                        prev,
3860                                        ctx.mysql_dialect,
3861                                    ) == core::cmp::Ordering::Less
3862                                }
3863                            };
3864                            if upd {
3865                                st.extreme = Some(arg_ref.clone().into_owned());
3866                            }
3867                        }
3868                    }
3869                    AggKind::AnyValue => {
3870                        if !matches!(arg_ref, Value::Null) {
3871                            let st = &mut entry.1[i];
3872                            if st.extreme.is_none() {
3873                                st.extreme = Some(arg_ref.clone().into_owned());
3874                            }
3875                        }
3876                    }
3877                    AggKind::CountStar => {
3878                        entry.1[i].num.count += 1;
3879                    }
3880                    AggKind::Count => {
3881                        if !matches!(arg_ref, Value::Null) {
3882                            entry.1[i].num.count += 1;
3883                        }
3884                    }
3885                    AggKind::BoolOr => match arg_ref {
3886                        Value::Bool(b) => {
3887                            let st = &mut entry.1[i];
3888                            st.bool_acc = Some(st.bool_acc.unwrap_or(false) || *b);
3889                        }
3890                        Value::Null => {}
3891                        _ => update_state(
3892                            &mut entry.1[i],
3893                            spec.kind,
3894                            &spec.name,
3895                            arg_ref,
3896                            arg2_val.as_ref(),
3897                            order_keys,
3898                            spec.enum_labels.as_deref(),
3899                            spec.arg_collation.as_deref(),
3900                            ctx.mysql_dialect,
3901                        )?,
3902                    },
3903                    AggKind::BoolAnd => match arg_ref {
3904                        Value::Bool(b) => {
3905                            let st = &mut entry.1[i];
3906                            st.bool_acc = Some(st.bool_acc.unwrap_or(true) && *b);
3907                        }
3908                        Value::Null => {}
3909                        _ => update_state(
3910                            &mut entry.1[i],
3911                            spec.kind,
3912                            &spec.name,
3913                            arg_ref,
3914                            arg2_val.as_ref(),
3915                            order_keys,
3916                            spec.enum_labels.as_deref(),
3917                            spec.arg_collation.as_deref(),
3918                            ctx.mysql_dialect,
3919                        )?,
3920                    },
3921                    _ => {
3922                        update_state(
3923                            &mut entry.1[i],
3924                            spec.kind,
3925                            &spec.name,
3926                            arg_ref,
3927                            arg2_val.as_ref(),
3928                            order_keys,
3929                            spec.enum_labels.as_deref(),
3930                            spec.arg_collation.as_deref(),
3931                            ctx.mysql_dialect,
3932                        )?;
3933                    }
3934                }
3935            }
3936            continue;
3937        }
3938        // v7.32 (P4 increment 2) — eval (non-bound) path: present the
3939        // row as a borrowed Row once (Owned → zero-cost borrow; a join
3940        // tuple materialises here exactly once, never on the bound fast
3941        // path above), then the original eval loop runs unchanged.
3942        let row_materialised = row.as_row();
3943        let row: &Row<'static> = &row_materialised;
3944        let group_vals: Vec<Value<'static>> = group_exprs
3945            .iter()
3946            .map(|g| eval::eval_expr(g, row, &ctx))
3947            .collect::<Result<_, _>>()?;
3948        // v7.17.0 Phase 2.5b — case-insensitive group keying: fold
3949        // only the ci columns, and only when any exist. Display
3950        // value (`group_vals`) stays original — only the key folds.
3951        let key = if ci_positions.is_empty() {
3952            encode_key(&group_vals)
3953        } else {
3954            let mut key_vals = group_vals.clone();
3955            for &i in &ci_positions {
3956                if let Value::Text(s) = &key_vals[i] {
3957                    // v7.39 (round 370, M4 P4a) — a MySQL folding column
3958                    // (stored CaseInsensitive) folds case AND accent; a PG
3959                    // CITEXT column stays ASCII-only.
3960                    key_vals[i] = Value::text(if ctx.mysql_dialect {
3961                        spg_storage::mysql_compare_fold(s)
3962                    } else {
3963                        s.to_ascii_lowercase()
3964                    });
3965                }
3966            }
3967            encode_key(&key_vals)
3968        };
3969        // Probe by index; the map owns the key once on vacant insert.
3970        let idx = match groups.get(key.as_str()) {
3971            Some(&i) => i,
3972            None => {
3973                let i = order.len();
3974                let init: Vec<AggState> =
3975                    (0..agg_specs.len()).map(|_| AggState::default()).collect();
3976                order.push((group_vals.clone(), init));
3977                groups.insert(key, i);
3978                i
3979            }
3980        };
3981        let entry = &mut order[idx];
3982        for (i, spec) in agg_specs.iter().enumerate() {
3983            // v7.32 (round-29) — FILTER (WHERE cond): exclude rows where
3984            // cond is not TRUE before accumulation (and before DISTINCT).
3985            if let Some(f) = &spec.filter
3986                && !matches!(eval_arg(f, row, &ctx)?, Value::Bool(true))
3987            {
3988                continue;
3989            }
3990            let arg_val = match &spec.arg {
3991                None => Value::Bool(true), // count_star: sentinel non-null
3992                Some(e) => eval_arg(e, row, &ctx)?,
3993            };
3994            // v7.17.0 — `string_agg(value, separator)` evaluates the
3995            // separator per row. v7.39 (round 762, F31-C2) — PG uses
3996            // the PER-ROW value (element i prefixed by row i's
3997            // separator, PG18-measured `a<b>b<c>c`); update_state
3998            // records it alongside the item now (the old note claimed
3999            // PG "treats it as constant" — measured false).
4000            let arg2_val = match &spec.arg2 {
4001                None => None,
4002                Some(e) => Some(eval_arg(e, row, &ctx)?),
4003            };
4004            // v7.24 (round-16 A) — aggregate-internal ORDER BY:
4005            // evaluate the key tuple against the source row.
4006            let order_keys: Option<Vec<Value<'static>>> = if spec.order_by.is_empty() {
4007                None
4008            } else {
4009                let mut keys: Vec<Value<'static>> = Vec::with_capacity(spec.order_by.len());
4010                for o in &spec.order_by {
4011                    keys.push(eval_arg(&o.expr, row, &ctx)?);
4012                }
4013                Some(keys)
4014            };
4015            // v7.33 (array_agg argmax) — first_ordered: keep the running
4016            // first-by-order element only (mirrors the bound fast path).
4017            if spec.first_ordered {
4018                if let Some(keys) = order_keys {
4019                    let st = &mut entry.1[i];
4020                    let better = match &st.first_best {
4021                        None => true,
4022                        Some((bk, _)) => {
4023                            cmp_order_keys(
4024                                &spec.order_by,
4025                                &spec.order_enum_labels,
4026                                &spec.order_collations,
4027                                &keys,
4028                                bk,
4029                                ctx.mysql_dialect,
4030                            ) == core::cmp::Ordering::Less
4031                        }
4032                    };
4033                    if better {
4034                        st.first_best = Some((keys, arg_val.clone().into_owned()));
4035                    }
4036                }
4037                continue;
4038            }
4039            // v7.25 (round-17) — DISTINCT: drop repeated inputs
4040            // before they reach the accumulator. NULLs flow through
4041            // (each aggregate's own NULL rule applies; PG also
4042            // treats NULL as a single distinct value for array_agg).
4043            // v7.37.x — single-Text fast path same shape as the
4044            // bound/slow paths above.
4045            if spec.distinct {
4046                // v7.37.x (docker-fair DISTA) — single-family fast
4047                // paths skip encode_key for Text/BigInt/Int.
4048                let inserted = match &arg_val {
4049                    Value::Text(s) => entry.1[i].seen.insert(s.to_string()),
4050                    Value::BigInt(n) => entry.1[i]
4051                        .seen_int
4052                        .get_or_insert_with(BTreeSet::new)
4053                        .insert(*n),
4054                    Value::Int(n) => entry.1[i]
4055                        .seen_int
4056                        .get_or_insert_with(BTreeSet::new)
4057                        .insert(i64::from(*n)),
4058                    _ => {
4059                        let key = encode_key(core::slice::from_ref(&arg_val));
4060                        entry.1[i].seen.insert(key)
4061                    }
4062                };
4063                if !inserted {
4064                    continue;
4065                }
4066            }
4067            update_state(
4068                &mut entry.1[i],
4069                spec.kind,
4070                &spec.name,
4071                &arg_val,
4072                arg2_val.as_ref(),
4073                order_keys,
4074                spec.enum_labels.as_deref(),
4075                spec.arg_collation.as_deref(),
4076                ctx.mysql_dialect,
4077            )?;
4078        }
4079    }
4080    Ok(order)
4081}
4082
4083/// (2a) Build the synthetic per-group schema: `__grp_0..K` then
4084/// `__agg_0..N`. Group types are probed from the first row; aggregate
4085/// types from each spec.
4086fn build_synth_schema(
4087    rows: AggRows<'_>,
4088    group_exprs: &[Expr],
4089    agg_specs: &[AggSpec],
4090    schema_cols: &[ColumnSchema],
4091    table_alias: Option<&str>,
4092    catalog: Option<&spg_storage::Catalog>,
4093    engine: Option<&crate::Engine>,
4094) -> Result<Vec<ColumnSchema>, EvalError> {
4095    let ctx = with_catalog(EvalContext::new(schema_cols, table_alias), catalog, engine);
4096    // Build synthetic schema: __grp_0..K then __agg_0..N.
4097    let group_types: Vec<DataType> = if rows.is_empty() {
4098        // Use Text as a safe stand-in — empty result means schema isn't
4099        // observable. Avoids needing to evaluate group exprs on no row.
4100        group_exprs.iter().map(|_| DataType::Text).collect()
4101    } else {
4102        let probe = rows.get(0).expect("non-empty checked above");
4103        let probe_row = probe.as_row();
4104        let probe: &Row<'static> = &probe_row;
4105        group_exprs
4106            .iter()
4107            .map(|g| {
4108                eval::eval_expr(g, probe, &ctx).map(|v| v.data_type().unwrap_or(DataType::Text))
4109            })
4110            .collect::<Result<_, _>>()?
4111    };
4112    let agg_types: Vec<DataType> = agg_specs
4113        .iter()
4114        .map(|spec| infer_agg_type(spec, schema_cols))
4115        .collect();
4116    let mut synth_schema: Vec<ColumnSchema> = Vec::new();
4117    for (i, ty) in group_types.iter().enumerate() {
4118        let mut col = ColumnSchema::new(format!("__grp_{i}"), *ty, true);
4119        // v7.39 (enum order knife) — a bare enum-column group key keeps
4120        // its enum identity so HAVING comparisons and the grouped-output
4121        // ORDER BY sort by member order downstream.
4122        if let Some(Expr::Column(c)) = group_exprs.get(i) {
4123            let src = schema_cols.iter().find(|sc| sc.name == c.name);
4124            col.user_enum_type = src.and_then(|sc| sc.user_enum_type.clone());
4125            // v7.39 (round 686) — and its collation, for the same reason and
4126            // by the same route. A `__grp_j` column is where a GROUP BY key
4127            // lives from here on, so anything the downstream ORDER BY needs
4128            // about the original column has to travel with it. Without this
4129            // the resolver looks the key up in the synthetic schema, finds
4130            // `__grp_0` with no collation, and the group-by ordering silently
4131            // stays byte-wise.
4132            col.collation_name = src.and_then(|sc| sc.collation_name.clone());
4133            // v7.38.14 — and the collation ENUM, which is a different field
4134            // and the one every MySQL text comparison actually reads. The
4135            // note above carried the NAME and stopped, exactly as round 688
4136            // did in `join.rs::build_combined_schema`; both left the enum
4137            // behind, and `ColumnSchema::new` defaults it to `Binary`, which
4138            // downstream reads as "byte-wise ON PURPOSE" rather than as
4139            // "unknown". So a `__grp_j` column claimed to be an explicit
4140            // binary column and `SELECT DISTINCT ... GROUP BY` stopped
4141            // folding. Sixth field through this hole, second site with the
4142            // identical shape.
4143            if let Some(sc) = src {
4144                col.collation = sc.collation;
4145            }
4146        }
4147        synth_schema.push(col);
4148    }
4149    for (i, ty) in agg_types.iter().enumerate() {
4150        synth_schema.push(ColumnSchema::new(format!("__agg_{i}"), *ty, true));
4151    }
4152    Ok(synth_schema)
4153}
4154
4155/// (2b) Materialise one synthetic row per group (insertion order):
4156/// apply each aggregate's internal ORDER BY, then finalise the running
4157/// state into the group + aggregate cells.
4158/// v7.33 — compare two aggregate-internal ORDER BY key tuples under the
4159/// per-key DESC / NULLS directives. This is the exact comparator the
4160/// finalize sort uses, factored out so the `first_ordered` argmax
4161/// accumulator's "keep first" decision is provably identical to taking
4162/// element `[1]` of the fully-sorted array.
4163fn cmp_order_keys(
4164    order_by: &[spg_sql::ast::OrderBy],
4165    order_enum_labels: &[Option<Vec<String>>],
4166    order_collations: &[Option<alloc::string::String>],
4167    a: &[Value<'static>],
4168    b: &[Value<'static>],
4169    mysql: bool,
4170) -> core::cmp::Ordering {
4171    for (k, o) in order_by.iter().enumerate() {
4172        // v7.39 (enum order knife) — an enum-typed sort key compares by
4173        // member order; NULLs and non-members keep the generic path.
4174        if let Some(Some(labels)) = order_enum_labels.get(k)
4175            && !matches!(&a[k], Value::Null)
4176            && !matches!(&b[k], Value::Null)
4177            && let Some(ord) = crate::eval::enum_ord_cmp(labels, &a[k], &b[k])
4178        {
4179            let ord = if o.desc { ord.reverse() } else { ord };
4180            if ord != core::cmp::Ordering::Equal {
4181                return ord;
4182            }
4183            continue;
4184        }
4185        // v7.37 (M4 P2) — `ORDER BY BINARY x` forces byte-wise sorting
4186        // even under the folding MySQL dialect, so a per-key BINARY
4187        // coercion turns folding back off for that key alone.
4188        let fold = mysql && !crate::eval::is_binary_coerced(&o.expr);
4189        // v7.38.18 — the key's declared collation, so the sort inside an
4190        // aggregate orders a collated column the way the statement's own
4191        // ORDER BY orders it.
4192        let coll = order_collations.get(k).and_then(Option::as_deref);
4193        let cmp = crate::orderby::order_by_value_cmp_coll(
4194            o.desc,
4195            o.nulls_first,
4196            &a[k],
4197            &b[k],
4198            fold,
4199            coll,
4200        );
4201        if cmp != core::cmp::Ordering::Equal {
4202            return cmp;
4203        }
4204    }
4205    core::cmp::Ordering::Equal
4206}
4207
4208#[allow(clippy::too_many_arguments)]
4209fn finalize_synth_rows(
4210    order: &[(Vec<Value<'static>>, Vec<AggState>)],
4211    agg_specs: &[AggSpec],
4212    synth_schema: &[ColumnSchema],
4213    rows: AggRows<'_>,
4214    schema_cols: &[ColumnSchema],
4215    table_alias: Option<&str>,
4216    catalog: Option<&spg_storage::Catalog>,
4217    engine: Option<&crate::Engine>,
4218    runner: Option<&dyn crate::ParallelRunner>,
4219) -> Result<Vec<Row<'static>>, EvalError> {
4220    let ctx = with_catalog(EvalContext::new(schema_cols, table_alias), catalog, engine);
4221    // v7.39 (round 747) — GROUP-parallel finalize for the collection
4222    // aggregates. `string_agg(s, ',' ORDER BY id) GROUP BY g` sorted
4223    // and joined every group's items serially — the panel's last
4224    // >=2.0x cell. Groups are independent; shards produce their row
4225    // ranges in group order and concatenate. Admission: every spec a
4226    // collection kind (their finalize reads items/keys/separator and
4227    // the dialect only — nothing that needs the engine hook), no
4228    // ordered-set / first_ordered / regression shapes.
4229    let collections_only = agg_specs.iter().all(|s| {
4230        matches!(
4231            classify_agg_name(&s.name),
4232            AggKind::StringAgg | AggKind::ArrayAgg | AggKind::JsonAgg
4233        ) && !s.first_ordered
4234            && !is_within_group_name(&s.name)
4235    });
4236    if collections_only
4237        && order.len() >= 16
4238        && let Some(r) = runner
4239    {
4240        let group_len_probe = order.first().map(|(g, _)| g.len()).unwrap_or(0);
4241        let _ = group_len_probe;
4242        let n_shards = (order.len() / 8).clamp(2, 8);
4243        let chunk = order.len().div_ceil(n_shards);
4244        type ShardOut = Result<Vec<Row<'static>>, EvalError>;
4245        let mysql = ctx.mysql_dialect;
4246        let style = ctx.render_style;
4247        let results = r.run_shards(n_shards, &|si| {
4248            let lo = si * chunk;
4249            let hi = ((si + 1) * chunk).min(order.len());
4250            let mut sctx = EvalContext::new(schema_cols, table_alias);
4251            sctx.mysql_dialect = mysql;
4252            sctx.render_style = style;
4253            let run = || -> ShardOut {
4254                let mut out: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
4255                for (gvals, states) in &order[lo..hi] {
4256                    out.push(finalize_one_group(
4257                        gvals,
4258                        states,
4259                        agg_specs,
4260                        synth_schema,
4261                        &sctx,
4262                    )?);
4263                }
4264                Ok(out)
4265            };
4266            alloc::boxed::Box::new(run())
4267        });
4268        let mut synth_rows: Vec<Row<'static>> = Vec::with_capacity(order.len());
4269        for boxed in results {
4270            let shard = boxed
4271                .downcast::<ShardOut>()
4272                .expect("runner echoes the closure's box");
4273            synth_rows.extend((*shard)?);
4274        }
4275        return Ok(synth_rows);
4276    }
4277    // v7.32 (round-29) — ordered-set direct arguments (the percentile
4278    // fraction) are constant per PG, so evaluate each once up front.
4279    let direct_arg_vals: Vec<Option<Value>> = agg_specs
4280        .iter()
4281        .map(|spec| match (&spec.direct_arg, rows.first().as_ref()) {
4282            (Some(e), Some(r)) => eval::eval_expr(e, &r.as_row(), &ctx).map(Some),
4283            _ => Ok(None),
4284        })
4285        .collect::<Result<_, _>>()?;
4286    // v7.39 (read01 orderedsetaggs.c) — the remaining hypothetical direct
4287    // arguments of a multi-key call, evaluated once like the first.
4288    let direct_extra_vals: Vec<Vec<Value>> = agg_specs
4289        .iter()
4290        .map(|spec| match rows.first().as_ref() {
4291            Some(r) if !spec.direct_args_extra.is_empty() => spec
4292                .direct_args_extra
4293                .iter()
4294                .map(|e| eval::eval_expr(e, &r.as_row(), &ctx))
4295                .collect(),
4296            _ => Ok(Vec::new()),
4297        })
4298        .collect::<Result<_, _>>()?;
4299
4300    // Materialise synthetic rows (insertion order = `order`).
4301    let mut synth_rows: Vec<Row<'static>> = Vec::new();
4302    for (gvals, states) in order {
4303        let mut values: Vec<Value<'static>> = Vec::with_capacity(synth_schema.len());
4304        // The synth schema is [group keys…, aggregates…]; the aggregate at
4305        // index `i` therefore sits at `group_len + i`.
4306        let group_len = gvals.len();
4307        values.extend(gvals.iter().cloned());
4308        for (i, st) in states.iter().enumerate() {
4309            // v7.33 (array_agg argmax) — first_ordered: the running
4310            // first-by-order value IS the result; no array build/sort.
4311            if agg_specs[i].first_ordered {
4312                values.push(
4313                    st.first_best
4314                        .as_ref()
4315                        .map_or(Value::Null, |(_, v)| v.clone()),
4316                );
4317                continue;
4318            }
4319            // v7.24 (round-16 A) — order the collected items per the
4320            // aggregate-internal ORDER BY before finalize consumes
4321            // them.
4322            let st_sorted;
4323            let kw = agg_specs[i].order_by.len();
4324            let st_final: &AggState = if kw > 0 && st.item_keys.len() == st.items.len() * kw {
4325                let mut idx: Vec<usize> = (0..st.items.len()).collect();
4326                let ob = &agg_specs[i].order_by;
4327                idx.sort_by(|&x, &y| {
4328                    cmp_order_keys(
4329                        ob,
4330                        &agg_specs[i].order_enum_labels,
4331                        &agg_specs[i].order_collations,
4332                        &st.item_keys[x * kw..(x + 1) * kw],
4333                        &st.item_keys[y * kw..(y + 1) * kw],
4334                        ctx.mysql_dialect,
4335                    )
4336                });
4337                // Permute by MOVE out of the clone — the old form
4338                // cloned every item a second time on top of
4339                // `st.clone()`'s first (5000 Strings twice per group).
4340                let mut sorted = st.clone();
4341                let mut new_items: Vec<Value<'static>> = Vec::with_capacity(idx.len());
4342                for &j in &idx {
4343                    new_items.push(core::mem::replace(&mut sorted.items[j], Value::Null));
4344                }
4345                // v7.39 (round 762, F31-C2) — the per-row separators
4346                // travel with their items through the sort.
4347                if sorted.item_seps.len() == sorted.items.len() {
4348                    let mut new_seps: Vec<Option<String>> = Vec::with_capacity(idx.len());
4349                    for &j in &idx {
4350                        new_seps.push(core::mem::take(&mut sorted.item_seps[j]));
4351                    }
4352                    sorted.item_seps = new_seps;
4353                }
4354                sorted.items = new_items;
4355                st_sorted = sorted;
4356                &st_sorted
4357            } else if agg_specs[i].distinct && st.items.len() > 1 {
4358                // v7.39 (round 257) — PG dedups a DISTINCT aggregate by
4359                // SORTING its input, so the collection aggregates emit
4360                // their values in sort order (probed across array_agg /
4361                // string_agg / json_agg, ints and text, NULLs last):
4362                // `array_agg(DISTINCT x)` over 2,1,2 is `{1,2}`, where
4363                // SPG kept first-seen order and answered `{2,1}`. An
4364                // explicit ORDER BY takes the branch above instead, and
4365                // the scalar aggregates (count / sum / …) are
4366                // order-insensitive, so this only moves the collections.
4367                // v7.39 (round 258) — an ENUM input sorts by MEMBER
4368                // ORDER, not by its text (`{sad,ok,happy}`, not
4369                // `{happy,ok,sad}`); `spec.enum_labels` already
4370                // carries the aggregate argument's labels for exactly
4371                // this. Round 257 shipped this sort with the generic
4372                // value comparison and regressed enum columns.
4373                let labels = agg_specs[i].enum_labels.as_deref();
4374                let mut sorted = st.clone();
4375                // v7.39 (round 762, F31-C2) — DISTINCT re-sorts items
4376                // alone; per-row separators cannot follow, so the
4377                // constant-separator path applies (the last row's).
4378                sorted.item_seps.clear();
4379                sorted.items.sort_by(|a, b| {
4380                    if let Some(labels) = labels
4381                        && !matches!(a, Value::Null)
4382                        && !matches!(b, Value::Null)
4383                        && let Some(ord) = crate::eval::enum_ord_cmp(labels, a, b)
4384                    {
4385                        return ord;
4386                    }
4387                    crate::order_by_value_cmp_in(false, Some(false), a, b, ctx.mysql_dialect)
4388                });
4389                st_sorted = sorted;
4390                &st_sorted
4391            } else {
4392                st
4393            };
4394            // Ordered-set aggregates compute from the sorted items + the
4395            // direct fraction; everything else uses the running state.
4396            let v = if is_within_group_name(&agg_specs[i].name) {
4397                finalize_ordered_set(
4398                    &agg_specs[i].name,
4399                    st_final,
4400                    direct_arg_vals[i].as_ref(),
4401                    &direct_extra_vals[i],
4402                    &agg_specs[i].order_by,
4403                    &agg_specs[i].order_collations,
4404                    ctx.mysql_dialect,
4405                )?
4406            } else {
4407                finalize(&agg_specs[i].name, st_final, ctx.mysql_dialect)
4408            };
4409            // v7.39 (round 327, V44) — keep the zone identity. SPG carries a
4410            // timestamptz at runtime as `Value::Timestamp`, so the array
4411            // `array_agg` builds is a `TimestampArray` and `pg_typeof`
4412            // answered `timestamp without time zone[]` for
4413            // `array_agg(timestamptz_col)`. The STATIC type in the synth
4414            // schema already knows better (`infer_agg_type` maps
4415            // Timestamptz ⇒ TimestamptzArray); re-tag the value to match
4416            // it. Third code path in this family — V31 fixed the array
4417            // constructor, V43 the literal cast.
4418            let v = match (v, synth_schema.get(group_len + i).map(|c| c.ty)) {
4419                (Value::TimestampArray(items), Some(DataType::TimestamptzArray)) => {
4420                    Value::TimestamptzArray(items)
4421                }
4422                (v, _) => v,
4423            };
4424            values.push(v);
4425        }
4426        synth_rows.push(Row::new(values));
4427    }
4428    Ok(synth_rows)
4429}
4430
4431/// v7.39 (round 747) — one group's synth row for the COLLECTION
4432/// aggregates (string_agg / array_agg / json_agg): the ordered/distinct
4433/// sort branches verbatim from the serial loop, then `finalize`. The
4434/// group-parallel path calls this; admission guarantees no
4435/// first_ordered / within-group / timestamptz-retag shapes reach it
4436/// (json/array of timestamptz retag is still applied for safety).
4437fn finalize_one_group(
4438    gvals: &[Value<'static>],
4439    states: &[AggState],
4440    agg_specs: &[AggSpec],
4441    synth_schema: &[ColumnSchema],
4442    ctx: &EvalContext<'_>,
4443) -> Result<Row<'static>, EvalError> {
4444    let group_len = gvals.len();
4445    let mut values: Vec<Value<'static>> = Vec::with_capacity(synth_schema.len());
4446    values.extend(gvals.iter().cloned());
4447    for (i, st) in states.iter().enumerate() {
4448        let st_sorted;
4449        let kw = agg_specs[i].order_by.len();
4450        let st_final: &AggState = if kw > 0 && st.item_keys.len() == st.items.len() * kw {
4451            let mut idx: Vec<usize> = (0..st.items.len()).collect();
4452            let ob = &agg_specs[i].order_by;
4453            idx.sort_by(|&x, &y| {
4454                cmp_order_keys(
4455                    ob,
4456                    &agg_specs[i].order_enum_labels,
4457                    &agg_specs[i].order_collations,
4458                    &st.item_keys[x * kw..(x + 1) * kw],
4459                    &st.item_keys[y * kw..(y + 1) * kw],
4460                    ctx.mysql_dialect,
4461                )
4462            });
4463            let mut sorted = st.clone();
4464            let mut new_items: Vec<Value<'static>> = Vec::with_capacity(idx.len());
4465            for &j in &idx {
4466                new_items.push(core::mem::replace(&mut sorted.items[j], Value::Null));
4467            }
4468            // v7.39 (round 762, F31-C2) — separators travel with items.
4469            if sorted.item_seps.len() == sorted.items.len() {
4470                let mut new_seps: Vec<Option<String>> = Vec::with_capacity(idx.len());
4471                for &j in &idx {
4472                    new_seps.push(core::mem::take(&mut sorted.item_seps[j]));
4473                }
4474                sorted.item_seps = new_seps;
4475            }
4476            sorted.items = new_items;
4477            st_sorted = sorted;
4478            &st_sorted
4479        } else if agg_specs[i].distinct && st.items.len() > 1 {
4480            let labels = agg_specs[i].enum_labels.as_deref();
4481            let mut sorted = st.clone();
4482            // v7.39 (round 762, F31-C2) — see the sibling branch above.
4483            sorted.item_seps.clear();
4484            sorted.items.sort_by(|a, b| {
4485                if let Some(labels) = labels
4486                    && !matches!(a, Value::Null)
4487                    && !matches!(b, Value::Null)
4488                    && let Some(ord) = crate::eval::enum_ord_cmp(labels, a, b)
4489                {
4490                    return ord;
4491                }
4492                crate::order_by_value_cmp_in(false, Some(false), a, b, ctx.mysql_dialect)
4493            });
4494            st_sorted = sorted;
4495            &st_sorted
4496        } else {
4497            st
4498        };
4499        let v = finalize(&agg_specs[i].name, st_final, ctx.mysql_dialect);
4500        let v = match (v, synth_schema.get(group_len + i).map(|c| c.ty)) {
4501            (Value::TimestampArray(items), Some(DataType::TimestamptzArray)) => {
4502                Value::TimestamptzArray(items)
4503            }
4504            (v, _) => v,
4505        };
4506        values.push(v);
4507    }
4508    Ok(Row::new(values))
4509}
4510
4511/// (3) Rewrite the user's SELECT items + HAVING to reference the
4512/// synthetic columns, filter groups by HAVING, and project each
4513/// surviving group into an output row. The synth rows ride alongside
4514/// (`kept_synth`) so post-LIMIT deferred subqueries can evaluate later.
4515#[allow(clippy::too_many_lines)]
4516fn project_groups(
4517    synth_rows: Vec<Row<'static>>,
4518    stmt: &SelectStatement,
4519    group_exprs: &[Expr],
4520    agg_specs: &[AggSpec],
4521    synth_schema: &[ColumnSchema],
4522    correlated_eval: Option<CorrelatedEval<'_>>,
4523    defer_projection: bool,
4524    catalog: Option<&spg_storage::Catalog>,
4525    mysql: bool,
4526) -> Result<Projection, EvalError> {
4527    // Rewrite the user's SELECT items + ORDER BY to reference synthetic
4528    // columns. After rewriting, every remaining `Expr::Column` must
4529    // resolve against the synthetic schema (i.e. must have been a GROUP
4530    // BY expression).
4531    let columns: Vec<ColumnSchema> = stmt
4532        .items
4533        .iter()
4534        .map(|item| match item {
4535            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
4536                Err(EvalError::TypeMismatch {
4537                    detail: "SELECT * with aggregates is not supported".into(),
4538                })
4539            }
4540            SelectItem::Expr { expr, alias } => {
4541                let rewritten = rewrite_expr(expr, group_exprs, agg_specs);
4542                let name = alias
4543                    .clone()
4544                    .unwrap_or_else(|| crate::select::default_output_name(expr, mysql));
4545                // v7.38.14 — the type is looked up in the synthetic schema
4546                // here; the COLLATION has to travel by the same route or the
4547                // output column claims `ColumnSchema::new`'s default, which
4548                // is `Binary` and reads downstream as "byte-wise on
4549                // purpose". That is what made `SELECT DISTINCT ... GROUP BY`
4550                // stop folding: the de-duplication asked the output schema
4551                // and the output schema had forgotten.
4552                //
4553                // Third site with this exact shape in one release, after
4554                // `join.rs::build_combined_schema` and `synth_schema` above.
4555                // Each one hand-picks which attributes survive; none picks
4556                // all of them. See S4 of the v7.38.14 roadmap.
4557                let mut col =
4558                    ColumnSchema::new(name, agg_or_group_type(&rewritten, synth_schema), true);
4559                if let Expr::Column(c) = &rewritten
4560                    && let Some(sc) = synth_schema
4561                        .iter()
4562                        .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
4563                {
4564                    col.collation = sc.collation;
4565                    col.collation_name.clone_from(&sc.collation_name);
4566                }
4567                Ok(col)
4568            }
4569        })
4570        .collect::<Result<_, _>>()?;
4571
4572    // Project per synthetic row. HAVING filters out groups *before*
4573    // we keep the projected row — same semantics as PG: HAVING runs
4574    // against the aggregated row (so `HAVING count(*) > 1` works) and
4575    // sees only group-by'd columns plus aggregate values.
4576    let mut synth_ctx = EvalContext::new(synth_schema, None);
4577    // v7.39 (enum order knife) — HAVING comparisons over enum group keys
4578    // need the catalog for member-order semantics (both the compile-time
4579    // Subtree fallback witness and the eval hook read it).
4580    if let Some(cat) = catalog {
4581        synth_ctx = synth_ctx.with_catalog(cat);
4582    }
4583    // v7.39 (round 404) — a MySQL session lets HAVING name a SELECT alias.
4584    // Build the (alias, expr) map from renaming SELECT items, then subst
4585    // before the aggregate rewrite.
4586    let having_aliases: Vec<(String, Expr)> = if mysql {
4587        stmt.items
4588            .iter()
4589            .filter_map(|it| match it {
4590                SelectItem::Expr {
4591                    expr,
4592                    alias: Some(a),
4593                } if !matches!(expr, Expr::Column(c)
4594                    if c.qualifier.is_none() && c.name.eq_ignore_ascii_case(a)) =>
4595                {
4596                    Some((a.clone(), expr.clone()))
4597                }
4598                _ => None,
4599            })
4600            .collect()
4601    } else {
4602        Vec::new()
4603    };
4604    let having_rewritten = stmt.having.as_ref().map(|h| {
4605        let h = if having_aliases.is_empty() {
4606            h.clone()
4607        } else {
4608            substitute_having_aliases(h.clone(), &having_aliases)
4609        };
4610        rewrite_expr(&h, group_exprs, agg_specs)
4611    });
4612    // v7.30 (phase 3e-1) - rewrite SELECT items ONCE. This ran per
4613    // GROUP (23.5k x 9 items of AST cloning = ~48% of the inbox
4614    // query in sampled stacks); the rewrite is group-independent.
4615    // Stable addresses also let the per-expression subquery plans
4616    // (v7.29 3c) hit across groups instead of rebuilding.
4617    let items_rewritten: alloc::vec::Vec<Option<Expr>> = stmt
4618        .items
4619        .iter()
4620        .map(|item| match item {
4621            SelectItem::Expr { expr, .. } => Some(rewrite_expr(expr, group_exprs, agg_specs)),
4622            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => None,
4623        })
4624        .collect();
4625    // v7.31 (perf — PG lesson #1): subquery-bearing select items
4626    // deferred to post-LIMIT, when no sort/filter key can observe
4627    // them. ORDER BY rewrites are hoisted here so the safety check
4628    // and the sort below share one rewrite pass.
4629    let order_rewritten: Vec<Expr> = stmt
4630        .order_by
4631        .iter()
4632        .map(|o| rewrite_expr(&o.expr, group_exprs, agg_specs))
4633        .collect();
4634    let defer_enabled = correlated_eval.is_some()
4635        && !stmt.distinct
4636        && !having_rewritten
4637            .as_ref()
4638            .is_some_and(crate::expr_has_subquery)
4639        && !order_rewritten.iter().any(crate::expr_has_subquery);
4640    let deferred: Vec<(usize, Expr)> = if defer_enabled {
4641        items_rewritten
4642            .iter()
4643            .enumerate()
4644            .filter_map(|(i, r)| {
4645                r.as_ref()
4646                    .filter(|e| crate::expr_has_subquery(e))
4647                    .map(|e| (i, e.clone()))
4648            })
4649            .collect()
4650    } else {
4651        Vec::new()
4652    };
4653    // v7.32 (architecture v2, P2) — compile the per-group synth-row
4654    // expressions ONCE. The projection / HAVING here run per GROUP
4655    // (24k for the inbox shape) × per item; the rewritten exprs are
4656    // mostly `Column(__agg_N)` / `Column(__grp_K)` against the synth
4657    // schema — flat step programs, no tree walk per group.
4658    let having_compiled = having_rewritten
4659        .as_ref()
4660        .filter(|h| eval::fully_compilable(h))
4661        .map(|h| eval::compile_expr(h, &synth_ctx));
4662    let items_compiled: Vec<Option<eval::CompiledExpr>> = items_rewritten
4663        .iter()
4664        .enumerate()
4665        .map(|(i, r)| {
4666            r.as_ref()
4667                .filter(|e| !deferred.iter().any(|(c, _)| *c == i) && eval::fully_compilable(e))
4668                .map(|e| eval::compile_expr(e, &synth_ctx))
4669        })
4670        .collect();
4671    // v7.39 (round 621) — which items are set-returning, after the rewrite
4672    // (so `unnest(array_agg(x))` is seen as the SRF it is, over a synthetic
4673    // aggregate column). Only the builtin SRFs are recognised here; a user
4674    // `RETURNS SETOF` function inside an aggregate query keeps the old error,
4675    // because running its body needs the executor and this is not it.
4676    let srf_items: Vec<bool> = items_rewritten
4677        .iter()
4678        .map(|r| {
4679            r.as_ref()
4680                .is_some_and(|e| crate::select::top_level_srf_kind(e).is_some())
4681        })
4682        .collect();
4683    let any_srf = srf_items.iter().any(|b| *b);
4684    let mut kept_synth: Vec<Row<'static>> = Vec::new();
4685    let mut out_rows: Vec<Row<'static>> = Vec::new();
4686    let mut stack: Vec<Value<'static>> = Vec::new();
4687    for srow in synth_rows {
4688        if let Some(hc) = &having_compiled {
4689            let cond = eval::eval_compiled(hc, &srow, &synth_ctx, &mut stack)?;
4690            if !crate::eval::predicate_is_true(&cond, "HAVING", synth_ctx.mysql_dialect)? {
4691                continue;
4692            }
4693        } else if let Some(h) = &having_rewritten {
4694            let cond = match correlated_eval {
4695                Some(f) if crate::expr_has_subquery(h) => f(h, &srow, &synth_ctx)?,
4696                _ => eval::eval_expr(h, &srow, &synth_ctx)?,
4697            };
4698            if !crate::eval::predicate_is_true(&cond, "HAVING", synth_ctx.mysql_dialect)? {
4699                continue;
4700            }
4701        }
4702        // v7.37.x — when caller pre-truncates via ORDER BY+LIMIT, skip
4703        // per-item projection here; the caller fills the placeholder
4704        // out_rows from the top-K survivors below.
4705        if defer_projection {
4706            kept_synth.push(srow);
4707            out_rows.push(Row::new(Vec::new()));
4708            continue;
4709        }
4710        let mut values: Vec<Value<'static>> = Vec::with_capacity(columns.len());
4711        for (i, rewritten) in items_rewritten.iter().enumerate() {
4712            let Some(rewritten) = rewritten else { continue };
4713            if deferred.iter().any(|(c, _)| *c == i) {
4714                values.push(Value::Null);
4715                continue;
4716            }
4717            // v7.39 (round 621) — a SET-RETURNING item is collected as its
4718            // whole list; the rows it makes are built after the loop.
4719            if srf_items[i] {
4720                values.push(Value::Null);
4721                continue;
4722            }
4723            values.push(if let Some(cc) = &items_compiled[i] {
4724                eval::eval_compiled(cc, &srow, &synth_ctx, &mut stack)?
4725            } else {
4726                match correlated_eval {
4727                    Some(f) if crate::expr_has_subquery(rewritten) => {
4728                        f(rewritten, &srow, &synth_ctx)?
4729                    }
4730                    _ => eval::eval_expr(rewritten, &srow, &synth_ctx)?,
4731                }
4732            });
4733        }
4734        if any_srf {
4735            // v7.39 (round 621) — the aggregate's own output row is what a
4736            // target-list SRF expands over. `SELECT unnest(ARRAY[1,2]),
4737            // count(*) FROM t` answered `function unnest(integer[]) does not
4738            // exist`, because this projection evaluates each item scalarly and
4739            // there is exactly one row per group to put it in. PG answers two
4740            // rows, both carrying the same count — and the shape that matters
4741            // most is `unnest(array_agg(x))`, where the SRF's ARGUMENT is the
4742            // aggregate.
4743            //
4744            // Several SRFs in one list expand in LOCKSTEP with the shorter
4745            // padded to NULL, which is round 67's rule for every other path.
4746            let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(items_rewritten.len());
4747            for (i, rewritten) in items_rewritten.iter().enumerate() {
4748                match (srf_items[i], rewritten) {
4749                    (true, Some(r)) => {
4750                        lists.push(
4751                            crate::select::top_level_srf_output(r, &srow, &synth_ctx).map_err(
4752                                |e| match e {
4753                                    crate::EngineError::Eval(ev) => ev,
4754                                    other => EvalError::TypeMismatch {
4755                                        detail: alloc::format!("{other}"),
4756                                    },
4757                                },
4758                            )?,
4759                        );
4760                    }
4761                    _ => lists.push(Vec::new()),
4762                }
4763            }
4764            let n = lists.iter().map(Vec::len).max().unwrap_or(0);
4765            for k in 0..n {
4766                let mut vals = values.clone();
4767                for (i, list) in lists.iter().enumerate() {
4768                    if srf_items[i]
4769                        && let Some(slot) = vals.get_mut(i)
4770                    {
4771                        *slot = list.get(k).cloned().unwrap_or(Value::Null);
4772                    }
4773                }
4774                kept_synth.push(srow.clone());
4775                out_rows.push(Row::new(vals));
4776            }
4777            continue;
4778        }
4779        kept_synth.push(srow);
4780        out_rows.push(Row::new(values));
4781    }
4782    let deferred_project_state = if defer_projection {
4783        Some(DeferredProject {
4784            items_rewritten,
4785            items_compiled,
4786        })
4787    } else {
4788        None
4789    };
4790    Ok(Projection {
4791        columns,
4792        out_rows,
4793        kept_synth,
4794        deferred,
4795        order_rewritten,
4796        deferred_project: deferred_project_state,
4797    })
4798}
4799
4800/// (4) Sort the projected output by the rewritten ORDER BY keys. The
4801/// synth rows ride through the sort so deferred subqueries evaluate
4802/// against the surviving groups after the caller's LIMIT truncation.
4803fn sort_synth_by_order_by(
4804    synth_schema: &[ColumnSchema],
4805    out_columns: &[ColumnSchema],
4806    order_by: &[spg_sql::ast::OrderBy],
4807    order_rewritten: &[Expr],
4808    mut kept_synth: Vec<Row<'static>>,
4809    mut out_rows: Vec<Row<'static>>,
4810    correlated_eval: Option<CorrelatedEval<'_>>,
4811    keep_n: Option<usize>,
4812    catalog: Option<&spg_storage::Catalog>,
4813    mysql: bool,
4814) -> Result<(Vec<Row<'static>>, Vec<Row<'static>>), EvalError> {
4815    let mut synth_ctx = EvalContext::new(synth_schema, None);
4816    if let Some(cat) = catalog {
4817        synth_ctx = synth_ctx.with_catalog(cat);
4818    }
4819    // v7.39 (enum order knife) — per-key member labels when the rewritten
4820    // sort key is an enum-typed column (`__grp_K` carrying user_enum_type).
4821    let key_enum_labels: Vec<Option<&[String]>> = order_rewritten
4822        .iter()
4823        .map(|e| crate::eval::expr_enum_labels(e, synth_schema, catalog))
4824        .collect();
4825    // v7.39 (round 686) — per-key declared collation, built exactly like the
4826    // enum labels above because it is the same kind of thing: metadata the
4827    // comparator needs, resolved once per sort from the key expression.
4828    //
4829    // Located by forcing this call site to reverse and watching
4830    // `GROUP BY loc ORDER BY loc` flip. Rounds 682 and 685 wired eleven
4831    // sites between them without doing that, and none was on the path.
4832    let key_colls: Vec<Option<alloc::string::String>> = order_rewritten
4833        .iter()
4834        .map(|e| {
4835            let spg_sql::ast::Expr::Column(c) = e else {
4836                return None;
4837            };
4838            let pos = crate::eval::find_column_pos(c, &synth_ctx)?;
4839            let name = synth_schema.get(pos)?.collation_name.clone()?;
4840            crate::collate::is_supported(&name).then_some(name)
4841        })
4842        .collect();
4843    // v6.4.0 — multi-key ORDER BY on aggregate output. Each key
4844    // gets its own rewrite + per-key DESC flag. (Rewrites hoisted
4845    // above as `order_rewritten` — shared with the deferral
4846    // safety check.)
4847    let keys_meta: Vec<(bool, Option<bool>)> =
4848        order_by.iter().map(|o| (o.desc, o.nulls_first)).collect();
4849    // P2: compile order-by keys once (per-group sort keys are
4850    // the same `__agg_N` / `__grp_K` shape as the projection).
4851    let order_compiled: Vec<Option<eval::CompiledExpr>> = order_rewritten
4852        .iter()
4853        .map(|e| {
4854            Some(e)
4855                .filter(|e| eval::fully_compilable(e))
4856                .map(|e| eval::compile_expr(e, &synth_ctx))
4857        })
4858        .collect();
4859    // The synth row rides through the sort so deferred exprs can
4860    // evaluate against the surviving groups after the caller's
4861    // LIMIT truncation.
4862    // v7.37 (round 1000) — a sort key that names an OUTPUT column.
4863    //
4864    // `ORDER BY 1` over a set-returning item does not substitute the
4865    // item's expression: round 80 resolved it to the item's output NAME
4866    // instead, because a positional key means the Nth OUTPUT column and
4867    // substituting the expression would make the key "the whole set",
4868    // evaluated once per group, which silently sorted nothing. The
4869    // non-aggregate paths then evaluate that name against the output
4870    // schema.
4871    //
4872    // This one evaluated it against the SYNTHETIC schema, which carries
4873    // `__agg_N` / `__grp_K` and no output aliases, so
4874    // `SELECT unnest(ARRAY[1,2]) AS u, count(*) … GROUP BY g ORDER BY 1`
4875    // answered `column "u" does not exist` — a query PG18.4 answers.
4876    // Spelling it `ORDER BY u` failed differently and for the same
4877    // reason: the alias resolved to the expression, and a set-returning
4878    // call cannot be evaluated scalarly on a group row.
4879    //
4880    // So: a key that names an output column and NOTHING in the synthetic
4881    // schema is read from the projected row, where expansion has already
4882    // put the per-row value. Synthetic names keep precedence, so nothing
4883    // that resolved before resolves differently now.
4884    let out_key_idx: Vec<Option<usize>> = order_rewritten
4885        .iter()
4886        .map(|e| {
4887            let spg_sql::ast::Expr::Column(c) = e else {
4888                return None;
4889            };
4890            if c.qualifier.is_some() || crate::eval::find_column_pos(c, &synth_ctx).is_some() {
4891                return None;
4892            }
4893            out_columns
4894                .iter()
4895                .position(|oc| oc.name.eq_ignore_ascii_case(&c.name))
4896        })
4897        .collect();
4898    let mut keystack: Vec<Value<'static>> = Vec::new();
4899    let mut tagged: Vec<(Vec<Value<'static>>, Row, Row)> = Vec::with_capacity(kept_synth.len());
4900    for (s, o) in kept_synth.into_iter().zip(out_rows) {
4901        let mut keys = Vec::with_capacity(order_rewritten.len());
4902        for (i, (e, oc)) in order_rewritten.iter().zip(&order_compiled).enumerate() {
4903            if let Some(oi) = out_key_idx[i] {
4904                keys.push(o.values.get(oi).cloned().unwrap_or(Value::Null));
4905                continue;
4906            }
4907            keys.push(if let Some(oc) = oc {
4908                eval::eval_compiled(oc, &s, &synth_ctx, &mut keystack)?
4909            } else {
4910                match correlated_eval {
4911                    Some(f) if crate::expr_has_subquery(e) => f(e, &s, &synth_ctx)?,
4912                    _ => eval::eval_expr(e, &s, &synth_ctx)?,
4913                }
4914            });
4915        }
4916        tagged.push((keys, s, o));
4917    }
4918    let cmp = |a: &(Vec<Value<'static>>, Row, Row), b: &(Vec<Value<'static>>, Row, Row)| {
4919        use core::cmp::Ordering;
4920        for (i, (ka, kb)) in a.0.iter().zip(b.0.iter()).enumerate() {
4921            let (desc, nf) = keys_meta[i];
4922            // v7.39 (enum order knife) — enum keys sort by member order.
4923            if let Some(Some(labels)) = key_enum_labels.get(i)
4924                && !matches!(ka, Value::Null)
4925                && !matches!(kb, Value::Null)
4926                && let Some(ord) = crate::eval::enum_ord_cmp(labels, ka, kb)
4927            {
4928                let ord = if desc { ord.reverse() } else { ord };
4929                if ord != Ordering::Equal {
4930                    return ord;
4931                }
4932                continue;
4933            }
4934            let c = crate::orderby::order_by_value_cmp_coll(
4935                desc,
4936                nf,
4937                ka,
4938                kb,
4939                mysql,
4940                key_colls.get(i).and_then(|c| c.as_deref()),
4941            );
4942            if c != Ordering::Equal {
4943                return c;
4944            }
4945        }
4946        Ordering::Equal
4947    };
4948    // v7.37.3 — top-K partial sort when `keep_n` is small enough to
4949    // matter (`Some(k)` with `k < tagged.len()` and `k > 0`).
4950    // `select_nth_unstable_by` partitions in O(N), then we sort the
4951    // surviving prefix in O(K log K). Total = O(N + K log K) vs
4952    // O(N log N) the full sort would pay — matches the inbox-listing
4953    // shape PG uses.
4954    //
4955    match keep_n {
4956        Some(k) if k < tagged.len() && k > 0 => {
4957            let pivot = k - 1;
4958            tagged.select_nth_unstable_by(pivot, cmp);
4959            tagged[..k].sort_by(cmp);
4960            tagged.truncate(k);
4961        }
4962        _ => {
4963            tagged.sort_by(cmp);
4964        }
4965    }
4966    kept_synth = Vec::with_capacity(tagged.len());
4967    out_rows = Vec::with_capacity(tagged.len());
4968    for (_, s, o) in tagged {
4969        kept_synth.push(s);
4970        out_rows.push(o);
4971    }
4972    Ok((kept_synth, out_rows))
4973}
4974
4975/// v7.17.0 — walk the statement again to validate the positional
4976/// arity of every aggregate call site. Done after AST collection
4977/// rather than inside `collect_aggregates` so the collector stays
4978/// infallible; callers in `run()` can do a single early-error
4979/// exit before any per-row work.
4980fn validate_agg_arities(stmt: &SelectStatement, _specs: &[AggSpec]) -> Result<(), EvalError> {
4981    fn walk(e: &Expr) -> Result<(), EvalError> {
4982        if let Expr::FunctionCall { name, args } = e {
4983            let lower = name.to_ascii_lowercase();
4984            let expected: Option<usize> = match lower.as_str() {
4985                "count_star" => Some(0),
4986                "count" | "sum" | "avg" | "min" | "max" | "array_agg"
4987                | "any_value" | "range_agg" | "range_intersect_agg"
4988                // v7.17.0 — boolean aggregates also take exactly
4989                // one arg. `every` is an alias normalised inside
4990                // collect_aggregates / rewrite_expr.
4991                | "bool_and" | "bool_or" | "every"
4992                // v7.32 (round-29) — statistical + bitwise aggregates
4993                // + single-arg JSON aggregate.
4994                | "stddev" | "stddev_samp" | "stddev_pop"
4995                | "variance" | "var_samp" | "var_pop"
4996                | "bit_and" | "bit_or" | "bit_xor"
4997                | "json_agg" | "jsonb_agg" | "xmlagg"
4998                | "json_arrayagg" | "json_agg_strict" | "jsonb_agg_strict" => Some(1),
4999                // v7.39 (round 354, M12) — GROUP_CONCAT takes any number of
5000                // arguments: MySQL concatenates them PER ROW
5001                // (`GROUP_CONCAT(n, ':', t)` is `3:c,1:a,…`, measured), and
5002                // the parser lowers a `SEPARATOR '<s>'` tail onto the last
5003                // one. Fixing the arity at 1 refused both.
5004                "group_concat" => None,
5005                // v7.32 (round-29) — two-argument aggregates: string_agg,
5006                // the regression family f(Y, X), and json_object_agg.
5007                "string_agg"
5008                | "covar_pop" | "covar_samp" | "corr"
5009                | "regr_count" | "regr_avgx" | "regr_avgy" | "regr_slope"
5010                | "regr_intercept" | "regr_r2" | "regr_sxx" | "regr_syy" | "regr_sxy"
5011                | "json_object_agg" | "jsonb_object_agg"
5012                | "json_objectagg"
5013                | "json_object_agg_strict" | "jsonb_object_agg_strict"
5014                | "json_object_agg_unique" | "jsonb_object_agg_unique"
5015                | "json_object_agg_unique_strict" | "jsonb_object_agg_unique_strict" => Some(2),
5016                _ => None,
5017            };
5018            if let Some(want) = expected
5019                && args.len() != want
5020            {
5021                return Err(EvalError::TypeMismatch {
5022                    detail: alloc::format!("{lower}() takes {want} arg(s), got {}", args.len()),
5023                });
5024            }
5025            for a in args {
5026                walk(a)?;
5027            }
5028        } else if let Expr::Binary { lhs, rhs, .. } = e {
5029            walk(lhs)?;
5030            walk(rhs)?;
5031        } else if let Expr::Unary { expr, .. }
5032        | Expr::Cast { expr, .. }
5033        | Expr::IsNull { expr, .. }
5034        | Expr::BoolTest { expr, .. } = e
5035        {
5036            walk(expr)?;
5037        }
5038        Ok(())
5039    }
5040    for item in &stmt.items {
5041        if let SelectItem::Expr { expr, .. } = item {
5042            walk(expr)?;
5043        }
5044    }
5045    for o in &stmt.order_by {
5046        walk(&o.expr)?;
5047    }
5048    if let Some(h) = &stmt.having {
5049        walk(h)?;
5050    }
5051    Ok(())
5052}
5053
5054/// v7.33 (array_agg argmax) — recognise `(array_agg(x ORDER BY y))[1]`,
5055/// the argmax/argmin idiom: a non-DISTINCT ordered `array_agg`
5056/// subscripted by the constant 1. Returns `(value_arg, order_by,
5057/// filter)` on a match. When matched, the whole per-group array build +
5058/// sort + materialise is replaced by a running first-by-order scalar
5059/// accumulator and the subscript node is consumed (replaced by the
5060/// synthetic column). collect_aggregates and rewrite_expr share this one
5061/// matcher so their `__agg_<i>` assignment stays in lockstep.
5062fn first_ordered_array_agg(e: &Expr) -> Option<(&Expr, &[spg_sql::ast::OrderBy], Option<&Expr>)> {
5063    let Expr::ArraySubscript { target, index } = e else {
5064        return None;
5065    };
5066    if !matches!(
5067        index.as_ref(),
5068        Expr::Literal(spg_sql::ast::Literal::Integer(1))
5069    ) {
5070        return None;
5071    }
5072    let Expr::AggregateOrdered {
5073        call,
5074        order_by,
5075        distinct,
5076        filter,
5077    } = target.as_ref()
5078    else {
5079        return None;
5080    };
5081    if *distinct || order_by.is_empty() {
5082        return None;
5083    }
5084    let Expr::FunctionCall { name, args } = call.as_ref() else {
5085        return None;
5086    };
5087    if !name.eq_ignore_ascii_case("array_agg") || args.len() != 1 {
5088        return None;
5089    }
5090    Some((&args[0], order_by, filter.as_deref()))
5091}
5092
5093/// v7.39 (round 615) — the exact pair the finaliser reads: the BigNumeric
5094/// accumulator combined with whatever the i128 one still holds. Read-only,
5095/// because finalisation only borrows the state.
5096fn stddev_exact_pair(
5097    st: &AggState,
5098) -> Option<(
5099    spg_storage::bignum::BigNumeric,
5100    spg_storage::bignum::BigNumeric,
5101)> {
5102    use spg_storage::bignum::BigNumeric as BN;
5103    let fast =
5104        (!st.stddev_i_spent && (st.stddev_i_sum != 0 || st.stddev_i_sum_sq != 0)).then(|| {
5105            (
5106                BN::from_i128(st.stddev_i_sum, 0),
5107                BN::from_i128(st.stddev_i_sum_sq, 0),
5108            )
5109        });
5110    match (st.stddev_sum.as_ref(), st.stddev_sum_sq.as_ref(), fast) {
5111        (Some(s), Some(sq), Some((fs, fsq))) => Some((s.add(&fs), sq.add(&fsq))),
5112        (Some(s), Some(sq), None) => Some((s.clone(), sq.clone())),
5113        (None, None, Some(pair)) => Some(pair),
5114        _ => None,
5115    }
5116}
5117
5118/// v7.39 (round 615) — fold the i128 Σx / Σx² into the exact BigNumeric
5119/// pair and retire the fast accumulator. Called once when an input needs the
5120/// slow path, and once at finalisation; both are idempotent because the fast
5121/// pair is zeroed as it is spent.
5122fn spend_stddev_i128(st: &mut AggState) {
5123    if st.stddev_i_spent {
5124        return;
5125    }
5126    st.stddev_i_spent = true;
5127    if st.stddev_i_sum == 0 && st.stddev_i_sum_sq == 0 {
5128        // Nothing accumulated: leave the pair as it was (None means "no
5129        // exact input yet", which the finaliser reads).
5130        return;
5131    }
5132    use spg_storage::bignum::BigNumeric as BN;
5133    let sum = BN::from_i128(st.stddev_i_sum, 0);
5134    let sum_sq = BN::from_i128(st.stddev_i_sum_sq, 0);
5135    st.stddev_sum = Some(st.stddev_sum.as_ref().map_or(sum.clone(), |s| s.add(&sum)));
5136    st.stddev_sum_sq = Some(
5137        st.stddev_sum_sq
5138            .as_ref()
5139            .map_or(sum_sq.clone(), |s| s.add(&sum_sq)),
5140    );
5141}
5142
5143fn collect_aggregates(e: &Expr, out: &mut Vec<AggSpec>) {
5144    match e {
5145        Expr::NamedArg { expr, .. } => collect_aggregates(expr, out),
5146        Expr::Variadic(expr) => collect_aggregates(expr, out),
5147        // v7.24 (round-16 A) — ordered aggregate: register the inner
5148        // call's spec with the ordering attached.
5149        Expr::AggregateOrdered {
5150            call,
5151            order_by,
5152            distinct,
5153            filter,
5154        } => {
5155            if let Expr::FunctionCall { name, args } = call.as_ref() {
5156                let lower = name.to_ascii_lowercase();
5157                if is_aggregate_name(&lower) {
5158                    let canonical = if lower == "every" {
5159                        "bool_and".to_string()
5160                    } else {
5161                        lower
5162                    };
5163                    // Ordered-set aggregates (`percentile_cont(f)
5164                    // WITHIN GROUP (ORDER BY x)`) take the value to
5165                    // aggregate from the sort spec and the in-parens
5166                    // arg as the direct (fraction) argument.
5167                    let ordered_set = is_within_group_name(&canonical);
5168                    let (arg, direct_arg, direct_args_extra) = if ordered_set {
5169                        (
5170                            order_by.first().map(|o| o.expr.clone()),
5171                            args.first().cloned(),
5172                            args.iter().skip(1).cloned().collect(),
5173                        )
5174                    } else {
5175                        (args.first().cloned(), None, Vec::new())
5176                    };
5177                    let spec = AggSpec {
5178                        kind: classify_agg_name(&canonical),
5179                        enum_labels: None,
5180                        arg_collation: None,
5181                        order_enum_labels: Vec::new(),
5182                        order_collations: Vec::new(),
5183                        name: canonical.clone(),
5184                        arg,
5185                        arg2: if agg_uses_second_arg(&canonical) {
5186                            args.get(1).cloned()
5187                        } else {
5188                            None
5189                        },
5190                        distinct: *distinct,
5191                        order_by: order_by.clone(),
5192                        filter: filter.as_deref().cloned(),
5193                        direct_arg,
5194                        direct_args_extra,
5195                        first_ordered: false,
5196                    };
5197                    if !out.iter().any(|s| {
5198                        s.name == spec.name
5199                            && s.arg == spec.arg
5200                            && s.arg2 == spec.arg2
5201                            && s.distinct == spec.distinct
5202                            && s.order_by == spec.order_by
5203                            && s.filter == spec.filter
5204                            && s.direct_arg == spec.direct_arg
5205                            && s.direct_args_extra == spec.direct_args_extra
5206                            && s.first_ordered == spec.first_ordered
5207                    }) {
5208                        out.push(spec);
5209                    }
5210                    return;
5211                }
5212            }
5213            collect_aggregates(call, out);
5214            for o in order_by {
5215                collect_aggregates(&o.expr, out);
5216            }
5217        }
5218        Expr::FunctionCall { name, args } => {
5219            let lower = name.to_ascii_lowercase();
5220            if is_aggregate_name(&lower) {
5221                let arg = if lower == "count_star" {
5222                    None
5223                } else {
5224                    args.first().cloned()
5225                };
5226                // v7.17.0 — second positional arg for
5227                // `string_agg(value, separator)`; v7.32 — also the
5228                // regression family `f(Y, X)` and `json_object_agg`.
5229                let arg2 = if agg_uses_second_arg(&lower) {
5230                    args.get(1).cloned()
5231                } else {
5232                    None
5233                };
5234                // v7.17.0 — `every` is the SQL-standard alias for
5235                // `bool_and`; collapse at collection time so
5236                // update_state / finalize need only one arm.
5237                let canonical = if lower == "every" {
5238                    "bool_and".to_string()
5239                } else {
5240                    lower
5241                };
5242                let spec = AggSpec {
5243                    kind: classify_agg_name(&canonical),
5244                    enum_labels: None,
5245                    arg_collation: None,
5246                    order_enum_labels: Vec::new(),
5247                    order_collations: Vec::new(),
5248                    name: canonical,
5249                    arg: arg.clone(),
5250                    arg2: arg2.clone(),
5251                    distinct: false,
5252                    order_by: Vec::new(),
5253                    filter: None,
5254                    direct_arg: None,
5255                    direct_args_extra: Vec::new(),
5256                    first_ordered: false,
5257                };
5258                if !out.iter().any(|s| {
5259                    s.name == spec.name
5260                        && s.arg == spec.arg
5261                        && s.arg2 == spec.arg2
5262                        && !s.distinct
5263                        && s.order_by == spec.order_by
5264                        && s.filter.is_none()
5265                        && !s.first_ordered
5266                }) {
5267                    out.push(spec);
5268                }
5269                // Don't recurse into the arg — nested aggregates are
5270                // illegal in standard SQL.
5271            } else {
5272                for a in args {
5273                    collect_aggregates(a, out);
5274                }
5275            }
5276        }
5277        Expr::Binary { lhs, rhs, .. } => {
5278            collect_aggregates(lhs, out);
5279            collect_aggregates(rhs, out);
5280        }
5281        Expr::Unary { expr, .. }
5282        | Expr::Cast { expr, .. }
5283        | Expr::IsNull { expr, .. }
5284        | Expr::BoolTest { expr, .. }
5285        | Expr::FieldAccess { base: expr, .. } => {
5286            collect_aggregates(expr, out);
5287        }
5288        Expr::Like { expr, pattern, .. } => {
5289            collect_aggregates(expr, out);
5290            collect_aggregates(pattern, out);
5291        }
5292        Expr::InList { expr, list, .. } => {
5293            collect_aggregates(expr, out);
5294            for item in list {
5295                collect_aggregates(item, out);
5296            }
5297        }
5298        Expr::Extract { source, .. } => collect_aggregates(source, out),
5299        // v4.10 subquery + v4.12 window / Literal / Column —
5300        // non-recursing leaves for the aggregate collector.
5301        Expr::ScalarSubquery(_)
5302        | Expr::Exists { .. }
5303        | Expr::InSubquery { .. }
5304        | Expr::RowInSubquery { .. }
5305        | Expr::RowCmpSubquery { .. }
5306        | Expr::WindowFunction { .. }
5307        | Expr::Literal(_)
5308        | Expr::Placeholder(_)
5309        | Expr::Column(_) => {}
5310        // v7.10.10 — recurse into array constructor children +
5311        // subscript / ANY/ALL operands.
5312        Expr::Array(items) => {
5313            for elem in items {
5314                collect_aggregates(elem, out);
5315            }
5316        }
5317        Expr::ArraySubscript { target, index } => {
5318            // v7.33 (array_agg argmax) — `(array_agg(x ORDER BY y))[1]`
5319            // collects as a first_ordered spec; the subscript is consumed
5320            // here (do NOT recurse into the array_agg, or it would also
5321            // register a plain full-array spec).
5322            if let Some((arg, order_by, filter)) = first_ordered_array_agg(e) {
5323                let spec = AggSpec {
5324                    kind: AggKind::ArrayAgg,
5325                    enum_labels: None,
5326                    arg_collation: None,
5327                    order_enum_labels: Vec::new(),
5328                    order_collations: Vec::new(),
5329                    name: "array_agg".to_string(),
5330                    arg: Some(arg.clone()),
5331                    arg2: None,
5332                    distinct: false,
5333                    order_by: order_by.to_vec(),
5334                    filter: filter.cloned(),
5335                    direct_arg: None,
5336                    direct_args_extra: Vec::new(),
5337                    first_ordered: true,
5338                };
5339                if !out.iter().any(|s| {
5340                    s.name == spec.name
5341                        && s.arg == spec.arg
5342                        && s.order_by == spec.order_by
5343                        && s.filter == spec.filter
5344                        && s.first_ordered
5345                }) {
5346                    out.push(spec);
5347                }
5348                return;
5349            }
5350            collect_aggregates(target, out);
5351            collect_aggregates(index, out);
5352        }
5353        Expr::ArraySlice { target, lo, hi } => {
5354            collect_aggregates(target, out);
5355            if let Some(l) = lo {
5356                collect_aggregates(l, out);
5357            }
5358            if let Some(h) = hi {
5359                collect_aggregates(h, out);
5360            }
5361        }
5362        Expr::AnyAll { expr, array, .. } => {
5363            collect_aggregates(expr, out);
5364            collect_aggregates(array, out);
5365        }
5366        Expr::Case {
5367            operand,
5368            branches,
5369            else_branch,
5370        } => {
5371            if let Some(o) = operand {
5372                collect_aggregates(o, out);
5373            }
5374            for (w, t) in branches {
5375                collect_aggregates(w, out);
5376                collect_aggregates(t, out);
5377            }
5378            if let Some(e) = else_branch {
5379                collect_aggregates(e, out);
5380            }
5381        }
5382    }
5383}
5384
5385pub(crate) fn update_state(
5386    st: &mut AggState,
5387    kind: AggKind,
5388    name: &str,
5389    v: &Value<'_>,
5390    arg2: Option<&Value<'_>>,
5391    order_keys: Option<Vec<Value<'static>>>,
5392    enum_labels: Option<&[String]>,
5393    // v7.39 (round 690) — the argument column's collation, beside
5394    // `enum_labels` because it is the same kind of fact about the argument.
5395    arg_collation: Option<&str>,
5396    mysql: bool,
5397) -> Result<(), EvalError> {
5398    let is_null = matches!(v, Value::Null);
5399    // v7.37.4 (R34) — dispatch by pre-classified `kind` (`Copy`
5400    // enum), not by per-row string match. Hot inner loop on
5401    // multi-aggregate queries (mailrs `/api/conversations`: 14
5402    // aggregates × 100 k rows = 1.4 M dispatches) sees an enum
5403    // jump table instead of a sequence of `eq_str` checks. `name`
5404    // is still threaded through for error messages so the user-
5405    // facing wording is unchanged.
5406    match kind {
5407        AggKind::CountStar => st.num.count += 1,
5408        AggKind::Count => {
5409            if !is_null {
5410                st.num.count += 1;
5411            }
5412        }
5413        AggKind::Sum | AggKind::Avg => {
5414            // v7.39 (round 665) — was a hand-copied duplicate of `acc_cell`,
5415            // arm for arm, down to the wording of the type error. Verified
5416            // equivalent before collapsing: same nine variants, same error,
5417            // and the two apparent differences are both unobservable — this
5418            // one counted before the match so a value that errors bumped the
5419            // count first (the error aborts the query, so it is discarded),
5420            // and its `is_null` early return is literally
5421            // `matches!(v, Value::Null)`, which is the arm `acc_cell` has.
5422            //
5423            // Round 626 had to add a SMALLINT arm HERE that the other three
5424            // copies already carried; `SELECT sum(x)` over a smallint column
5425            // answered "sum/avg need numeric, got smallint" until then. That
5426            // is the failure mode this collapse removes.
5427            acc_cell(&mut st.num, v)?;
5428        }
5429        AggKind::Min => {
5430            if is_null {
5431                return Ok(());
5432            }
5433            if !mysql && min_max_unsupported_type(v) {
5434                return Err(EvalError::TypeMismatch {
5435                    detail: format!(
5436                        "function min({}) does not exist",
5437                        crate::conversions::pg_type_name_for_error_opt(v.data_type())
5438                    ),
5439                });
5440            }
5441            match &st.extreme {
5442                None => st.extreme = Some(v.clone().into_owned()),
5443                Some(cur) => {
5444                    if extreme_cmp_in(enum_labels, arg_collation, v, cur, mysql)
5445                        == core::cmp::Ordering::Less
5446                    {
5447                        st.extreme = Some(v.clone().into_owned());
5448                    }
5449                }
5450            }
5451        }
5452        AggKind::AnyValue => {
5453            if is_null {
5454                return Ok(());
5455            }
5456            if st.extreme.is_none() {
5457                st.extreme = Some(v.clone().into_owned());
5458            }
5459        }
5460        AggKind::RangeAgg => {
5461            if is_null {
5462                return Ok(());
5463            }
5464            let Value::Range {
5465                kind,
5466                lower,
5467                upper,
5468                lower_inc,
5469                upper_inc,
5470                empty,
5471            } = v
5472            else {
5473                return Err(EvalError::TypeMismatch {
5474                    detail: format!(
5475                        "range_agg requires a range value, got {}",
5476                        crate::conversions::pg_type_name_for_error_opt(v.data_type())
5477                    ),
5478                });
5479            };
5480            // Initialise the accumulator on first sight (even for
5481            // an empty range, so all-empty groups finalize to {}).
5482            if st.extreme.is_none() {
5483                st.extreme = Some(Value::Multirange {
5484                    kind: *kind,
5485                    ranges: alloc::vec::Vec::new(),
5486                });
5487            }
5488            if !empty && let Some(Value::Multirange { ranges, .. }) = &mut st.extreme {
5489                ranges.push(spg_storage::RangeSpan {
5490                    lower: lower.clone(),
5491                    upper: upper.clone(),
5492                    lower_inc: *lower_inc,
5493                    upper_inc: *upper_inc,
5494                    empty: false,
5495                });
5496            }
5497        }
5498        AggKind::RangeIntersectAgg => {
5499            if is_null {
5500                return Ok(());
5501            }
5502            if !matches!(v, Value::Range { .. }) {
5503                return Err(EvalError::TypeMismatch {
5504                    detail: format!(
5505                        "range_intersect_agg requires a range value, got {}",
5506                        crate::conversions::pg_type_name_for_error_opt(v.data_type())
5507                    ),
5508                });
5509            }
5510            match &st.extreme {
5511                None => st.extreme = Some(v.clone().into_owned()),
5512                Some(prev) => {
5513                    st.extreme = Some(range_intersect(prev, &v.clone().into_owned()));
5514                }
5515            }
5516        }
5517        AggKind::Max => {
5518            if is_null {
5519                return Ok(());
5520            }
5521            if !mysql && min_max_unsupported_type(v) {
5522                return Err(EvalError::TypeMismatch {
5523                    detail: format!(
5524                        "function max({}) does not exist",
5525                        crate::conversions::pg_type_name_for_error_opt(v.data_type())
5526                    ),
5527                });
5528            }
5529            match &st.extreme {
5530                None => st.extreme = Some(v.clone().into_owned()),
5531                Some(cur) => {
5532                    if extreme_cmp_in(enum_labels, arg_collation, v, cur, mysql)
5533                        == core::cmp::Ordering::Greater
5534                    {
5535                        st.extreme = Some(v.clone().into_owned());
5536                    }
5537                }
5538            }
5539        }
5540        // v7.17.0 — string_agg(value, separator). NULL value is
5541        // skipped (PG aggregate-skip-null). v7.39 (round 762,
5542        // F31-C2) — the separator is PER ROW in PG (the old note's
5543        // "using the last value at finalize" claim was measured
5544        // false): each surviving item records its own row's
5545        // separator in `item_seps`; the `separator` snapshot stays
5546        // for the constant-path consumers. count is bumped so we can
5547        // distinguish "empty group → NULL" from "all-NULL group →
5548        // NULL".
5549        AggKind::StringAgg => {
5550            let has_arg2 = arg2.is_some();
5551            if let Some(sep) = arg2
5552                && let Value::Text(s) = sep
5553            {
5554                st.separator = Some(s.to_string());
5555            }
5556            if is_null {
5557                return Ok(());
5558            }
5559            // Text collects as-is; other scalars coerce to their
5560            // text rendering (MySQL group_concat semantics — also
5561            // matches PG's cast-then-aggregate idiom for
5562            // string_agg(v::text, sep)).
5563            let rendered = render_string_agg_item(v);
5564            if let Some(item) = rendered {
5565                st.items.push(item);
5566                // v7.39 (round 762, F31-C2) — the row's own separator
5567                // rides with its item (NULL separator → None → empty).
5568                if has_arg2 {
5569                    st.item_seps.push(match arg2 {
5570                        Some(Value::Text(sp)) => Some(sp.to_string()),
5571                        _ => None,
5572                    });
5573                }
5574                if let Some(k) = order_keys {
5575                    st.item_keys.extend(k);
5576                }
5577                st.num.count += 1;
5578            } else {
5579                return Err(EvalError::TypeMismatch {
5580                    detail: format!(
5581                        "string_agg requires text value, got {}",
5582                        crate::conversions::pg_type_name_for_error_opt(v.data_type())
5583                    ),
5584                });
5585            }
5586        }
5587        // v7.17.0 — array_agg(value). Unlike string_agg, NULL
5588        // elements are KEPT in the array (PG behaviour); the
5589        // result is NULL only when ZERO rows fed in. Element type
5590        // is locked from the first row's value type; subsequent
5591        // rows must match (PG also rejects mixed-type array_agg).
5592        AggKind::ArrayAgg => {
5593            st.items.push(v.clone().into_owned());
5594            if let Some(k) = order_keys {
5595                st.item_keys.extend(k);
5596            }
5597            st.num.count += 1;
5598        }
5599        // v7.17.0 — bool_and(p): TRUE iff every non-NULL input is
5600        // TRUE. NULL skipped; running accumulator stays at TRUE
5601        // until the first non-NULL FALSE.
5602        AggKind::BoolAnd => {
5603            if is_null {
5604                return Ok(());
5605            }
5606            let b = match v {
5607                Value::Bool(b) => *b,
5608                other => {
5609                    return Err(EvalError::TypeMismatch {
5610                        detail: format!(
5611                            "bool_and requires bool, got {}",
5612                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
5613                        ),
5614                    });
5615                }
5616            };
5617            st.bool_acc = Some(st.bool_acc.map_or(b, |acc| acc && b));
5618        }
5619        // v7.17.0 — bool_or(p): TRUE iff any non-NULL input is
5620        // TRUE. NULL skipped.
5621        AggKind::BoolOr => {
5622            if is_null {
5623                return Ok(());
5624            }
5625            let b = match v {
5626                Value::Bool(b) => *b,
5627                other => {
5628                    return Err(EvalError::TypeMismatch {
5629                        detail: format!(
5630                            "bool_or requires bool, got {}",
5631                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
5632                        ),
5633                    });
5634                }
5635            };
5636            st.bool_acc = Some(st.bool_acc.map_or(b, |acc| acc || b));
5637        }
5638        // v7.32 (round-29) — variance / stddev family. Accumulate the
5639        // running sum (sum_float) and sum of squares (sum_sq) over the
5640        // non-NULL numeric inputs; finalize divides by n or n-1.
5641        AggKind::StddevFamily => {
5642            if is_null {
5643                return Ok(());
5644            }
5645            // v7.38 (read01) — keep an exact NUMERIC Σx / Σx² alongside the f64
5646            // pair for as long as every input is exact; a float input abandons it.
5647            if !st.stddev_saw_float {
5648                // v7.39 (round 615) — an integer input stays in i128, which is
5649                // exact and allocates nothing. Anything else, or an overflow,
5650                // spends the fast accumulator into the BigNumeric pair and
5651                // takes the old path from there.
5652                let as_int = match v {
5653                    Value::SmallInt(n) => Some(i128::from(*n)),
5654                    Value::Int(n) => Some(i128::from(*n)),
5655                    Value::BigInt(n) => Some(i128::from(*n)),
5656                    _ => None,
5657                };
5658                let folded = if st.stddev_i_spent {
5659                    None
5660                } else if let Some(x) = as_int {
5661                    match (
5662                        st.stddev_i_sum.checked_add(x),
5663                        x.checked_mul(x)
5664                            .and_then(|xx| st.stddev_i_sum_sq.checked_add(xx)),
5665                    ) {
5666                        (Some(s), Some(sq)) => {
5667                            st.stddev_i_sum = s;
5668                            st.stddev_i_sum_sq = sq;
5669                            Some(())
5670                        }
5671                        _ => None,
5672                    }
5673                } else {
5674                    None
5675                };
5676                if folded.is_none() {
5677                    spend_stddev_i128(st);
5678                    match crate::eval::binop::value_to_bignum(v) {
5679                        Some(b) => {
5680                            let sq = b.mul(&b);
5681                            st.stddev_sum = Some(
5682                                st.stddev_sum
5683                                    .as_ref()
5684                                    .map_or_else(|| b.clone(), |s| s.add(&b)),
5685                            );
5686                            st.stddev_sum_sq = Some(
5687                                st.stddev_sum_sq
5688                                    .as_ref()
5689                                    .map_or_else(|| sq.clone(), |s| s.add(&sq)),
5690                            );
5691                        }
5692                        None => st.stddev_saw_float = true,
5693                    }
5694                }
5695            }
5696            let Some(x) = agg_value_to_f64(v) else {
5697                return Err(EvalError::TypeMismatch {
5698                    detail: format!(
5699                        "{name} needs numeric, got {}",
5700                        crate::conversions::pg_type_name_for_error_opt(v.data_type())
5701                    ),
5702                });
5703            };
5704            st.num.count += 1;
5705            st.num.sum_float += x;
5706            st.sum_sq += x * x;
5707        }
5708        // v7.32 (round-29) — bitwise aggregates over integer inputs.
5709        AggKind::BitAnd | AggKind::BitOr | AggKind::BitXor => {
5710            if is_null {
5711                return Ok(());
5712            }
5713            let n = match v {
5714                Value::Int(n) => i64::from(*n),
5715                Value::SmallInt(n) => i64::from(*n),
5716                Value::BigInt(n) => *n,
5717                other => {
5718                    return Err(EvalError::TypeMismatch {
5719                        detail: format!(
5720                            "{name} needs integer, got {}",
5721                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
5722                        ),
5723                    });
5724                }
5725            };
5726            if matches!(v, Value::BigInt(_)) {
5727                st.bit_wide = true;
5728            }
5729            st.bit_acc = Some(match (st.bit_acc, kind) {
5730                (None, _) => n,
5731                (Some(acc), AggKind::BitAnd) => acc & n,
5732                (Some(acc), AggKind::BitOr) => acc | n,
5733                (Some(acc), _) => acc ^ n, // BitXor
5734            });
5735        }
5736        // v7.32 (round-29) — WITHIN GROUP aggregates (ordered-set +
5737        // hypothetical-set) collect the sort value (NULLs ignored, per
5738        // PG) into `items`, sorted at finalize by the parallel
5739        // `item_keys`.
5740        AggKind::WithinGroup => {
5741            // Counted before the NULL skip: the hypothetical-set
5742            // fractions divide by the full input size (PG).
5743            st.within_group_rows += 1;
5744            if is_null {
5745                return Ok(());
5746            }
5747            st.items.push(v.clone().into_owned());
5748            if let Some(k) = order_keys {
5749                st.item_keys.extend(k);
5750            }
5751            st.num.count += 1;
5752        }
5753        // v7.32 (round-29) — regression family f(Y, X). Only rows with
5754        // BOTH inputs non-NULL contribute (PG semantics). `v` is Y,
5755        // `arg2` is X.
5756        AggKind::Regression => {
5757            let (Some(y), Some(x)) = (agg_value_to_f64(v), arg2.and_then(agg_value_to_f64)) else {
5758                return Ok(()); // NULL (or non-numeric) in either input
5759            };
5760            // v7.39 (read01 round 115) — accumulate the sums of squared
5761            // deviations (Sxx / Syy / Sxy) incrementally via the Youngs-Cramer
5762            // update, matching PG's float8 regression aggregates to the last
5763            // ULP. The old naive form (`Σx² − (Σx)²/n` at finalize time) is
5764            // mathematically equal but rounds differently, so `corr` drifted in
5765            // the 16th digit. reg_sx / reg_sy stay raw sums (for the averages).
5766            st.reg_n += 1;
5767            let new_n = st.reg_n as f64;
5768            let new_sx = st.reg_sx + x;
5769            let new_sy = st.reg_sy + y;
5770            if st.reg_n > 1 {
5771                let n_prev = new_n - 1.0;
5772                let tmp_x = x * new_n - new_sx;
5773                let tmp_y = y * new_n - new_sy;
5774                let scale = 1.0 / (n_prev * new_n);
5775                st.reg_sxx += tmp_x * tmp_x * scale;
5776                st.reg_syy += tmp_y * tmp_y * scale;
5777                st.reg_sxy += tmp_x * tmp_y * scale;
5778            }
5779            st.reg_sx = new_sx;
5780            st.reg_sy = new_sy;
5781        }
5782        // v7.32 (round-29) — json_agg / jsonb_agg collect every input
5783        // (NULL becomes JSON null, per PG) in row order.
5784        AggKind::JsonAgg => {
5785            // v7.39 (read01 json.c) — the _strict variants skip NULLs.
5786            if is_null && name.ends_with("_strict") {
5787                return Ok(());
5788            }
5789            st.items.push(v.clone().into_owned());
5790            // Attach the ORDER BY key so finalize_synth_rows sorts the
5791            // elements (`json_agg(x ORDER BY x DESC)`), the same way
5792            // string_agg / array_agg do.
5793            if let Some(k) = order_keys {
5794                st.item_keys.extend(k);
5795            }
5796            st.num.count += 1;
5797        }
5798        // v7.32 (round-29) — json_object_agg(key, value): keys in
5799        // `items`, values in `aux_items`. A NULL key is skipped (PG
5800        // raises; we drop it rather than abort the whole query).
5801        AggKind::JsonObjectAgg => {
5802            if is_null {
5803                return Ok(());
5804            }
5805            // v7.39 (read01 json.c) — _strict skips NULL VALUES; _unique
5806            // raises PG's duplicate-key error.
5807            let val = arg2.cloned().map(Value::into_owned).unwrap_or(Value::Null);
5808            if matches!(val, Value::Null) && name.contains("_strict") {
5809                return Ok(());
5810            }
5811            if name.contains("_unique") {
5812                let kt = match v {
5813                    Value::Text(s) | Value::Json(s) => s.to_string(),
5814                    other => crate::json::value_to_json_text(other),
5815                };
5816                let dup = st.items.iter().any(|k| match k {
5817                    Value::Text(s) | Value::Json(s) => *s == kt,
5818                    other => crate::json::value_to_json_text(other) == kt,
5819                });
5820                if dup {
5821                    return Err(EvalError::TypeMismatch {
5822                        detail: alloc::format!("duplicate JSON object key value: {kt:?}"),
5823                    });
5824                }
5825            }
5826            st.items.push(v.clone().into_owned());
5827            st.aux_items.push(val);
5828            st.num.count += 1;
5829        }
5830    }
5831    Ok(())
5832}
5833
5834#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
5835pub(crate) fn finalize(name: &str, st: &AggState, mysql: bool) -> Value<'static> {
5836    match name {
5837        "count" | "count_star" => Value::BigInt(st.num.count),
5838        "sum" => {
5839            if st.num.count == 0 {
5840                Value::Null
5841            } else if st.num.use_interval {
5842                Value::Interval {
5843                    months: st.num.sum_iv_months as i32,
5844                    days: st.num.sum_iv_days as i32,
5845                    micros: st.num.sum_iv_micros as i64,
5846                    kind: spg_storage::IntervalKind::Finite,
5847                }
5848            } else if st.num.use_money {
5849                Value::Money(st.num.sum_money as i64)
5850            } else if st.num.use_numeric {
5851                // v7.38 (read01, T6.P3) — a NaN / ±Infinity input propagates.
5852                if st.num.sum_num_kind != spg_storage::NumericKind::Finite {
5853                    Value::numeric_special(st.num.sum_num_kind)
5854                } else if let Some(big) = &st.num.sum_big {
5855                    // v7.39 (read01 numeric.c) — the sum spilled past i128;
5856                    // fold in the int lane and render exactly.
5857                    let tot = big.add(&spg_storage::bignum::BigNumeric::from_i128(
5858                        i128::from(st.num.sum_int),
5859                        0,
5860                    ));
5861                    crate::eval::binop::bignum_to_value(tot)
5862                } else {
5863                    let (scaled, scale) = crate::numeric::numeric_add(
5864                        st.num.sum_num_scaled,
5865                        st.num.sum_num_scale,
5866                        i128::from(st.num.sum_int),
5867                        0,
5868                    );
5869                    Value::Numeric {
5870                        scaled,
5871                        scale,
5872                        kind: spg_storage::NumericKind::Finite,
5873                    }
5874                }
5875            } else if st.num.use_float {
5876                let total = st.num.sum_float + (st.num.sum_int as f64);
5877                // v7.39 (round 269) — sum over REAL input stays real in
5878                // PG; it widens only when something wider joined the
5879                // accumulation. avg is deliberately not the same:
5880                // avg(real) IS double precision (measured on 18.4).
5881                if st.num.float_not_real {
5882                    Value::Float(total)
5883                } else {
5884                    #[allow(clippy::cast_possible_truncation)]
5885                    Value::Real(total as f32)
5886                }
5887            } else {
5888                Value::BigInt(st.num.sum_int)
5889            }
5890        }
5891        "avg" => {
5892            if st.num.count == 0 {
5893                Value::Null
5894            } else if st.num.use_interval {
5895                // PG interval_div: the month quotient truncates and its
5896                // remainder spills into DAYS (a month = 30 days), taking the
5897                // whole-day part into the day field and only the sub-day
5898                // fraction into time; the day remainder then spills into time.
5899                let n = i128::from(st.num.count);
5900                let day_us = 86_400_000_000i128;
5901                let months = i128::from(st.num.sum_iv_months);
5902                let days = i128::from(st.num.sum_iv_days);
5903                let month_out = months / n;
5904                let mrem_days_total = (months % n) * 30; // days (still over n)
5905                let days_from_month = mrem_days_total / n;
5906                let mrem_frac_us = (mrem_days_total % n) * day_us / n;
5907                let day_out = days / n;
5908                let drem_us = (days % n) * day_us / n;
5909                let micros = st.num.sum_iv_micros / n + mrem_frac_us + drem_us;
5910                Value::Interval {
5911                    months: month_out as i32,
5912                    days: (day_out + days_from_month) as i32,
5913                    micros: micros as i64,
5914                    kind: spg_storage::IntervalKind::Finite,
5915                }
5916            } else if st.num.use_money {
5917                // PG has no avg(money); we accept it as a sensible superset —
5918                // average of the cent totals, rounded half-away-from-zero.
5919                //
5920                // DELIBERATE. Round 664 read "PG refuses, SPG answers" off
5921                // the F29 list and wrote guards on four accumulators to
5922                // remove this before a test caught it. Per the round-641
5923                // policy such a divergence is judged by correctness risk,
5924                // and this one carries none: money IS cents, so rounding is
5925                // the type's granularity rather than a loss introduced
5926                // here, and no PG application can reach the shape, because
5927                // PG rejects it. Pinned at eight shapes in
5928                // `e2e_avg_money_round664`.
5929                let n = i128::from(st.num.count);
5930                let q =
5931                    (st.num.sum_money * 2 + if st.num.sum_money >= 0 { n } else { -n }) / (2 * n);
5932                Value::Money(q as i64)
5933            } else if st.num.use_numeric {
5934                // v7.38 (read01, T6.P3) — avg of a special is that special
5935                // (NaN→NaN, ±Inf→±Inf); PG matches.
5936                if st.num.sum_num_kind != spg_storage::NumericKind::Finite {
5937                    Value::numeric_special(st.num.sum_num_kind)
5938                } else if let Some(big) = &st.num.sum_big {
5939                    // v7.39 (read01 numeric.c) — bignum avg = spilled sum /
5940                    // count at PG's division display scale.
5941                    use spg_storage::bignum::BigNumeric;
5942                    let sum_tot = big.add(&BigNumeric::from_i128(i128::from(st.num.sum_int), 0));
5943                    let cnt = BigNumeric::from_i128(i128::from(st.num.count), 0);
5944                    let rscale = crate::numeric::division_display_scale_big(&sum_tot, &cnt);
5945                    match sum_tot.div(&cnt, rscale) {
5946                        Some(q) => crate::eval::binop::bignum_to_value(q),
5947                        None => Value::Null,
5948                    }
5949                } else {
5950                    let (sum_scaled, sum_scale) = crate::numeric::numeric_add(
5951                        st.num.sum_num_scaled,
5952                        st.num.sum_num_scale,
5953                        i128::from(st.num.sum_int),
5954                        0,
5955                    );
5956                    let (scaled, scale) = crate::numeric::numeric_avg(
5957                        sum_scaled,
5958                        sum_scale,
5959                        i128::from(st.num.count),
5960                    );
5961                    Value::Numeric {
5962                        scaled,
5963                        scale,
5964                        kind: spg_storage::NumericKind::Finite,
5965                    }
5966                }
5967            } else if st.num.use_float {
5968                Value::Float((st.num.sum_float + (st.num.sum_int as f64)) / (st.num.count as f64))
5969            } else {
5970                // v7.38 (read01, T4) — avg over integer input is exact NUMERIC
5971                // (PG: avg(int)/avg(bigint) → numeric), at PG's division display
5972                // scale. sum(int) is unaffected (it reads sum_int as BigInt).
5973                let (scaled, scale) = crate::numeric::numeric_avg(
5974                    i128::from(st.num.sum_int),
5975                    0,
5976                    i128::from(st.num.count),
5977                );
5978                Value::Numeric {
5979                    scaled,
5980                    scale,
5981                    kind: spg_storage::NumericKind::Finite,
5982                }
5983            }
5984        }
5985        "min" | "max" | "any_value" => st.extreme.clone().unwrap_or(Value::Null),
5986        // PG: range_agg over an empty group is NULL; all-empty
5987        // ranges finalize to the empty multirange {}.
5988        // v7.39 (round 231) — range_agg collects its inputs verbatim while
5989        // accumulating; PG's result is a *normalized* multirange, so the
5990        // spans are sorted, merged where they overlap or abut, and emptied
5991        // ones dropped exactly once, here. Without this
5992        // `range_agg` over `[1,3),[5,9),[2,6)` answered all three spans
5993        // where PG answers the single `{[1,9)}` they cover.
5994        "range_agg" => match st.extreme.clone() {
5995            Some(Value::Multirange { kind, ranges }) => Value::Multirange {
5996                kind,
5997                ranges: crate::eval::binop::normalize_multirange_spans(kind, &ranges),
5998            },
5999            other => other.unwrap_or(Value::Null),
6000        },
6001        "range_intersect_agg" => st.extreme.clone().unwrap_or(Value::Null),
6002        // v7.17.0 — string_agg: join all collected text items with
6003        // the captured separator. Empty / all-NULL group → NULL
6004        // (PG semantics).
6005        "string_agg" | "group_concat" | "xmlagg" => {
6006            if st.items.is_empty() {
6007                return Value::Null;
6008            }
6009            // group_concat defaults to ',' (MySQL); xmlagg and a
6010            // separator-less string_agg join bare.
6011            let sep = st.separator.clone().unwrap_or_else(|| {
6012                if name == "group_concat" {
6013                    ",".into()
6014                } else {
6015                    String::new()
6016                }
6017            });
6018            // v7.39 (round 762, F31-C2) — per-row separators, when the
6019            // accumulate path carried them (aligned with items).
6020            let per_row: Option<&[Option<String>]> =
6021                if !st.item_seps.is_empty() && st.item_seps.len() == st.items.len() {
6022                    Some(&st.item_seps)
6023                } else {
6024                    None
6025                };
6026            let mut out = String::new();
6027            for (i, item) in st.items.iter().enumerate() {
6028                if i > 0 {
6029                    match per_row {
6030                        Some(seps) => {
6031                            if let Some(sp) = &seps[i] {
6032                                out.push_str(sp);
6033                            }
6034                        }
6035                        None => out.push_str(&sep),
6036                    }
6037                }
6038                match item {
6039                    Value::Text(s) => out.push_str(s),
6040                    // MySQL group_concat coerces scalars to text;
6041                    // harmless for string_agg (typed inputs are
6042                    // Text already).
6043                    Value::Int(n) => out.push_str(&n.to_string()),
6044                    Value::BigInt(n) => out.push_str(&n.to_string()),
6045                    Value::SmallInt(n) => out.push_str(&n.to_string()),
6046                    Value::Float(f) => out.push_str(&f.to_string()),
6047                    Value::Bool(b) => {
6048                        out.push_str(if *b { "1" } else { "0" });
6049                    }
6050                    _ => {}
6051                }
6052            }
6053            Value::text(out)
6054        }
6055        // v7.17.0 — array_agg: collect into a typed array. NULL
6056        // elements are preserved per PG. Result type is decided
6057        // by the first non-NULL element seen (or Text fallback
6058        // when the whole group is NULL — PG would surface the
6059        // declared input type, but SPG hasn't yet wired the
6060        // aggregate's static input-type from `describe`).
6061        // v7.39 (read01 round 73) — ONE builder, shared with the `ARRAY[…]`
6062        // literal. This finalize used to dispatch on the first non-NULL element
6063        // with arms for int and bigint and a text fallback for everything else,
6064        // so `array_agg(bool_col)` came back as text[] — the same fallback-in-
6065        // place-of-a-decision that rounds 71/72 dug out of the literal path and
6066        // the array functions. Fifth site; now there is only one.
6067        "array_agg" => {
6068            if st.items.is_empty() {
6069                return Value::Null;
6070            }
6071            crate::eval::values::build_array_from_values(&st.items)
6072        }
6073        "bool_and" | "bool_or" => st.bool_acc.map_or(Value::Null, Value::Bool),
6074        // v7.32 (round-29) — variance / stddev. PG: `variance` ==
6075        // `var_samp`, `stddev` == `stddev_samp`. samp needs n >= 2
6076        // (n < 2 → NULL); pop needs n >= 1 (n == 1 → 0).
6077        "variance" | "var_samp" | "var_pop" | "stddev" | "stddev_samp" | "stddev_pop" => {
6078            let n = st.num.count;
6079            if n == 0 {
6080                return Value::Null;
6081            }
6082            let nf = n as f64;
6083            // v7.39 (round 381) — MySQL's bare STDDEV / VARIANCE are the
6084            // POPULATION statistics (`STDDEV` = `STDDEV_POP`, `VARIANCE` =
6085            // `VAR_POP` on MariaDB 11), where PG's bare forms are the
6086            // SAMPLE ones. `_samp` / `_pop` are explicit and unchanged.
6087            let pop = name.ends_with("_pop") || (mysql && (name == "stddev" || name == "variance"));
6088            if !pop && n < 2 {
6089                // var_samp / stddev (samp) with n == 1 → NULL.
6090                return Value::Null;
6091            }
6092            // v7.38 (read01) — over exact inputs PG's numeric overload applies:
6093            // variance = (N·Σx² − (Σx)²) / (N² | N·(N−1)) using numeric division's
6094            // display scale, and stddev is its numeric sqrt. Falls through to the
6095            // f64 path (a double result, PG's float8 overload) on a float input.
6096            if !st.stddev_saw_float {
6097                // v7.39 (round 615) — fold whatever the i128 accumulator holds
6098                // into the exact pair, once, here.
6099                if let Some((sum, sum_sq)) = stddev_exact_pair(st) {
6100                    let (sum, sum_sq) = (&sum, &sum_sq);
6101                    use spg_storage::bignum::BigNumeric as BN;
6102                    let nb = BN::from_i128(i128::from(n), 0);
6103                    let numerator = nb.mul(sum_sq).sub(&sum.mul(sum));
6104                    let divisor = if pop {
6105                        nb.mul(&nb)
6106                    } else {
6107                        nb.mul(&BN::from_i128(i128::from(n - 1), 0))
6108                    };
6109                    // PG returns a bare `0` (scale 0) for a zero / clamped-negative
6110                    // numerator rather than the division's padded zero.
6111                    if numerator.is_zero() || numerator.parts().0 {
6112                        return Value::Numeric {
6113                            scaled: 0,
6114                            scale: 0,
6115                            kind: spg_storage::NumericKind::Finite,
6116                        };
6117                    }
6118                    let rscale = crate::numeric::division_display_scale_big(&numerator, &divisor);
6119                    if let Some(var) = numerator.div(&divisor, rscale) {
6120                        let out = if name.starts_with("stddev") {
6121                            var.sqrt(crate::numeric::sqrt_display_scale_big(&var))
6122                        } else {
6123                            Some(var)
6124                        };
6125                        if let Some(o) = out {
6126                            return crate::eval::binop::bignum_to_value(o);
6127                        }
6128                    }
6129                }
6130            }
6131            // Match PG's float8 accumulator operation order exactly
6132            // (utils/adt/float.c float8_var_pop / _samp): the numerator
6133            // is `N*Σx² - (Σx)²` and the divisor is `N²` (pop) or
6134            // `N*(N-1)` (samp). SPG previously used the algebraically
6135            // equal `(Σx² - (Σx)²/N) / denom`, whose different float
6136            // rounding drifted a ULP from PG on stddev (only masked
6137            // before by an imprecise hand-rolled sqrt).
6138            let numerator = (nf * st.sum_sq - st.num.sum_float * st.num.sum_float).max(0.0);
6139            let divisor = if pop { nf * nf } else { nf * (nf - 1.0) };
6140            let var = numerator / divisor;
6141            let result = if name.starts_with("stddev") {
6142                crate::eval::f64_sqrt(var)
6143            } else {
6144                var
6145            };
6146            // A float input resolves PG's float8 overload → double precision.
6147            Value::Float(result)
6148        }
6149        // v7.32 (round-29) — bitwise aggregates: None (empty / all-NULL)
6150        // → SQL NULL.
6151        "bit_and" | "bit_or" | "bit_xor" => st.bit_acc.map_or(Value::Null, |acc| {
6152            if st.bit_wide {
6153                Value::BigInt(acc)
6154            } else {
6155                Value::Int(acc as i32)
6156            }
6157        }),
6158        // v7.32 (round-29) — regression family. `regr_count` is the
6159        // paired n; everything else is NULL over an empty set. Terms
6160        // are the mean-centred sums of squares / cross-products.
6161        "regr_count" => Value::BigInt(st.reg_n),
6162        "covar_pop" | "covar_samp" | "corr" | "regr_avgx" | "regr_avgy" | "regr_slope"
6163        | "regr_intercept" | "regr_r2" | "regr_sxx" | "regr_syy" | "regr_sxy" => {
6164            let n = st.reg_n;
6165            if n == 0 {
6166                return Value::Null;
6167            }
6168            let nf = n as f64;
6169            // v7.39 (read01 round 115) — Sxx / Syy / Sxy are now the
6170            // Youngs-Cramer running deviation sums (accumulated above), so they
6171            // are used directly rather than re-derived from the raw squares.
6172            let sxx = st.reg_sxx;
6173            let syy = st.reg_syy;
6174            let sxy = st.reg_sxy;
6175            let avgx = st.reg_sx / nf;
6176            let avgy = st.reg_sy / nf;
6177            let out = match name {
6178                "regr_avgx" => Some(avgx),
6179                "regr_avgy" => Some(avgy),
6180                "regr_sxx" => Some(sxx),
6181                "regr_syy" => Some(syy),
6182                "regr_sxy" => Some(sxy),
6183                "covar_pop" => Some(sxy / nf),
6184                "covar_samp" => (n >= 2).then(|| sxy / (nf - 1.0)),
6185                "regr_slope" => (sxx != 0.0).then(|| sxy / sxx),
6186                "regr_intercept" => (sxx != 0.0).then(|| avgy - (sxy / sxx) * avgx),
6187                "corr" => {
6188                    let d = sxx * syy;
6189                    (d > 0.0).then(|| sxy / crate::eval::f64_sqrt(d))
6190                }
6191                // PG: NULL when sxx==0; 1 when syy==0 (and sxx>0).
6192                "regr_r2" => {
6193                    if sxx == 0.0 {
6194                        None
6195                    } else if syy == 0.0 {
6196                        Some(1.0)
6197                    } else {
6198                        Some((sxy * sxy) / (sxx * syy))
6199                    }
6200                }
6201                _ => None,
6202            };
6203            out.map_or(Value::Null, Value::Float)
6204        }
6205        // v7.32 (round-29) — json_agg / jsonb_agg: a JSON array of every
6206        // collected element in row order; empty set → SQL NULL.
6207        "json_agg" | "jsonb_agg" | "json_arrayagg" | "json_agg_strict" | "jsonb_agg_strict" => {
6208            if st.items.is_empty() {
6209                return Value::Null;
6210            }
6211            let mut out = String::from("[");
6212            for (i, item) in st.items.iter().enumerate() {
6213                if i > 0 {
6214                    out.push_str(", ");
6215                }
6216                out.push_str(&crate::json::value_to_json_text(item));
6217            }
6218            out.push(']');
6219            // jsonb_agg yields canonical jsonb (nested object keys sorted,
6220            // numbers normalised); json_agg keeps the input verbatim.
6221            let result = Value::json(out);
6222            if name.starts_with("jsonb_agg") {
6223                crate::json::canonicalize_value(result)
6224            } else {
6225                result
6226            }
6227        }
6228        // v7.32 (round-29) — json_object_agg: a JSON object built from
6229        // the parallel key (`items`) / value (`aux_items`) streams.
6230        "json_object_agg"
6231        | "jsonb_object_agg"
6232        | "json_objectagg"
6233        | "json_object_agg_strict"
6234        | "jsonb_object_agg_strict"
6235        | "json_object_agg_unique"
6236        | "jsonb_object_agg_unique"
6237        | "json_object_agg_unique_strict"
6238        | "jsonb_object_agg_unique_strict" => {
6239            if st.items.is_empty() {
6240                return Value::Null;
6241            }
6242            // Object keys are always JSON strings (PG coerces).
6243            let key_text = |key: &Value| -> String {
6244                match key {
6245                    Value::Text(s) | Value::Json(s) => s.to_string(),
6246                    other => crate::json::value_to_json_text(other),
6247                }
6248            };
6249            // jsonb dedups keys keeping the last value (jsonb is a
6250            // map); json preserves every pair including duplicates.
6251            let dedup = name.starts_with("jsonb_object_agg");
6252            // (key, value-index) pairs in first-seen key order; for
6253            // jsonb a repeated key updates its value-index in place.
6254            let mut pairs: Vec<(String, usize)> = Vec::with_capacity(st.items.len());
6255            for (i, key) in st.items.iter().enumerate() {
6256                let kt = key_text(key);
6257                if dedup {
6258                    if let Some(slot) = pairs.iter_mut().find(|(k, _)| *k == kt) {
6259                        slot.1 = i;
6260                        continue;
6261                    }
6262                }
6263                pairs.push((kt, i));
6264            }
6265            // v7.39 (read01 json.c) — PG's json_object_agg emits the
6266            // distinctive "{ \"k\" : v, ... }" spacing (jsonb variants
6267            // canonicalize it away below).
6268            let mut out = String::from("{ ");
6269            for (n, (kt, i)) in pairs.iter().enumerate() {
6270                if n > 0 {
6271                    out.push_str(", ");
6272                }
6273                out.push_str(&crate::json::value_to_json_text(&Value::text(kt.clone())));
6274                out.push_str(" : ");
6275                let val = st.aux_items.get(*i).unwrap_or(&Value::Null);
6276                out.push_str(&crate::json::value_to_json_text(val));
6277            }
6278            out.push_str(" }");
6279            // jsonb_object_agg emits canonical jsonb — keys sorted by PG's
6280            // (length, byte) order; json_object_agg keeps first-seen order.
6281            let result = Value::json(out);
6282            if dedup {
6283                crate::json::canonicalize_value(result)
6284            } else {
6285                result
6286            }
6287        }
6288        // Ordered-set aggregates are finalized in `run` (they need the
6289        // sorted items + the direct fraction argument), never here.
6290        _ => unreachable!(),
6291    }
6292}
6293
6294/// v7.32 (round-29) — numeric coercion for the percentile interpolation.
6295fn agg_value_to_f64(v: &Value) -> Option<f64> {
6296    match v {
6297        Value::Int(n) => Some(f64::from(*n)),
6298        Value::SmallInt(n) => Some(f64::from(*n)),
6299        Value::BigInt(n) => Some(*n as f64),
6300        Value::Float(x) => Some(*x),
6301        Value::Real(x) => Some(f64::from(*x)),
6302        Value::Numeric { scaled, scale, .. } => Some(numeric_to_f64(*scaled, *scale)),
6303        _ => None,
6304    }
6305}
6306
6307/// The array form of a `percentile_cont/disc` direct argument
6308/// (`percentile_cont(ARRAY[0.25,0.5,0.75])`), as f64 fractions. `None` when the
6309/// direct argument is a plain scalar fraction. A NULL element stays `None` —
6310/// PG yields a NULL result element for it.
6311fn percentile_fraction_array(v: Option<&Value>) -> Option<Vec<Option<f64>>> {
6312    match v? {
6313        Value::FloatArray(a) => Some(a.clone()),
6314        Value::NumericArray(a) => Some(
6315            a.iter()
6316                .map(|x| x.map(|(scaled, scale)| numeric_to_f64(scaled, scale)))
6317                .collect(),
6318        ),
6319        Value::IntArray(a) => Some(a.iter().map(|x| x.map(f64::from)).collect()),
6320        // Array literals (`ARRAY[0.25,0.5,0.75]`) evaluate to a TextArray of the
6321        // element renderings; parse each back to f64.
6322        Value::TextArray(a) => Some(
6323            a.iter()
6324                .map(|x| x.as_deref().and_then(|s| s.parse::<f64>().ok()))
6325                .collect(),
6326        ),
6327        _ => None,
6328    }
6329}
6330
6331/// Build an array Value from a list of scalar values, dispatching on the first
6332/// non-NULL element's type (mirrors array_agg's finalize). Used by the array
6333/// form of `percentile_disc`, whose result is an array of the ordered-column
6334/// element type.
6335fn values_to_array(picked: &[Value<'_>]) -> Value<'static> {
6336    let owned: alloc::vec::Vec<Value<'static>> =
6337        picked.iter().map(|v| v.clone().into_owned()).collect();
6338    crate::eval::values::build_array_from_values(&owned)
6339}
6340
6341/// NUMERIC → f64 for the float-math aggregates (stddev / variance / corr /
6342/// percentile_cont). `scaled × 10^-scale`; `10^scale` fits in i128 for the
6343/// NUMERIC scale range, so no `f64::powi` (unavailable under no_std) is needed.
6344#[allow(clippy::cast_precision_loss)]
6345fn numeric_to_f64(scaled: i128, scale: u16) -> f64 {
6346    (scaled as f64) / (10i128.pow(u32::from(scale)) as f64)
6347}
6348
6349/// v7.32 (round-29) — finalize a WITHIN GROUP aggregate. `st.items` is
6350/// already sorted by the `WITHIN GROUP (ORDER BY …)` spec. `direct` is
6351/// the evaluated direct argument: the fraction for `percentile_*`, the
6352/// first hypothetical value for the hypothetical-set family (`rank`
6353/// etc. — `direct_extra` carries the rest of a multi-key call), and
6354/// unused by `mode`. `order_by` is the sort spec; the hypothetical-set
6355/// family compares in the sort direction (multi-key via `st.item_keys`).
6356#[allow(
6357    clippy::cast_precision_loss,
6358    clippy::cast_possible_truncation,
6359    clippy::cast_sign_loss,
6360    clippy::too_many_lines
6361)]
6362fn finalize_ordered_set(
6363    name: &str,
6364    st: &AggState,
6365    direct: Option<&Value>,
6366    direct_extra: &[Value<'static>],
6367    order_by: &[spg_sql::ast::OrderBy],
6368    order_collations: &[Option<alloc::string::String>],
6369    mysql: bool,
6370) -> Result<Value<'static>, EvalError> {
6371    let fraction = direct;
6372    // v7.39 (read01 orderedsetaggs.c) — PG validates the percentile
6373    // fraction before looking at the rows (an out-of-range fraction
6374    // errors even over an empty group), and a NULL fraction is NULL.
6375    let check_fraction = |f: f64| -> Result<f64, EvalError> {
6376        if !(0.0..=1.0).contains(&f) || f.is_nan() {
6377            return Err(EvalError::TypeMismatch {
6378                detail: format!("percentile value {f} is not between 0 and 1"),
6379            });
6380        }
6381        Ok(f)
6382    };
6383    let scalar_fraction: Option<Result<f64, EvalError>> =
6384        if matches!(name, "percentile_cont" | "percentile_disc") {
6385            match fraction {
6386                None | Some(Value::Null) => return Ok(Value::Null),
6387                Some(v) => match percentile_fraction_array(Some(v)) {
6388                    Some(fracs) => {
6389                        for f in fracs.iter().flatten() {
6390                            check_fraction(*f)?;
6391                        }
6392                        None
6393                    }
6394                    None => Some(
6395                        agg_value_to_f64(v)
6396                            .ok_or_else(|| EvalError::TypeMismatch {
6397                                detail: format!(
6398                                    "percentile fraction must be numeric, got {}",
6399                                    crate::conversions::pg_type_name_for_error_opt(v.data_type())
6400                                ),
6401                            })
6402                            .and_then(check_fraction),
6403                    ),
6404                },
6405            }
6406        } else {
6407            None
6408        };
6409    let items = &st.items;
6410    if items.is_empty() {
6411        // A hypothetical row ranks first over an empty group; the
6412        // distribution functions are 0 / divide-by-(n+1).
6413        return Ok(match name {
6414            "rank" | "dense_rank" => Value::BigInt(1),
6415            "percent_rank" => Value::Float(0.0),
6416            "cume_dist" => Value::Float(1.0),
6417            _ => Value::Null,
6418        });
6419    }
6420    let n = items.len();
6421    Ok(match name {
6422        // v7.32 (round-29) — hypothetical-set: the rank the direct value
6423        // would have if inserted into the group, in the sort direction.
6424        "rank" | "dense_rank" | "percent_rank" | "cume_dist" => {
6425            let Some(h) = fraction else {
6426                return Ok(Value::Null);
6427            };
6428            // v7.39 (read01 orderedsetaggs.c) — the multi-key form
6429            // compares the hypothetical tuple against the collected
6430            // `item_keys` tuples with the full sort spec.
6431            let kw = order_by.len();
6432            let multi = kw > 1 && st.item_keys.len() == items.len() * kw;
6433            let hv: Vec<Value<'static>> = core::iter::once(h.clone().into_owned())
6434                .chain(direct_extra.iter().cloned())
6435                .collect();
6436            let (desc, nulls_first) = order_by
6437                .first()
6438                .map_or((false, None), |o| (o.desc, o.nulls_first));
6439            let cmp_i = |i: usize| -> core::cmp::Ordering {
6440                if multi {
6441                    cmp_order_keys(
6442                        order_by,
6443                        &[],
6444                        order_collations,
6445                        &st.item_keys[i * kw..(i + 1) * kw],
6446                        &hv,
6447                        mysql,
6448                    )
6449                } else {
6450                    crate::order_by_value_cmp_in(desc, nulls_first, &items[i], h, mysql)
6451                }
6452            };
6453            let mut before: Vec<usize> = Vec::new(); // sort strictly before h
6454            let mut before_or_eq = 0usize; // sort before-or-peer with h
6455            for i in 0..n {
6456                match cmp_i(i) {
6457                    core::cmp::Ordering::Less => {
6458                        before.push(i);
6459                        before_or_eq += 1;
6460                    }
6461                    core::cmp::Ordering::Equal => before_or_eq += 1,
6462                    core::cmp::Ordering::Greater => {}
6463                }
6464            }
6465            // PG divides by the FULL input size (NULL rows included);
6466            // `n` counts only the non-NULL values `items` holds.
6467            let nn = st.within_group_rows.max(n) as f64;
6468            match name {
6469                "rank" => Value::BigInt((before.len() + 1) as i64),
6470                "dense_rank" => {
6471                    // Count distinct sort-key tuples among the strictly-
6472                    // before rows (items arrive unsorted relative to
6473                    // item_keys in the multi-key form, so sort + dedup).
6474                    let tuple_cmp = |&x: &usize, &y: &usize| -> core::cmp::Ordering {
6475                        if multi {
6476                            cmp_order_keys(
6477                                order_by,
6478                                &[],
6479                                order_collations,
6480                                &st.item_keys[x * kw..(x + 1) * kw],
6481                                &st.item_keys[y * kw..(y + 1) * kw],
6482                                mysql,
6483                            )
6484                        } else {
6485                            value_cmp(&items[x], &items[y])
6486                        }
6487                    };
6488                    let mut sorted = before.clone();
6489                    sorted.sort_by(tuple_cmp);
6490                    let mut distinct = 0usize;
6491                    for (k, &i) in sorted.iter().enumerate() {
6492                        if k == 0 || tuple_cmp(&sorted[k - 1], &i) != core::cmp::Ordering::Equal {
6493                            distinct += 1;
6494                        }
6495                    }
6496                    Value::BigInt((distinct + 1) as i64)
6497                }
6498                "percent_rank" => Value::Float(before.len() as f64 / nn),
6499                "cume_dist" => Value::Float((before_or_eq as f64 + 1.0) / (nn + 1.0)),
6500                _ => unreachable!(),
6501            }
6502        }
6503        // Most frequent value; equal values are adjacent in the sorted
6504        // run, and a frequency tie resolves to the earliest run (the
6505        // smallest value under an ascending sort), matching PG.
6506        "mode" => {
6507            let (mut best_i, mut best_cnt) = (0usize, 1usize);
6508            let (mut run_i, mut run_cnt) = (0usize, 1usize);
6509            for i in 1..n {
6510                if value_cmp(&items[i], &items[run_i]) == core::cmp::Ordering::Equal {
6511                    run_cnt += 1;
6512                } else {
6513                    run_i = i;
6514                    run_cnt = 1;
6515                }
6516                if run_cnt > best_cnt {
6517                    best_cnt = run_cnt;
6518                    best_i = run_i;
6519                }
6520            }
6521            items[best_i].clone()
6522        }
6523        // The first value whose cumulative fraction reaches `f`. PG accepts
6524        // both a scalar fraction (→ the element) and an array of fractions (→
6525        // an array of the ordered-column element type, with NULL fractions
6526        // yielding NULL elements).
6527        "percentile_disc" => {
6528            let idx_at = |f: f64| -> usize {
6529                if f <= 0.0 {
6530                    0
6531                } else {
6532                    (crate::eval::f64_ceil(f * n as f64) as usize)
6533                        .saturating_sub(1)
6534                        .min(n - 1)
6535                }
6536            };
6537            if let Some(fracs) = percentile_fraction_array(fraction) {
6538                let picked: Vec<Value> = fracs
6539                    .iter()
6540                    .map(|f| f.map_or(Value::Null, |f| items[idx_at(f)].clone()))
6541                    .collect();
6542                return Ok(values_to_array(&picked));
6543            }
6544            let f = scalar_fraction.transpose()?.unwrap_or(0.0);
6545            items[idx_at(f)].clone()
6546        }
6547        // Linear interpolation between the two bracketing values. PG accepts
6548        // both a scalar fraction (→ float) and an array of fractions (→ a
6549        // float array, one interpolated value per requested percentile).
6550        "percentile_cont" => {
6551            // v7.39 (read01 orderedsetaggs.c) — the INTERVAL overload
6552            // interpolates component-wise with PG's month→day→time
6553            // remainder spill (a month is 30 days, a day 86400 s).
6554            if items.iter().all(|v| matches!(v, Value::Interval { .. })) {
6555                let iv = |i: usize| -> (f64, f64, f64) {
6556                    match &items[i] {
6557                        Value::Interval {
6558                            months,
6559                            days,
6560                            micros,
6561                            kind,
6562                        } => (f64::from(*months), f64::from(*days), *micros as f64),
6563                        _ => unreachable!(),
6564                    }
6565                };
6566                let at = |f: f64| -> Value<'static> {
6567                    if n == 1 {
6568                        return items[0].clone();
6569                    }
6570                    let rank = f * (n as f64 - 1.0);
6571                    let lo = crate::eval::f64_floor(rank) as usize;
6572                    let hi = crate::eval::f64_ceil(rank) as usize;
6573                    let frac = rank - lo as f64;
6574                    let (lm, ld, lu) = iv(lo);
6575                    let (hm, hd, hu) = iv(hi);
6576                    let dm = (hm - lm) * frac;
6577                    let m_i = dm as i64; // trunc toward zero
6578                    let rem_days = (dm - m_i as f64) * 30.0 + (hd - ld) * frac;
6579                    let d_i = rem_days as i64;
6580                    let us = (rem_days - d_i as f64) * 86_400_000_000.0 + (hu - lu) * frac;
6581                    Value::Interval {
6582                        months: (lm as i64 + m_i) as i32,
6583                        days: (ld as i64 + d_i) as i32,
6584                        micros: lu as i64 + libm::round(us) as i64,
6585                        kind: spg_storage::IntervalKind::Finite,
6586                    }
6587                };
6588                if let Some(fracs) = percentile_fraction_array(fraction) {
6589                    let picked: Vec<Value> =
6590                        fracs.iter().map(|f| f.map_or(Value::Null, at)).collect();
6591                    return Ok(values_to_array(&picked));
6592                }
6593                let f = scalar_fraction.transpose()?.unwrap_or(0.0);
6594                return Ok(at(f));
6595            }
6596            let Some(nums) = items
6597                .iter()
6598                .map(agg_value_to_f64)
6599                .collect::<Option<Vec<f64>>>()
6600            else {
6601                return Ok(Value::Null); // non-numeric ordered set
6602            };
6603            let at = |f: f64| -> f64 {
6604                if n == 1 {
6605                    return nums[0];
6606                }
6607                let rank = f * (n as f64 - 1.0);
6608                let lo = crate::eval::f64_floor(rank) as usize;
6609                let hi = crate::eval::f64_ceil(rank) as usize;
6610                let frac = rank - lo as f64;
6611                nums[lo] + (nums[hi] - nums[lo]) * frac
6612            };
6613            if let Some(fracs) = percentile_fraction_array(fraction) {
6614                return Ok(Value::FloatArray(fracs.iter().map(|f| f.map(at)).collect()));
6615            }
6616            let f = scalar_fraction.transpose()?.unwrap_or(0.0);
6617            Value::Float(at(f))
6618        }
6619        _ => unreachable!(),
6620    })
6621}
6622
6623fn infer_agg_type(spec: &AggSpec, schema_cols: &[ColumnSchema]) -> DataType {
6624    // v7.26 (round-20 C) — the argument's statically-derived shape
6625    // types MIN/MAX/SUM/array_agg properly; RowDescription used to
6626    // report TEXT for these, breaking every sqlx typed decode.
6627    let arg_ty = spec
6628        .arg
6629        .as_ref()
6630        .and_then(|a| crate::describe::describe_expr(a, schema_cols))
6631        .map(|shape| shape.ty);
6632    // v7.33 (array_agg argmax) — `(array_agg(x ORDER BY y))[1]` yields the
6633    // ELEMENT type (x), not the array type.
6634    if spec.first_ordered {
6635        return arg_ty.unwrap_or(DataType::Text);
6636    }
6637    match spec.name.as_str() {
6638        "count" | "count_star" => DataType::BigInt,
6639        // v7.38 (read01, T4) — sum(int) → bigint, sum(bigint) → numeric (PG
6640        // widens to numeric to defend against i64 overflow), sum(float) → float.
6641        "sum" => match arg_ty {
6642            Some(DataType::Float) => DataType::Float,
6643            Some(DataType::BigInt) => DataType::Numeric {
6644                precision: 0,
6645                scale: 0,
6646            },
6647            _ => DataType::BigInt,
6648        },
6649        // v7.38 (read01, T4) — avg over any integer / numeric input is NUMERIC
6650        // (PG); only avg(float8) stays double precision.
6651        "avg" => match arg_ty {
6652            Some(DataType::Float) => DataType::Float,
6653            _ => DataType::Numeric {
6654                precision: 0,
6655                scale: 0,
6656            },
6657        },
6658        // v7.17.0 — string_agg always returns TEXT.
6659        "string_agg" | "group_concat" | "xmlagg" => DataType::Text,
6660        // v7.39 (read01 round 73) — the STATIC type follows the same rule the
6661        // finalize does, so `pg_typeof(array_agg(b))` is `boolean[]`.
6662        "array_agg" => match arg_ty {
6663            Some(DataType::Int | DataType::SmallInt) => DataType::IntArray,
6664            Some(DataType::BigInt) => DataType::BigIntArray,
6665            Some(DataType::Bool) => DataType::BoolArray,
6666            Some(DataType::Date) => DataType::DateArray,
6667            Some(DataType::Timestamp) => DataType::TimestampArray,
6668            Some(DataType::Timestamptz) => DataType::TimestamptzArray,
6669            Some(DataType::Uuid) => DataType::UuidArray,
6670            Some(DataType::Float) => DataType::FloatArray,
6671            Some(DataType::Numeric { .. }) => DataType::NumericArray,
6672            Some(DataType::Bytes) => DataType::BytesArray,
6673            _ => DataType::TextArray,
6674        },
6675        // v7.17.0 — boolean aggregates always return BOOL (nullable
6676        // — empty / all-NULL group → NULL).
6677        "bool_and" | "bool_or" => DataType::Bool,
6678        // v7.32 (round-29) — variance / stddev are floating point;
6679        // percentile_cont interpolates to float; the regression family
6680        // (except regr_count) is floating point.
6681        // v7.38 (read01, T4.3) — PG stddev / variance return NUMERIC.
6682        "stddev" | "stddev_samp" | "stddev_pop" | "variance" | "var_samp" | "var_pop" => {
6683            DataType::Numeric {
6684                precision: 0,
6685                scale: 0,
6686            }
6687        }
6688        "percentile_cont" | "covar_pop" | "covar_samp" | "corr" | "regr_avgx" | "regr_avgy"
6689        | "regr_slope" | "regr_intercept" | "regr_r2" | "regr_sxx" | "regr_syy" | "regr_sxy" => {
6690            DataType::Float
6691        }
6692        // v7.32 (round-29) — bitwise aggregates, regr_count, and the
6693        // integer hypothetical-set ranks return an integer.
6694        // v7.38 (read01, T4.4) — bit_and/or/xor return the INPUT integer type
6695        // (PG: bit_and(int) → integer, bit_and(bigint) → bigint).
6696        "bit_and" | "bit_or" | "bit_xor" => match arg_ty {
6697            Some(DataType::SmallInt) => DataType::SmallInt,
6698            Some(DataType::BigInt) => DataType::BigInt,
6699            _ => DataType::Int,
6700        },
6701        "regr_count" | "rank" | "dense_rank" => DataType::BigInt,
6702        // v7.32 (round-29) — hypothetical-set distribution functions.
6703        "percent_rank" | "cume_dist" => DataType::Float,
6704        // v7.32 (round-29) — JSON aggregates return JSON.
6705        "json_agg" | "jsonb_agg" | "json_object_agg" | "jsonb_object_agg" | "json_arrayagg"
6706        | "json_objectagg" => DataType::Json,
6707        // min/max, percentile_disc, mode, and anything pass-through:
6708        // the argument's shape (for ordered-set aggs `spec.arg` is the
6709        // WITHIN GROUP value expression).
6710        _ => arg_ty.unwrap_or(DataType::Text),
6711    }
6712}
6713
6714fn agg_or_group_type(e: &Expr, synth: &[ColumnSchema]) -> DataType {
6715    if let Expr::Column(c) = e
6716        && let Some(s) = synth.iter().find(|s| s.name == c.name)
6717    {
6718        return s.ty;
6719    }
6720    // v7.26 (round-20 C) — compound expressions over aggregates
6721    // (COALESCE(BOOL_OR(…), false), (array_agg(…))[1], CASE …)
6722    // derive their shape statically against the synth schema; the
6723    // old Text fallback broke sqlx typed decodes of exactly these
6724    // columns.
6725    crate::describe::describe_expr(e, synth)
6726        .map(|shape| shape.ty)
6727        .unwrap_or(DataType::Text)
6728}
6729
6730/// v7.39 (round 620) — PG's strict GROUP BY rule, and the diagnosis it earns.
6731///
6732/// `SELECT id, count(*) FROM dc` answered `column "id" does not exist`. The
6733/// column plainly exists; what it is not is grouped. The message came out that
6734/// way because there was no rule at all — the grouped row carries only the
6735/// grouping keys and the aggregates, so the reference simply failed to resolve
6736/// at evaluation time, and the resolver said the only thing it knew. A user
6737/// reading it goes looking for a typo or a missing table.
6738///
6739/// Returns the first bare column reference that is a real input column, is not
6740/// covered by a grouping expression, and is not inside an aggregate. Variants
6741/// this walker does not descend into are left alone, so an uncovered nesting
6742/// keeps the old behaviour rather than inventing an error: under-reporting is
6743/// the status quo, over-reporting would break queries that run today.
6744fn first_ungrouped_column<'a>(
6745    e: &'a Expr,
6746    group_exprs: &[Expr],
6747    columns: &[ColumnSchema],
6748    licensed: &[alloc::string::String],
6749) -> Option<&'a spg_sql::ast::ColumnName> {
6750    if group_exprs.iter().any(|g| g == e) {
6751        return None;
6752    }
6753    let rec = |x: &'a Expr| first_ungrouped_column(x, group_exprs, columns, licensed);
6754    match e {
6755        Expr::Column(c) => {
6756            (column_ref_is_input(c, columns) && !column_is_key_determined(c, licensed)).then_some(c)
6757        }
6758        // An aggregate's arguments are exactly what does not need grouping.
6759        Expr::FunctionCall { name, .. } if is_aggregate_name(&name.to_ascii_lowercase()) => None,
6760        Expr::AggregateOrdered { .. } => None,
6761        // A subquery carries its own scope and its own rules.
6762        Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => None,
6763        Expr::FunctionCall { args, .. } => args.iter().find_map(rec),
6764        Expr::Binary { lhs, rhs, .. } => rec(lhs).or_else(|| rec(rhs)),
6765        Expr::Unary { expr, .. }
6766        | Expr::Cast { expr, .. }
6767        | Expr::IsNull { expr, .. }
6768        | Expr::BoolTest { expr, .. } => rec(expr),
6769        Expr::Like { expr, pattern, .. } => rec(expr).or_else(|| rec(pattern)),
6770        Expr::InList { expr, list, .. } => rec(expr).or_else(|| list.iter().find_map(rec)),
6771        Expr::Case {
6772            operand,
6773            branches,
6774            else_branch,
6775        } => operand
6776            .as_deref()
6777            .and_then(rec)
6778            .or_else(|| branches.iter().find_map(|(w, t)| rec(w).or_else(|| rec(t))))
6779            .or_else(|| else_branch.as_deref().and_then(rec)),
6780        _ => None,
6781    }
6782}
6783
6784/// v7.39 (round 620) — does this column reference name an INPUT column?
6785///
6786/// A joined schema names its columns `a.s`; a single-table one names them `s`
6787/// and answers to the active alias. Matching only the bare name — which the
6788/// first cut of round 620 did — makes every qualified reference in a join
6789/// invisible to both the check and the rewrite below, which is how they
6790/// reached evaluation and came back `missing FROM-clause entry for table "a"`.
6791fn column_ref_is_input(c: &spg_sql::ast::ColumnName, columns: &[ColumnSchema]) -> bool {
6792    if let Some(q) = &c.qualifier {
6793        let composite = alloc::format!("{q}.{}", c.name);
6794        if columns
6795            .iter()
6796            .any(|col| col.name.eq_ignore_ascii_case(&composite))
6797        {
6798            return true;
6799        }
6800    }
6801    columns
6802        .iter()
6803        .any(|col| col.name.eq_ignore_ascii_case(&c.name))
6804}
6805
6806/// v7.39 (round 620) — the qualifiers whose PRIMARY KEY is wholly present in
6807/// the GROUP BY list, which licenses every OTHER column of those tables.
6808///
6809/// `SELECT s, count(*) FROM dc GROUP BY id` where `id` is the primary key is
6810/// answered by PG and was REFUSED here — a query that runs on PG and fails on
6811/// SPG, which is worse than any wording. One row per `id` means `s` has
6812/// exactly one value in the group, so there is nothing ambiguous to resolve;
6813/// the rule is the SQL standard's functional dependency, and PG applies it for
6814/// a base table's primary key.
6815///
6816/// Every FROM entry is considered separately, so a join licenses the side
6817/// whose key is grouped and not the other: `SELECT a.s, b.t … JOIN … GROUP BY
6818/// a.id` answers `a.s` and still refuses `b.t`, which is what PG does.
6819///
6820/// The empty string stands for the unqualified single-table case.
6821fn qualifiers_grouped_by_primary_key(
6822    stmt: &SelectStatement,
6823    group_exprs: &[Expr],
6824    columns: &[ColumnSchema],
6825    catalog: Option<&spg_storage::Catalog>,
6826) -> Vec<alloc::string::String> {
6827    let (Some(from), Some(cat)) = (stmt.from.as_ref(), catalog) else {
6828        return Vec::new();
6829    };
6830    let mut out = Vec::new();
6831    let refs = core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table));
6832    let single = from.joins.is_empty();
6833    for tr in refs {
6834        if tr.unnest_expr.is_some() {
6835            continue;
6836        }
6837        let Some(table) = cat.get(&tr.name) else {
6838            continue;
6839        };
6840        let schema = table.schema();
6841        let Some(pk) = schema
6842            .uniqueness_constraints
6843            .iter()
6844            .find(|u| u.is_primary_key && !u.columns.is_empty())
6845        else {
6846            continue;
6847        };
6848        let qual = tr.alias.as_deref().unwrap_or(tr.name.as_str());
6849        let all_keys_grouped = pk.columns.iter().all(|&pos| {
6850            let Some(name) = schema.columns.get(pos).map(|c| &c.name) else {
6851                return false;
6852            };
6853            // The key column has to be grouped by AS ITSELF, and as this
6854            // table's: an unqualified spelling only counts when there is one
6855            // table for it to mean.
6856            group_exprs.iter().any(|g| match g {
6857                Expr::Column(c) if c.name.eq_ignore_ascii_case(name) => {
6858                    let belongs = match &c.qualifier {
6859                        Some(q) => q.eq_ignore_ascii_case(qual),
6860                        None => single,
6861                    };
6862                    belongs && column_ref_is_input(c, columns)
6863                }
6864                _ => false,
6865            })
6866        });
6867        if all_keys_grouped {
6868            out.push(alloc::string::String::from(qual));
6869            if single {
6870                out.push(alloc::string::String::new());
6871            }
6872        }
6873    }
6874    out
6875}
6876
6877/// True when this column reference is licensed by one of those keys.
6878fn column_is_key_determined(
6879    c: &spg_sql::ast::ColumnName,
6880    licensed: &[alloc::string::String],
6881) -> bool {
6882    let q = c.qualifier.as_deref().unwrap_or("");
6883    licensed.iter().any(|l| l.eq_ignore_ascii_case(q))
6884}
6885
6886/// v7.39 (round 405) — MySQL's loose GROUP BY: a non-aggregated column
6887/// that is not in GROUP BY is allowed and reads any (the first-seen) row's
6888/// value in the group. PG (and SPG until now) rejects it. Wrapping such a
6889/// bare column in `any_value(col)` reuses the existing aggregate machinery.
6890/// A whole grouping expression stays as-is; an aggregate call is not
6891/// descended into (its inner columns are already fine); a non-aggregate
6892/// function's argument columns are wrapped individually
6893/// (`UPPER(name)` → `UPPER(any_value(name))`).
6894fn wrap_loose_group_columns(
6895    e: Expr,
6896    group_exprs: &[Expr],
6897    columns: &[ColumnSchema],
6898    // v7.39 (round 620) — `None` wraps every ungrouped column, which is what
6899    // MySQL's loose GROUP BY means. `Some(quals)` wraps only the columns a
6900    // grouped primary key determines, so a join licenses the side whose key is
6901    // grouped and leaves the other to be refused.
6902    licensed: Option<&[alloc::string::String]>,
6903) -> Expr {
6904    if group_exprs.iter().any(|g| *g == e) {
6905        return e;
6906    }
6907    let wrap = |x: Expr| wrap_loose_group_columns(x, group_exprs, columns, licensed);
6908    match e {
6909        Expr::Column(c) => {
6910            let claimed = column_ref_is_input(&c, columns)
6911                && licensed.is_none_or(|l| column_is_key_determined(&c, l));
6912            if claimed {
6913                Expr::FunctionCall {
6914                    name: String::from("any_value"),
6915                    args: alloc::vec![Expr::Column(c)],
6916                }
6917            } else {
6918                Expr::Column(c)
6919            }
6920        }
6921        Expr::FunctionCall { name, args } if is_aggregate_name(&name.to_ascii_lowercase()) => {
6922            Expr::FunctionCall { name, args }
6923        }
6924        Expr::AggregateOrdered { .. } => e,
6925        Expr::FunctionCall { name, args } => Expr::FunctionCall {
6926            name,
6927            args: args.into_iter().map(wrap).collect(),
6928        },
6929        Expr::Binary { op, lhs, rhs } => Expr::Binary {
6930            op,
6931            lhs: Box::new(wrap(*lhs)),
6932            rhs: Box::new(wrap(*rhs)),
6933        },
6934        Expr::Unary { op, expr } => Expr::Unary {
6935            op,
6936            expr: Box::new(wrap(*expr)),
6937        },
6938        Expr::Cast { expr, target } => Expr::Cast {
6939            expr: Box::new(wrap(*expr)),
6940            target,
6941        },
6942        Expr::IsNull { expr, negated } => Expr::IsNull {
6943            expr: Box::new(wrap(*expr)),
6944            negated,
6945        },
6946        Expr::BoolTest {
6947            expr,
6948            value,
6949            negated,
6950        } => Expr::BoolTest {
6951            expr: Box::new(wrap(*expr)),
6952            value,
6953            negated,
6954        },
6955        Expr::Like {
6956            expr,
6957            pattern,
6958            negated,
6959            case_insensitive,
6960        } => Expr::Like {
6961            expr: Box::new(wrap(*expr)),
6962            pattern: Box::new(wrap(*pattern)),
6963            negated,
6964            case_insensitive,
6965        },
6966        Expr::InList {
6967            expr,
6968            list,
6969            negated,
6970        } => Expr::InList {
6971            expr: Box::new(wrap(*expr)),
6972            list: list.into_iter().map(wrap).collect(),
6973            negated,
6974        },
6975        Expr::Case {
6976            operand,
6977            branches,
6978            else_branch,
6979        } => Expr::Case {
6980            operand: operand.map(|o| Box::new(wrap(*o))),
6981            branches: branches
6982                .into_iter()
6983                .map(|(w, t)| (wrap(w), wrap(t)))
6984                .collect(),
6985            else_branch: else_branch.map(|b| Box::new(wrap(*b))),
6986        },
6987        other => other,
6988    }
6989}
6990
6991/// v7.39 (round 404) — MySQL lets HAVING (and ORDER BY) reference a
6992/// SELECT-list alias (`SELECT g, SUM(v) AS sv … HAVING sv > 30`); PG does
6993/// not. Before the aggregate rewrite, replace a bare `Column(alias)` with
6994/// the SELECT expression it names, so the aggregate rewrite then maps it to
6995/// its synthetic column. A nesting this walker does not cover simply leaves
6996/// the column unresolved (the pre-existing "column does not exist" error),
6997/// never a wrong result.
6998fn substitute_having_aliases(e: Expr, aliases: &[(String, Expr)]) -> Expr {
6999    use spg_sql::ast::ColumnName;
7000    let sub = |x: Expr| substitute_having_aliases(x, aliases);
7001    match e {
7002        Expr::Column(ColumnName {
7003            qualifier: None,
7004            name,
7005        }) => aliases
7006            .iter()
7007            .find(|(a, _)| a.eq_ignore_ascii_case(&name))
7008            .map_or_else(
7009                || {
7010                    Expr::Column(ColumnName {
7011                        qualifier: None,
7012                        name,
7013                    })
7014                },
7015                |(_, expr)| expr.clone(),
7016            ),
7017        Expr::Binary { op, lhs, rhs } => Expr::Binary {
7018            op,
7019            lhs: Box::new(sub(*lhs)),
7020            rhs: Box::new(sub(*rhs)),
7021        },
7022        Expr::Unary { op, expr } => Expr::Unary {
7023            op,
7024            expr: Box::new(sub(*expr)),
7025        },
7026        Expr::FunctionCall { name, args } => Expr::FunctionCall {
7027            name,
7028            args: args.into_iter().map(sub).collect(),
7029        },
7030        Expr::IsNull { expr, negated } => Expr::IsNull {
7031            expr: Box::new(sub(*expr)),
7032            negated,
7033        },
7034        Expr::BoolTest {
7035            expr,
7036            value,
7037            negated,
7038        } => Expr::BoolTest {
7039            expr: Box::new(sub(*expr)),
7040            value,
7041            negated,
7042        },
7043        Expr::Like {
7044            expr,
7045            pattern,
7046            negated,
7047            case_insensitive,
7048        } => Expr::Like {
7049            expr: Box::new(sub(*expr)),
7050            pattern: Box::new(sub(*pattern)),
7051            negated,
7052            case_insensitive,
7053        },
7054        Expr::InList {
7055            expr,
7056            list,
7057            negated,
7058        } => Expr::InList {
7059            expr: Box::new(sub(*expr)),
7060            list: list.into_iter().map(sub).collect(),
7061            negated,
7062        },
7063        Expr::Case {
7064            operand,
7065            branches,
7066            else_branch,
7067        } => Expr::Case {
7068            operand: operand.map(|o| Box::new(sub(*o))),
7069            branches: branches
7070                .into_iter()
7071                .map(|(w, t)| (sub(w), sub(t)))
7072                .collect(),
7073            else_branch: else_branch.map(|b| Box::new(sub(*b))),
7074        },
7075        Expr::Cast { expr, target } => Expr::Cast {
7076            expr: Box::new(sub(*expr)),
7077            target,
7078        },
7079        other => other,
7080    }
7081}
7082
7083fn rewrite_expr(e: &Expr, group_exprs: &[Expr], aggs: &[AggSpec]) -> Expr {
7084    // v7.33 (array_agg argmax) — `(array_agg(x ORDER BY y))[1]` rewrites
7085    // to its first_ordered synth column, consuming the subscript. Checked
7086    // before the AggregateOrdered/recursion arms (which would otherwise
7087    // rewrite the inner array_agg and leave the subscript). Same matcher
7088    // as collect_aggregates, so the spec it finds is the one collected.
7089    if let Some((arg, order_by, filter)) = first_ordered_array_agg(e) {
7090        let arg_owned = Some(arg.clone());
7091        let filter_owned = filter.cloned();
7092        for (i, spec) in aggs.iter().enumerate() {
7093            if spec.first_ordered
7094                && spec.name == "array_agg"
7095                && spec.arg == arg_owned
7096                && spec.order_by == *order_by
7097                && spec.filter == filter_owned
7098            {
7099                return Expr::Column(spg_sql::ast::ColumnName {
7100                    qualifier: None,
7101                    name: format!("__agg_{i}"),
7102                });
7103            }
7104        }
7105    }
7106    // v7.24 (round-16 A) — ordered aggregate: match on the inner
7107    // call PLUS the ordering keys.
7108    if let Expr::AggregateOrdered {
7109        call,
7110        order_by,
7111        distinct,
7112        filter,
7113    } = e
7114        && let Expr::FunctionCall { name, args } = call.as_ref()
7115    {
7116        let lower = name.to_ascii_lowercase();
7117        if is_aggregate_name(&lower) {
7118            let canonical: &str = if lower == "every" { "bool_and" } else { &lower };
7119            // Mirror collect_aggregates: ordered-set aggregates take the
7120            // value from the sort spec and the in-parens arg as direct.
7121            let (arg, direct_arg) = if is_within_group_name(canonical) {
7122                (
7123                    order_by.first().map(|o| o.expr.clone()),
7124                    args.first().cloned(),
7125                )
7126            } else {
7127                (args.first().cloned(), None)
7128            };
7129            let arg2 = if agg_uses_second_arg(canonical) {
7130                args.get(1).cloned()
7131            } else {
7132                None
7133            };
7134            let filter_owned = filter.as_deref().cloned();
7135            for (i, spec) in aggs.iter().enumerate() {
7136                if spec.name == canonical
7137                    && spec.arg == arg
7138                    && spec.arg2 == arg2
7139                    && spec.distinct == *distinct
7140                    && spec.order_by == *order_by
7141                    && spec.filter == filter_owned
7142                    && spec.direct_arg == direct_arg
7143                {
7144                    return Expr::Column(spg_sql::ast::ColumnName {
7145                        qualifier: None,
7146                        name: format!("__agg_{i}"),
7147                    });
7148                }
7149            }
7150        }
7151    }
7152    // Match aggregate FunctionCalls first — they sit outside group_by.
7153    if let Expr::FunctionCall { name, args } = e {
7154        let lower = name.to_ascii_lowercase();
7155        if is_aggregate_name(&lower) {
7156            let arg = if lower == "count_star" {
7157                None
7158            } else {
7159                args.first().cloned()
7160            };
7161            // v7.17.0 — match the spec we registered for
7162            // string_agg(value, separator) on the full pair; v7.32 also
7163            // the regression family and json_object_agg.
7164            let arg2 = if agg_uses_second_arg(&lower) {
7165                args.get(1).cloned()
7166            } else {
7167                None
7168            };
7169            // v7.17.0 — `every` collapses into `bool_and` at
7170            // collection; mirror that here so the rewrite finds
7171            // the matching synth column.
7172            let canonical: &str = if lower == "every" {
7173                "bool_and"
7174            } else {
7175                lower.as_str()
7176            };
7177            for (i, spec) in aggs.iter().enumerate() {
7178                if spec.name == canonical
7179                    && spec.arg == arg
7180                    && spec.arg2 == arg2
7181                    && !spec.distinct
7182                    && spec.order_by.is_empty()
7183                {
7184                    return Expr::Column(spg_sql::ast::ColumnName {
7185                        qualifier: None,
7186                        name: format!("__agg_{i}"),
7187                    });
7188                }
7189            }
7190        }
7191    }
7192    // Match a group_by expression by AST equality.
7193    for (i, g) in group_exprs.iter().enumerate() {
7194        if g == e {
7195            return Expr::Column(spg_sql::ast::ColumnName {
7196                qualifier: None,
7197                name: format!("__grp_{i}"),
7198            });
7199        }
7200    }
7201    // Recurse into children.
7202    match e {
7203        Expr::NamedArg { name, expr } => Expr::NamedArg {
7204            name: name.clone(),
7205            expr: alloc::boxed::Box::new(rewrite_expr(expr, group_exprs, aggs)),
7206        },
7207        Expr::Variadic(expr) => Expr::Variadic(alloc::boxed::Box::new(rewrite_expr(
7208            expr,
7209            group_exprs,
7210            aggs,
7211        ))),
7212        Expr::AggregateOrdered {
7213            call,
7214            order_by,
7215            distinct,
7216            filter,
7217        } => Expr::AggregateOrdered {
7218            call: Box::new(rewrite_expr(call, group_exprs, aggs)),
7219            distinct: *distinct,
7220            order_by: order_by
7221                .iter()
7222                .map(|o| spg_sql::ast::OrderBy {
7223                    expr: rewrite_expr(&o.expr, group_exprs, aggs),
7224                    desc: o.desc,
7225                    nulls_first: o.nulls_first,
7226                    collation: o.collation.clone(),
7227                })
7228                .collect(),
7229            // The filter is evaluated against SOURCE rows during
7230            // accumulation, never against synth rows — keep it as-is.
7231            filter: filter.clone(),
7232        },
7233        Expr::Binary { lhs, op, rhs } => Expr::Binary {
7234            lhs: Box::new(rewrite_expr(lhs, group_exprs, aggs)),
7235            op: *op,
7236            rhs: Box::new(rewrite_expr(rhs, group_exprs, aggs)),
7237        },
7238        Expr::Unary { op, expr } => Expr::Unary {
7239            op: *op,
7240            expr: Box::new(rewrite_expr(expr, group_exprs, aggs)),
7241        },
7242        Expr::Cast { expr, target } => Expr::Cast {
7243            expr: Box::new(rewrite_expr(expr, group_exprs, aggs)),
7244            target: target.clone(),
7245        },
7246        Expr::FieldAccess { base, field } => Expr::FieldAccess {
7247            base: Box::new(rewrite_expr(base, group_exprs, aggs)),
7248            field: field.clone(),
7249        },
7250        Expr::IsNull { expr, negated } => Expr::IsNull {
7251            expr: Box::new(rewrite_expr(expr, group_exprs, aggs)),
7252            negated: *negated,
7253        },
7254        Expr::BoolTest {
7255            expr,
7256            value,
7257            negated,
7258        } => Expr::BoolTest {
7259            expr: Box::new(rewrite_expr(expr, group_exprs, aggs)),
7260            value: *value,
7261            negated: *negated,
7262        },
7263        Expr::FunctionCall { name, args } => Expr::FunctionCall {
7264            name: name.clone(),
7265            args: args
7266                .iter()
7267                .map(|a| rewrite_expr(a, group_exprs, aggs))
7268                .collect(),
7269        },
7270        Expr::Like {
7271            expr,
7272            pattern,
7273            negated,
7274            case_insensitive,
7275        } => Expr::Like {
7276            expr: Box::new(rewrite_expr(expr, group_exprs, aggs)),
7277            pattern: Box::new(rewrite_expr(pattern, group_exprs, aggs)),
7278            negated: *negated,
7279            case_insensitive: *case_insensitive,
7280        },
7281        Expr::Extract { field, source } => Expr::Extract {
7282            field: field.clone(),
7283            source: Box::new(rewrite_expr(source, group_exprs, aggs)),
7284        },
7285        // v7.25.2 (round-19 A) — subquery nodes: rewrite group-key
7286        // references INSIDE the body to `__grp_N` so the correlated
7287        // resolver can substitute them against the synthesised group
7288        // row (aggs are NOT matched inside the body — a COUNT in the
7289        // subquery is the subquery's own aggregate).
7290        Expr::ScalarSubquery(s) => {
7291            Expr::ScalarSubquery(Box::new(rewrite_group_keys_in_select(s, group_exprs)))
7292        }
7293        Expr::Exists { subquery, negated } => Expr::Exists {
7294            subquery: Box::new(rewrite_group_keys_in_select(subquery, group_exprs)),
7295            negated: *negated,
7296        },
7297        Expr::InSubquery {
7298            expr,
7299            subquery,
7300            negated,
7301        } => Expr::InSubquery {
7302            expr: Box::new(rewrite_expr(expr, group_exprs, aggs)),
7303            subquery: Box::new(rewrite_group_keys_in_select(subquery, group_exprs)),
7304            negated: *negated,
7305        },
7306        Expr::RowInSubquery {
7307            row,
7308            subquery,
7309            negated,
7310        } => Expr::RowInSubquery {
7311            row: row
7312                .iter()
7313                .map(|el| rewrite_expr(el, group_exprs, aggs))
7314                .collect(),
7315            subquery: Box::new(rewrite_group_keys_in_select(subquery, group_exprs)),
7316            negated: *negated,
7317        },
7318        Expr::RowCmpSubquery { row, op, subquery } => Expr::RowCmpSubquery {
7319            row: row
7320                .iter()
7321                .map(|el| rewrite_expr(el, group_exprs, aggs))
7322                .collect(),
7323            op: *op,
7324            subquery: Box::new(rewrite_group_keys_in_select(subquery, group_exprs)),
7325        },
7326        // v4.12 window / Literal / Column — clone-pass (these don't
7327        // participate in aggregate rewrite).
7328        Expr::WindowFunction { .. } | Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {
7329            e.clone()
7330        }
7331        // v7.10.10 — recurse children for array nodes.
7332        Expr::Array(items) => Expr::Array(
7333            items
7334                .iter()
7335                .map(|elem| rewrite_expr(elem, group_exprs, aggs))
7336                .collect(),
7337        ),
7338        Expr::ArraySubscript { target, index } => Expr::ArraySubscript {
7339            target: Box::new(rewrite_expr(target, group_exprs, aggs)),
7340            index: Box::new(rewrite_expr(index, group_exprs, aggs)),
7341        },
7342        Expr::ArraySlice { target, lo, hi } => Expr::ArraySlice {
7343            target: Box::new(rewrite_expr(target, group_exprs, aggs)),
7344            lo: lo
7345                .as_ref()
7346                .map(|b| Box::new(rewrite_expr(b, group_exprs, aggs))),
7347            hi: hi
7348                .as_ref()
7349                .map(|b| Box::new(rewrite_expr(b, group_exprs, aggs))),
7350        },
7351        Expr::AnyAll {
7352            expr,
7353            op,
7354            array,
7355            is_any,
7356        } => Expr::AnyAll {
7357            expr: Box::new(rewrite_expr(expr, group_exprs, aggs)),
7358            op: *op,
7359            array: Box::new(rewrite_expr(array, group_exprs, aggs)),
7360            is_any: *is_any,
7361        },
7362        Expr::InList {
7363            expr,
7364            list,
7365            negated,
7366        } => Expr::InList {
7367            expr: Box::new(rewrite_expr(expr, group_exprs, aggs)),
7368            list: list
7369                .iter()
7370                .map(|item| rewrite_expr(item, group_exprs, aggs))
7371                .collect(),
7372            negated: *negated,
7373        },
7374        Expr::Case {
7375            operand,
7376            branches,
7377            else_branch,
7378        } => Expr::Case {
7379            operand: operand
7380                .as_deref()
7381                .map(|o| Box::new(rewrite_expr(o, group_exprs, aggs))),
7382            branches: branches
7383                .iter()
7384                .map(|(w, t)| {
7385                    (
7386                        rewrite_expr(w, group_exprs, aggs),
7387                        rewrite_expr(t, group_exprs, aggs),
7388                    )
7389                })
7390                .collect(),
7391            else_branch: else_branch
7392                .as_deref()
7393                .map(|e| Box::new(rewrite_expr(e, group_exprs, aggs))),
7394        },
7395    }
7396}
7397
7398/// v7.25.2 (round-19 A) — rewrite group-key references inside a
7399/// subquery body to `__grp_N` synthetic columns (aggregates are
7400/// not touched: empty spec list). Runs through the canonical
7401/// Select walker so every expression slot is covered.
7402fn rewrite_group_keys_in_select(
7403    s: &spg_sql::ast::SelectStatement,
7404    group_exprs: &[Expr],
7405) -> spg_sql::ast::SelectStatement {
7406    let mut out = s.clone();
7407    let _ = crate::walk_select_exprs_mut(&mut out, &mut |e| {
7408        *e = rewrite_expr(e, group_exprs, &[]);
7409        Ok(())
7410    });
7411    out
7412}
7413
7414/// Canonical string key for a tuple of group values. Used as map key.
7415/// Per-value group-key encoding (shared by owned and borrowed paths).
7416fn encode_one(out: &mut String, v: &Value) {
7417    encode_one_in(out, v, false);
7418}
7419
7420/// v7.39 (round 364, M4 P2) — key encoder with the session dialect. On a
7421/// MySQL session a text group / distinct key is FOLDED (accent- and
7422/// case-insensitive) so `Foo`/`foo`/`FOO` share one group and `bar`/`Bär`
7423/// merge — while the group's OUTPUT value stays the first row's original,
7424/// because only the key is folded, not the stored value.
7425fn encode_one_in(out: &mut String, v: &Value, mysql: bool) {
7426    use core::fmt::Write;
7427    if mysql {
7428        if let Value::Text(s) | Value::Json(s) = v {
7429            let _ = write!(out, "S{}|", spg_storage::mysql_compare_fold(s));
7430            return;
7431        }
7432        if let Value::BpChar(s) = v {
7433            let folded = spg_storage::mysql_compare_fold_char(s);
7434            let _ = write!(out, "S{folded}|");
7435            return;
7436        }
7437    }
7438    encode_one_raw(out, v);
7439}
7440
7441fn encode_one_raw(out: &mut String, v: &Value) {
7442    use core::fmt::Write;
7443    match v {
7444        Value::Null => out.push_str("N|"),
7445        // v7.36 (perf — mailrs Phase 1) — switch the integer / float
7446        // encoders to `write!`. `n.to_string()` allocates a fresh
7447        // `String` per cell just to push its bytes into the
7448        // (already-cleared) reuse buffer — for the 25 k-row JOIN
7449        // probe in `count_messages` that's 25 k heap allocs per
7450        // query. `write!(&mut String, ...)` formats straight into
7451        // the buffer; no intermediate alloc.
7452        Value::SmallInt(n) => {
7453            let _ = write!(out, "s{n}|");
7454        }
7455        Value::Int(n) => {
7456            let _ = write!(out, "I{n}|");
7457        }
7458        Value::BigInt(n) => {
7459            let _ = write!(out, "B{n}|");
7460        }
7461        Value::Float(x) => {
7462            // v7.37.16 — fold -0.0 into 0.0: PG's float8 equality (hash and
7463            // btree opclasses) treats them as one value, so GROUP BY /
7464            // DISTINCT must key them together (count(DISTINCT) differential).
7465            // NaN needs no fold — every NaN renders "NaN" here already.
7466            let x = if *x == 0.0 { 0.0 } else { *x };
7467            let _ = write!(out, "F{x}|");
7468        }
7469        Value::Real(x) => {
7470            let x = if *x == 0.0 { 0.0 } else { *x };
7471            let _ = write!(out, "R{x}|");
7472        }
7473        Value::Bool(b) => {
7474            out.push(if *b { 'T' } else { 'f' });
7475            out.push('|');
7476        }
7477        Value::Text(s) => {
7478            out.push('S');
7479            out.push_str(s);
7480            out.push('|');
7481        }
7482        // v7.38 (read01, T11/R3) — bpchar groups / dedups blank-insensitively,
7483        // and shares the text key so `'ab'::char(4)` and `'ab'` co-group.
7484        Value::BpChar(s) => {
7485            out.push('S');
7486            out.push_str(s.trim_end_matches(' '));
7487            out.push('|');
7488        }
7489        Value::Vector(v) => {
7490            out.push('V');
7491            for x in v.iter() {
7492                out.push_str(&x.to_string());
7493                out.push(',');
7494            }
7495            out.push('|');
7496        }
7497        // v6.0.1: GROUP BY on a `VECTOR(N) USING SQ8` column.
7498        // Two cells with byte-identical `(min, max, bytes)`
7499        // share the same group; equivalence is byte-equality
7500        // (same as f32 grouping today — neither path tries to
7501        // normalise nan/-0).
7502        Value::Sq8Vector(q) => {
7503            out.push('Q');
7504            out.push_str(&q.min.to_string());
7505            out.push('@');
7506            out.push_str(&q.max.to_string());
7507            out.push(':');
7508            for b in &q.bytes {
7509                out.push_str(&b.to_string());
7510                out.push(',');
7511            }
7512            out.push('|');
7513        }
7514        // v6.0.3: GROUP BY on a `VECTOR(N) USING HALF` column.
7515        // Byte-equality over the raw u16 bits; matches the SQ8
7516        // path's byte-key model.
7517        Value::HalfVector(h) => {
7518            out.push('H');
7519            for b in &h.bytes {
7520                out.push_str(&b.to_string());
7521                out.push(',');
7522            }
7523            out.push('|');
7524        }
7525        Value::Numeric { scaled, scale, .. } => {
7526            // v7.38 (read01) — DISTINCT keys numerically-equal decimals as one
7527            // regardless of scale (1.0 = 1.00), so strip trailing fractional
7528            // zeros before encoding, matching PG (and set-op / GROUP BY dedup).
7529            let (mut s, mut sc) = (*scaled, *scale);
7530            while sc > 0 && s % 10 == 0 {
7531                s /= 10;
7532                sc -= 1;
7533            }
7534            out.push('D');
7535            out.push_str(&s.to_string());
7536            out.push('@');
7537            out.push_str(&sc.to_string());
7538            out.push('|');
7539        }
7540        Value::Date(d) => {
7541            out.push('d');
7542            out.push_str(&d.to_string());
7543            out.push('|');
7544        }
7545        Value::Timestamp(t) => {
7546            out.push('t');
7547            out.push_str(&t.to_string());
7548            out.push('|');
7549        }
7550        Value::Interval {
7551            months,
7552            days,
7553            micros,
7554            kind,
7555        } => {
7556            out.push('i');
7557            out.push_str(&months.to_string());
7558            out.push('m');
7559            out.push_str(&days.to_string());
7560            out.push('d');
7561            out.push_str(&micros.to_string());
7562            out.push('|');
7563        }
7564        Value::Json(s) => {
7565            out.push('j');
7566            out.push_str(s);
7567            out.push('|');
7568        }
7569        // v7.5.0 — Value is #[non_exhaustive] for downstream
7570        // forward-compat. Any future variant lacking explicit
7571        // handling here will share a debug-derived group key,
7572        // which is observably wrong but won't crash.
7573        _ => {
7574            out.push('?');
7575            out.push_str(&format!("{v:?}"));
7576            out.push('|');
7577        }
7578    }
7579}
7580
7581/// v7.30 (perf campaign) - encode from borrowed cells without
7582/// materialising an owned Vec<Value<'static>> first.
7583pub(crate) fn encode_key_refs(vals: &[&Value]) -> String {
7584    let mut out = String::new();
7585    for v in vals {
7586        encode_one(&mut out, v);
7587    }
7588    out
7589}
7590
7591/// v7.31 (perf 3e) — encode into a caller-owned scratch buffer.
7592/// The per-row key paths (group hash, DISTINCT set, join build/
7593/// probe) ran 24k+ String allocations per query through the
7594/// allocator just to LOOK UP a map; the scratch form allocates
7595/// only when a map actually has to take ownership (vacant insert).
7596/// v7.39 (round 590) — append ONE value's encoding, for the join key that
7597/// mixes stored cells with computed ones and so cannot clear as it goes.
7598/// v7.39 (round 590, moved here round 593+) — one component of a key with a COMPUTED side.
7599///
7600/// The whole requirement is that two values SQL calls equal encode the same,
7601/// or the join silently loses rows. Across the numeric family that is not
7602/// free: `5` as INT, `5` as BIGINT, `5.0` as double and `5.00` as NUMERIC all
7603/// compare equal and would otherwise carry four different tags, so they are
7604/// all rendered as one canonical decimal. A non-integral value can never
7605/// equal an integer, so it simply renders as itself; NaN equals nothing and
7606/// any encoding will do. Everything outside the numeric family keeps the
7607/// encoder the column-to-column path already uses.
7608pub(crate) fn push_canonical_key(out: &mut String, v: &Value) {
7609    use core::fmt::Write;
7610    match v {
7611        Value::SmallInt(n) => {
7612            let _ = write!(out, "n{n}|");
7613        }
7614        Value::Int(n) => {
7615            let _ = write!(out, "n{n}|");
7616        }
7617        Value::BigInt(n) => {
7618            let _ = write!(out, "n{n}|");
7619        }
7620        // `-0.0` prints with its sign but equals `0`.
7621        Value::Float(f) if *f == 0.0 => out.push_str("n0|"),
7622        Value::Float(f) => {
7623            let _ = write!(out, "n{f}|");
7624        }
7625        Value::Numeric { .. } => {
7626            let t = crate::eval::value_to_text(v);
7627            let t = if t.contains('.') {
7628                t.trim_end_matches('0').trim_end_matches('.')
7629            } else {
7630                t.as_str()
7631            };
7632            let _ = write!(out, "n{t}|");
7633        }
7634        _ => encode_one_into(out, v),
7635    }
7636}
7637
7638/// v7.39 (round 596) — a whole key encoded the canonical way, for the two
7639/// sides of a decorrelated EXISTS: the set is built from the inner column's
7640/// values and probed with the outer EXPRESSION's, and those need not share a
7641/// numeric width for `=` to call them equal.
7642pub(crate) fn encode_canonical_key(vals: &[Value<'_>]) -> String {
7643    let mut out = String::new();
7644    for v in vals {
7645        push_canonical_key(&mut out, v);
7646    }
7647    out
7648}
7649
7650pub(crate) fn encode_one_into(out: &mut String, v: &Value) {
7651    encode_one_raw(out, v);
7652}
7653
7654pub(crate) fn encode_key_refs_into(vals: &[&Value], out: &mut String) {
7655    encode_key_refs_into_in(vals, out, false);
7656}
7657
7658/// v7.38.14 — key encode with a per-POSITION fold decision.
7659///
7660/// `encode_key_refs_into_in` takes one bool for the whole key, which
7661/// cannot express the case a join actually presents: one key column
7662/// declared `COLLATE utf8mb4_bin` beside another that folds. `folds` is
7663/// resolved once per join from the key columns' collations; a short or
7664/// missing entry means "do not fold", which is what every existing
7665/// caller wants.
7666pub(crate) fn encode_key_refs_folded(vals: &[&Value], out: &mut String, folds: &[bool]) {
7667    out.clear();
7668    for (i, v) in vals.iter().enumerate() {
7669        encode_one_in(out, v, folds.get(i).copied().unwrap_or(false));
7670    }
7671}
7672
7673/// v7.39 (round 364, M4 P2) — key encode with the session dialect.
7674pub(crate) fn encode_key_refs_into_in(vals: &[&Value], out: &mut String, mysql: bool) {
7675    out.clear();
7676    for v in vals {
7677        encode_one_in(out, v, mysql);
7678    }
7679}
7680
7681pub(crate) fn encode_key(vals: &[Value<'static>]) -> String {
7682    let mut out = String::new();
7683    for v in vals {
7684        encode_one(&mut out, v);
7685    }
7686    out
7687}
7688
7689#[allow(clippy::cast_precision_loss)]
7690/// v7.37.17 (17.6 siblings) — intersect two ranges (same kind).
7691/// The greater lower bound wins (tie keeps inclusivity only when
7692/// both are inclusive); the smaller upper bound mirrors it; an
7693/// unbounded side loses to a bounded one. lower > upper — or a
7694/// touch that isn't inclusive on both ends — collapses to empty,
7695/// and any empty input pins the fold at empty.
7696fn range_intersect(a: &Value<'static>, b: &Value<'static>) -> Value<'static> {
7697    let (
7698        Value::Range {
7699            kind,
7700            lower: la,
7701            upper: ua,
7702            lower_inc: lia,
7703            upper_inc: uia,
7704            empty: ea,
7705        },
7706        Value::Range {
7707            lower: lb,
7708            upper: ub,
7709            lower_inc: lib_,
7710            upper_inc: uib,
7711            empty: eb,
7712            ..
7713        },
7714    ) = (a, b)
7715    else {
7716        return Value::Null;
7717    };
7718    let kind = *kind;
7719    let empty_range = Value::Range {
7720        kind,
7721        lower: None,
7722        upper: None,
7723        lower_inc: false,
7724        upper_inc: false,
7725        empty: true,
7726    };
7727    if *ea || *eb {
7728        return empty_range;
7729    }
7730    // Greater lower bound (None = -infinity loses to any bound).
7731    let (lower, lower_inc) = match (la, lb) {
7732        (None, None) => (None, false),
7733        (Some(x), None) => (Some(x.clone()), *lia),
7734        (None, Some(y)) => (Some(y.clone()), *lib_),
7735        (Some(x), Some(y)) => match value_cmp(x, y) {
7736            core::cmp::Ordering::Greater => (Some(x.clone()), *lia),
7737            core::cmp::Ordering::Less => (Some(y.clone()), *lib_),
7738            core::cmp::Ordering::Equal => (Some(x.clone()), *lia && *lib_),
7739        },
7740    };
7741    // Smaller upper bound (None = +infinity loses to any bound).
7742    let (upper, upper_inc) = match (ua, ub) {
7743        (None, None) => (None, false),
7744        (Some(x), None) => (Some(x.clone()), *uia),
7745        (None, Some(y)) => (Some(y.clone()), *uib),
7746        (Some(x), Some(y)) => match value_cmp(x, y) {
7747            core::cmp::Ordering::Less => (Some(x.clone()), *uia),
7748            core::cmp::Ordering::Greater => (Some(y.clone()), *uib),
7749            core::cmp::Ordering::Equal => (Some(x.clone()), *uia && *uib),
7750        },
7751    };
7752    if let (Some(lo), Some(up)) = (&lower, &upper) {
7753        match value_cmp(lo, up) {
7754            core::cmp::Ordering::Greater => return empty_range,
7755            core::cmp::Ordering::Equal if !(lower_inc && upper_inc) => {
7756                return empty_range;
7757            }
7758            _ => {}
7759        }
7760    }
7761    Value::Range {
7762        kind,
7763        lower,
7764        upper,
7765        lower_inc,
7766        upper_inc,
7767        empty: false,
7768    }
7769}
7770
7771/// v7.38 (read01, T6.P3) — fold a NUMERIC input's kind into a running sum's kind:
7772/// NaN wins; ±Inf + finite → that Inf; +Inf + -Inf → NaN; else unchanged.
7773fn fold_sum_kind(
7774    acc: spg_storage::NumericKind,
7775    incoming: spg_storage::NumericKind,
7776) -> spg_storage::NumericKind {
7777    use spg_storage::NumericKind as NK;
7778    match (acc, incoming) {
7779        (NK::NaN, _) | (_, NK::NaN) => NK::NaN,
7780        (NK::Finite, k) | (k, NK::Finite) => k,
7781        (a, b) if a == b => a,
7782        _ => NK::NaN,
7783    }
7784}
7785
7786/// v7.39 (enum order knife) — min/max extreme comparison: member order when
7787/// the spec's argument is enum-typed, the generic value order otherwise.
7788fn extreme_cmp(
7789    enum_labels: Option<&[String]>,
7790    a: &Value,
7791    b: &Value,
7792    mysql: bool,
7793) -> core::cmp::Ordering {
7794    extreme_cmp_in(enum_labels, None, a, b, mysql)
7795}
7796
7797/// v7.39 (round 690) — `extreme_cmp` with the argument column's collation.
7798///
7799/// `min`/`max` over a column declared `COLLATE "en_US.utf8"` answered
7800/// `Banana` and `Ápple` where PG18 gives `apple` and `Zebra`. The collation
7801/// rides beside `enum_labels`, which is already exactly this: per-aggregate
7802/// metadata about the argument, resolved once where the spec is built.
7803///
7804/// No derivation needed here — `min(loc)`'s argument is the column itself.
7805/// An expression argument gets None and keeps byte order, which is the same
7806/// limit `ORDER BY upper(loc)` has.
7807fn extreme_cmp_in(
7808    enum_labels: Option<&[String]>,
7809    collation: Option<&str>,
7810    a: &Value,
7811    b: &Value,
7812    mysql: bool,
7813) -> core::cmp::Ordering {
7814    if let Some(labels) = enum_labels
7815        && let Some(ord) = crate::eval::enum_ord_cmp(labels, a, b)
7816    {
7817        return ord;
7818    }
7819    if let (Value::Text(x), Value::Text(y), Some(c)) = (a, b, collation)
7820        && let Some(ord) = crate::collate::compare(c, x, y)
7821    {
7822        return ord;
7823    }
7824    // v7.39 (round 412) — MIN / MAX over text under the MySQL default
7825    // collation compares by the folded form (case- and accent-insensitive,
7826    // PAD SPACE), matching ORDER BY (round 411).
7827    if mysql {
7828        // v7.38.18 — each side on its own type; see `mysql_fold_value`.
7829        if let (Some(x), Some(y)) = (
7830            spg_storage::mysql_fold_value(a),
7831            spg_storage::mysql_fold_value(b),
7832        ) {
7833            return x.cmp(&y);
7834        }
7835    }
7836    value_cmp(a, b)
7837}
7838
7839/// Compare two values for `min` / `max`.
7840///
7841/// v7.39 (round 674) — the 228 lines that used to live here were a SECOND
7842/// comparison matrix, written independently of `orderby::value_cmp`. A
7843/// census of which `Value` variants each named found them diverged rather
7844/// than duplicated, and two silent wrongs fell out of the gap: `ORDER BY
7845/// time_col` did not sort (round 672) and `min`/`max` over `CHAR(n)`
7846/// returned the first row (round 672). Round 673 found four more on the
7847/// orderby side, where a canonical-text fallback had `ORDER BY money`
7848/// putting $100 before $9.
7849///
7850/// What stays here is the ONLY thing the two legitimately disagreed about:
7851/// where NULL sorts. This one puts NULLs last so `min`/`max` skip them;
7852/// `orderby::value_cmp` puts them first and the ORDER BY layer above it
7853/// applies NULLS FIRST / NULLS LAST. Both were correct in context, which is
7854/// why merging the matrices wholesale would have flipped one of them —
7855/// verified before collapsing, not after, and the eight NULL shapes are
7856/// pinned.
7857fn value_cmp(a: &Value, b: &Value) -> core::cmp::Ordering {
7858    use core::cmp::Ordering;
7859    match (a, b) {
7860        (Value::Null, Value::Null) => Ordering::Equal,
7861        // NULLs last, so a NULL never wins a min() or a max().
7862        (Value::Null, _) => Ordering::Greater,
7863        (_, Value::Null) => Ordering::Less,
7864        _ => crate::orderby::value_cmp(a, b),
7865    }
7866}
7867
7868/// v7.37.9 Phase 0 diagnostic counters — see
7869/// `.claude/notes/v7.37.9-class-a-c-cascade-closure-plan.md`. These
7870/// are read-only telemetry, do not gate any code path. Used by
7871/// `xtests/dogfood_replay/src/bin/counter_dump.rs` to verify
7872/// whether the DISTA A-3 + array_agg-ordered fast paths actually
7873/// fire on the mailrs Class A SQL shape.
7874pub static DISTA_LITERAL_ARG2_CACHE_FIRE: core::sync::atomic::AtomicU64 =
7875    core::sync::atomic::AtomicU64::new(0);
7876pub static AGGREGATE_ARRAY_AGG_ORDER_BY_FIRE: core::sync::atomic::AtomicU64 =
7877    core::sync::atomic::AtomicU64::new(0);
7878
7879/// v7.37.9 Phase 1A-ext — per-row spec dispatch branches in
7880/// `accumulate_groups`'s hot loop. Verifies the Phase 1A
7881/// decomposition agent's S06 assumption ("14 specs × eval_expr per
7882/// row"). Sum should equal `n_specs × n_input_rows`. Branch
7883/// distribution tells which attack target ROI is highest:
7884/// FAST_POS many = baseline OK; COMPILED_MISS many = Step-VM is
7885/// hot path; EVAL_FALLBACK > 0 = uncompilable specs walking the
7886/// eval_expr tree per row × Cow row materialise.
7887pub static AGG_PER_ROW_FAST_POS: core::sync::atomic::AtomicU64 =
7888    core::sync::atomic::AtomicU64::new(0);
7889pub static AGG_PER_ROW_COMPILED_HIT: core::sync::atomic::AtomicU64 =
7890    core::sync::atomic::AtomicU64::new(0);
7891pub static AGG_PER_ROW_COMPILED_MISS: core::sync::atomic::AtomicU64 =
7892    core::sync::atomic::AtomicU64::new(0);
7893pub static AGG_PER_ROW_EVAL_FALLBACK: core::sync::atomic::AtomicU64 =
7894    core::sync::atomic::AtomicU64::new(0);
7895pub static AGG_PER_ROW_COUNT_STAR_SENTINEL: core::sync::atomic::AtomicU64 =
7896    core::sync::atomic::AtomicU64::new(0);
7897
7898#[cfg(test)]
7899mod value_cmp_mixed_numeric_tests {
7900    //! v7.37.16 Slice A — direct coverage of the mixed NUMERIC↔int/float
7901    //! arms in the aggregate-local `value_cmp` (drives min / max / argmin
7902    //! / argmax / mode / ordered-set aggregates). These pairs previously
7903    //! hit `_ => Equal`, which made `min`/`max` over a mixed NUMERIC/int
7904    //! key keep whichever row arrived first. Semantics now mirror
7905    //! binop.rs: int→NUMERIC exact promotion, NUMERIC→f64 demotion vs a
7906    //! float.
7907    use super::value_cmp;
7908    use core::cmp::Ordering;
7909    use spg_storage::Value;
7910
7911    fn num(scaled: i128, scale: u16) -> Value<'static> {
7912        Value::Numeric {
7913            scaled,
7914            scale,
7915            kind: spg_storage::NumericKind::Finite,
7916        }
7917    }
7918
7919    #[test]
7920    fn numeric_vs_integer_and_float() {
7921        assert_eq!(value_cmp(&num(250, 2), &Value::Int(5)), Ordering::Less);
7922        assert_eq!(value_cmp(&Value::Int(5), &num(250, 2)), Ordering::Greater);
7923        // debug-string/Equal fallback bug: 1000 vs 9 must be Greater.
7924        assert_eq!(
7925            value_cmp(&num(1000, 0), &Value::SmallInt(9)),
7926            Ordering::Greater
7927        );
7928        assert_eq!(value_cmp(&num(20, 1), &Value::BigInt(2)), Ordering::Equal);
7929        assert_eq!(value_cmp(&Value::BigInt(2), &num(20, 1)), Ordering::Equal);
7930        // NUMERIC↔float demotion.
7931        assert_eq!(value_cmp(&num(35, 1), &Value::Float(3.5)), Ordering::Equal);
7932        assert_eq!(
7933            value_cmp(&num(35, 1), &Value::Float(3.0)),
7934            Ordering::Greater
7935        );
7936        assert_eq!(value_cmp(&Value::Float(1.0), &num(25, 1)), Ordering::Less);
7937    }
7938
7939    /// v7.39 (round 231) — `is_aggregate_name` admits a name and
7940    /// `classify_agg_name` panics on anything it doesn't know, so the two
7941    /// lists drifting apart turns into a SQL-reachable abort. That is how
7942    /// `every(x) OVER (…)` crashed the query in round 230. Walk the whole
7943    /// admitted set and classify each one.
7944    #[test]
7945    fn every_aggregate_name_classifies() {
7946        const NAMES: &[&str] = &[
7947            "count",
7948            "count_star",
7949            "sum",
7950            "min",
7951            "max",
7952            "avg",
7953            "any_value",
7954            "range_agg",
7955            "range_intersect_agg",
7956            "string_agg",
7957            "group_concat",
7958            "xmlagg",
7959            "array_agg",
7960            "bool_and",
7961            "bool_or",
7962            "every",
7963            "stddev",
7964            "stddev_samp",
7965            "stddev_pop",
7966            "variance",
7967            "var_samp",
7968            "var_pop",
7969            "bit_and",
7970            "bit_or",
7971            "bit_xor",
7972            "json_agg",
7973            "jsonb_agg",
7974            "json_object_agg",
7975            "jsonb_object_agg",
7976        ];
7977        for n in NAMES {
7978            assert!(
7979                super::is_aggregate_name(n),
7980                "{n} should be an aggregate name"
7981            );
7982            // Panics if the classifier doesn't know it.
7983            let _ = super::classify_agg_name(super::canonical_agg_name(n));
7984        }
7985        // Anything `is_aggregate_name` admits must classify, so a name added
7986        // to one list and not the other fails here rather than at runtime.
7987        for n in NAMES {
7988            assert!(
7989                super::is_aggregate_name(&n.to_ascii_uppercase()),
7990                "{n} should be case-insensitive"
7991            );
7992        }
7993    }
7994}