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