Skip to main content

spg_engine/
aggregate.rs

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