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