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