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