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