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