spg_engine/select.rs
1//! SELECT execution — the window / meta-view / CTE variants and the
2//! subquery-resolution pre-pass. Lifted out of `lib.rs` (v7.32 engine
3//! modularisation). These `impl Engine` methods are dispatched from the
4//! bare-SELECT entry points and drive the non-trivial SELECT shapes.
5
6use alloc::borrow::Cow;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9
10use spg_sql::ast::{
11 ColumnName, Expr, FromClause, SelectItem, SelectStatement, Statement, TableRef, UnionKind,
12};
13use spg_storage::{
14 Catalog, ColumnSchema, DataType, Row, StorageError, TableSchema, Value, VecEncoding,
15};
16
17use crate::describe;
18use crate::eval::{EvalContext, EvalError};
19use crate::join::RowRef;
20use crate::system_catalog::collect_view_refs;
21use crate::{
22 ByteBudget, CancelToken, Engine, EngineError, OrderKey, QueryResult, aggregate,
23 apply_offset_and_limit, apply_offset_and_limit_tagged, approx_row_bytes, build_order_keys,
24 collect_meta_view_names, collect_qualified_refs, collect_scalar_subqueries,
25 collect_window_nodes, compute_window_partition, eval, expr_tree_has_subquery,
26 materialise_in_order, materialise_meta_view, memoize, order_by_value_cmp_in, partition_key_cmp,
27 rewrite_window_to_columns, select_has_window, select_references_meta_view, select_refers_to,
28 sort_by_keys, synth_info_key_column_usage, synth_info_referential_constraints,
29 synth_info_routines, synth_info_statistics, synth_information_schema_columns,
30 synth_information_schema_tables, synth_mysql_db, synth_mysql_user, synth_pg_attribute,
31 synth_pg_class, synth_pg_constraint, synth_pg_database, synth_pg_extension, synth_pg_index_raw,
32 synth_pg_indexes, synth_pg_namespace, synth_pg_operator, synth_pg_proc, synth_pg_roles,
33 synth_pg_sequence, synth_pg_settings, synth_pg_timezone_abbrevs, synth_pg_timezone_names,
34 synth_pg_trigger, synth_pg_type, synth_pg_views, topk_trim, try_gin_jsonb_seek, try_gin_seek,
35 try_index_seek, try_nsw_knn, try_pk_walk_top_n, try_trgm_seek, value_is_bigint,
36 value_is_integer, value_to_i64,
37};
38
39/// v7.39 (round 618) — a recursive term that can be run over the working set
40/// directly, instead of through a whole query execution per round.
41///
42/// PG plans the recursive term ONCE and re-scans a worktable each iteration.
43/// SPG emptied and refilled a real table and then called `exec_select_cancel`
44/// — FROM resolution, schema build, predicate compilation, projection build
45/// and result materialisation — for every round. Measured with the counting
46/// allocator on `WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM r
47/// WHERE n < N)`: about 40 allocations and 99 kB PER ROUND while the working
48/// set is one row, or 1.98 GB at N = 20000.
49///
50/// This is the shape that covers the ordinary recursive term: read the CTE,
51/// filter it, project it. Anything else — a join, an aggregate, a window, a
52/// subquery, DISTINCT, GROUP BY, ORDER BY, LIMIT, a locking clause, a
53/// non-table source — returns `None` and keeps the general path, so the
54/// answers it gives are the ones that path gave.
55struct RecursiveTermPlan<'t> {
56 items: Vec<&'t Expr>,
57 where_: Option<&'t Expr>,
58 alias: String,
59}
60
61fn plan_recursive_term<'t>(
62 t: &'t SelectStatement,
63 cte_name: &str,
64 ncols: usize,
65) -> Option<RecursiveTermPlan<'t>> {
66 if !t.unions.is_empty()
67 || !t.ctes.is_empty()
68 || t.distinct
69 || !t.distinct_on.is_empty()
70 || t.group_by.is_some()
71 || t.group_by_all
72 || t.having.is_some()
73 || !t.order_by.is_empty()
74 || t.limit.is_some()
75 || t.offset.is_some()
76 || t.limit_with_ties
77 || t.locking.is_some()
78 {
79 return None;
80 }
81 let from = t.from.as_ref()?;
82 if !from.joins.is_empty() {
83 return None;
84 }
85 let p = &from.primary;
86 if !p.name.eq_ignore_ascii_case(cte_name)
87 || p.as_of_segment.is_some()
88 || p.unnest_expr.is_some()
89 || !p.unnest_column_aliases.is_empty()
90 || p.with_ordinality
91 || p.generate_series_args.is_some()
92 || p.lateral_subquery.is_some()
93 || p.jsonb_each_text_arg.is_some()
94 || p.table_fn_call.is_some()
95 {
96 return None;
97 }
98 let unsupported = |e: &Expr| {
99 crate::aggregate::contains_aggregate(e)
100 || crate::subquery::expr_has_subquery(e)
101 || crate::window::expr_has_window_pub(e)
102 };
103 let mut items: Vec<&Expr> = Vec::with_capacity(t.items.len());
104 for it in &t.items {
105 match it {
106 SelectItem::Expr { expr, .. } => {
107 if unsupported(expr) {
108 return None;
109 }
110 items.push(expr);
111 }
112 // `*` would have to be expanded against the CTE's own schema;
113 // the general path already does that, so leave it there.
114 _ => return None,
115 }
116 }
117 if items.len() != ncols {
118 return None;
119 }
120 if let Some(w) = &t.where_
121 && unsupported(w)
122 {
123 return None;
124 }
125 Some(RecursiveTermPlan {
126 items,
127 where_: t.where_.as_ref(),
128 alias: p.alias.clone().unwrap_or_else(|| p.name.clone()),
129 })
130}
131
132impl Engine {
133 /// v4.12 window executor. Implements `ROW_NUMBER` / `RANK` /
134 /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
135 /// `AVG` / `COUNT` / `MIN` / `MAX`. The plan is:
136 /// 1. Apply the WHERE filter.
137 /// 2. For each unique `WindowFunction` node in the projection,
138 /// partition + sort, compute the per-row value.
139 /// 3. Append the window values as synthetic columns (`__win_N`)
140 /// to the row schema.
141 /// 4. Rewrite the projection to read those columns.
142 /// 5. Hand off to the regular project / ORDER BY / LIMIT pipe.
143 #[allow(
144 clippy::too_many_lines,
145 clippy::type_complexity,
146 clippy::needless_range_loop
147 )] // window-eval is one cohesive pipe; splitting fragments
148 pub(crate) fn exec_select_with_window(
149 &self,
150 stmt: &SelectStatement,
151 cancel: CancelToken<'_>,
152 ) -> Result<QueryResult, EngineError> {
153 let from = stmt.from.as_ref().ok_or_else(|| {
154 EngineError::Unsupported("window functions require a FROM clause".into())
155 })?;
156 // v7.17.0 Phase 3.P0-43 — JOIN + window functions. Phase
157 // 3.6 rejected this combination outright ("queued for
158 // v5.x"); P0-43 materialises the join + WHERE through the
159 // existing nested-loop helper and runs the window pipeline
160 // on the joined row set with the combined `alias.col`
161 // schema. The window expressions resolve through the
162 // qualifier-aware column resolver same as the aggregate /
163 // projection paths on JOIN.
164 let (schema_cols_owned, alias_opt): (Vec<ColumnSchema>, Option<&str>);
165 // v7.39 (round 976) — rows this walk OWNS. A derived FROM item and
166 // a JOIN both produce rows that exist nowhere else, so they land
167 // here; a plain stored table does not, and borrows instead.
168 //
169 // It used to clone every row out of the table, on the reasoning
170 // that "the clone is cheap relative to the window computation that
171 // follows". Measured on 400k rows, `row_number() OVER ()` cost
172 // 31.881 ms against 46.520 with a 200-byte column added — so the
173 // clone tracks row width at about 36 ns per row per 200 bytes, and
174 // the window computation it was being compared against is a
175 // counter increment per row. Nothing downstream needs the rows
176 // owned: the very next statement used to be
177 // `filtered.iter().collect()` into the `&Row` slice the window
178 // pipeline actually reads.
179 let mut owned_rows: Vec<Row<'static>> = Vec::new();
180 // What the pipeline reads. Borrows `owned_rows` or the table.
181 let mut filtered: Vec<&Row<'static>> = Vec::new();
182 // Set by the branches that fill `owned_rows`, because "empty" is
183 // an answer a query can legitimately have and so cannot be the
184 // signal for which of the two holds the rows.
185 let mut rows_are_owned = false;
186 if from.joins.is_empty() {
187 let primary = &from.primary;
188 // v7.37 D.13 — window functions over a derived table (subquery /
189 // VALUES / unnest / generate_series). The catalog-by-name lookup
190 // below only finds real tables, so a derived primary threw
191 // TableNotFound. Materialise the derived rows + schema through the
192 // same helper the non-window FROM-primary path uses, then WHERE-
193 // filter and feed the identical window pipeline.
194 let is_derived = primary.lateral_subquery.is_some()
195 || primary.unnest_expr.is_some()
196 || primary.generate_series_args.is_some()
197 || primary.jsonb_each_text_arg.is_some()
198 || primary.table_fn_call.is_some();
199 if is_derived {
200 let (drows, dcols) = self.materialise_table_ref(primary)?;
201 schema_cols_owned = dcols;
202 alias_opt = primary.alias.as_deref();
203 let ctx = self.ev_ctx(&schema_cols_owned, alias_opt);
204 let mut owned: Vec<Row<'static>> = Vec::new();
205 for (i, row) in drows.into_iter().enumerate() {
206 if i.is_multiple_of(256) {
207 cancel.check()?;
208 }
209 if let Some(w) = &stmt.where_ {
210 let cond = eval::eval_expr(w, &row, &ctx)?;
211 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
212 continue;
213 }
214 }
215 owned.push(row);
216 }
217 owned_rows = owned;
218 rows_are_owned = true;
219 } else {
220 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
221 StorageError::TableNotFound {
222 name: primary.name.clone(),
223 }
224 })?;
225 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
226 schema_cols_owned = table.schema().columns.clone();
227 alias_opt = Some(alias);
228 let ctx = self.ev_ctx(&schema_cols_owned, alias_opt);
229 // The WHERE test, in ONE place, for all four ways a row can
230 // reach this walk. It deliberately does not touch the row
231 // collections: a closure that pushed into them would tie
232 // its argument to the closure body and no borrowed row
233 // could escape it, which is what forced the clone-shaped
234 // version of this loop in the first place.
235 let passes = |row: &Row<'static>| -> Result<bool, EngineError> {
236 if let Some(w) = &stmt.where_ {
237 let cond = eval::eval_expr(w, row, &ctx)?;
238 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
239 return Ok(false);
240 }
241 }
242 Ok(true)
243 };
244 // v7.37.15 Phase B — scan_visible filters rows by the
245 // engine's current snapshot. Phase B's `current_snapshot()`
246 // returns `Snapshot::unbounded()` so every row is visible,
247 // matching pre-v7.37.15 byte-for-byte. Phase C will wire
248 // real per-tx snapshots through this same callsite — no
249 // code change needed here when that lands.
250 let snap = self.current_snapshot();
251 if table.has_cold_rows_fast() {
252 // v7.36 (cold-tier coverage) — a cold segment's rows
253 // are produced on demand and live in a temporary this
254 // walk cannot borrow from, so a table carrying any owns
255 // its rows. Hot iter then cold iter, both through the
256 // same WHERE, as before.
257 let mut owned: Vec<Row<'static>> = Vec::new();
258 for (i, row) in table.scan_visible(&snap) {
259 if i.is_multiple_of(256) {
260 cancel.check()?;
261 }
262 if passes(row)? {
263 owned.push(row.clone());
264 }
265 }
266 let hot_len = table.row_count();
267 for (offset, row) in self.iter_cold_rows_of_table(table).iter().enumerate() {
268 let i = hot_len + offset;
269 if i.is_multiple_of(256) {
270 cancel.check()?;
271 }
272 if passes(row)? {
273 owned.push(row.clone());
274 }
275 }
276 owned_rows = owned;
277 rows_are_owned = true;
278 } else {
279 // v7.39 (round 975) — ask the indices first, the way
280 // the streaming walk has since round 970. This walk had
281 // the same hole and it is reached by any statement
282 // carrying a window function, so a WHERE that names an
283 // indexed column read the whole table: measured on 400k
284 // rows, `row_number() OVER () … WHERE id = 500` — a
285 // ONE-row answer on a primary key — took 13.762 ms
286 // against PG18.4's 0.151, while the same predicate
287 // without the window took 0.091. The cost was
288 // independent of how many rows survived (999 survivors
289 // cost 13.312 ms) and of row width (13.312 narrow vs
290 // 13.327 wide), which is what a full table walk looks
291 // like and what a result-shaped cost does not.
292 //
293 // The seek only NARROWS — `passes` still applies the
294 // whole WHERE — so no answer can change. Positions
295 // arrive visibility-filtered by the same predicate the
296 // scan applies and capped at a quarter of the table,
297 // and `None` walks the table exactly as before.
298 let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
299 crate::index_access::try_index_seek_positions(
300 w,
301 &schema_cols_owned,
302 table,
303 alias,
304 &snap,
305 self.speaks_mysql,
306 )
307 });
308 match seek_positions {
309 Some(mut positions) => {
310 // Table order, which is the order the scan
311 // would have produced.
312 positions.sort_unstable();
313 for (n, pos) in positions.into_iter().enumerate() {
314 if n.is_multiple_of(256) {
315 cancel.check()?;
316 }
317 let Some(row) = table.rows().get(pos) else {
318 continue;
319 };
320 if passes(row)? {
321 filtered.push(row);
322 }
323 }
324 }
325 None => {
326 for (i, row) in table.scan_visible(&snap) {
327 if i.is_multiple_of(256) {
328 cancel.check()?;
329 }
330 if passes(row)? {
331 filtered.push(row);
332 }
333 }
334 }
335 }
336 }
337 }
338 } else {
339 let deferred = self.build_joined_filtered_rows(
340 from,
341 stmt.where_.as_ref(),
342 cancel,
343 None,
344 &mut ByteBudget::new(self.max_query_bytes),
345 )?;
346 // A join's survivors are row-index tuples over its sources, so
347 // there is no single row to borrow — this branch owns them.
348 owned_rows = deferred.materialise();
349 rows_are_owned = true;
350 schema_cols_owned = deferred.combined_schema;
351 alias_opt = None;
352 }
353 if rows_are_owned {
354 filtered = owned_rows.iter().collect();
355 }
356 let schema_cols = &schema_cols_owned;
357 let ctx = self.ev_ctx(schema_cols, alias_opt);
358 let alias = alias_opt.unwrap_or("");
359 let n_rows = filtered.len();
360 // The window pipeline reads `&[&Row<'static>]`, and `filtered`
361 // already is one whichever branch produced it — the separate
362 // `filtered_refs` this used to build was the collect that made
363 // owning the rows look necessary.
364
365 // 2) Collect unique window function nodes from projection.
366 let mut window_nodes: Vec<Expr> = Vec::new();
367 for item in &stmt.items {
368 if let SelectItem::Expr { expr, .. } = item {
369 collect_window_nodes(expr, &mut window_nodes);
370 }
371 }
372 // v7.39 (round 592) — and from ORDER BY, which may name a window the
373 // select list never mentions. The order-key builder below rewrites
374 // window calls to `__win_N` columns, and a call that was never
375 // collected has no column to become.
376 for o in &stmt.order_by {
377 collect_window_nodes(&o.expr, &mut window_nodes);
378 }
379
380 // 3) For each window, compute per-row value.
381 // Index: same order as window_nodes; for row i, win_vals[w][i].
382 let mut win_vals: Vec<Vec<Value<'static>>> = Vec::with_capacity(window_nodes.len());
383 for wnode in &window_nodes {
384 let Expr::WindowFunction {
385 name,
386 args,
387 partition_by,
388 order_by,
389 frame,
390 null_treatment,
391 filter,
392 } = wnode
393 else {
394 unreachable!("collect_window_nodes pushes only WindowFunction");
395 };
396 // Compute (partition_key, order_key, original_index) for each row.
397 // v7.39 (round 593) — a key that is a plain column sits at the same
398 // position in every row, but was resolved BY NAME for each one. A
399 // per-library profile of `lag(id) OVER (ORDER BY id)` put
400 // `resolve_column` at 5.8% of the query on its own, with
401 // `rehydrate_cell` and the `eval_expr` dispatch behind it. Resolve
402 // once; anything that is not a plain column keeps the resolver.
403 let p_bound: Vec<Option<usize>> = partition_by
404 .iter()
405 .map(|e| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
406 .collect();
407 let o_bound: Vec<Option<usize>> = order_by
408 .iter()
409 .map(|(e, _, _)| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
410 .collect();
411 let arg_bound = args
412 .first()
413 .and_then(|a| crate::orderby::bound_column_position(a, schema_cols, alias_opt));
414 // v7.39 (round 690) — a window's ORDER BY over a column that
415 // declares a collation sorts by it, the same as a top-level
416 // ORDER BY. Resolved from the bound position, so only a bare
417 // column gets one; an expression produces a new value and the
418 // derivation that would give IT a collation is unbuilt.
419 let o_colls: Vec<Option<alloc::string::String>> = o_bound
420 .iter()
421 .map(|p| {
422 p.and_then(|pos| schema_cols.get(pos))
423 .and_then(|sc| sc.collation_name.clone())
424 .filter(|n| crate::collate::is_supported(n))
425 })
426 .collect();
427 let mut indexed: Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)> =
428 Vec::with_capacity(n_rows);
429 // v7.39 (round 731) — single bound INT partition key, no window
430 // ORDER BY: group on the i64 directly. The generic build paid
431 // two heap Vecs per row (pkey + empty okey) plus a canonical
432 // string encode per row just to bucket 500k rows into 100
433 // groups; the whole per-row key apparatus disappears here.
434 // Neither key Vec is read downstream on this path: the hash
435 // grouping replaces partition_key_cmp, and okey is empty by
436 // construction.
437 let int_pkey_fast = order_by.is_empty()
438 && partition_by.len() == 1
439 && p_bound[0].is_some_and(|pos| {
440 matches!(
441 schema_cols.get(pos).map(|c| c.ty),
442 Some(
443 spg_storage::DataType::Int
444 | spg_storage::DataType::BigInt
445 | spg_storage::DataType::SmallInt
446 )
447 )
448 });
449 // v7.39 (round 979) — the same idea for a single bound INT
450 // window ORDER BY: sort on the i64 instead of on a heap vector
451 // per row.
452 //
453 // Measured at 400k rows (round 978, ablation, answer checked
454 // byte-for-byte against the general path on a key column that
455 // is a permutation): `row_number() OVER (ORDER BY k)` went
456 // 157.057-157.868 ms to 31.253-31.679, which is 79.8% and puts
457 // it on top of the `OVER ()` baseline — the sort essentially
458 // disappears. Round 977 had already shown the cost was
459 // key-shaped rather than row-shaped: the sort's share was
460 // 132.0 ms on a three-integer table and 132.5 with a 200-byte
461 // column added, and a per-row COPY does scale with width
462 // (round 976 measured that at +36 ns/row/200 bytes).
463 //
464 // Gated to ROW_NUMBER, which is the one function that reads
465 // neither key vector — it numbers the order it is handed.
466 // `rank` and `dense_rank` compare adjacent entries' order keys
467 // in `compute_window_partition`, so leaving those vectors
468 // empty would silently give every row rank 1. A wider version
469 // would carry the i64 in the entry and teach those two to use
470 // it; this one is the part that can be shown correct by
471 // construction.
472 let int_okey_fast = partition_by.is_empty()
473 && order_by.len() == 1
474 && frame.is_none()
475 && filter.is_none()
476 && matches!(null_treatment, spg_sql::ast::NullTreatment::Respect)
477 && name.eq_ignore_ascii_case("row_number")
478 && o_bound[0].is_some_and(|pos| {
479 matches!(
480 schema_cols.get(pos).map(|c| c.ty),
481 Some(
482 spg_storage::DataType::Int
483 | spg_storage::DataType::BigInt
484 | spg_storage::DataType::SmallInt
485 )
486 )
487 });
488 // Set when a cell in that column turns out not to be an
489 // integer after all. The declared type says it should be, but
490 // "should" is not a thing to sort 400k rows on, so the general
491 // path takes over and this build is discarded.
492 let mut int_okey_bailed = false;
493 if int_okey_fast {
494 let pos = o_bound[0].expect("gated bound");
495 let desc = order_by[0].1;
496 // PG orders NULLs last ascending and first descending
497 // unless the query says otherwise.
498 let nulls_first = order_by[0].2.unwrap_or(desc);
499 let mut keyed: Vec<(bool, i64, usize)> = Vec::with_capacity(n_rows);
500 for (i, row) in filtered.iter().enumerate() {
501 match row.values.get(pos) {
502 Some(Value::Int(n)) => keyed.push((false, i64::from(*n), i)),
503 Some(Value::BigInt(n)) => keyed.push((false, *n, i)),
504 Some(Value::SmallInt(n)) => keyed.push((false, i64::from(*n), i)),
505 Some(Value::Null) | None => keyed.push((true, 0, i)),
506 Some(_) => {
507 int_okey_bailed = true;
508 break;
509 }
510 }
511 }
512 if !int_okey_bailed {
513 // `null_rank` puts NULLs on the side the query asked
514 // for; the row's original index breaks every tie, so
515 // equal keys keep the order the scan produced — what
516 // the stable sort below would have given them.
517 let null_rank = |is_null: bool| -> u8 { u8::from(is_null != nulls_first) };
518 keyed.sort_unstable_by(|a, b| {
519 null_rank(a.0)
520 .cmp(&null_rank(b.0))
521 .then_with(|| {
522 if a.0 {
523 core::cmp::Ordering::Equal
524 } else if desc {
525 b.1.cmp(&a.1)
526 } else {
527 a.1.cmp(&b.1)
528 }
529 })
530 .then_with(|| a.2.cmp(&b.2))
531 });
532 for (_, _, i) in keyed {
533 indexed.push((Vec::new(), Vec::new(), i));
534 }
535 } else {
536 indexed.clear();
537 }
538 }
539 if int_okey_fast && !int_okey_bailed {
540 // Ordered above; nothing else to build.
541 } else if int_pkey_fast {
542 let pos = p_bound[0].expect("gated bound");
543 let mut slot: hashbrown::HashMap<Option<i64>, usize> = hashbrown::HashMap::new();
544 let mut groups: Vec<Vec<usize>> = Vec::new();
545 for (i, row) in filtered.iter().enumerate() {
546 let k: Option<i64> = match row.values.get(pos) {
547 Some(Value::BigInt(n)) => Some(*n),
548 Some(Value::Int(n)) => Some(i64::from(*n)),
549 Some(Value::SmallInt(n)) => Some(i64::from(*n)),
550 _ => None,
551 };
552 match slot.get(&k) {
553 Some(&gi) => groups[gi].push(i),
554 None => {
555 slot.insert(k, groups.len());
556 groups.push(alloc::vec![i]);
557 }
558 }
559 }
560 // The downstream partition-boundary scan compares pkeys
561 // of ADJACENT entries, so the key must ride along — one
562 // single-element Vec per row (half the generic build's
563 // allocations, no string encode).
564 for g in groups {
565 for i in g {
566 let k: Value<'static> = match filtered[i].values.get(pos) {
567 Some(v) => v.clone(),
568 None => Value::Null,
569 };
570 indexed.push((alloc::vec![k], Vec::new(), i));
571 }
572 }
573 } else {
574 for (i, row) in filtered.iter().enumerate() {
575 let pkey: Vec<Value<'static>> = partition_by
576 .iter()
577 .enumerate()
578 .map(
579 |(k, p)| match p_bound[k].and_then(|pos| row.values.get(pos)) {
580 Some(v) => Ok(v.clone()),
581 None => eval::eval_expr(p, row, &ctx),
582 },
583 )
584 .collect::<Result<_, _>>()?;
585 // v7.39 (read01 round 54) — a window's ORDER BY over an enum
586 // column must sort by MEMBER order (enumsortorder), not the
587 // label's text. Enum values are Text at runtime, so the raw
588 // value key sorted alphabetically — `row_number() OVER (ORDER
589 // BY mood)` numbered the rows happy,ok,sad. Substitute the
590 // member ordinal, the same key the top-level ORDER BY uses.
591 // (Closes the enum-order knife's recorded window residual.)
592 let okey: Vec<(Value, bool, Option<bool>)> = order_by
593 .iter()
594 .enumerate()
595 .map(|(k, (e, desc, nf))| -> Result<_, EngineError> {
596 let v = match o_bound[k].and_then(|pos| row.values.get(pos)) {
597 Some(v) => v.clone(),
598 None => eval::eval_expr(e, row, &ctx)?,
599 };
600 let v = match crate::orderby::enum_order_ordinal(e, &v, &ctx) {
601 Some(ord) => Value::Float(ord),
602 None => v,
603 };
604 Ok((v, *desc, *nf))
605 })
606 .collect::<Result<_, _>>()?;
607 indexed.push((pkey, okey, i));
608 }
609 }
610 // Sort by (partition_key, order_key). Partition key uses
611 // a stable encoded form; order key respects ASC/DESC.
612 // v7.39 (round 731) — with NO window ORDER BY the sort's only
613 // job was putting same-partition rows next to each other, and a
614 // 500k-row comparison sort is a spectacular way to hash-group:
615 // the panel's `sum(id) OVER (PARTITION BY g)` spent ~100 ms
616 // here. Group by encoded key instead, preserving row order
617 // inside each group — exactly what the stable sort preserved,
618 // so every function (row_number included) answers the same.
619 if int_okey_fast && !int_okey_bailed {
620 // Already ordered by the i64 key above.
621 } else if int_pkey_fast {
622 // Already grouped above; same-partition rows are adjacent
623 // in original row order.
624 } else if order_by.is_empty() && !partition_by.is_empty() {
625 let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
626 let mut groups: Vec<
627 Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)>,
628 > = Vec::new();
629 let mut keybuf = String::new();
630 for entry in indexed.drain(..) {
631 keybuf.clear();
632 for v in &entry.0 {
633 crate::aggregate::push_canonical_key(&mut keybuf, v);
634 }
635 match slot.get(keybuf.as_str()) {
636 Some(&gi) => groups[gi].push(entry),
637 None => {
638 slot.insert(keybuf.clone(), groups.len());
639 groups.push(alloc::vec![entry]);
640 }
641 }
642 }
643 for g in groups {
644 indexed.extend(g);
645 }
646 } else {
647 indexed.sort_by(|a, b| {
648 let p_cmp = partition_key_cmp(&a.0, &b.0);
649 if p_cmp != core::cmp::Ordering::Equal {
650 return p_cmp;
651 }
652 crate::window::order_key_cmp_in(&a.1, &b.1, &o_colls)
653 });
654 }
655 // Per-partition compute.
656 let mut out_vals: Vec<Value<'static>> = alloc::vec![Value::Null; n_rows];
657 let mut p_start = 0;
658 while p_start < indexed.len() {
659 let mut p_end = p_start + 1;
660 while p_end < indexed.len()
661 && partition_key_cmp(&indexed[p_start].0, &indexed[p_end].0)
662 == core::cmp::Ordering::Equal
663 {
664 p_end += 1;
665 }
666 // Compute the function within this partition slice.
667 compute_window_partition(
668 name,
669 args,
670 arg_bound,
671 !order_by.is_empty(),
672 frame.as_ref(),
673 *null_treatment,
674 filter.as_deref(),
675 &indexed[p_start..p_end],
676 &filtered,
677 &ctx,
678 &mut out_vals,
679 )?;
680 p_start = p_end;
681 }
682 win_vals.push(out_vals);
683 }
684
685 // 4) Build extended schema: original columns + synthetic.
686 let mut ext_cols = schema_cols.clone();
687 for (i, wnode) in window_nodes.iter().enumerate() {
688 // v7.39.12 — the synthetic column carries the window call's
689 // TYPE.
690 //
691 // The comment here said "type doesn't matter for projection
692 // eval", and for the eval it does not — the values are
693 // already computed. It is the type that travels in the
694 // RowDescription, and psql aligns a column by that: on
695 // `SELECT count(*) AS plaincnt, count(*) OVER () AS wincnt`
696 // PostgreSQL right-aligns both and SPG left-aligned the
697 // second, because the first was bigint and the second was
698 // this `Text`. `\gdesc` — which asks the extended
699 // protocol's Describe — reported the right type for both,
700 // so the two descriptions of one column disagreed.
701 //
702 // Reported by sentori against 7.39.11, found by the
703 // alignment. Text stays as the fallback for a call whose
704 // type this build cannot name, which is what it was.
705 let ty =
706 crate::describe::describe_expr_type(wnode, schema_cols).unwrap_or(DataType::Text);
707 ext_cols.push(ColumnSchema::new(alloc::format!("__win_{i}"), ty, true));
708 }
709 // 6) Rewrite the projection: WindowFunction nodes → Column(__win_N).
710 let mut rewritten_items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
711 for item in &stmt.items {
712 let new_item = match item {
713 SelectItem::Wildcard => SelectItem::Wildcard,
714 SelectItem::QualifiedWildcard(q) => SelectItem::QualifiedWildcard(q.clone()),
715 SelectItem::Expr { expr, alias } => {
716 let mut e = expr.clone();
717 rewrite_window_to_columns(&mut e, &window_nodes);
718 // The rewrite swaps the window call for a synthetic
719 // `__win_N` column, and the projection then reported
720 // THAT as the column name — `SELECT count(*) OVER ()`
721 // answered `__win_0`, an internal name, where PG18
722 // answers `count`. Pin the name while the call the
723 // column is named for is still in hand.
724 let alias = if alias.is_none() && e != *expr {
725 Some(default_output_name(expr, self.speaks_mysql))
726 } else {
727 alias.clone()
728 };
729 SelectItem::Expr { expr: e, alias }
730 }
731 };
732 rewritten_items.push(new_item);
733 }
734
735 // 7) Project into final rows. JOIN case uses None so the
736 // qualifier check in `resolve_column` falls through to the
737 // composite `alias.col` schema lookup; single-table case
738 // keeps the bare alias so `bare_col` resolution still
739 // works for the projection's per-row column references.
740 // v7.39 (read01 round 54) — build through `ev_ctx`, the canonical
741 // constructor: it threads the catalog (plus render style / tz / GUCs)
742 // that a bare `EvalContext::new` drops. Without the catalog the OUTER
743 // `ORDER BY <enum col>` of a windowed query sorted by TEXT — the
744 // window values were right, the row order silently was not.
745 let ext_ctx = self.ev_ctx(&ext_cols, alias_opt);
746 let projection = build_projection_hiding_tail(
747 &rewritten_items,
748 &ext_cols,
749 alias,
750 self.speaks_mysql,
751 window_nodes.len(),
752 Some(self.active_catalog()),
753 )?;
754 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(n_rows);
755 // v7.39 (round 592) — the extended row (input columns plus the window
756 // values) used to be materialised for EVERY input row and kept until
757 // the projection had run: the input values cloned into a fresh Vec,
758 // then grown once to take the window columns. A counting allocator put
759 // the window path at 4 allocations a row where a plain derived table
760 // takes 1, and named all four — the input row, the clone, the growth,
761 // and the projected row. Only the last has to exist afterwards, so the
762 // extended row is one buffer refilled per row.
763 let mut ext_row: Row<'static> =
764 Row::new(Vec::with_capacity(schema_cols.len() + window_nodes.len()));
765 for i in 0..n_rows {
766 if i.is_multiple_of(256) {
767 cancel.check()?;
768 }
769 ext_row.values.clear();
770 ext_row.values.extend(filtered[i].values.iter().cloned());
771 for w in 0..window_nodes.len() {
772 ext_row.values.push(win_vals[w][i].clone());
773 }
774 let row = &ext_row;
775 let mut values = Vec::with_capacity(projection.len());
776 for p in &projection {
777 values.push(eval::eval_expr(&p.expr, row, &ext_ctx)?);
778 }
779 let order_keys = if stmt.order_by.is_empty() {
780 Vec::new()
781 } else {
782 let mut keys = Vec::with_capacity(stmt.order_by.len());
783 for o in &stmt.order_by {
784 let mut e = o.expr.clone();
785 rewrite_window_to_columns(&mut e, &window_nodes);
786 let key = eval::eval_expr(&e, row, &ext_ctx)?;
787 // v7.39 (read01 round 54) — this path builds its order keys
788 // itself instead of going through `build_order_keys`, so it
789 // skipped the enum-ordinal substitution: the OUTER
790 // `ORDER BY <enum col>` of a windowed query sorted by the
791 // label's TEXT, not by member order. The window values were
792 // right and only the row order was wrong — silently.
793 match crate::orderby::enum_order_ordinal(&e, &key, &ext_ctx) {
794 Some(ord) => keys.push(value_to_order_key(&Value::Float(ord))?),
795 None => keys.push(value_to_order_key(&key)?),
796 }
797 }
798 keys
799 };
800 tagged.push((order_keys, Row::new(values)));
801 }
802 // ORDER BY + LIMIT/OFFSET on the projected rows.
803 if !stmt.order_by.is_empty() {
804 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
805 // v7.39.11 — and the collation, which this path was sorting
806 // without.
807 //
808 // Reported by sentori against 7.39.10: `SELECT t, count(*)
809 // OVER () FROM t ORDER BY t` answered `A B a b` on a
810 // database collating `en_US.utf8` where the same query
811 // without the window function answers `a A b B`. No row is
812 // wrong and nothing raises; only the order changes.
813 //
814 // Same cause as the enum-ordinal defect the comment above
815 // records: this branch builds its order keys itself instead
816 // of going through `build_order_keys`, so anything that
817 // path resolves has to be resolved again here, and the
818 // collation was not. `order_by_collations` is the one place
819 // that answers it — explicit `COLLATE` first, then the
820 // column's declaration, then the database's — so calling it
821 // here cannot disagree with the ungrouped path.
822 let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
823 crate::orderby::sort_by_keys_in(
824 &mut tagged,
825 &descs,
826 &colls,
827 self.session_parallel_workers(),
828 );
829 }
830 let mut out_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
831 // v7.37 D.41 — `SELECT DISTINCT` over a window projection: the window
832 // pipeline builds one output row per input row, so DISTINCT must dedup the
833 // projected rows (PG evaluates window functions before DISTINCT). Applied
834 // after ORDER BY (duplicate rows share sort keys, so order is preserved)
835 // and before LIMIT.
836 if stmt.distinct {
837 // v7.38.14 — see the synthetic-source sites below: the mask was
838 // always available here, from the same projection this function
839 // already built.
840 out_rows = dedup_rows(
841 out_rows,
842 FoldSpec::of_masks(
843 self.speaks_mysql,
844 &fold_mask(&projection),
845 &pad_mask(&projection),
846 ),
847 );
848 }
849 apply_offset_and_limit(&mut out_rows, stmt.offset_literal(), stmt.limit_literal());
850 let final_cols: Vec<ColumnSchema> = projection
851 .into_iter()
852 .map(|p| p.to_column_schema())
853 .collect();
854 Ok(QueryResult::Rows {
855 columns: final_cols,
856 rows: out_rows,
857 })
858 }
859
860 /// v4.11: materialise each CTE into a temp table inside a
861 /// cloned catalog, then run the body SELECT against a fresh
862 /// engine instance that owns the enriched catalog. The clone
863 /// is moderately expensive — only paid by CTE-bearing queries.
864 /// Subqueries inside CTE bodies / the main body resolve as
865 /// usual; `clock_fn` is propagated so `NOW()` lines up.
866 /// v7.16.2 — mailrs round-10 A.3. Materialise the
867 /// `information_schema.*` / `pg_catalog.*` virtual views
868 /// the SELECT references, then re-execute the SELECT
869 /// against an enriched catalog where those views are real
870 /// tables. Same pattern as `exec_with_ctes`. The temp
871 /// engine carries `meta_views_materialised = true` so its
872 /// own meta-dispatch short-circuits — without that we'd
873 /// infinite-recurse since the temp catalog's view name
874 /// still starts with `__spg_info_` and re-triggers the
875 /// check.
876 pub(crate) fn exec_select_with_meta_views(
877 &self,
878 stmt: &SelectStatement,
879 cancel: CancelToken<'_>,
880 ) -> Result<QueryResult, EngineError> {
881 let catalog = self.meta_view_catalog(stmt)?;
882 let mut temp = Engine::restore(catalog);
883 if let Some(c) = self.clock {
884 temp = temp.with_clock(c);
885 }
886 if let Some(f) = self.salt_fn {
887 temp = temp.with_salt_fn(f);
888 }
889 // v7.39 (round 522) — the temp engine holds the materialised
890 // catalog and, until now, nothing of the SESSION. So every
891 // session-scoped answer changed the moment a system view
892 // appeared in the FROM clause: `SELECT current_user` said
893 // `unmei` and `SELECT current_user FROM pg_class` said `admin`;
894 // `current_setting('work_mem')` fell back to the boot default
895 // after a SET; `application_name` read empty. A privilege check
896 // written against a catalog join was reading a different
897 // identity than the same check written without one.
898 //
899 // Carry what a session can be observed through — its parameters
900 // (which is also where the session user lives), the role store
901 // the privilege builtins read, the dialect, and the rendering
902 // settings a timestamp is spelled with.
903 temp.session_params.clone_from(&self.session_params);
904 temp.users.clone_from(&self.users);
905 temp.backslash_escapes = self.backslash_escapes;
906 temp.speaks_mysql = self.speaks_mysql;
907 temp.mysql_strict = self.mysql_strict;
908 temp.render_style = self.render_style;
909 temp.tz_offset_fn = self.tz_offset_fn;
910 temp.tz_localize_fn = self.tz_localize_fn;
911 temp.tz_abbrev_fn = self.tz_abbrev_fn;
912 temp.meta_views_materialised = true;
913 temp.exec_select_cancel(stmt, cancel)
914 }
915
916 /// v7.39 (round 462) — the catalog a meta-view SELECT resolves
917 /// against: this engine's catalog with every `__spg_*` view the
918 /// statement references materialised into it.
919 ///
920 /// Split out of `exec_select_with_meta_views` so Describe can reach
921 /// the same shapes execution reaches. Describe used to look the FROM
922 /// relation up in the plain catalog, where a system view does not
923 /// exist, and reported "no columns" for every one of them — so an
924 /// extended-protocol client reading `pg_stat_user_tables` got rows
925 /// with no column metadata. Sharing the materialisation means a
926 /// view added here is described correctly the day it is added.
927 pub(crate) fn meta_view_catalog(&self, stmt: &SelectStatement) -> Result<Catalog, EngineError> {
928 let mut needed: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
929 collect_meta_view_names(stmt, &mut needed);
930 let mut catalog = self.active_catalog().clone();
931 for view in &needed {
932 if catalog.get(view).is_some() {
933 continue;
934 }
935 match view.as_str() {
936 "__spg_info_columns" => {
937 let (schema, rows) = synth_information_schema_columns(
938 self.active_catalog(),
939 self.speaks_mysql,
940 &self.mysql_schema_name(),
941 );
942 materialise_meta_view(&mut catalog, view, schema, rows)?;
943 }
944 "__spg_info_tables" => {
945 let (schema, rows) = synth_information_schema_tables(
946 self.active_catalog(),
947 self.speaks_mysql,
948 &self.mysql_schema_name(),
949 );
950 materialise_meta_view(&mut catalog, view, schema, rows)?;
951 }
952 "__spg_pg_class" => {
953 let (schema, rows) = synth_pg_class(
954 self.active_catalog(),
955 i64::try_from(self.vacuum_oldest_active()).unwrap_or(i64::MAX),
956 );
957 materialise_meta_view(&mut catalog, view, schema, rows)?;
958 }
959 "__spg_pg_attribute" => {
960 let (schema, rows) = synth_pg_attribute(self.active_catalog());
961 materialise_meta_view(&mut catalog, view, schema, rows)?;
962 }
963 // v7.17.0 Phase 3.P0-50 — pg_catalog.pg_type for
964 // sqlx / SQLAlchemy / Diesel / pgAdmin lookups.
965 "__spg_pg_type" => {
966 let (schema, rows) = synth_pg_type(self.active_catalog());
967 materialise_meta_view(&mut catalog, view, schema, rows)?;
968 }
969 // v7.39 (round 621) — pg_catalog.pg_operator, which did not
970 // exist at all.
971 "__spg_pg_operator" => {
972 let (schema, rows) = synth_pg_operator(self.active_catalog());
973 materialise_meta_view(&mut catalog, view, schema, rows)?;
974 }
975 // v7.17.0 Phase 3.P0-51 — pg_catalog.pg_proc for
976 // function-name introspection (ORM / pgAdmin).
977 "__spg_pg_proc" => {
978 let (schema, rows) = synth_pg_proc(self.active_catalog());
979 materialise_meta_view(&mut catalog, view, schema, rows)?;
980 }
981 // v7.24 (round-16 D) — pg_catalog.pg_trigger. The
982 // round-16 "why doesn't prod fire the trigger"
983 // question was unanswerable because triggers had NO
984 // introspection surface; tgname/tgenabled plus the
985 // pragmatic relname/timing/events/function columns
986 // make "is it registered and enabled" a one-liner.
987 "__spg_pg_trigger" => {
988 let (schema, rows) = synth_pg_trigger(self.active_catalog());
989 materialise_meta_view(&mut catalog, view, schema, rows)?;
990 }
991 // v7.17.0 Phase 3.P0-52 — pg_catalog.pg_namespace
992 // (schema list for admin tools' tree views).
993 "__spg_pg_namespace" => {
994 let (schema, rows) = synth_pg_namespace(self.active_catalog());
995 materialise_meta_view(&mut catalog, view, schema, rows)?;
996 }
997 // v7.39 — pg_tables convenience view (was a pgwire
998 // canned response that ignored projections).
999 "__spg_pg_tables" => {
1000 let (schema, rows) =
1001 crate::system_catalog::synth_pg_tables(self.active_catalog());
1002 materialise_meta_view(&mut catalog, view, schema, rows)?;
1003 }
1004 // v7.37.24 (24.1) — pg_catalog.pg_enum (label list
1005 // for ENUM types; sqlx / ORM enum codecs read this).
1006 "__spg_pg_enum" => {
1007 let (schema, rows) =
1008 crate::system_catalog::synth_pg_enum(self.active_catalog());
1009 materialise_meta_view(&mut catalog, view, schema, rows)?;
1010 }
1011 // v7.37.21 (21.13) — pg_catalog.pg_replication_slots
1012 // (shape-stable empty until 21.12 persists slot state).
1013 // v7.39 (round 277) — session-scoped prepared statements.
1014 "__spg_pg_prepared_statements" => {
1015 let (schema, rows) = crate::system_catalog::synth_pg_prepared_statements(
1016 &self.prepared_statements,
1017 );
1018 materialise_meta_view(&mut catalog, view, schema, rows)?;
1019 }
1020 "__spg_pg_replication_slots" => {
1021 let (schema, rows) =
1022 crate::system_catalog::synth_pg_replication_slots(self.active_catalog());
1023 materialise_meta_view(&mut catalog, view, schema, rows)?;
1024 }
1025 // v7.37.21 (21.13-b) — pg_catalog.pg_publication
1026 // (one row per CREATE PUBLICATION).
1027 "__spg_pg_publication" => {
1028 let (schema, rows) = crate::system_catalog::synth_pg_publication(self);
1029 materialise_meta_view(&mut catalog, view, schema, rows)?;
1030 }
1031 // v7.37.21 (21.13-c) — pg_catalog.pg_subscription
1032 // (one row per CREATE SUBSCRIPTION; subconninfo
1033 // redacted so dashboards can't leak credentials).
1034 "__spg_pg_subscription" => {
1035 let (schema, rows) = crate::system_catalog::synth_pg_subscription(self);
1036 materialise_meta_view(&mut catalog, view, schema, rows)?;
1037 }
1038 // v7.37.22 (22.x-stat-db) — pg_catalog.pg_stat_database
1039 // (one row for SPG's single database; counters are
1040 // shape-stable 0 until wiring lands).
1041 "__spg_pg_stat_database" => {
1042 let (schema, rows) = crate::system_catalog::synth_pg_stat_database(
1043 self,
1044 self.stat_tup_inserted,
1045 self.stat_tup_updated,
1046 self.stat_tup_deleted,
1047 );
1048 materialise_meta_view(&mut catalog, view, schema, rows)?;
1049 }
1050 // v7.37.22 (22.14) — pg_catalog.pg_stat_user_tables
1051 // (per-table churn counters; live_tup = row count).
1052 "__spg_pg_stat_user_tables" => {
1053 // r192 — DML counters come from the engine-side
1054 // non-transactional map, not the (tx-shadowed)
1055 // catalog tables.
1056 let (schema, rows) = crate::system_catalog::synth_pg_stat_user_tables(
1057 self.active_catalog(),
1058 &self.table_write_stats,
1059 );
1060 materialise_meta_view(&mut catalog, view, schema, rows)?;
1061 }
1062 // v7.37.22 (22.15) — pg_catalog.pg_stat_user_indexes
1063 // (per-index usage counters; flag unused indexes).
1064 "__spg_pg_stat_user_indexes" => {
1065 let (schema, rows) =
1066 crate::system_catalog::synth_pg_stat_user_indexes(self.active_catalog());
1067 materialise_meta_view(&mut catalog, view, schema, rows)?;
1068 }
1069 // v7.37.22 (22.16) — pg_catalog.pg_stat_bgwriter.
1070 "__spg_pg_stat_bgwriter" => {
1071 let (schema, rows) =
1072 crate::system_catalog::synth_pg_stat_bgwriter(self.active_catalog());
1073 materialise_meta_view(&mut catalog, view, schema, rows)?;
1074 }
1075 // v7.38 (read01 P3.14) — pg_catalog.pg_stat_checkpointer /
1076 // pg_stat_wal shell views (shape-stable, counters pending).
1077 "__spg_pg_stat_checkpointer" => {
1078 let (schema, rows) =
1079 crate::system_catalog::synth_pg_stat_checkpointer(self.active_catalog());
1080 materialise_meta_view(&mut catalog, view, schema, rows)?;
1081 }
1082 "__spg_pg_stat_wal" => {
1083 let (schema, rows) =
1084 crate::system_catalog::synth_pg_stat_wal(self.active_catalog());
1085 materialise_meta_view(&mut catalog, view, schema, rows)?;
1086 }
1087 // v7.38 (read01 P3.15) — pg_catalog.pg_stat_slru /
1088 // pg_stat_subscription_stats shell views.
1089 "__spg_pg_stat_slru" => {
1090 let (schema, rows) =
1091 crate::system_catalog::synth_pg_stat_slru(self.active_catalog());
1092 materialise_meta_view(&mut catalog, view, schema, rows)?;
1093 }
1094 "__spg_pg_stat_subscription_stats" => {
1095 let (schema, rows) = crate::system_catalog::synth_pg_stat_subscription_stats(
1096 self.active_catalog(),
1097 );
1098 materialise_meta_view(&mut catalog, view, schema, rows)?;
1099 }
1100 // v7.37.22 (22.17) — pg_catalog.pg_stat_archiver.
1101 "__spg_pg_stat_archiver" => {
1102 let (schema, rows) =
1103 crate::system_catalog::synth_pg_stat_archiver(self.active_catalog());
1104 materialise_meta_view(&mut catalog, view, schema, rows)?;
1105 }
1106 // v7.37.21 (21.13-d) — pg_catalog.pg_stat_replication.
1107 "__spg_pg_stat_replication" => {
1108 let (schema, rows) =
1109 crate::system_catalog::synth_pg_stat_replication(self.active_catalog());
1110 materialise_meta_view(&mut catalog, view, schema, rows)?;
1111 }
1112 // v7.37.24 (24.13) — pg_catalog.pg_am.
1113 "__spg_pg_am" => {
1114 let (schema, rows) = crate::system_catalog::synth_pg_am(self.active_catalog());
1115 materialise_meta_view(&mut catalog, view, schema, rows)?;
1116 }
1117 // v7.37.22 (22.18) — pg_catalog.pg_stat_io (PG 16+).
1118 "__spg_pg_stat_io" => {
1119 let (schema, rows) =
1120 crate::system_catalog::synth_pg_stat_io(self.active_catalog());
1121 materialise_meta_view(&mut catalog, view, schema, rows)?;
1122 }
1123 // v7.37.22 (22.19) — pg_catalog.pg_stat_user_functions.
1124 "__spg_pg_stat_user_functions" => {
1125 let (schema, rows) =
1126 crate::system_catalog::synth_pg_stat_user_functions(self.active_catalog());
1127 materialise_meta_view(&mut catalog, view, schema, rows)?;
1128 }
1129 // v7.39 (round 287) — pg_catalog.pg_largeobject{,_metadata}.
1130 "__spg_pg_largeobject" => {
1131 let (schema, rows) =
1132 crate::system_catalog::synth_pg_largeobject(self.active_catalog());
1133 materialise_meta_view(&mut catalog, view, schema, rows)?;
1134 }
1135 "__spg_pg_largeobject_metadata" => {
1136 let (schema, rows) =
1137 crate::system_catalog::synth_pg_largeobject_metadata(self.active_catalog());
1138 materialise_meta_view(&mut catalog, view, schema, rows)?;
1139 }
1140 // v7.37.23 (23.7-a) — pg_catalog.pg_statistic_ext.
1141 "__spg_pg_statistic_ext" => {
1142 let (schema, rows) =
1143 crate::system_catalog::synth_pg_statistic_ext(self.active_catalog());
1144 materialise_meta_view(&mut catalog, view, schema, rows)?;
1145 }
1146 // v7.38.18 — pg_catalog.pg_stats, the readable view.
1147 "__spg_pg_stats" => {
1148 let (schema, rows) = crate::system_catalog::synth_pg_stats(
1149 self.active_catalog(),
1150 &self.statistics,
1151 );
1152 materialise_meta_view(&mut catalog, view, schema, rows)?;
1153 }
1154 // v7.37.24 (24.15) — pg_catalog.pg_statistic.
1155 "__spg_pg_statistic" => {
1156 let (schema, rows) = crate::system_catalog::synth_pg_statistic(
1157 self.active_catalog(),
1158 &self.statistics,
1159 );
1160 materialise_meta_view(&mut catalog, view, schema, rows)?;
1161 }
1162 // v7.37.22 (22.20) — pg_catalog.pg_stat_progress_vacuum.
1163 "__spg_pg_stat_progress_vacuum" => {
1164 let (schema, rows) =
1165 crate::system_catalog::synth_pg_stat_progress_vacuum(self.active_catalog());
1166 materialise_meta_view(&mut catalog, view, schema, rows)?;
1167 }
1168 // v7.37.22 (22.21) — pg_catalog.pg_stat_progress_create_index.
1169 "__spg_pg_stat_progress_create_index" => {
1170 let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_create_index(
1171 self.active_catalog(),
1172 );
1173 materialise_meta_view(&mut catalog, view, schema, rows)?;
1174 }
1175 // v7.37.22 (22.22) — pg_catalog.pg_stat_progress_analyze.
1176 "__spg_pg_stat_progress_analyze" => {
1177 let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_analyze(
1178 self.active_catalog(),
1179 );
1180 materialise_meta_view(&mut catalog, view, schema, rows)?;
1181 }
1182 // v7.37.24 (24.16) — pg_catalog.pg_inherits
1183 // (partition parent → child OID mapping).
1184 "__spg_pg_inherits" => {
1185 let (schema, rows) =
1186 crate::system_catalog::synth_pg_inherits(self.active_catalog());
1187 materialise_meta_view(&mut catalog, view, schema, rows)?;
1188 }
1189 // v7.39 (round 650) — the text-search catalogs, filled
1190 // with what SPG actually has rather than PG's thirty.
1191 "__spg_pg_ts_config_map" => {
1192 let (schema, rows) =
1193 crate::system_catalog::synth_pg_ts_config_map(self.active_catalog());
1194 materialise_meta_view(&mut catalog, view, schema, rows)?;
1195 }
1196 "__spg_pg_ts_config" => {
1197 let (schema, rows) =
1198 crate::system_catalog::synth_pg_ts_config(self.active_catalog());
1199 materialise_meta_view(&mut catalog, view, schema, rows)?;
1200 }
1201 "__spg_pg_ts_dict" => {
1202 let (schema, rows) =
1203 crate::system_catalog::synth_pg_ts_dict(self.active_catalog());
1204 materialise_meta_view(&mut catalog, view, schema, rows)?;
1205 }
1206 "__spg_pg_ts_parser" => {
1207 let (schema, rows) =
1208 crate::system_catalog::synth_pg_ts_parser(self.active_catalog());
1209 materialise_meta_view(&mut catalog, view, schema, rows)?;
1210 }
1211 "__spg_pg_ts_template" => {
1212 let (schema, rows) =
1213 crate::system_catalog::synth_pg_ts_template(self.active_catalog());
1214 materialise_meta_view(&mut catalog, view, schema, rows)?;
1215 }
1216 // v7.37.24 (24.17) — pg_catalog.pg_depend
1217 // (dependency graph; shape-stable empty since
1218 // SPG's drop enforcement is per-kind, not per-object).
1219 "__spg_pg_depend" => {
1220 let (schema, rows) =
1221 crate::system_catalog::synth_pg_depend(self.active_catalog());
1222 materialise_meta_view(&mut catalog, view, schema, rows)?;
1223 }
1224 // 7.38.1 S5.1 — pg_catalog.pg_opclass (pg_dump wall #1).
1225 "__spg_pg_opclass" => {
1226 let (schema, rows) =
1227 crate::system_catalog::synth_pg_opclass(self.active_catalog());
1228 materialise_meta_view(&mut catalog, view, schema, rows)?;
1229 }
1230 "__spg_pg_opfamily" => {
1231 let (schema, rows) =
1232 crate::system_catalog::synth_pg_opfamily(self.active_catalog());
1233 materialise_meta_view(&mut catalog, view, schema, rows)?;
1234 }
1235 "__spg_pg_amop" => {
1236 let (schema, rows) =
1237 crate::system_catalog::synth_pg_amop(self.active_catalog());
1238 materialise_meta_view(&mut catalog, view, schema, rows)?;
1239 }
1240 "__spg_pg_amproc" => {
1241 let (schema, rows) =
1242 crate::system_catalog::synth_pg_amproc(self.active_catalog());
1243 materialise_meta_view(&mut catalog, view, schema, rows)?;
1244 }
1245 // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1246 // ORM reflection + pg_dump read the deparsed default text).
1247 "__spg_pg_attrdef" => {
1248 let (schema, rows) =
1249 crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1250 materialise_meta_view(&mut catalog, view, schema, rows)?;
1251 }
1252 // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1253 "__spg_pg_policy" => {
1254 let (schema, rows) =
1255 crate::system_catalog::synth_pg_policy(self.active_catalog());
1256 materialise_meta_view(&mut catalog, view, schema, rows)?;
1257 }
1258 "__spg_pg_policies" => {
1259 let (schema, rows) =
1260 crate::system_catalog::synth_pg_policies(self.active_catalog());
1261 materialise_meta_view(&mut catalog, view, schema, rows)?;
1262 }
1263 // v7.37.24 (24.14) — pg_catalog.pg_collation.
1264 "__spg_pg_collation" => {
1265 let (schema, rows) =
1266 crate::system_catalog::synth_pg_collation(self.active_catalog());
1267 materialise_meta_view(&mut catalog, view, schema, rows)?;
1268 }
1269 // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1270 "__spg_pg_tablespace" => {
1271 let (schema, rows) =
1272 crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1273 materialise_meta_view(&mut catalog, view, schema, rows)?;
1274 }
1275 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1276 // for pgAdmin / DataGrip "indexes per table" listings.
1277 "__spg_pg_indexes" => {
1278 let (schema, rows) = synth_pg_indexes(self.active_catalog());
1279 materialise_meta_view(&mut catalog, view, schema, rows)?;
1280 }
1281 // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1282 // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1283 "__spg_pg_description" => {
1284 let (schema, rows) =
1285 crate::system_catalog::synth_pg_description(self.active_catalog());
1286 materialise_meta_view(&mut catalog, view, schema, rows)?;
1287 }
1288 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1289 // for index introspection by ORM compilers.
1290 "__spg_pg_index" => {
1291 let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1292 materialise_meta_view(&mut catalog, view, schema, rows)?;
1293 }
1294 // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1295 // for FK / UNIQUE / PK / CHECK introspection.
1296 "__spg_pg_constraint" => {
1297 let (schema, rows) = synth_pg_constraint(self.active_catalog());
1298 materialise_meta_view(&mut catalog, view, schema, rows)?;
1299 }
1300 // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1301 // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1302 "__spg_pg_sequence" => {
1303 let (schema, rows) = synth_pg_sequence(self.active_catalog());
1304 materialise_meta_view(&mut catalog, view, schema, rows)?;
1305 }
1306 // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1307 // pg_roles / pg_user. SPG is single-database so
1308 // pg_database surfaces just `postgres`; pg_roles
1309 // / pg_user walk the engine's UserStore.
1310 "__spg_pg_database" => {
1311 let (schema, rows) = synth_pg_database(self);
1312 materialise_meta_view(&mut catalog, view, schema, rows)?;
1313 }
1314 "__spg_pg_roles" => {
1315 let (schema, rows) = synth_pg_roles(self);
1316 materialise_meta_view(&mut catalog, view, schema, rows)?;
1317 }
1318 // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1319 // same roles, with PG's own `use*` column names. It used to
1320 // publish pg_roles' columns under this name.
1321 "__spg_pg_user" => {
1322 let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1323 materialise_meta_view(&mut catalog, view, schema, rows)?;
1324 }
1325 // v7.39 (read01 round 58) — role membership.
1326 "__spg_pg_auth_members" => {
1327 let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1328 materialise_meta_view(&mut catalog, view, schema, rows)?;
1329 }
1330 // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1331 // pg_views surfaces every CREATE VIEW result; SPG
1332 // ships one row per declared view from the catalog.
1333 "__spg_pg_views" => {
1334 let (schema, rows) = synth_pg_views(self.active_catalog());
1335 materialise_meta_view(&mut catalog, view, schema, rows)?;
1336 }
1337 // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1338 // catalogued query-rewrite RULE.
1339 "__spg_pg_rules" => {
1340 let (schema, rows) =
1341 crate::system_catalog::synth_pg_rules(self.active_catalog());
1342 materialise_meta_view(&mut catalog, view, schema, rows)?;
1343 }
1344 // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1345 // catalogue `pg_get_ruledef(oid)` resolves against.
1346 "__spg_pg_rewrite" => {
1347 let (schema, rows) =
1348 crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1349 materialise_meta_view(&mut catalog, view, schema, rows)?;
1350 }
1351 // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1352 // and PG's own column names.
1353 "__spg_pg_matviews" => {
1354 let (schema, rows) =
1355 crate::system_catalog::synth_pg_matviews(self.active_catalog());
1356 materialise_meta_view(&mut catalog, view, schema, rows)?;
1357 }
1358 // pg_catalog.pg_extension — native capability list
1359 // (mailrs embed round-12).
1360 // v7.39 (round 546) — the catalogs SPG has real content
1361 // for, from the facts it already holds.
1362 "__spg_pg_db_role_setting" => {
1363 let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1364 materialise_meta_view(&mut catalog, view, schema, rows)?;
1365 }
1366 "__spg_pg_language" => {
1367 let (schema, rows) = crate::system_catalog::synth_pg_language();
1368 materialise_meta_view(&mut catalog, view, schema, rows)?;
1369 }
1370 "__spg_pg_sequences" => {
1371 let (schema, rows) =
1372 crate::system_catalog::synth_pg_sequences(self.active_catalog());
1373 materialise_meta_view(&mut catalog, view, schema, rows)?;
1374 }
1375 "__spg_pg_range" => {
1376 let (schema, rows) = crate::system_catalog::synth_pg_range();
1377 materialise_meta_view(&mut catalog, view, schema, rows)?;
1378 }
1379 "__spg_pg_partitioned_table" => {
1380 let (schema, rows) =
1381 crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1382 materialise_meta_view(&mut catalog, view, schema, rows)?;
1383 }
1384 "__spg_pg_authid" => {
1385 let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1386 materialise_meta_view(&mut catalog, view, schema, rows)?;
1387 }
1388 "__spg_pg_group" => {
1389 let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1390 materialise_meta_view(&mut catalog, view, schema, rows)?;
1391 }
1392 "__spg_pg_shadow" => {
1393 let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1394 materialise_meta_view(&mut catalog, view, schema, rows)?;
1395 }
1396 // v7.39 (round 544) — pg_cast, probed from the real
1397 // cast implementation.
1398 "__spg_pg_cast" => {
1399 let (schema, rows) = crate::system_catalog::synth_pg_cast();
1400 materialise_meta_view(&mut catalog, view, schema, rows)?;
1401 }
1402 // v7.39 (round 541) — an empty catalog that exists.
1403 "__spg_pg_foreign_table" => {
1404 let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1405 materialise_meta_view(&mut catalog, view, schema, rows)?;
1406 }
1407 "__spg_pg_extension" => {
1408 let (schema, rows) = synth_pg_extension();
1409 materialise_meta_view(&mut catalog, view, schema, rows)?;
1410 }
1411 // v7.39 (round 502) — the timezone catalogues.
1412 "__spg_pg_timezone_names" => {
1413 let (schema, rows) = synth_pg_timezone_names(self);
1414 materialise_meta_view(&mut catalog, view, schema, rows)?;
1415 }
1416 "__spg_pg_timezone_abbrevs" => {
1417 let (schema, rows) = synth_pg_timezone_abbrevs(self);
1418 materialise_meta_view(&mut catalog, view, schema, rows)?;
1419 }
1420 // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1421 "__spg_pg_settings" => {
1422 let (schema, rows) = synth_pg_settings(self);
1423 materialise_meta_view(&mut catalog, view, schema, rows)?;
1424 }
1425 // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1426 // v7.39 (read01 round 51) — information_schema.role_table_grants
1427 // and .table_privileges. Both report the owner's seven implicit
1428 // table privileges; SPG's single role owns everything.
1429 // v7.39 (read01 round 59) — information_schema.column_privileges.
1430 "__spg_info_column_privileges" => {
1431 let (schema, rows) =
1432 crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1433 materialise_meta_view(&mut catalog, view, schema, rows)?;
1434 }
1435 "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1436 let grantee = self.current_role().to_string();
1437 let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1438 self.active_catalog(),
1439 &grantee,
1440 );
1441 materialise_meta_view(&mut catalog, view, schema, rows)?;
1442 }
1443 "__spg_info_key_column_usage" => {
1444 // v7.39.11 — the session's dialect decides the
1445 // column list; see the synthesiser.
1446 let mysql = self.in_mysql_dialect();
1447 let (schema, rows) = synth_info_key_column_usage(self.active_catalog(), mysql);
1448 materialise_meta_view(&mut catalog, view, schema, rows)?;
1449 }
1450 // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1451 "__spg_info_referential_constraints" => {
1452 let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1453 materialise_meta_view(&mut catalog, view, schema, rows)?;
1454 }
1455 // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1456 "__spg_info_statistics" => {
1457 let (schema, rows) = synth_info_statistics(self.active_catalog());
1458 materialise_meta_view(&mut catalog, view, schema, rows)?;
1459 }
1460 // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1461 "__spg_info_routines" => {
1462 let (schema, rows) = synth_info_routines();
1463 materialise_meta_view(&mut catalog, view, schema, rows)?;
1464 }
1465 // v7.37.24 (24.3) — information_schema.attributes.
1466 "__spg_info_attributes" => {
1467 let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1468 self.active_catalog(),
1469 );
1470 materialise_meta_view(&mut catalog, view, schema, rows)?;
1471 }
1472 // v7.37.24 (24.2) — information_schema.domains.
1473 "__spg_info_domains" => {
1474 let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1475 self.active_catalog(),
1476 );
1477 materialise_meta_view(&mut catalog, view, schema, rows)?;
1478 }
1479 // v7.37.24 (24.9) — information_schema.schemata.
1480 "__spg_info_schemata" => {
1481 let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1482 self.active_catalog(),
1483 self.speaks_mysql,
1484 &self.listed_database_names(),
1485 );
1486 materialise_meta_view(&mut catalog, view, schema, rows)?;
1487 }
1488 // v7.37.24 (24.9) — information_schema.views.
1489 "__spg_info_views" => {
1490 let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1491 self.active_catalog(),
1492 );
1493 materialise_meta_view(&mut catalog, view, schema, rows)?;
1494 }
1495 // v7.37.24 (24.9) — information_schema.table_constraints.
1496 "__spg_info_table_constraints" => {
1497 let (schema, rows) =
1498 crate::system_catalog::synth_information_schema_table_constraints(
1499 self.active_catalog(),
1500 self.speaks_mysql,
1501 );
1502 materialise_meta_view(&mut catalog, view, schema, rows)?;
1503 }
1504 // v7.37.17 — information_schema.constraint_column_usage.
1505 "__spg_info_constraint_column_usage" => {
1506 let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1507 self.active_catalog(),
1508 );
1509 materialise_meta_view(&mut catalog, view, schema, rows)?;
1510 }
1511 // v7.37.17 — information_schema.triggers.
1512 "__spg_info_triggers" => {
1513 let (schema, rows) =
1514 crate::system_catalog::synth_info_triggers(self.active_catalog());
1515 materialise_meta_view(&mut catalog, view, schema, rows)?;
1516 }
1517 // v7.37.17 — information_schema.check_constraints.
1518 "__spg_info_check_constraints" => {
1519 let (schema, rows) =
1520 crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1521 materialise_meta_view(&mut catalog, view, schema, rows)?;
1522 }
1523 // v7.37.17 — information_schema.sequences.
1524 "__spg_info_sequences" => {
1525 let (schema, rows) =
1526 crate::system_catalog::synth_info_sequences(self.active_catalog());
1527 materialise_meta_view(&mut catalog, view, schema, rows)?;
1528 }
1529 // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1530 "__spg_mysql_user" => {
1531 let (schema, rows) = synth_mysql_user(self);
1532 materialise_meta_view(&mut catalog, view, schema, rows)?;
1533 }
1534 "__spg_mysql_db" => {
1535 let (schema, rows) = synth_mysql_db();
1536 materialise_meta_view(&mut catalog, view, schema, rows)?;
1537 }
1538 // v7.39 (round 541) — the catalogs PG has that SPG is
1539 // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1540 other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1541 let (schema, rows) =
1542 crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1543 materialise_meta_view(&mut catalog, view, schema, rows)?;
1544 }
1545 _ => {
1546 return Err(EngineError::Unsupported(alloc::format!(
1547 "meta view {view:?} is not yet materialisable; \
1548 v7.16.2 covers information_schema.columns / .tables \
1549 and pg_catalog.pg_class / pg_attribute; \
1550 v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1551 pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1552 pg_user / pg_views / pg_matviews / pg_settings"
1553 )));
1554 }
1555 }
1556 }
1557 Ok(catalog)
1558 }
1559
1560 pub(crate) fn exec_with_ctes(
1561 &self,
1562 stmt: &SelectStatement,
1563 cancel: CancelToken<'_>,
1564 ) -> Result<QueryResult, EngineError> {
1565 cancel.check()?;
1566 // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1567 // bodies are supported here. Writable CTEs on a SELECT
1568 // outer require `&mut self` and route through the
1569 // top-level `exec_select_cancel_mut` entry; sentori
1570 // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1571 // INSERT, not a SELECT, so this restriction is harmless
1572 // in practice.
1573 if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1574 // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1575 // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1576 // of a statement, not nested inside a subquery; this path is
1577 // reached exactly when one is nested. The old text described SPG's
1578 // own executor plumbing ("the top-level mutable entry"), which
1579 // means nothing to a client.
1580 return Err(EngineError::Unsupported(
1581 "WITH clause containing a data-modifying statement must be at the top level".into(),
1582 ));
1583 }
1584 let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1585 // Strip CTEs from the body before running on the temp engine
1586 // so we don't recurse forever.
1587 let mut body = stmt.clone();
1588 body.ctes = Vec::new();
1589 let mut temp = Engine::restore(catalog);
1590 if let Some(c) = self.clock {
1591 temp = temp.with_clock(c);
1592 }
1593 if let Some(f) = self.salt_fn {
1594 temp = temp.with_salt_fn(f);
1595 }
1596 temp.exec_select_cancel(&body, cancel)
1597 }
1598
1599 /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1600 /// `&self` SELECT path. Caller guarantees no modifying CTE
1601 /// bodies are present.
1602 pub(crate) fn materialise_ctes_readonly(
1603 &self,
1604 ctes: &[spg_sql::ast::Cte],
1605 cancel: CancelToken<'_>,
1606 ) -> Result<crate::Catalog, EngineError> {
1607 cancel.check()?;
1608 let mut catalog = self.active_catalog().clone();
1609 for cte in ctes {
1610 let body_select = cte.body.as_select().ok_or_else(|| {
1611 EngineError::Unsupported(alloc::format!(
1612 "data-modifying CTE not supported on this SELECT entry"
1613 ))
1614 })?;
1615 // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1616 // (PG scoping: the WITH name wins for the outer query and later
1617 // CTEs, while THIS body still sees the real table — a
1618 // non-recursive body's self-name is the table, probe P2). This
1619 // materialiser works on a CLONE, so the shadow is simply: run
1620 // the body against the untouched clone, then drop the real
1621 // table from the clone before installing the CTE's temp. A
1622 // RECURSIVE self-reference is the CTE itself (P6), so there the
1623 // drop happens before the iterating materialiser runs.
1624 let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1625 let synthetic = spg_sql::ast::Cte {
1626 name: cte.name.clone(),
1627 body: spg_sql::ast::CteBody::Select(body_select.clone()),
1628 recursive: true,
1629 column_overrides: cte.column_overrides.clone(),
1630 search: None,
1631 cycle: None,
1632 };
1633 if catalog.get(&cte.name).is_some() {
1634 let _ = catalog.drop_table(&cte.name);
1635 }
1636 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1637 } else {
1638 let mut cte_engine = Engine::restore(catalog.clone());
1639 if let Some(c) = self.clock {
1640 cte_engine = cte_engine.with_clock(c);
1641 }
1642 if let Some(f) = self.salt_fn {
1643 cte_engine = cte_engine.with_salt_fn(f);
1644 }
1645 let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1646 let QueryResult::Rows { columns, rows } = body_result else {
1647 return Err(EngineError::Unsupported(alloc::format!(
1648 "CTE {:?} body did not return rows",
1649 cte.name
1650 )));
1651 };
1652 (columns, rows)
1653 };
1654 let inferred = infer_column_types(&columns, &rows);
1655 let mut columns = inferred;
1656 if !cte.column_overrides.is_empty() {
1657 if cte.column_overrides.len() != columns.len() {
1658 return Err(EngineError::Unsupported(alloc::format!(
1659 "CTE {:?} column list has {} names but body returns {} columns",
1660 cte.name,
1661 cte.column_overrides.len(),
1662 columns.len()
1663 )));
1664 }
1665 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1666 col.name.clone_from(name);
1667 }
1668 }
1669 let schema = TableSchema::new(cte.name.clone(), columns);
1670 // v7.39 (round 156) — the body ran against the untouched clone;
1671 // from here on the CTE name resolves to the temp (PG scoping).
1672 if catalog.get(&cte.name).is_some() {
1673 let _ = catalog.drop_table(&cte.name);
1674 }
1675 catalog.create_table(schema).map_err(EngineError::Storage)?;
1676 let table = catalog
1677 .get_mut(&cte.name)
1678 .expect("just-created CTE table must exist");
1679 for row in rows {
1680 table.insert(row).map_err(EngineError::Storage)?;
1681 }
1682 }
1683 Ok(catalog)
1684 }
1685
1686 /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1687 /// Retained for non-DML callers; the DML path (writable CTE on
1688 /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1689 /// `dml.rs` which installs the CTE temps directly on the
1690 /// active catalog so the outer statement's writes hit real
1691 /// tables.
1692 #[allow(dead_code)]
1693 pub(crate) fn materialise_ctes(
1694 &mut self,
1695 ctes: &[spg_sql::ast::Cte],
1696 cancel: CancelToken<'_>,
1697 ) -> Result<crate::Catalog, EngineError> {
1698 cancel.check()?;
1699 // v7.37.43-T4.4 — modifying CTEs need to write through the
1700 // SAME catalog as the outer statement, not a clone (PG's
1701 // writable CTE puts all modifications in one transaction).
1702 // For the read-only case the original logic cloned, but
1703 // since the outer statement also goes through the cloned
1704 // engine and ALL writes must converge, we now drive the
1705 // accumulator off `self.active_catalog().clone()` and
1706 // commit the modifying writes directly to `self`'s active
1707 // catalog so the surface is consistent.
1708 let mut catalog = self.active_catalog().clone();
1709 // v7.39 (round 149) — a modifying CTE body's target must be a
1710 // real relation, never a sibling CTE (PG: relation does not
1711 // exist); checked before any alias lands in the accumulator.
1712 for cte in ctes {
1713 let body_target = match &cte.body {
1714 spg_sql::ast::CteBody::Select(_) => None,
1715 spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1716 spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1717 spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1718 spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1719 };
1720 if let Some(t) = body_target
1721 && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1722 && catalog.get(t).is_none()
1723 {
1724 return Err(EngineError::Storage(
1725 spg_storage::StorageError::TableNotFound { name: t.into() },
1726 ));
1727 }
1728 }
1729 for cte in ctes {
1730 if catalog.get(&cte.name).is_some() {
1731 return Err(EngineError::Unsupported(alloc::format!(
1732 "CTE name {:?} shadows an existing table; rename the CTE",
1733 cte.name
1734 )));
1735 }
1736 let (columns, rows) = match &cte.body {
1737 // v7.39 (round 145) — see the sibling site: only a body that
1738 // truly self-references takes the iterating materialiser.
1739 spg_sql::ast::CteBody::Select(body)
1740 if cte.recursive && select_refers_to(body, &cte.name) =>
1741 {
1742 // Recursive CTE — the existing helper takes a
1743 // SELECT body and the snapshot catalog.
1744 let synthetic = spg_sql::ast::Cte {
1745 name: cte.name.clone(),
1746 body: spg_sql::ast::CteBody::Select(body.clone()),
1747 recursive: true,
1748 column_overrides: cte.column_overrides.clone(),
1749 search: None,
1750 cycle: None,
1751 };
1752 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1753 }
1754 spg_sql::ast::CteBody::Select(body) => {
1755 // v7.25 (round-17) — run against the accumulated
1756 // catalog so later CTEs can reference earlier
1757 // ones in the same WITH clause.
1758 let mut cte_engine = Engine::restore(catalog.clone());
1759 if let Some(c) = self.clock {
1760 cte_engine = cte_engine.with_clock(c);
1761 }
1762 if let Some(f) = self.salt_fn {
1763 cte_engine = cte_engine.with_salt_fn(f);
1764 }
1765 let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1766 let QueryResult::Rows { columns, rows } = body_result else {
1767 return Err(EngineError::Unsupported(alloc::format!(
1768 "CTE {:?} body did not return rows",
1769 cte.name
1770 )));
1771 };
1772 (columns, rows)
1773 }
1774 spg_sql::ast::CteBody::Insert(body) => {
1775 self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1776 }
1777 spg_sql::ast::CteBody::Update(body) => {
1778 self.exec_modifying_cte_update(&cte.name, body, cancel)?
1779 }
1780 spg_sql::ast::CteBody::Delete(body) => {
1781 self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1782 }
1783 spg_sql::ast::CteBody::Merge(body) => {
1784 self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1785 }
1786 };
1787 // v4.22: the projection builder labels any non-column
1788 // expression as Text — including literal SELECT 1.
1789 // Promote each column's type to whatever the rows
1790 // actually carry so the CTE storage table accepts them.
1791 let inferred = infer_column_types(&columns, &rows);
1792 let mut columns = inferred;
1793 if !cte.column_overrides.is_empty() {
1794 if cte.column_overrides.len() != columns.len() {
1795 return Err(EngineError::Unsupported(alloc::format!(
1796 "CTE {:?} column list has {} names but body returns {} columns",
1797 cte.name,
1798 cte.column_overrides.len(),
1799 columns.len()
1800 )));
1801 }
1802 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1803 col.name.clone_from(name);
1804 }
1805 }
1806 let schema = TableSchema::new(cte.name.clone(), columns);
1807 catalog.create_table(schema).map_err(EngineError::Storage)?;
1808 let table = catalog
1809 .get_mut(&cte.name)
1810 .expect("just-created CTE table must exist");
1811 for row in rows {
1812 table.insert(row).map_err(EngineError::Storage)?;
1813 }
1814 }
1815 Ok(catalog)
1816 }
1817
1818 /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1819 /// against `self` (so the mutation lands in the active catalog
1820 /// inside the current transaction) and captures the RETURNING
1821 /// projection — column schema + rows — to materialise as the
1822 /// CTE alias's table. An INSERT without RETURNING produces a
1823 /// 0-row table with a synthetic single-column placeholder
1824 /// (matches PG: the CTE alias is still defined, but referencing
1825 /// it from the outer query without RETURNING raises a
1826 /// column-resolution error at scan time).
1827 fn exec_modifying_cte_insert(
1828 &mut self,
1829 cte_name: &str,
1830 body: &spg_sql::ast::InsertStatement,
1831 _cancel: CancelToken<'_>,
1832 ) -> Result<
1833 (
1834 Vec<spg_storage::ColumnSchema>,
1835 Vec<spg_storage::Row<'static>>,
1836 ),
1837 EngineError,
1838 > {
1839 // round 151 — a WITH-headed body keeps its own ctes; the body
1840 // statement routes through its writable-CTE entry (outer CTEs
1841 // are never copied into bodies, so no recursion risk).
1842 let body = body.clone();
1843 let result = self.exec_insert(body)?;
1844 match result {
1845 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1846 QueryResult::CommandOk { .. } => {
1847 // No RETURNING — emit a sentinel single-column
1848 // schema with zero rows so the alias is defined.
1849 let placeholder = spg_storage::ColumnSchema::new(
1850 alloc::format!("{cte_name}_returning_absent"),
1851 spg_storage::DataType::Text,
1852 true,
1853 );
1854 Ok((alloc::vec![placeholder], Vec::new()))
1855 }
1856 }
1857 }
1858
1859 /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1860 /// as INSERT above.
1861 fn exec_modifying_cte_update(
1862 &mut self,
1863 cte_name: &str,
1864 body: &spg_sql::ast::UpdateStatement,
1865 cancel: CancelToken<'_>,
1866 ) -> Result<
1867 (
1868 Vec<spg_storage::ColumnSchema>,
1869 Vec<spg_storage::Row<'static>>,
1870 ),
1871 EngineError,
1872 > {
1873 let body = body.clone();
1874 let result = self.exec_update_cancel(&body, cancel)?;
1875 match result {
1876 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1877 QueryResult::CommandOk { .. } => {
1878 let placeholder = spg_storage::ColumnSchema::new(
1879 alloc::format!("{cte_name}_returning_absent"),
1880 spg_storage::DataType::Text,
1881 true,
1882 );
1883 Ok((alloc::vec![placeholder], Vec::new()))
1884 }
1885 }
1886 }
1887
1888 /// v7.37.43-T4.4 — execute a DELETE CTE body.
1889 fn exec_modifying_cte_delete(
1890 &mut self,
1891 cte_name: &str,
1892 body: &spg_sql::ast::DeleteStatement,
1893 cancel: CancelToken<'_>,
1894 ) -> Result<
1895 (
1896 Vec<spg_storage::ColumnSchema>,
1897 Vec<spg_storage::Row<'static>>,
1898 ),
1899 EngineError,
1900 > {
1901 let body = body.clone();
1902 let result = self.exec_delete_cancel(&body, cancel)?;
1903 match result {
1904 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1905 QueryResult::CommandOk { .. } => {
1906 let placeholder = spg_storage::ColumnSchema::new(
1907 alloc::format!("{cte_name}_returning_absent"),
1908 spg_storage::DataType::Text,
1909 true,
1910 );
1911 Ok((alloc::vec![placeholder], Vec::new()))
1912 }
1913 }
1914 }
1915
1916 /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1917 fn exec_modifying_cte_merge(
1918 &mut self,
1919 cte_name: &str,
1920 body: &spg_sql::ast::MergeStatement,
1921 cancel: CancelToken<'_>,
1922 ) -> Result<
1923 (
1924 Vec<spg_storage::ColumnSchema>,
1925 Vec<spg_storage::Row<'static>>,
1926 ),
1927 EngineError,
1928 > {
1929 let body = body.clone();
1930 let result = self.exec_merge_cancel(&body, cancel)?;
1931 match result {
1932 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1933 QueryResult::CommandOk { .. } => {
1934 let placeholder = spg_storage::ColumnSchema::new(
1935 alloc::format!("{cte_name}_returning_absent"),
1936 spg_storage::DataType::Text,
1937 true,
1938 );
1939 Ok((alloc::vec![placeholder], Vec::new()))
1940 }
1941 }
1942 }
1943
1944 /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1945 /// UNION (or UNION ALL) of an anchor that does not reference
1946 /// the CTE name, and one or more recursive terms that do. The
1947 /// anchor runs first; each subsequent iteration runs the
1948 /// recursive term against a temp catalog where the CTE name is
1949 /// bound to the *previous* iteration's output. Iteration stops
1950 /// when the recursive term yields no rows; UNION (DISTINCT)
1951 /// deduplicates against the accumulated result, UNION ALL does
1952 /// not. A hard cap on total rows prevents runaway queries.
1953 #[allow(clippy::too_many_lines)]
1954 pub(crate) fn materialise_recursive_cte(
1955 &self,
1956 cte: &spg_sql::ast::Cte,
1957 base_catalog: &Catalog,
1958 cancel: CancelToken<'_>,
1959 ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1960 const MAX_TOTAL_ROWS: usize = 1_000_000;
1961 const MAX_ITERATIONS: usize = 100_000;
1962 cancel.check()?;
1963 // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1964 // a modifying recursive CTE is parser-rejectable but we
1965 // guard here defensively.
1966 let body_select = cte.body.as_select().ok_or_else(|| {
1967 EngineError::Unsupported(alloc::format!(
1968 "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1969 cte.name
1970 ))
1971 })?;
1972 if body_select.unions.is_empty() {
1973 return Err(EngineError::Unsupported(alloc::format!(
1974 "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1975 cte.name
1976 )));
1977 }
1978 // Anchor: the body's leading SELECT, with unions stripped.
1979 let mut anchor = body_select.clone();
1980 let all_union_terms = core::mem::take(&mut anchor.unions);
1981 anchor.ctes = Vec::new();
1982 // v7.37 D.42 — split the UNION members: those that do NOT reference the
1983 // CTE are additional ANCHOR terms, only the ones that do recurse. A
1984 // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1985 // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1986 // treating the non-recursive `SELECT r2` as a recursive term made it
1987 // re-emit its constant row every iteration → runaway loop.
1988 let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1989 .into_iter()
1990 .partition(|(_, t)| !select_refers_to(t, &cte.name));
1991 let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1992 let QueryResult::Rows {
1993 columns: anchor_cols,
1994 rows: mut anchor_rows,
1995 } = anchor_result
1996 else {
1997 return Err(EngineError::Unsupported(alloc::format!(
1998 "WITH RECURSIVE {:?}: anchor did not return rows",
1999 cte.name
2000 )));
2001 };
2002 // Append every non-recursive UNION member's rows to the anchor set.
2003 for (_, term) in &anchor_terms {
2004 let mut term = term.clone();
2005 term.ctes = Vec::new();
2006 if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
2007 anchor_rows.extend(rows);
2008 }
2009 }
2010 // The projection builder labels non-column expressions Text;
2011 // refine column types from the anchor's actual values so the
2012 // intermediate iter-catalog tables accept them.
2013 let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
2014 if !cte.column_overrides.is_empty() {
2015 if cte.column_overrides.len() != columns.len() {
2016 return Err(EngineError::Unsupported(alloc::format!(
2017 "CTE {:?} column list has {} names but anchor returns {} columns",
2018 cte.name,
2019 cte.column_overrides.len(),
2020 columns.len()
2021 )));
2022 }
2023 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
2024 col.name.clone_from(name);
2025 }
2026 }
2027 let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
2028 let mut working_set: Vec<Row<'static>> = anchor_rows;
2029 let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
2030 // Track at least one "all UNION ALL" flag — if every union
2031 // kind is ALL we skip the dedup step (faster + matches PG).
2032 let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
2033 if !all_union_all {
2034 for r in &all_rows {
2035 seen.insert(encode_row_key(r));
2036 }
2037 }
2038 // v7.39 (round 598) — the engine and its catalog are built ONCE.
2039 // Each iteration used to clone the catalog, create the CTE table,
2040 // and construct a whole `Engine` — which initialises 82 fields — to
2041 // hold that round's working set. A counting allocator put the loop
2042 // at 63 allocations and 104 kB per iteration, or 1 GB for a
2043 // 10,000-row recursive CTE, and none of it varied with how much
2044 // else was in the catalog: the per-round rebuild WAS the cost. The
2045 // table is emptied and refilled instead.
2046 let mut iter_catalog = base_catalog.clone();
2047 let schema = TableSchema::new(cte.name.clone(), columns.clone());
2048 iter_catalog
2049 .create_table(schema)
2050 .map_err(EngineError::Storage)?;
2051 let mut iter_engine = Engine::restore(iter_catalog);
2052 if let Some(c) = self.clock {
2053 iter_engine = iter_engine.with_clock(c);
2054 }
2055 if let Some(f) = self.salt_fn {
2056 iter_engine = iter_engine.with_salt_fn(f);
2057 }
2058 // The recursive terms are cloned once too — the clone stripped the
2059 // CTE list off each of them, per term per iteration.
2060 let recursive_terms: Vec<SelectStatement> = union_terms
2061 .iter()
2062 .map(|(_, t)| {
2063 let mut t = t.clone();
2064 t.ctes = Vec::new();
2065 t
2066 })
2067 .collect();
2068 // v7.39 (round 618) — plan every recursive term once. Taken only if
2069 // ALL of them plan, so a query never runs half on each path.
2070 let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2071 .iter()
2072 .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2073 .collect();
2074 let fast_ctx = term_plans.as_ref().map(|plans| {
2075 let alias = plans[0].alias.clone();
2076 (alias, ())
2077 });
2078 for iter in 0..MAX_ITERATIONS {
2079 cancel.check()?;
2080 if working_set.is_empty() {
2081 break;
2082 }
2083 if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2084 // The worktable IS the working set: no table to empty and
2085 // refill, and no query execution per round.
2086 let mut next_set: Vec<Row<'static>> = Vec::new();
2087 for plan in plans {
2088 let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2089 for row in &working_set {
2090 cancel.check()?;
2091 if let Some(w) = plan.where_ {
2092 let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2093 if !matches!(v, Value::Bool(true)) {
2094 continue;
2095 }
2096 }
2097 let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2098 for it in &plan.items {
2099 vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2100 }
2101 let out = Row::new(vals);
2102 if !all_union_all {
2103 let key = encode_row_key(&out);
2104 if !seen.insert(key) {
2105 continue;
2106 }
2107 }
2108 next_set.push(out);
2109 }
2110 }
2111 if next_set.is_empty() {
2112 break;
2113 }
2114 all_rows.extend(next_set.iter().cloned());
2115 working_set = next_set;
2116 if all_rows.len() > MAX_TOTAL_ROWS {
2117 return Err(EngineError::Unsupported(alloc::format!(
2118 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2119 cte.name
2120 )));
2121 }
2122 if iter + 1 == MAX_ITERATIONS {
2123 return Err(EngineError::Unsupported(alloc::format!(
2124 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2125 cte.name
2126 )));
2127 }
2128 continue;
2129 }
2130 {
2131 // Truncated rather than dropped and recreated: the table's
2132 // own structure is what dropping it throws away, and it is
2133 // identical every round.
2134 let cat = iter_engine.base_catalog_mut();
2135 let table = cat.get_mut(&cte.name).expect("created above");
2136 table.truncate();
2137 for row in &working_set {
2138 table.insert(row.clone()).map_err(EngineError::Storage)?;
2139 }
2140 }
2141 // Run each recursive term in sequence and collect new rows.
2142 let mut next_set: Vec<Row<'static>> = Vec::new();
2143 for term in &recursive_terms {
2144 let r = iter_engine.exec_select_cancel(term, cancel)?;
2145 let QueryResult::Rows {
2146 columns: rc,
2147 rows: rs,
2148 } = r
2149 else {
2150 return Err(EngineError::Unsupported(alloc::format!(
2151 "WITH RECURSIVE {:?}: recursive term did not return rows",
2152 cte.name
2153 )));
2154 };
2155 if rc.len() != columns.len() {
2156 return Err(EngineError::Unsupported(alloc::format!(
2157 "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2158 cte.name,
2159 rc.len(),
2160 columns.len()
2161 )));
2162 }
2163 for row in rs {
2164 if !all_union_all {
2165 let key = encode_row_key(&row);
2166 if !seen.insert(key) {
2167 continue;
2168 }
2169 }
2170 next_set.push(row);
2171 }
2172 }
2173 if next_set.is_empty() {
2174 break;
2175 }
2176 all_rows.extend(next_set.iter().cloned());
2177 working_set = next_set;
2178 if all_rows.len() > MAX_TOTAL_ROWS {
2179 return Err(EngineError::Unsupported(alloc::format!(
2180 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2181 cte.name
2182 )));
2183 }
2184 if iter + 1 == MAX_ITERATIONS {
2185 return Err(EngineError::Unsupported(alloc::format!(
2186 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2187 cte.name
2188 )));
2189 }
2190 }
2191 Ok((columns, all_rows))
2192 }
2193
2194 pub(crate) fn resolve_select_subqueries(
2195 &self,
2196 stmt: &mut SelectStatement,
2197 cancel: CancelToken<'_>,
2198 ) -> Result<(), EngineError> {
2199 for item in &mut stmt.items {
2200 if let SelectItem::Expr { expr, alias } = item {
2201 // An UNCORRELATED subquery is replaced by its value right
2202 // here, and the shape the column was named for goes with
2203 // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2204 // boolean literal, so SPG answered `?column?` where PG18
2205 // answers `exists`. Only a subquery at the TOP of the item
2206 // loses its name this way — one nested inside a call still
2207 // reports the call.
2208 if alias.is_none()
2209 && matches!(
2210 expr,
2211 Expr::ScalarSubquery(_)
2212 | Expr::Exists { .. }
2213 | Expr::InSubquery { .. }
2214 | Expr::RowInSubquery { .. }
2215 | Expr::RowCmpSubquery { .. }
2216 )
2217 {
2218 *alias = Some(default_output_name(expr, self.speaks_mysql));
2219 }
2220 self.resolve_expr_subqueries(expr, cancel)?;
2221 }
2222 }
2223 if let Some(w) = &mut stmt.where_ {
2224 self.resolve_expr_subqueries(w, cancel)?;
2225 }
2226 // v7.24.1 — JOIN ON conditions can carry subqueries too;
2227 // they were never walked, so even an UNCORRELATED subquery
2228 // in ON hit "subquery reached row eval".
2229 if let Some(from) = &mut stmt.from {
2230 for j in &mut from.joins {
2231 if let Some(on) = &mut j.on {
2232 self.resolve_expr_subqueries(on, cancel)?;
2233 }
2234 }
2235 }
2236 if let Some(gs) = &mut stmt.group_by {
2237 for g in gs {
2238 self.resolve_expr_subqueries(g, cancel)?;
2239 }
2240 }
2241 if let Some(h) = &mut stmt.having {
2242 self.resolve_expr_subqueries(h, cancel)?;
2243 }
2244 for o in &mut stmt.order_by {
2245 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2246 }
2247 for (_, peer) in &mut stmt.unions {
2248 self.resolve_select_subqueries(peer, cancel)?;
2249 }
2250 Ok(())
2251 }
2252
2253 #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2254 pub(crate) fn resolve_expr_subqueries(
2255 &self,
2256 e: &mut Expr,
2257 cancel: CancelToken<'_>,
2258 ) -> Result<(), EngineError> {
2259 // Replace-on-this-node cases first.
2260 if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2261 *e = replacement;
2262 return Ok(());
2263 }
2264 match e {
2265 Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } => {
2266 self.resolve_expr_subqueries(expr, cancel)?
2267 }
2268 Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2269 Expr::AggregateOrdered { call, order_by, .. } => {
2270 self.resolve_expr_subqueries(call, cancel)?;
2271 for o in order_by.iter_mut() {
2272 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2273 }
2274 }
2275 Expr::Binary { lhs, rhs, .. } => {
2276 self.resolve_expr_subqueries(lhs, cancel)?;
2277 self.resolve_expr_subqueries(rhs, cancel)?;
2278 }
2279 Expr::Unary { expr, .. }
2280 | Expr::Cast { expr, .. }
2281 | Expr::IsNull { expr, .. }
2282 | Expr::BoolTest { expr, .. }
2283 | Expr::FieldAccess { base: expr, .. } => {
2284 self.resolve_expr_subqueries(expr, cancel)?;
2285 }
2286 Expr::FunctionCall { args, .. } => {
2287 for a in args {
2288 self.resolve_expr_subqueries(a, cancel)?;
2289 }
2290 }
2291 Expr::Like { expr, pattern, .. } => {
2292 self.resolve_expr_subqueries(expr, cancel)?;
2293 self.resolve_expr_subqueries(pattern, cancel)?;
2294 }
2295 Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2296 // v4.12 window functions — recurse into args + ORDER BY
2297 // + PARTITION BY in case they carry inner subqueries.
2298 Expr::WindowFunction {
2299 args,
2300 partition_by,
2301 order_by,
2302 ..
2303 } => {
2304 for a in args {
2305 self.resolve_expr_subqueries(a, cancel)?;
2306 }
2307 for p in partition_by {
2308 self.resolve_expr_subqueries(p, cancel)?;
2309 }
2310 for (e, _, _) in order_by {
2311 self.resolve_expr_subqueries(e, cancel)?;
2312 }
2313 }
2314 // Subquery nodes are handled in subquery_replacement
2315 // (which returned None — defensive no-op); Literal /
2316 // Column are leaves.
2317 Expr::ScalarSubquery(_)
2318 | Expr::Exists { .. }
2319 | Expr::InSubquery { .. }
2320 | Expr::RowInSubquery { .. }
2321 | Expr::RowCmpSubquery { .. }
2322 | Expr::Literal(_)
2323 | Expr::Placeholder(_)
2324 | Expr::Column(_) => {}
2325 // v7.30.2 — list elements can carry scalar subqueries
2326 // (`x IN (1, (SELECT …))`).
2327 Expr::InList { expr, list, .. } => {
2328 self.resolve_expr_subqueries(expr, cancel)?;
2329 for item in list {
2330 self.resolve_expr_subqueries(item, cancel)?;
2331 }
2332 }
2333 // v7.10.10 — recurse children.
2334 Expr::Array(items) => {
2335 for elem in items {
2336 self.resolve_expr_subqueries(elem, cancel)?;
2337 }
2338 }
2339 Expr::ArraySubscript { target, index } => {
2340 self.resolve_expr_subqueries(target, cancel)?;
2341 self.resolve_expr_subqueries(index, cancel)?;
2342 }
2343 Expr::ArraySlice { target, lo, hi } => {
2344 self.resolve_expr_subqueries(target, cancel)?;
2345 if let Some(l) = lo {
2346 self.resolve_expr_subqueries(l, cancel)?;
2347 }
2348 if let Some(h) = hi {
2349 self.resolve_expr_subqueries(h, cancel)?;
2350 }
2351 }
2352 Expr::AnyAll { expr, array, .. } => {
2353 self.resolve_expr_subqueries(expr, cancel)?;
2354 // Quantified subquery — an uncorrelated one
2355 // materialises up front; a correlated one stays for
2356 // the per-row resolver.
2357 if let Expr::ScalarSubquery(inner) = array.as_mut() {
2358 if !crate::subquery::select_is_correlated(inner) {
2359 let s = (**inner).clone();
2360 **array = self.materialize_quantified_rows(&s, cancel)?;
2361 }
2362 } else {
2363 self.resolve_expr_subqueries(array, cancel)?;
2364 }
2365 }
2366 Expr::Case {
2367 operand,
2368 branches,
2369 else_branch,
2370 } => {
2371 if let Some(o) = operand {
2372 self.resolve_expr_subqueries(o, cancel)?;
2373 }
2374 for (w, t) in branches {
2375 self.resolve_expr_subqueries(w, cancel)?;
2376 self.resolve_expr_subqueries(t, cancel)?;
2377 }
2378 if let Some(e) = else_branch {
2379 self.resolve_expr_subqueries(e, cancel)?;
2380 }
2381 }
2382 }
2383 Ok(())
2384 }
2385}
2386
2387impl Engine {
2388 /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2389 /// `SelectItem::Wildcard` to all schema columns and
2390 /// `SelectItem::Expr` via the regular eval path.
2391 pub(crate) fn project_row_simple(
2392 &self,
2393 row: &Row<'static>,
2394 items: &[SelectItem],
2395 schema_cols: &[ColumnSchema],
2396 alias: &str,
2397 ) -> Result<Row<'static>, EngineError> {
2398 let ctx = self.ev_ctx(schema_cols, Some(alias));
2399 let cancel = CancelToken::none();
2400 let mut out_vals = Vec::new();
2401 for item in items {
2402 match item {
2403 // In a single-table projection (AS OF SEGMENT / RETURNING) a
2404 // qualified `t.*` covers exactly the same columns as a bare `*`.
2405 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2406 out_vals.extend(row.values.iter().cloned());
2407 }
2408 SelectItem::Expr { expr, .. } => {
2409 let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2410 out_vals.push(v);
2411 }
2412 }
2413 }
2414 Ok(Row::new(out_vals))
2415 }
2416
2417 /// v6.10.2 — derive the output `ColumnSchema` list for an
2418 /// AS OF SEGMENT projection. Wildcards take the full schema;
2419 /// expressions take the alias if present or a synthetic
2420 /// `?column?` (PG convention) otherwise.
2421 pub(crate) fn derive_output_columns(
2422 &self,
2423 items: &[SelectItem],
2424 schema_cols: &[ColumnSchema],
2425 table_alias: &str,
2426 ) -> Vec<ColumnSchema> {
2427 let mut out = Vec::new();
2428 for item in items {
2429 match item {
2430 // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2431 // a single-table projection.
2432 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2433 out.extend(schema_cols.iter().cloned());
2434 }
2435 SelectItem::Expr { expr, alias } => {
2436 // Bare column references inherit the schema
2437 // column's name + type — PG names `RETURNING id`
2438 // "id" and types it BIGINT, and the sqlx embed
2439 // path type-checks RowDescription against the
2440 // Rust target (mailrs embed round-12).
2441 if let Expr::Column(col) = expr
2442 && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2443 {
2444 let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2445 let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2446 // v7.39 (read01 round 54) — carry the enum identity:
2447 // it lives outside the DataType lattice, so a derived
2448 // table built from this schema otherwise forgets it and
2449 // the OUTER `ORDER BY <enum col>` silently sorts by the
2450 // label's TEXT instead of member order.
2451 c.user_enum_type = sc.user_enum_type.clone();
2452 out.push(c);
2453 continue;
2454 }
2455 let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2456 // v7.30.4 (mailrs round-27, P0) — type the
2457 // expression with the same inference the SELECT
2458 // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2459 // The old Text default broke every typed decode
2460 // of `RETURNING uidnext - 1 AS uid`: four days
2461 // of inbound mail indexed nowhere. Inference
2462 // failure keeps the old Text fallback rather
2463 // than inventing new error paths here.
2464 // v7.39 (round 258) — take the enum identity from the
2465 // same projection build, not just the type: a constant
2466 // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2467 // VALUES row lowers to) is an EXPRESSION, so it landed
2468 // here and the derived table forgot the enum.
2469 let (ty, nullable) = build_projection(
2470 core::slice::from_ref(item),
2471 schema_cols,
2472 table_alias,
2473 self.speaks_mysql,
2474 Some(self.active_catalog()),
2475 )
2476 .ok()
2477 .and_then(|p| p.into_iter().next())
2478 .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2479 out.push(ColumnSchema::new(name, ty, nullable));
2480 }
2481 }
2482 }
2483 out
2484 }
2485
2486 /// v4.5: SELECT with cooperative cancellation. The token is
2487 /// honoured between UNION peers and inside the bare-SELECT row
2488 /// loop; HNSW kNN graph walks and the aggregate executor don't
2489 /// honour it yet (deferred — those paths bound their work
2490 /// internally by `LIMIT k` and `GROUP BY` cardinality).
2491 /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2492 /// its (lowercased) name, or None if the name isn't a virtual view.
2493 /// Callers decide whether to return it directly (`SELECT *`) or stage
2494 /// it as a temp table for the full query pipeline.
2495 fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2496 Some(match name {
2497 "spg_statistic" => self.exec_spg_statistic(),
2498 "spg_stat_replication" => self.exec_spg_stat_replication(),
2499 "spg_stat_segment" => self.exec_spg_stat_segment(),
2500 "spg_memory_stats" => self.exec_spg_memory_stats(),
2501 "spg_stat_query" => self.exec_spg_stat_query(),
2502 "pg_stat_statements" => self.exec_pg_stat_statements(),
2503 "spg_stat_activity" => self.exec_spg_stat_activity(),
2504 "pg_stat_activity" => self.exec_pg_stat_activity(),
2505 "pg_locks" => self.exec_pg_locks(),
2506 "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2507 "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2508 "spg_partition_health" => self.exec_spg_partition_health(),
2509 "spg_audit_chain" => self.exec_spg_audit_chain(),
2510 "spg_audit_verify" => self.exec_spg_audit_verify(),
2511 "spg_table_ddl" => self.exec_spg_table_ddl(),
2512 "spg_role_ddl" => self.exec_spg_role_ddl(),
2513 "spg_database_ddl" => self.exec_spg_database_ddl(),
2514 _ => return None,
2515 })
2516 }
2517
2518 /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2519 /// describes against: this engine's catalog with the view staged as a
2520 /// table, exactly as `exec_select_cancel_as` stages it for a
2521 /// non-bare query.
2522 ///
2523 /// These views never reach the catalog — each is a fixed row set built
2524 /// inside its own `exec_*` — so Describe reported no columns for all
2525 /// seventeen of them. Rows are deliberately not inserted: Describe
2526 /// only needs the shape, and `infer_column_types` reads the rows we
2527 /// already have in hand.
2528 pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2529 let from = stmt.from.as_ref()?;
2530 if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2531 return None;
2532 }
2533 let lower = from.primary.name.to_ascii_lowercase();
2534 let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2535 return None;
2536 };
2537 let mut catalog = self.active_catalog().clone();
2538 let cols = infer_column_types(&columns, &rows);
2539 catalog
2540 .create_table(TableSchema::new(from.primary.name.clone(), cols))
2541 .ok()?;
2542 Some(catalog)
2543 }
2544
2545 pub(crate) fn exec_select_cancel(
2546 &self,
2547 stmt: &SelectStatement,
2548 cancel: CancelToken<'_>,
2549 ) -> Result<QueryResult, EngineError> {
2550 self.exec_select_cancel_as(stmt, cancel, None)
2551 }
2552
2553 /// v7.39 (round 334, V55) — the same read core, authorised as
2554 /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2555 /// function's OWNER: that is the entire point of the form, and without
2556 /// it every definer function failed with "permission denied" on the
2557 /// very table it exists to expose.
2558 /// v7.39 (round 559) — see the call site. `None` for anything but
2559 /// the bare shape, so every other query keeps its old path.
2560 fn try_bare_count_star(
2561 &self,
2562 stmt: &SelectStatement,
2563 as_role: Option<&str>,
2564 ) -> Result<Option<QueryResult>, EngineError> {
2565 use spg_sql::ast::SelectItem;
2566 if as_role.is_some()
2567 || !stmt.ctes.is_empty()
2568 || !stmt.unions.is_empty()
2569 || stmt.where_.is_some()
2570 || stmt.group_by.is_some()
2571 || stmt.having.is_some()
2572 || stmt.distinct
2573 || !stmt.order_by.is_empty()
2574 || stmt.limit.is_some()
2575 || stmt.offset.is_some()
2576 || stmt.items.len() != 1
2577 {
2578 return Ok(None);
2579 }
2580 let Some(from) = &stmt.from else {
2581 return Ok(None);
2582 };
2583 if !from.joins.is_empty()
2584 || stmt.locking.is_some()
2585 || from.primary.lateral_subquery.is_some()
2586 || from.primary.unnest_expr.is_some()
2587 || from.primary.generate_series_args.is_some()
2588 || from.primary.name.is_empty()
2589 || from.primary.name.starts_with("__spg_")
2590 {
2591 return Ok(None);
2592 }
2593 // A partition PARENT holds no rows of its own — they live in the
2594 // children — so its header count is 0 and the ordinary path has
2595 // to fan out. Caught by the partition conformance cases.
2596 //
2597 // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2598 // of them, which is worse: its header count is a real number,
2599 // just not the answer. `SELECT count(*) FROM par` returned 1
2600 // where PG returns 2, because this shortcut fired before the
2601 // fan-out could. The question is "does anything descend from
2602 // this", not "was it declared a partition parent".
2603 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2604 return Ok(None);
2605 }
2606 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2607 return Ok(None);
2608 };
2609 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2610 return Ok(None);
2611 };
2612 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2613 return Ok(None);
2614 }
2615 // A row-security policy filters rows, so the header count is not
2616 // the answer; the ordinary path applies the policy.
2617 let Some(table) = self.active_catalog().get(&from.primary.name) else {
2618 return Ok(None);
2619 };
2620 if table.schema().row_security {
2621 return Ok(None);
2622 }
2623 // Rows frozen to the cold tier are not in `headers`, so the
2624 // header count would miss them. Caught by the cold-tier e2e.
2625 if table.has_cold_rows_fast() {
2626 return Ok(None);
2627 }
2628 let n = table.count_visible(&self.current_snapshot());
2629 let col = alias.clone().unwrap_or_else(|| String::from("count"));
2630 Ok(Some(QueryResult::Rows {
2631 columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2632 rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2633 i64::try_from(n).unwrap_or(i64::MAX)
2634 )])],
2635 }))
2636 }
2637
2638 /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2639 /// that col>` served from the index, never reading a row.
2640 ///
2641 /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2642 /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2643 /// count (2x at 1k). PG needs its visibility map for this — a heap
2644 /// tuple carries its own visibility, so an index entry alone cannot
2645 /// say whether the row is live, and PG reads the heap for any page
2646 /// the map does not mark all-visible. SPG keeps a header array
2647 /// beside the rows, so the locator answers it directly and there is
2648 /// no map to be stale.
2649 /// v7.39 (round 564) — the shape test, once, for both the
2650 /// materialising scan and the streaming one.
2651 ///
2652 /// Two callers asking the same question in two places is how a fact
2653 /// starts drifting; the answer here is the single copy. Returns the
2654 /// table, the alias the predicate is written against, the projected
2655 /// column's position, and the name the single output column takes.
2656 pub(crate) fn index_only_shape<'s>(
2657 &'s self,
2658 stmt: &'s SelectStatement,
2659 ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2660 use spg_sql::ast::SelectItem;
2661 if !stmt.ctes.is_empty()
2662 || !stmt.unions.is_empty()
2663 || stmt.group_by.is_some()
2664 || stmt.having.is_some()
2665 || stmt.distinct
2666 || stmt.locking.is_some()
2667 || !stmt.order_by.is_empty()
2668 || stmt.limit.is_some()
2669 || stmt.offset.is_some()
2670 || stmt.items.len() != 1
2671 {
2672 return None;
2673 }
2674 let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2675 return None;
2676 };
2677 if !from.joins.is_empty()
2678 || from.primary.lateral_subquery.is_some()
2679 || from.primary.unnest_expr.is_some()
2680 || from.primary.generate_series_args.is_some()
2681 || from.primary.name.is_empty()
2682 || from.primary.name.starts_with("__spg_")
2683 {
2684 return None;
2685 }
2686 // v7.39 (round 645) — see the note on the sibling shortcut above:
2687 // an inheritance parent's own header count is not the answer.
2688 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2689 return None;
2690 }
2691 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2692 return None;
2693 };
2694 let spg_sql::ast::Expr::Column(c) = expr else {
2695 return None;
2696 };
2697 let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2698 if let Some(q) = c.qualifier.as_deref()
2699 && !q.eq_ignore_ascii_case(alias_name)
2700 {
2701 return None;
2702 }
2703 let table = self.active_catalog().get(&from.primary.name)?;
2704 if table.schema().row_security {
2705 return None;
2706 }
2707 let cols = &table.schema().columns;
2708 let pos = cols
2709 .iter()
2710 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2711 let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2712 Some((table, alias_name, pos, out))
2713 }
2714
2715 /// v7.39 (round 565) — would this statement be answered out of the
2716 /// index alone?
2717 ///
2718 /// EXPLAIN has to name the node the executor will actually run, and
2719 /// the only honest way to know is to ask the same two questions the
2720 /// executor asks: the statement's shape, and everything decidable
2721 /// about the scan before it walks. Neither is re-stated here.
2722 pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2723 let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2724 return false;
2725 };
2726 let Some(where_) = stmt.where_.as_ref() else {
2727 return false;
2728 };
2729 crate::index_access::index_only_precheck(
2730 where_,
2731 &table.schema().columns,
2732 table,
2733 alias_name,
2734 pos,
2735 self.speaks_mysql,
2736 )
2737 .is_some()
2738 }
2739
2740 fn try_index_only_scan(
2741 &self,
2742 stmt: &SelectStatement,
2743 ) -> Result<Option<QueryResult>, EngineError> {
2744 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2745 return Ok(None);
2746 };
2747 // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2748 // are not materialised here, and a partition parent's own
2749 // heap/indexes are empty (its rows live in the children).
2750 if !stmt.ctes.is_empty() {
2751 return Ok(None);
2752 }
2753 if let Some(from) = &stmt.from
2754 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2755 {
2756 return Ok(None);
2757 }
2758 let where_ = stmt.where_.as_ref().expect("shape checked it");
2759 let cols = &table.schema().columns;
2760 let Some(values) = crate::index_access::try_index_only_range(
2761 where_,
2762 cols,
2763 table,
2764 alias_name,
2765 &self.current_snapshot(),
2766 pos,
2767 self.speaks_mysql,
2768 ) else {
2769 return Ok(None);
2770 };
2771 let schema = alloc::vec![ColumnSchema::new(
2772 out_name,
2773 cols[pos].ty,
2774 cols[pos].nullable
2775 )];
2776 Ok(Some(QueryResult::Rows {
2777 columns: schema,
2778 rows: values
2779 .into_iter()
2780 .map(|v| Row::new(alloc::vec![v]))
2781 .collect(),
2782 }))
2783 }
2784
2785 /// v7.39 (round 564) — the same scan, emitting each value instead of
2786 /// building a `Vec<Row>` for the encoder to walk once and drop.
2787 ///
2788 /// A profile of the server serving a 50k-row range put 10.2% of the
2789 /// connection thread's CPU on BUILDING that vector and another 9.7%
2790 /// on dropping it — a fifth of the query, spent allocating and
2791 /// freeing one single-element `Vec` per output row so that the wire
2792 /// encoder could borrow each value for a few nanoseconds. The
2793 /// streaming interface it then hands them to takes `&[Value]`
2794 /// already.
2795 ///
2796 /// Returns `None` when the shape does not apply, so the caller falls
2797 /// back before anything has been emitted.
2798 pub(crate) fn try_index_only_stream<F>(
2799 &self,
2800 stmt: &SelectStatement,
2801 emit: &mut F,
2802 ) -> Result<Option<usize>, EngineError>
2803 where
2804 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2805 {
2806 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2807 return Ok(None);
2808 };
2809 // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2810 // are not materialised here, and a partition parent's own
2811 // heap/indexes are empty (its rows live in the children).
2812 if !stmt.ctes.is_empty() {
2813 return Ok(None);
2814 }
2815 if let Some(from) = &stmt.from
2816 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2817 {
2818 return Ok(None);
2819 }
2820 let where_ = stmt.where_.as_ref().expect("shape checked it");
2821 let cols = &table.schema().columns;
2822 let schema = alloc::vec![ColumnSchema::new(
2823 out_name,
2824 cols[pos].ty,
2825 cols[pos].nullable
2826 )];
2827 let snapshot = self.current_snapshot();
2828 // The header goes out only once the walk has agreed to run — a
2829 // shape rejection after it would leave the client with a
2830 // RowDescription for a result that never comes.
2831 let mut wrote_header = false;
2832 let counted = crate::index_access::index_only_range_each(
2833 where_,
2834 cols,
2835 table,
2836 alias_name,
2837 &snapshot,
2838 pos,
2839 self.speaks_mysql,
2840 &mut |v: spg_storage::Value<'_>| {
2841 if !wrote_header {
2842 emit(crate::StreamItem::Header(&schema))?;
2843 wrote_header = true;
2844 }
2845 emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2846 },
2847 );
2848 match counted {
2849 None => Ok(None),
2850 Some(Err(e)) => Err(e),
2851 Some(Ok(n)) => {
2852 if !wrote_header {
2853 emit(crate::StreamItem::Header(&schema))?;
2854 }
2855 Ok(Some(n))
2856 }
2857 }
2858 }
2859
2860 /// `DISTINCT ON`'s de-duplication, which runs after the inner
2861 /// SELECT has produced its rows.
2862 ///
2863 /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2864 /// reason round 848 established: a debug build gives every branch's
2865 /// locals a slot in the frame whichever branch runs, and this one is
2866 /// eighty lines of hashing, key slicing and survivor sorting that a
2867 /// statement without `DISTINCT ON` never touches. Round 867
2868 /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2869 /// reaches none of it — the segment that had been blamed on
2870 /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2871 #[inline(never)]
2872 fn apply_distinct_on(
2873 &self,
2874 result: QueryResult,
2875 don_hidden: usize,
2876 don_limit: &(
2877 Option<spg_sql::ast::LimitExpr>,
2878 Option<spg_sql::ast::LimitExpr>,
2879 ),
2880 don_top1: usize,
2881 orig_order_by: &[spg_sql::ast::OrderBy],
2882 ) -> Result<QueryResult, EngineError> {
2883 let QueryResult::Rows { columns, rows } = result else {
2884 return Ok(result);
2885 };
2886 // The keys are the hidden trailing columns appended above.
2887 // v7.39 (round 729) — top-1 mode: the trailing columns are the
2888 // DON keys plus the ORDER tail; keep each group's best in one
2889 // hash pass, then sort the SURVIVORS with the original spec.
2890 let mut kept: alloc::vec::Vec<Row<'static>>;
2891 let key_start;
2892 if don_top1 > 0 {
2893 let tail = don_top1 - 1;
2894 key_start = columns.len().saturating_sub(don_hidden + tail);
2895 let ord_start = key_start + don_hidden;
2896 let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2897 .iter()
2898 .map(|o| (o.desc, o.nulls_first))
2899 .collect();
2900 let mysql = self.speaks_mysql;
2901 let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2902 for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2903 let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2904 let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2905 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2906 core::cmp::Ordering::Less => return true,
2907 core::cmp::Ordering::Greater => return false,
2908 core::cmp::Ordering::Equal => {}
2909 }
2910 }
2911 false
2912 };
2913 let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2914 let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2915 let mut keybuf = String::new();
2916 for row in rows {
2917 keybuf.clear();
2918 for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2919 aggregate::push_canonical_key(&mut keybuf, v);
2920 }
2921 match slot.get(keybuf.as_str()) {
2922 Some(&i) => {
2923 if better(&row, &best[i]) {
2924 best[i] = row;
2925 }
2926 }
2927 None => {
2928 slot.insert(keybuf.clone(), best.len());
2929 best.push(row);
2930 }
2931 }
2932 }
2933 // Survivors sort with the FULL original spec (keys are still
2934 // aboard as hidden columns).
2935 let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2936 .iter()
2937 .map(|o| (o.desc, o.nulls_first))
2938 .collect();
2939 best.sort_by(|a, b| {
2940 for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2941 let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2942 let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2943 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2944 core::cmp::Ordering::Equal => {}
2945 o => return o,
2946 }
2947 }
2948 core::cmp::Ordering::Equal
2949 });
2950 for r in &mut best {
2951 r.values.truncate(key_start);
2952 }
2953 kept = best;
2954 } else {
2955 key_start = columns.len().saturating_sub(don_hidden);
2956 let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2957 kept = alloc::vec::Vec::new();
2958 for mut row in rows {
2959 let key: alloc::vec::Vec<Value<'static>> =
2960 row.values.get(key_start..).unwrap_or(&[]).to_vec();
2961 if seen.iter().any(|k| k == &key) {
2962 continue;
2963 }
2964 seen.push(key);
2965 row.values.truncate(key_start);
2966 kept.push(row);
2967 }
2968 }
2969 let mut columns = columns;
2970 columns.truncate(key_start);
2971 // PG limits what DISTINCT ON left, not what fed it.
2972 let kept = apply_deferred_limit(kept, don_limit);
2973 Ok(QueryResult::Rows {
2974 columns,
2975 rows: kept,
2976 })
2977 }
2978
2979 pub(crate) fn exec_select_cancel_as(
2980 &self,
2981 stmt: &SelectStatement,
2982 cancel: CancelToken<'_>,
2983 as_role: Option<&str>,
2984 ) -> Result<QueryResult, EngineError> {
2985 // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2986 // <all columns>` is legal PG (the wildcard expands to grouped
2987 // columns); SPG refused the whole shape. Expand the wildcard
2988 // into explicit column refs up front — the aggregate layer's
2989 // existing "must appear in the GROUP BY clause" validation
2990 // then answers PG's sentence for any non-grouped column.
2991 if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2992 return self.exec_select_cancel_as(&expanded, cancel, as_role);
2993 }
2994 // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2995 // a row.
2996 //
2997 // The aggregate layer already short-circuits this to
2998 // `rows.len()`, so the O(1) part was never the problem — the
2999 // cost is UPSTREAM, materialising every visible row so that
3000 // layer can take its length. Measured over pgwire on 500k rows:
3001 // PG18 8.2 ms with two parallel workers, 10.3 ms with
3002 // parallelism off, SPG 16.5 ms — 1.6x slower than a
3003 // single-threaded PG on the commonest aggregate there is, and no
3004 // ledger entry recorded it.
3005 //
3006 // Counting visible HEADERS needs no row at all. PG cannot do
3007 // this: its visibility lives in the heap tuples themselves, so
3008 // it has to read them (that is why its own count(*) is a full
3009 // scan, parallel or not).
3010 // v7.39 (read01 round 57) — the table-privilege gate on the common
3011 // read core. A superuser session returns from it immediately.
3012 // v7.39 (round 529) — resolve an ORDER BY that names an output
3013 // ALIAS. The statement-level pass never reached a SELECT nested in
3014 // a FROM clause, a CTE or a scalar subquery, so the same query
3015 // worked on its own and failed the moment anything wrapped it —
3016 // which is what generated SQL does constantly.
3017 let aliased;
3018 let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
3019 let mut s = stmt.clone();
3020 crate::orderby::resolve_order_by_position(&mut s);
3021 aliased = s;
3022 &aliased
3023 } else {
3024 stmt
3025 };
3026 // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
3027 //
3028 // Its keys were evaluated against the PROJECTED row, so a key that
3029 // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
3030 // ORDER BY g, v DESC`, the canonical "latest row per group" — could
3031 // not be read at all and the query failed. PG evaluates them on the
3032 // input. They are projected as hidden columns here and stripped
3033 // again below, the same way the grouping-set ordering columns
3034 // already travel.
3035 //
3036 // And the dedup ran AFTER the inner statement's LIMIT, so
3037 // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
3038 // PG answers two: the limit had already taken two rows of the same
3039 // group before anything deduplicated them. A paginated DISTINCT ON
3040 // returned short pages, with no error. The limit is deferred to
3041 // after the dedup, which is PG's order.
3042 let don_stmt;
3043 // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
3044 // order spec (the rewritten stmt's is emptied).
3045 let orig_order_by = stmt.order_by.clone();
3046 let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
3047 (stmt, 0, (None, None), 0usize)
3048 } else {
3049 let mut s = stmt.clone();
3050 let hidden = s.distinct_on.len();
3051 for (i, e) in stmt.distinct_on.iter().enumerate() {
3052 s.items.push(SelectItem::Expr {
3053 expr: e.clone(),
3054 alias: Some(alloc::format!("__distinct_on_{i}")),
3055 });
3056 }
3057 // v7.39 (round 729) — group-top-1 short circuit. When the
3058 // DISTINCT ON keys are exactly the ORDER BY's leading keys,
3059 // the answer is "per group, the row that wins the remaining
3060 // order" — a single O(n) hash pass. The old path sorted the
3061 // ENTIRE input first (500k rows, ~180 ms on the panel cell)
3062 // to keep 100. The inner query runs UNSORTED with every
3063 // order key appended as a hidden column; the dedup below
3064 // keeps each group's best, then sorts the SURVIVORS.
3065 // Declared-collation order keys stay on the sorting path
3066 // (the value comparator here is collation-blind).
3067 let prefix_matches = s.order_by.len() >= hidden
3068 && stmt
3069 .distinct_on
3070 .iter()
3071 .zip(s.order_by.iter())
3072 .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3073 let colls_plain =
3074 crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3075 .map(|cs| cs.iter().all(Option::is_none))
3076 .unwrap_or(false);
3077 let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3078 let tail = s.order_by.len() - hidden;
3079 for (j, o) in s.order_by[hidden..].iter().enumerate() {
3080 s.items.push(SelectItem::Expr {
3081 expr: o.expr.clone(),
3082 alias: Some(alloc::format!("__don_ord_{j}")),
3083 });
3084 }
3085 // Carry the tail's direction flags through the aliases'
3086 // ORDER; the survivors re-sort below with the full spec.
3087 s.order_by = Vec::new();
3088 tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3089 } else {
3090 0
3091 };
3092 // Only a folded literal is deferred; a placeholder or an
3093 // expression keeps the path it has today rather than being
3094 // resolved a second way here.
3095 let deferrable = matches!(
3096 (&s.limit, &s.offset),
3097 (
3098 None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3099 None | Some(spg_sql::ast::LimitExpr::Literal(_))
3100 )
3101 );
3102 let deferred = if deferrable {
3103 (s.limit.take(), s.offset.take())
3104 } else {
3105 (None, None)
3106 };
3107 don_stmt = s;
3108 (&don_stmt, hidden, deferred, top1_tail)
3109 };
3110 self.acl_check_select_as(stmt, as_role)?;
3111 validate_aggregate_placement(stmt)?;
3112 // BEFORE the fast paths below, not after: a name that resolves to
3113 // nothing is not a question the count fast path or the index-only
3114 // scan should get to answer first. Placed after them at first,
3115 // and the two of them swallowed `WHERE` and `ORDER BY` while
3116 // `GROUP BY` and `HAVING`, which cannot take those routes, raised
3117 // — the same statement answering two ways depending on the plan.
3118 self.validate_clause_columns(stmt)?;
3119 self.validate_function_arity(stmt)?;
3120 // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3121 // privilege gate above. Placed before it at first, and the
3122 // security-definer e2e caught it immediately: a SECURITY INVOKER
3123 // function whose body is `SELECT count(*) FROM t` answered
3124 // instead of being refused, because the fast path never reached
3125 // the check.
3126 if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3127 return Ok(r);
3128 }
3129 // v7.39 (round 560) — an index-only range scan. Same placement
3130 // reasoning as the count above: after the privilege gate.
3131 if let Some(r) = self.try_index_only_scan(stmt)? {
3132 return Ok(r);
3133 }
3134 validate_locking_clause(stmt)?;
3135 let result = self.exec_select_cancel_inner(stmt, cancel)?;
3136 // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3137 // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3138 // They carry the per-branch mask through the UNION-ALL sort and must not
3139 // appear in the output. Stripped per SELECT level (grouping-set queries
3140 // are often wrapped in a derived subquery), before DISTINCT ON.
3141 let result = strip_synthetic_order_cols(result);
3142 // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3143 // rows arrive here already ORDER BY'd; keep the FIRST row of
3144 // each group the expressions define (PG semantics). The
3145 // expressions evaluate against the projected schema — an
3146 // expression that isn't in the select list errors honestly.
3147 if stmt.distinct_on.is_empty() {
3148 return Ok(result);
3149 }
3150 self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3151 }
3152
3153 /// The UNION chain: execute the head as a bare block, then fold each
3154 /// peer in with left-associative dedup.
3155 ///
3156 /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3157 /// reason round 848 established. A statement with no unions returns
3158 /// one line above the call — and every nested subquery on a deep
3159 /// path is such a statement, so each level of the recursion carried
3160 /// 170 lines of locals it could not reach. Round 867 measured that
3161 /// frame at 34,800 bytes, the largest single one on the descent,
3162 /// after two earlier attributions had blamed its caller and then its
3163 /// callee: the gap between two marks is the frame of everything
3164 /// BETWEEN them, and this function had no mark of its own.
3165 #[inline(never)]
3166 fn exec_union_chain(
3167 &self,
3168 stmt_ref: &SelectStatement,
3169 stmt: &SelectStatement,
3170 cancel: CancelToken<'_>,
3171 ) -> Result<QueryResult, EngineError> {
3172 // UNION path: clone-strip the head into a bare block (its own
3173 // DISTINCT and any inner ORDER BY are dropped by parser rule —
3174 // the wrapper SelectStatement carries them), execute, then chain
3175 // peers with left-associative dedup semantics.
3176 // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3177 // output columns; a position past their count is PG's 42P10.
3178 crate::orderby::check_order_by_positions(stmt_ref)?;
3179 let mut head_unknown = branch_unknown_mask(stmt_ref);
3180 let head_regcast = branch_regcast_mask(stmt_ref);
3181 let mut head = stmt_ref.clone();
3182 head.unions = Vec::new();
3183 head.order_by = Vec::new();
3184 head.limit = None;
3185 let QueryResult::Rows {
3186 mut columns,
3187 mut rows,
3188 } = self.exec_bare_select_cancel(&head, cancel)?
3189 else {
3190 unreachable!("bare SELECT cannot return CommandOk")
3191 };
3192 for (kind, peer) in &stmt_ref.unions {
3193 // v7.37.17 (17.6 siblings) — a peer carrying its own
3194 // unions is a nested INTERSECT group (the parser's
3195 // precedence regrouping); recurse through the
3196 // union-aware wrapper for it.
3197 let peer_result = if peer.unions.is_empty() {
3198 self.exec_bare_select_cancel(peer, cancel)?
3199 } else {
3200 self.exec_select_cancel(peer, cancel)?
3201 };
3202 let QueryResult::Rows {
3203 columns: peer_cols,
3204 rows: mut peer_rows,
3205 } = peer_result
3206 else {
3207 unreachable!("bare SELECT cannot return CommandOk")
3208 };
3209 if peer_cols.len() != columns.len() {
3210 // v7.39 (round 232) — PG's wording, which clients match on.
3211 return Err(EngineError::Unsupported(alloc::format!(
3212 "each {} query must have the same number of columns",
3213 set_op_name(*kind)
3214 )));
3215 }
3216 // v7.39 (round 232+233) — PG resolves each result column to one
3217 // type before it merges anything, and refuses the query when the
3218 // two branches have no common type. SPG's unifier
3219 // (`unify_union_columns`) is value-driven and deliberately
3220 // conservative — "a column where any cell fails to coerce is left
3221 // exactly as it was" — so a mismatch produced a column holding
3222 // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3223 // back with integers and text interleaved) instead of an error.
3224 //
3225 // The check has to read the branch ASTs, not just their schemas:
3226 // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3227 // as TEXT and is indistinguishable from a real text column by
3228 // schema alone — yet PG treats the two completely differently
3229 // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3230 // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3231 let peer_unknown = branch_unknown_mask(peer);
3232 let peer_regcast = branch_regcast_mask(peer);
3233 for i in 0..columns.len() {
3234 let hu = head_unknown.get(i).copied().unwrap_or(false);
3235 let pu = peer_unknown.get(i).copied().unwrap_or(false);
3236 let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3237 let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3238 || head_regcast.get(i).copied().unwrap_or(false);
3239 match (hu, pu) {
3240 // Both sides carry a real type: they must share a category.
3241 (false, false) => {
3242 if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3243 return Err(EngineError::Unsupported(alloc::format!(
3244 "{} types {} and {} cannot be matched",
3245 set_op_name(*kind),
3246 crate::conversions::pg_type_name_for_error(ht),
3247 crate::conversions::pg_type_name_for_error(pt),
3248 )));
3249 }
3250 }
3251 // One side is an untyped literal: it takes the other's
3252 // type, and failing to convert is the error PG reports.
3253 (true, false) => {
3254 coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3255 columns[i].ty = pt;
3256 head_unknown[i] = false;
3257 }
3258 (false, true) => {
3259 coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3260 }
3261 // Both untyped — nothing to resolve against yet.
3262 (true, true) => {}
3263 }
3264 }
3265 // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3266 // nullable (PG semantics). Previously the result kept only the head's
3267 // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3268 // non-null `1`) wrongly reported the column NOT NULL, which let
3269 // `count(col)`'s NOT-NULL fast-path count the NULL row.
3270 for (i, pc) in peer_cols.iter().enumerate() {
3271 if pc.nullable {
3272 columns[i].nullable = true;
3273 }
3274 }
3275 // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3276 // text by the session collation (CI + accent + PAD SPACE), like
3277 // GROUP BY. PG stays byte-exact.
3278 let mysql = self.speaks_mysql;
3279 // v7.38.14 — the mask, which 7.38.13 recorded as impossible here
3280 // and was wrong about. `columns` and `peer_cols` are both in
3281 // scope; what was actually missing is that the branches' output
3282 // schemas did not CARRY the collation, so a mask built from them
3283 // would have marked every column byte-wise. Unifying the
3284 // projection-to-schema conversion fixed the supply side, and the
3285 // mask is now buildable from what was always there.
3286 //
3287 // Either side byte-wise keeps the position byte-wise, mirroring
3288 // `eval::resolve::mysql_text_fold_applies`: a set operation
3289 // between a folding column and a declared-binary one must not
3290 // quietly fold the binary one's values away.
3291 let set_mask: alloc::vec::Vec<bool> = columns
3292 .iter()
3293 .zip(peer_cols.iter())
3294 .map(|(l, r)| {
3295 matches!(l.collation, spg_storage::Collation::Binary)
3296 || matches!(r.collation, spg_storage::Collation::Binary)
3297 })
3298 .collect();
3299 let fold = FoldSpec::of(mysql, &set_mask);
3300 match kind {
3301 UnionKind::All => rows.extend(peer_rows),
3302 UnionKind::Distinct => {
3303 rows.extend(peer_rows);
3304 rows = dedup_rows(rows, fold);
3305 }
3306 // v7.37.17 (17.6 siblings) — PG set semantics.
3307 // v7.39 (round 591) — all four ask the same question of the
3308 // right side, and all four used to answer it by scanning it
3309 // once per left row. `PeerIndex` buckets it by the hash
3310 // DISTINCT already uses, so the answer is a lookup.
3311 // INTERSECT: distinct rows present on both sides.
3312 UnionKind::Intersect => {
3313 let idx = PeerIndex::build(&peer_rows, fold);
3314 rows = dedup_rows(rows, fold)
3315 .into_iter()
3316 .filter(|r| idx.contains(r))
3317 .collect();
3318 }
3319 // INTERSECT ALL: multiset intersection — each row
3320 // keeps min(left count, right count) occurrences.
3321 UnionKind::IntersectAll => {
3322 let mut idx = PeerIndex::build(&peer_rows, fold);
3323 let mut kept: Vec<Row<'static>> = Vec::new();
3324 for r in rows {
3325 if idx.take_one(&r) {
3326 kept.push(r);
3327 }
3328 }
3329 rows = kept;
3330 }
3331 // EXCEPT: distinct left rows absent from the right.
3332 UnionKind::Except => {
3333 let idx = PeerIndex::build(&peer_rows, fold);
3334 rows = dedup_rows(rows, fold)
3335 .into_iter()
3336 .filter(|r| !idx.contains(r))
3337 .collect();
3338 }
3339 // EXCEPT ALL: multiset subtraction — each right
3340 // occurrence cancels one left occurrence.
3341 UnionKind::ExceptAll => {
3342 let mut idx = PeerIndex::build(&peer_rows, fold);
3343 let mut kept: Vec<Row<'static>> = Vec::new();
3344 for r in rows {
3345 if !idx.take_one(&r) {
3346 kept.push(r);
3347 }
3348 }
3349 rows = kept;
3350 }
3351 }
3352 }
3353 // PG resolves a UNION / VALUES result column to one common type
3354 // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3355 // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3356 // built each branch independently, leaving mixed-type columns
3357 // that broke ORDER BY, comparisons, and value-based window
3358 // frames. Unify + coerce before the combined ORDER BY sees them.
3359 unify_union_columns(&mut columns, &mut rows);
3360 // ORDER BY at the top of a UNION applies to the combined result.
3361 // Eval against the projected schema (NOT the source table).
3362 if !stmt.order_by.is_empty() {
3363 // v7.39 (read01 round 54) — the combined-result ctx must carry the
3364 // catalog, and the projected columns must keep their enum identity
3365 // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3366 // by TEXT instead of member order — silently wrong rows, not an
3367 // error. (Same shape as the enum-order knife's GROUP BY fix.)
3368 let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3369 // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3370 // survive to here when the head projects a Wildcard (the
3371 // group-tail wrapper shape): map them onto the Nth
3372 // projected column so the combined sort works.
3373 let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3374 .order_by
3375 .iter()
3376 .map(|o| {
3377 let mut o = o.clone();
3378 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3379 && *n >= 1
3380 && let Ok(idx) = usize::try_from(*n - 1)
3381 && idx < columns.len()
3382 {
3383 o.expr = Expr::Column(spg_sql::ast::ColumnName {
3384 qualifier: None,
3385 name: columns[idx].name.clone(),
3386 });
3387 }
3388 o
3389 })
3390 .collect();
3391 let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3392 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3393 for r in rows {
3394 // v7.39.12 — a correlated subquery in ORDER BY is resolved
3395 // for this row before the key is built; see
3396 // `Engine::order_by_resolved_for_row`.
3397 let per_row =
3398 self.order_by_resolved_for_row(&resolved_order, &r, &synth_ctx, cancel)?;
3399 let keys = build_order_keys(
3400 per_row.as_deref().unwrap_or(&resolved_order),
3401 &r,
3402 &synth_ctx,
3403 )?;
3404 tagged.push((keys, r));
3405 }
3406 sort_by_keys(&mut tagged, &descs, self.session_parallel_workers());
3407 rows = tagged.into_iter().map(|(_, r)| r).collect();
3408 }
3409 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3410 Ok(QueryResult::Rows { columns, rows })
3411 }
3412
3413 fn exec_select_cancel_inner(
3414 &self,
3415 stmt: &SelectStatement,
3416 cancel: CancelToken<'_>,
3417 ) -> Result<QueryResult, EngineError> {
3418 cancel.check()?;
3419 // v7.38 P0 元机制 A — first observable point inside the
3420 // planner / executor. Tests use this to inject a delay or
3421 // a cancellation race before any row is produced. Release
3422 // build expands to `let _ = (...);` — zero cost.
3423 crate::injection_point!("planner_first_row_fetch", &stmt.from);
3424 // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3425 // PG analyses every definition, referenced or not, so `SELECT i FROM
3426 // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3427 // succeeded here (the parser used to drop the unreferenced defs
3428 // whole). The check is the CREATE VIEW check's shape (round 700): a
3429 // LIMIT-0 run of the same FROM with the definitions' key
3430 // expressions as the projection — it cannot disagree with what a
3431 // referencing window would have done, because it resolves the same
3432 // names the same way. Zero cost for the ordinary statement: the
3433 // list is empty unless a WINDOW clause left unreferenced defs.
3434 if !stmt.window_check_exprs.is_empty() {
3435 let mut probe = stmt.clone();
3436 probe.items = stmt
3437 .window_check_exprs
3438 .iter()
3439 .map(|e| spg_sql::ast::SelectItem::Expr {
3440 expr: e.clone(),
3441 alias: None,
3442 })
3443 .collect();
3444 probe.window_check_exprs = Vec::new();
3445 probe.distinct = false;
3446 probe.distinct_on = Vec::new();
3447 probe.group_by = None;
3448 probe.group_by_all = false;
3449 probe.having = None;
3450 probe.unions = Vec::new();
3451 probe.order_by = Vec::new();
3452 probe.locking = None;
3453 probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3454 probe.offset = None;
3455 probe.limit_with_ties = false;
3456 self.exec_select_cancel_inner(&probe, cancel)?;
3457 }
3458 // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3459 // takes the catalog, so the parser leaves a marker and the rewrite lands
3460 // here: the call moves into a LATERAL FROM item and the item becomes one
3461 // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3462 // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3463 // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3464 // second one.
3465 if let Some(lowered) = self.lower_record_expansion(stmt)? {
3466 return self.exec_select_cancel_inner(&lowered, cancel);
3467 }
3468 // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3469 // FROM / JOIN graph references any catalogued view name,
3470 // re-parse the view body and prepend it as a synthetic
3471 // CTE. Recurses on views-in-views via the regular CTE
3472 // dispatch below. Fast-path: skip the walker entirely when
3473 // the catalog has no views (the typical OLTP load).
3474 if !self.active_catalog().views_all().is_empty() {
3475 if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3476 return self.exec_select_cancel(&rewritten, cancel);
3477 }
3478 }
3479 // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3480 // gets rewritten to a UNION-ALL over the children that overlap
3481 // the WHERE-derived key range. Uses the same CTE-injection
3482 // trick as VIEW expansion above so downstream resolution
3483 // doesn't need a partition-aware code path.
3484 if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3485 return self.exec_select_cancel(&rewritten, cancel);
3486 }
3487 // v7.16.2 — information_schema / pg_catalog virtual
3488 // views (mailrs round-10 A.3). If the SELECT touches a
3489 // synthetic meta-table name (`__spg_info_*` /
3490 // `__spg_pg_*` — produced by the parser for
3491 // `information_schema.X` / `pg_catalog.X`), clone the
3492 // catalog, materialise the requested view as a real
3493 // temporary table, and re-execute against an enriched
3494 // engine. Same pattern as `exec_with_ctes` for CTEs.
3495 if !self.meta_views_materialised && select_references_meta_view(stmt) {
3496 return self.exec_select_with_meta_views(stmt, cancel);
3497 }
3498 // v6.10.2 — cold-tier time-travel short-circuit. When the
3499 // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3500 // dedicated cold-segment scan instead of the regular
3501 // hot+index path. The scope is intentionally narrow for
3502 // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3503 // optionally with a single-column-equality WHERE. JOINs /
3504 // aggregates / ORDER BY / subqueries on top of a time-
3505 // travelled scan are STABILITY § "Out of v6.10".
3506 if let Some(from) = &stmt.from
3507 && let Some(seg_id) = from.primary.as_of_segment
3508 {
3509 return self.exec_select_as_of_segment(stmt, from, seg_id);
3510 }
3511 // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3512 // pre-CTE because they don't read from the catalog and
3513 // shouldn't participate in regular FROM resolution.
3514 // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3515 // short-circuits. A meta-view FROM materialises to a fixed row
3516 // set. For a bare `SELECT *` we return it directly; otherwise we
3517 // stage it as a temp table and run the normal pipeline, so
3518 // projection / WHERE / ORDER BY / aggregates work over these views
3519 // (they were `SELECT *`-only before). A real table shadowing the
3520 // name wins (checked first), which also stops the staged re-run
3521 // from recursing back into meta-view detection.
3522 if let Some(from) = &stmt.from
3523 && from.joins.is_empty()
3524 && self.active_catalog().get(&from.primary.name).is_none()
3525 {
3526 let lower = from.primary.name.to_ascii_lowercase();
3527 if let Some(result) = self.meta_view_result(&lower) {
3528 let bare = stmt.where_.is_none()
3529 && stmt.group_by.is_none()
3530 && stmt.having.is_none()
3531 && stmt.unions.is_empty()
3532 && stmt.order_by.is_empty()
3533 && stmt.limit.is_none()
3534 && stmt.offset.is_none()
3535 && !stmt.distinct
3536 && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3537 if bare {
3538 return Ok(result);
3539 }
3540 if let QueryResult::Rows { columns, rows } = result {
3541 let mut catalog = self.active_catalog().clone();
3542 let cols = infer_column_types(&columns, &rows);
3543 let schema = TableSchema::new(from.primary.name.clone(), cols);
3544 catalog.create_table(schema).map_err(EngineError::Storage)?;
3545 let t = catalog
3546 .get_mut(&from.primary.name)
3547 .expect("just-created meta-view table must exist");
3548 for row in rows {
3549 t.insert(row).map_err(EngineError::Storage)?;
3550 }
3551 let mut eng = Engine::restore(catalog);
3552 if let Some(c) = self.clock {
3553 eng = eng.with_clock(c);
3554 }
3555 if let Some(f) = self.salt_fn {
3556 eng = eng.with_salt_fn(f);
3557 }
3558 // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3559 // connection identity so `WHERE pid = pg_backend_pid()`
3560 // matches inside the staged meta-view run.
3561 if let Some(f) = self.backend_pid_fn {
3562 eng.set_backend_pid_fn(f);
3563 }
3564 return eng.exec_select_cancel(stmt, cancel);
3565 }
3566 return Ok(result);
3567 }
3568 }
3569 // v4.11: CTEs materialise into a temporary enriched catalog
3570 // *before* anything else — the body SELECT can then refer
3571 // to CTE names via the regular FROM-clause resolution.
3572 // Uncorrelated only: each CTE body runs once against the
3573 // current catalog, not against later CTEs' results (left-
3574 // to-right materialisation would relax this, but we keep
3575 // it simple for v4.11 MVP).
3576 if !stmt.ctes.is_empty() {
3577 return self.exec_with_ctes(stmt, cancel);
3578 }
3579 // v4.10: subqueries (uncorrelated) are resolved here, before
3580 // the executor sees the row loop. We clone the statement so
3581 // we can mutate without disturbing the caller's AST — most
3582 // queries pass through with no subquery nodes and the clone
3583 // is cheap; with subqueries the materialisation cost
3584 // dominates anyway.
3585 let mut stmt_owned;
3586 let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3587 stmt_owned = stmt.clone();
3588 // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3589 // aggregate-wrapped correlated scalar subquery whose
3590 // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3591 // executor streams one join instead of splicing a per-row
3592 // subplan. Runs before the per-row/batch resolver, which then
3593 // only sees the subqueries the pull-up left behind.
3594 self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3595 // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3596 // the "per-key latest" scalar subquery shape (inbox / feed
3597 // / timeline applications) becomes a CTE + LEFT JOIN
3598 // against a GROUP BY pre-aggregation that reuses the v7.33
3599 // first_ordered argmax executor. Runs AFTER unique-key
3600 // pull-up (so the unique-key fast path still wins for
3601 // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3602 // Phase 1 (this commit) is skeleton only — no-op pass.
3603 self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3604 // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3605 // sublink pull-up to semi/anti-join, before the resolver gets
3606 // a chance to walk per-row.
3607 self.pull_up_exists_sublinks(&mut stmt_owned);
3608 // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3609 // exec_with_ctes so they materialise once before the body
3610 // SELECT runs. exec_with_ctes strips ctes from the body
3611 // clone, then re-enters select.
3612 if !stmt_owned.ctes.is_empty() {
3613 return self.exec_with_ctes(&stmt_owned, cancel);
3614 }
3615 // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3616 // SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3617 // BEFORE `resolve_select_subqueries` materialises the inner
3618 // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3619 // INSUBQ benchmark). Run the inner once, collect the result
3620 // values into a `HashSet<i64>` directly, then probe A.pk per
3621 // value and tally. Returns `Some` when the shape matches.
3622 if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3623 return Ok(out);
3624 }
3625 self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3626 &stmt_owned
3627 } else {
3628 stmt
3629 };
3630 if stmt_ref.unions.is_empty() {
3631 return self.exec_bare_select_cancel(stmt_ref, cancel);
3632 }
3633 self.exec_union_chain(stmt_ref, stmt, cancel)
3634 }
3635
3636 #[allow(clippy::too_many_lines)]
3637 #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3638 /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3639 /// Synthesises a single-column virtual table whose column type
3640 /// is TEXT and whose rows are the array elements. Routes
3641 /// through the regular projection / WHERE / ORDER BY / LIMIT
3642 /// machinery so set-returning UNNEST composes naturally with
3643 /// the rest of the SELECT surface.
3644 fn exec_select_unnest(
3645 &self,
3646 stmt: &SelectStatement,
3647 primary: &TableRef,
3648 cancel: CancelToken<'_>,
3649 ) -> Result<QueryResult, EngineError> {
3650 let expr = primary
3651 .unnest_expr
3652 .as_deref()
3653 .expect("caller guards unnest_expr.is_some()");
3654 // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3655 // N value columns instead of one; the shared builder does
3656 // the work and the tail below (WHERE / agg / projection)
3657 // runs against the wider schema.
3658 let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3659 match unnest_zip_args(expr) {
3660 Some(args) => Some(unnest_zip_rows(args)?),
3661 None => None,
3662 };
3663 // Evaluate the array expression once. Empty schema / empty
3664 // row — uncorrelated UNNEST cannot reference outer columns.
3665 // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3666 // introspection family (enum_range / enum_first / enum_last) resolves
3667 // its labels from the argument's STATIC enum type against the
3668 // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3669 // fell through to the generic arm, got NULL, and expanded to zero rows
3670 // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3671 // carry the catalog) worked.
3672 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3673 let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3674 let dummy_row = Row::new(alloc::vec::Vec::new());
3675 // v7.11.13 — unnest dispatches per array element type so
3676 // INT[] / BIGINT[] surface their PG types in projection.
3677 // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3678 // columns (PG: lexeme | positions | weights); everything else
3679 // keeps the alias / "unnest" defaults below.
3680 let mut composite_names: Option<&[&str]> = None;
3681 let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3682 if let Some(m) = multi {
3683 m
3684 } else {
3685 // v7.39 (round 236) — flatten a multidimensional array into
3686 // its row-major elements (PG) before the 1-D-only match.
3687 let unnest_src = {
3688 let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3689 crate::eval::values::flatten_2d(&v).unwrap_or(v)
3690 };
3691 let mut return_multi: Option<(
3692 alloc::vec::Vec<DataType>,
3693 alloc::vec::Vec<Row<'static>>,
3694 )> = None;
3695 let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3696 {
3697 Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3698 Value::TextArray(items) => {
3699 let rows = items
3700 .into_iter()
3701 .map(|item| {
3702 Row::new(alloc::vec![match item {
3703 Some(s) => Value::text(s),
3704 None => Value::Null,
3705 }])
3706 })
3707 .collect();
3708 (DataType::Text, rows)
3709 }
3710 Value::IntArray(items) => {
3711 let rows = items
3712 .into_iter()
3713 .map(|item| {
3714 Row::new(alloc::vec![match item {
3715 Some(n) => Value::Int(n),
3716 None => Value::Null,
3717 }])
3718 })
3719 .collect();
3720 (DataType::Int, rows)
3721 }
3722 Value::BigIntArray(items) => {
3723 let rows = items
3724 .into_iter()
3725 .map(|item| {
3726 Row::new(alloc::vec![match item {
3727 Some(n) => Value::BigInt(n),
3728 None => Value::Null,
3729 }])
3730 })
3731 .collect();
3732 (DataType::BigInt, rows)
3733 }
3734 Value::Multirange { kind, ranges } => {
3735 let rows = ranges
3736 .iter()
3737 .map(|sp| {
3738 Row::new(alloc::vec![Value::Range {
3739 kind,
3740 lower: sp.lower.clone(),
3741 upper: sp.upper.clone(),
3742 lower_inc: sp.lower_inc,
3743 upper_inc: sp.upper_inc,
3744 empty: false,
3745 }])
3746 })
3747 .collect();
3748 (DataType::Range(kind), rows)
3749 }
3750 // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3751 // one row per lexeme, PG18-measured columns
3752 // lexeme | positions | weights (`a | {1,3} |
3753 // {D,D}`); a position-less lexeme (a stripped
3754 // vector) reads NULL in both array columns.
3755 Value::TsVector(lexemes) => {
3756 composite_names = Some(&["lexeme", "positions", "weights"]);
3757 let rows = lexemes
3758 .iter()
3759 .map(|l| {
3760 let (pos, wts) = if l.positions.is_empty() {
3761 (Value::Null, Value::Null)
3762 } else {
3763 let letter = match l.weight {
3764 3 => "A",
3765 2 => "B",
3766 1 => "C",
3767 _ => "D",
3768 };
3769 (
3770 Value::SmallIntArray(
3771 l.positions
3772 .iter()
3773 .map(|p| {
3774 Some(i16::try_from(*p).unwrap_or(i16::MAX))
3775 })
3776 .collect(),
3777 ),
3778 Value::TextArray(
3779 l.positions
3780 .iter()
3781 .map(|_| Some(letter.into()))
3782 .collect(),
3783 ),
3784 )
3785 };
3786 Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3787 })
3788 .collect();
3789 return_multi = Some((
3790 alloc::vec![
3791 DataType::Text,
3792 DataType::SmallIntArray,
3793 DataType::TextArray
3794 ],
3795 rows,
3796 ));
3797 (DataType::Text, alloc::vec::Vec::new())
3798 }
3799 // v7.39.11 — every remaining array-family value,
3800 // through the one element menu, so a type does not
3801 // have to be written out here a second time to be
3802 // unnestable. `unnest(ARRAY[1,2]::smallint[])`
3803 // raised "expects an array argument, got
3804 // smallint[]" until this arm — the arms above name
3805 // int / bigint / text / json and stop — and so did
3806 // every catalog vector. Found while closing
3807 // sentori's §4 against 7.39.10.
3808 ref v if crate::eval::values::array_len(v).is_some() => {
3809 let elems = crate::eval::values::array_elements(v).unwrap_or_default();
3810 let dt = elems
3811 .iter()
3812 .find_map(spg_storage::Value::data_type)
3813 .unwrap_or(DataType::Text);
3814 let rows = elems
3815 .into_iter()
3816 .map(|e| Row::new(alloc::vec![e]))
3817 .collect();
3818 (dt, rows)
3819 }
3820 other => {
3821 // v7.39 (round 622, S05a) — see table_access.rs:
3822 // the same sentence, and it is a type mismatch.
3823 return Err(EngineError::Eval(EvalError::TypeMismatch {
3824 detail: alloc::format!(
3825 "unnest() expects an array argument, got {}",
3826 crate::conversions::pg_type_name_for_error_opt(other.data_type())
3827 ),
3828 }));
3829 }
3830 };
3831 if let Some(m) = return_multi {
3832 m
3833 } else {
3834 (alloc::vec![elem_dtype], rows)
3835 }
3836 };
3837 let alias = primary
3838 .alias
3839 .clone()
3840 .unwrap_or_else(|| "unnest".to_string());
3841 // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3842 // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3843 // entries map positionally over the value columns. Without
3844 // the column list, a single column falls back to the table
3845 // alias (pre-v7.13.2 behaviour); multi-arg columns default
3846 // to PG's `unnest`.
3847 let n_vals = dtypes.len();
3848 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3849 .iter()
3850 .enumerate()
3851 .map(|(i, dt)| {
3852 let name = primary
3853 .unnest_column_aliases
3854 .get(i)
3855 .cloned()
3856 .unwrap_or_else(|| {
3857 if let Some(names) = composite_names {
3858 names
3859 .get(i)
3860 .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3861 } else if n_vals == 1 {
3862 alias.clone()
3863 } else {
3864 "unnest".to_string()
3865 }
3866 });
3867 ColumnSchema::new(name, *dt, true)
3868 })
3869 .collect();
3870 // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3871 // parser desugared a base-type-returning function here (see
3872 // TableRef::scalar_fn_item); the marker rides the column so it survives
3873 // every EvalContext an inner stage rebuilds.
3874 if primary.scalar_fn_item && schema_cols.len() == 1 {
3875 schema_cols[0].scalar_row_source = true;
3876 }
3877 // WITH ORDINALITY — trailing BIGINT counting rows from 1
3878 // in element order. The alias entry after the value
3879 // columns renames it (PG default: `ordinality`).
3880 let rows = if primary.with_ordinality {
3881 let ord_name = primary
3882 .unnest_column_aliases
3883 .get(n_vals)
3884 .cloned()
3885 .unwrap_or_else(|| "ordinality".to_string());
3886 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3887 rows.into_iter()
3888 .enumerate()
3889 .map(|(i, row)| {
3890 let mut vals = row.values.clone();
3891 vals.push(Value::BigInt(i as i64 + 1));
3892 Row::new(vals)
3893 })
3894 .collect()
3895 } else {
3896 rows
3897 };
3898 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3899 // `EvalContext::new` drops it and every catalog-dependent cast
3900 // (regclass / enum / composite / domain) silently degrades.
3901 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3902 // Apply WHERE.
3903 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3904 let mut out = alloc::vec::Vec::with_capacity(rows.len());
3905 for row in rows {
3906 cancel.check()?;
3907 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3908 if matches!(v, Value::Bool(true)) {
3909 out.push(row);
3910 }
3911 }
3912 out
3913 } else {
3914 rows
3915 };
3916 // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3917 // unnest source. Same routing the relational scan path
3918 // already takes — without it `SELECT COUNT(*) FROM
3919 // unnest(ARRAY[…])` either errored at projection time or
3920 // returned the wrong shape.
3921 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
3922 // v7.29 — a per-query memo so correlated scalar
3923 // subqueries batch-evaluate once (group map) instead of
3924 // executing per group.
3925 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3926 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3927 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3928 .map_err(|err| match err {
3929 EngineError::Eval(ev) => ev,
3930 other => eval::EvalError::TypeMismatch {
3931 detail: alloc::format!("{other}"),
3932 },
3933 })
3934 };
3935 // v7.39 (round 656) — hand the rows over as they are rather than
3936 // collecting a second vector of `RowRef` wrappers. Note this is
3937 // a set-returning-function path, NOT the relational scan: the
3938 // measured O(rows) cost lived in `run_single_table_aggregate`,
3939 // and converting these four first was a miss that cost a full
3940 // round — every test stayed green and the number did not move.
3941 let agg = aggregate::run(
3942 stmt,
3943 crate::join::AggRows::Owned(&filtered),
3944 &schema_cols,
3945 Some(&alias),
3946 Some(&agg_correlated),
3947 self.parallel_runner.0.as_deref(),
3948 Some(self.active_catalog()),
3949 Some(self),
3950 )?;
3951 return self.finish_agg_result(agg, stmt, cancel);
3952 }
3953 // Projection.
3954 let projection = build_projection(
3955 &stmt.items,
3956 &schema_cols,
3957 &alias,
3958 self.speaks_mysql,
3959 Some(self.active_catalog()),
3960 )?;
3961 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3962 alloc::vec::Vec::with_capacity(filtered.len());
3963 // v7.19 P5 — Set-Returning-Function in projection
3964 // position (PG `SELECT unnest(arr) FROM t` shape). When a
3965 // SELECT item evaluates to a top-level unnest(arr) call,
3966 // expand it: for each input row, evaluate the array, emit
3967 // one output row per element, broadcasting non-SRF
3968 // projections from the same input row. Multi-SRF + LCM
3969 // padding stays a documented carve-out; mailrs uses
3970 // single-SRF for redirect_uris.
3971 // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3972 // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3973 let srf_idxs = self.srf_target_idxs(&projection);
3974 // v7.39 (round 621) — which input row each output row came from. An
3975 // SRF turns one input row into many, and the ORDER BY below used to
3976 // index the EXPANDED rows by the INPUT row's position: the result was
3977 // silently truncated to the input row count and left unsorted, so
3978 // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3979 // answered three of its six rows, in no order. Without the ORDER BY
3980 // the same query was already right.
3981 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3982 if !srf_idxs.is_empty() {
3983 let (rows, src) =
3984 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3985 projected_rows = rows;
3986 src_of_row = src;
3987 } else {
3988 // v7.24 (round-16 B) — select-list subqueries resolve
3989 // per row (correlated-aware; plain exprs take the fast
3990 // path inside).
3991 let mut proj_memo = memoize::MemoizeCache::default();
3992 for row in &filtered {
3993 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3994 for p in &projection {
3995 vals.push(self.eval_expr_with_correlated(
3996 &p.expr,
3997 row,
3998 &scan_ctx,
3999 cancel,
4000 Some(&mut proj_memo),
4001 )?);
4002 }
4003 projected_rows.push(Row::new(vals));
4004 }
4005 }
4006 // ORDER BY / LIMIT — apply on the projected rows (cheap;
4007 // unnest result sets are small by design).
4008 let columns: alloc::vec::Vec<ColumnSchema> = projection
4009 .iter()
4010 // v7.39 (read01 round 54) — keep the column's enum identity through
4011 // the projection (it lives outside the DataType lattice), or a
4012 // derived table / UNION / windowed result forgets it and any outer
4013 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4014 .map(|p| p.to_column_schema())
4015 .collect();
4016 // Re-evaluate ORDER BY against the source schema (pre-projection
4017 // so col refs by name still resolve through `scan_ctx`).
4018 // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
4019 // column. Evaluated as an expression it is just the constant N: the same
4020 // key for every row, so the sort ran and changed nothing.
4021 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4022 if !order_by.is_empty() {
4023 // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
4024 // A key that names a select-list item reads it out of the expanded
4025 // row (PG sorts AFTER the expansion); one that names a source
4026 // column the query does not project is evaluated on the input row
4027 // it came from, which is what `srf_order_output_cols` decides.
4028 let out_cols = if srf_idxs.is_empty() {
4029 alloc::vec![None; order_by.len()]
4030 } else {
4031 srf_order_output_cols(&order_by, &projection)
4032 };
4033 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4034 .iter()
4035 .enumerate()
4036 .map(|(k, out)| -> Result<_, EngineError> {
4037 let src = src_of_row.get(k).copied().unwrap_or(k);
4038 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4039 .iter()
4040 .zip(out_cols.iter())
4041 .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
4042 .collect();
4043 Ok((k, keys?))
4044 })
4045 .collect::<Result<_, _>>()?;
4046 indexed.sort_by(|a, b| {
4047 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4048 let o = &order_by[idx];
4049 let cmp = order_by_value_cmp_in(
4050 o.desc,
4051 o.nulls_first,
4052 ka,
4053 kb,
4054 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4055 );
4056 if cmp != core::cmp::Ordering::Equal {
4057 return cmp;
4058 }
4059 }
4060 core::cmp::Ordering::Equal
4061 });
4062 projected_rows = indexed
4063 .into_iter()
4064 .map(|(i, _)| projected_rows[i].clone())
4065 .collect();
4066 }
4067 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4068 if stmt.distinct {
4069 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4070 // spec folds EVERY text position, so a column declared
4071 // `COLLATE utf8mb4_bin` had its values merged here exactly the
4072 // way 3b494b6e fixed on the main scan path. The projection is
4073 // already in scope at each of these sites, so the mask needs no
4074 // new plumbing -- it was simply never asked for.
4075 projected_rows = dedup_rows(
4076 projected_rows,
4077 FoldSpec::of_masks(
4078 scan_ctx.mysql_dialect,
4079 &fold_mask(&projection),
4080 &pad_mask(&projection),
4081 ),
4082 );
4083 }
4084 // LIMIT / OFFSET — apply at the tail.
4085 if let Some(offset) = stmt.offset_literal() {
4086 let off = (offset as usize).min(projected_rows.len());
4087 projected_rows.drain(..off);
4088 }
4089 if let Some(limit) = stmt.limit_literal() {
4090 projected_rows.truncate(limit as usize);
4091 }
4092 Ok(QueryResult::Rows {
4093 columns,
4094 rows: projected_rows,
4095 })
4096 }
4097
4098 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
4099 /// step])` set-returning source. Mirrors `exec_select_unnest`'s
4100 /// shape: evaluate the arg list once against an empty row,
4101 /// materialise the row stream by stepping start → stop, then
4102 /// route through the standard WHERE / projection / ORDER BY /
4103 /// LIMIT pipeline. Two arg-type combos in v7.17:
4104 /// * integer / integer [/ integer] — SmallInt, Int, BigInt
4105 /// (widened to BigInt internally; step defaults to 1)
4106 /// * timestamp / timestamp / interval — date-range
4107 /// iteration (mailrs's daily-report pattern)
4108 fn exec_select_generate_series(
4109 &self,
4110 stmt: &SelectStatement,
4111 primary: &TableRef,
4112 cancel: CancelToken<'_>,
4113 ) -> Result<QueryResult, EngineError> {
4114 let args = primary
4115 .generate_series_args
4116 .as_ref()
4117 .expect("caller guards generate_series_args.is_some()");
4118 let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
4119 let alias = primary
4120 .alias
4121 .clone()
4122 .unwrap_or_else(|| "generate_series".to_string());
4123 // `AS t(n)` — the first column-alias entry renames the
4124 // series column (PG semantics); bare alias keeps the
4125 // pre-existing behaviour of naming the column after it.
4126 let col_name = primary
4127 .unnest_column_aliases
4128 .first()
4129 .cloned()
4130 .unwrap_or_else(|| alias.clone());
4131 let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
4132 let mut schema_cols = alloc::vec![col_schema.clone()];
4133 // WITH ORDINALITY — trailing BIGINT counting rows from 1;
4134 // the second column-alias entry renames it.
4135 let rows = if primary.with_ordinality {
4136 let ord_name = primary
4137 .unnest_column_aliases
4138 .get(1)
4139 .cloned()
4140 .unwrap_or_else(|| "ordinality".to_string());
4141 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4142 rows.into_iter()
4143 .enumerate()
4144 .map(|(i, row)| {
4145 let mut vals = row.values.clone();
4146 vals.push(Value::BigInt(i as i64 + 1));
4147 Row::new(vals)
4148 })
4149 .collect()
4150 } else {
4151 rows
4152 };
4153 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4154 // `EvalContext::new` drops it and every catalog-dependent cast
4155 // (regclass / enum / composite / domain) silently degrades.
4156 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4157 // WHERE.
4158 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4159 let mut out = alloc::vec::Vec::with_capacity(rows.len());
4160 for row in rows {
4161 cancel.check()?;
4162 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4163 if matches!(v, Value::Bool(true)) {
4164 out.push(row);
4165 }
4166 }
4167 out
4168 } else {
4169 rows
4170 };
4171 // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4172 // returning sources. When the SELECT projection contains
4173 // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4174 // …) we route the filtered row stream through the same
4175 // aggregate executor the relational scan path uses, so
4176 // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4177 // a single 100 row instead of erroring at projection
4178 // time. GROUP BY / HAVING / ORDER BY over the aggregate
4179 // output all ride through `aggregate::run`.
4180 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
4181 // v7.29 — a per-query memo so correlated scalar
4182 // subqueries batch-evaluate once (group map) instead of
4183 // executing per group.
4184 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4185 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4186 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4187 .map_err(|err| match err {
4188 EngineError::Eval(ev) => ev,
4189 other => eval::EvalError::TypeMismatch {
4190 detail: alloc::format!("{other}"),
4191 },
4192 })
4193 };
4194 // v7.39 (round 656) — hand the rows over as they are rather than
4195 // collecting a second vector of `RowRef` wrappers. Note this is
4196 // a set-returning-function path, NOT the relational scan: the
4197 // measured O(rows) cost lived in `run_single_table_aggregate`,
4198 // and converting these four first was a miss that cost a full
4199 // round — every test stayed green and the number did not move.
4200 let agg = aggregate::run(
4201 stmt,
4202 crate::join::AggRows::Owned(&filtered),
4203 &schema_cols,
4204 Some(&alias),
4205 Some(&agg_correlated),
4206 self.parallel_runner.0.as_deref(),
4207 Some(self.active_catalog()),
4208 Some(self),
4209 )?;
4210 return self.finish_agg_result(agg, stmt, cancel);
4211 }
4212 // Projection.
4213 let projection = build_projection(
4214 &stmt.items,
4215 &schema_cols,
4216 &alias,
4217 self.speaks_mysql,
4218 Some(self.active_catalog()),
4219 )?;
4220 // v7.39 (round 621) — and here, for the same reason.
4221 let srf_idxs = self.srf_target_idxs(&projection);
4222 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4223 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4224 alloc::vec::Vec::with_capacity(filtered.len());
4225 let mut proj_memo = memoize::MemoizeCache::default();
4226 if !srf_idxs.is_empty() {
4227 let (rows, src) =
4228 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4229 projected_rows = rows;
4230 src_of_row = src;
4231 } else {
4232 for row in &filtered {
4233 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4234 for p in &projection {
4235 // v7.24 (round-16 B) — correlated-aware.
4236 vals.push(self.eval_expr_with_correlated(
4237 &p.expr,
4238 row,
4239 &scan_ctx,
4240 cancel,
4241 Some(&mut proj_memo),
4242 )?);
4243 }
4244 projected_rows.push(Row::new(vals));
4245 }
4246 }
4247 let columns: alloc::vec::Vec<ColumnSchema> = projection
4248 .iter()
4249 // v7.39 (read01 round 54) — keep the column's enum identity through
4250 // the projection (it lives outside the DataType lattice), or a
4251 // derived table / UNION / windowed result forgets it and any outer
4252 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4253 .map(|p| p.to_column_schema())
4254 .collect();
4255 // ORDER BY against the source schema.
4256 // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4257 // more of them than there were inputs), and a positional key means the
4258 // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4259 // and what the other two synthetic-source tails already did.
4260 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4261 if !order_by.is_empty() {
4262 let out_cols = if srf_idxs.is_empty() {
4263 alloc::vec![None; order_by.len()]
4264 } else {
4265 srf_order_output_cols(&order_by, &projection)
4266 };
4267 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4268 .iter()
4269 .enumerate()
4270 .map(|(k, out)| -> Result<_, EngineError> {
4271 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4272 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4273 .iter()
4274 .zip(out_cols.iter())
4275 .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4276 .collect();
4277 Ok((k, keys?))
4278 })
4279 .collect::<Result<_, _>>()?;
4280 indexed.sort_by(|a, b| {
4281 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4282 let o = &stmt.order_by[idx];
4283 let cmp = order_by_value_cmp_in(
4284 o.desc,
4285 o.nulls_first,
4286 ka,
4287 kb,
4288 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4289 );
4290 if cmp != core::cmp::Ordering::Equal {
4291 return cmp;
4292 }
4293 }
4294 core::cmp::Ordering::Equal
4295 });
4296 projected_rows = indexed
4297 .into_iter()
4298 .map(|(i, _)| projected_rows[i].clone())
4299 .collect();
4300 }
4301 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4302 if stmt.distinct {
4303 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4304 // spec folds EVERY text position, so a column declared
4305 // `COLLATE utf8mb4_bin` had its values merged here exactly the
4306 // way 3b494b6e fixed on the main scan path. The projection is
4307 // already in scope at each of these sites, so the mask needs no
4308 // new plumbing -- it was simply never asked for.
4309 projected_rows = dedup_rows(
4310 projected_rows,
4311 FoldSpec::of_masks(
4312 scan_ctx.mysql_dialect,
4313 &fold_mask(&projection),
4314 &pad_mask(&projection),
4315 ),
4316 );
4317 }
4318 if let Some(offset) = stmt.offset_literal() {
4319 let off = (offset as usize).min(projected_rows.len());
4320 projected_rows.drain(..off);
4321 }
4322 if let Some(limit) = stmt.limit_literal() {
4323 projected_rows.truncate(limit as usize);
4324 }
4325 Ok(QueryResult::Rows {
4326 columns,
4327 rows: projected_rows,
4328 })
4329 }
4330
4331 /// The FROM shapes that are not an ordinary table scan — joins, the
4332 /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4333 ///
4334 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4335 /// reason round 848 established in the parser: a debug build gives
4336 /// EVERY branch's locals a slot in the frame, whichever branch runs.
4337 /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4338 /// stacks several of them; a plain scan reaches none of these
4339 /// branches. Moving them out took the frame to 52,336.
4340 ///
4341 /// `Ok(None)` means "not one of these shapes, carry on".
4342 #[inline(never)]
4343 fn try_from_shape_paths(
4344 &self,
4345 stmt: &SelectStatement,
4346 from: &spg_sql::ast::FromClause,
4347 cancel: CancelToken<'_>,
4348 ) -> Result<Option<QueryResult>, EngineError> {
4349 if !from.joins.is_empty() {
4350 // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4351 // elimination: when a LEFT JOIN's right side is referenced
4352 // ONLY in the ON equality and the right-side join key is
4353 // UNIQUE/PK, the join preserves outer cardinality exactly
4354 // and contributes no values used downstream. Drop the
4355 // entire join. PG does this on the
4356 // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4357 // — A's row count is what survives, B never has to be
4358 // touched.
4359 if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4360 return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4361 }
4362 // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4363 // the v7.32 joinfold rewrite that turns inner JOINs into a
4364 // single-table scan when the catalogue can prove key-only
4365 // dependency. Tests use this to assert "without joinfold,
4366 // the join still executes correctly" (joinfold is a
4367 // semantically-equivalent rewrite, not a correctness fix).
4368 if !self.env_cfg().disable_joinfold {
4369 if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4370 return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4371 }
4372 }
4373 return self.exec_joined_select(stmt, from, cancel).map(Some);
4374 }
4375 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4376 // single-column table at SELECT entry by evaluating the
4377 // expression once against the empty row (UNNEST is
4378 // uncorrelated in v7.11; correlated / LATERAL unnest is a
4379 // v7.12 carve-out). Build a virtual `Table` in a heap-only
4380 // catalog, then route to the regular scan path.
4381 if from.primary.unnest_expr.is_some() {
4382 return self
4383 .exec_select_unnest(stmt, &from.primary, cancel)
4384 .map(Some);
4385 }
4386 // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4387 // returning function. Same dispatch shape as unnest but
4388 // emits a two-column (key TEXT, value TEXT) row stream.
4389 if from.primary.jsonb_each_text_arg.is_some() {
4390 return self
4391 .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4392 .map(Some);
4393 }
4394 // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4395 // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4396 // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4397 // array form. Each function runs; the results zip in LOCKSTEP with the
4398 // shorter padded to NULL — the SAME rule the target-list SRFs follow
4399 // (round 67), which is why `srf_values` is what evaluates each entry.
4400 if from.primary.rows_from.is_some() {
4401 let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4402 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4403 if let Some(col) = schema_cols.get_mut(i) {
4404 col.name = new_name.clone();
4405 }
4406 }
4407 let alias = from
4408 .primary
4409 .alias
4410 .clone()
4411 .unwrap_or_else(|| from.primary.name.clone());
4412 return self
4413 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4414 .map(Some);
4415 }
4416 // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4417 // COLUMNS (...))`. Materialise the row stream + schema by
4418 // walking the row path, then run the regular pipeline over it.
4419 if let Some(jt) = &from.primary.json_table {
4420 let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4421 let alias = from
4422 .primary
4423 .alias
4424 .clone()
4425 .unwrap_or_else(|| from.primary.name.clone());
4426 return self
4427 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4428 .map(Some);
4429 }
4430 if from.primary.table_fn_call.is_some() {
4431 let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4432 // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4433 // (from 1, in output order) AFTER the function's own columns. The
4434 // alias list names it like any other, which is why it is appended
4435 // BEFORE the renaming pass below.
4436 let rows = if from.primary.with_ordinality {
4437 schema_cols.push(ColumnSchema::new(
4438 "ordinality".to_string(),
4439 DataType::BigInt,
4440 false,
4441 ));
4442 rows.into_iter()
4443 .enumerate()
4444 .map(|(i, r)| {
4445 let mut vals = r.values;
4446 vals.push(Value::BigInt(i as i64 + 1));
4447 Row::new(vals)
4448 })
4449 .collect()
4450 } else {
4451 rows
4452 };
4453 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4454 if let Some(col) = schema_cols.get_mut(i) {
4455 col.name = new_name.clone();
4456 }
4457 }
4458 let alias = from
4459 .primary
4460 .alias
4461 .clone()
4462 .unwrap_or_else(|| from.primary.name.clone());
4463 return self
4464 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4465 .map(Some);
4466 }
4467 // v7.37.17 (17.6 siblings) — plain derived table in primary
4468 // position: `FROM ( SELECT … ) alias` (no joins). The inner
4469 // SELECT materialises once (it is uncorrelated by
4470 // construction), then the outer projection / WHERE /
4471 // aggregate / ORDER BY pipeline runs over the synthetic
4472 // table. Joined derived tables keep riding the LATERAL
4473 // machinery in join.rs.
4474 if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4475 // v7.39 (round 727) — flatten first. A simple derived table
4476 // (bare-column projection over one stored table, nothing that
4477 // changes cardinality or order) used to force the inner
4478 // SELECT through the SERIAL row-at-a-time projection pipeline
4479 // just to materialise a synthetic table the outer query then
4480 // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4481 // measured 18.6 ms against PG's 5 — and bare count over the
4482 // same filter WITHOUT the wrapper is 2 ms here, because it
4483 // rides the fused parallel lane. Rewriting to the unwrapped
4484 // form is PG's subquery pull-up; the whole tree gets the
4485 // fast lanes back.
4486 if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4487 return self.exec_select_cancel(&flat, cancel).map(Some);
4488 }
4489 // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4490 // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4491 // ORDER BY never changes the row count, and OFFSET drops
4492 // exactly k. The materialising path sorted 500k rows to
4493 // count 10k (57 ms); PG runs its parallel sort anyway
4494 // (28 ms). The rewrite skips the sort entirely on both
4495 // counts — a plan PG itself does not have.
4496 if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4497 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4498 }
4499 // v7.39 (round 743) — `count(*) OVER a derived whose only
4500 // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4501 // a constant-length array unnests to exactly k rows per
4502 // input row, NULL elements included. PG expands the set to
4503 // count it (6.6 ms on the panel cell); the identity doesn't.
4504 if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4505 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4506 }
4507 return self
4508 .exec_select_derived(stmt, &from.primary, cancel)
4509 .map(Some);
4510 }
4511 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4512 // [, step])` set-returning source. Dispatch mirrors UNNEST:
4513 // materialise the row stream from a single eval pass, then
4514 // run the regular projection / WHERE / ORDER BY / LIMIT
4515 // pipeline over the synthetic single-column table.
4516 if from.primary.generate_series_args.is_some() {
4517 return self
4518 .exec_select_generate_series(stmt, &from.primary, cancel)
4519 .map(Some);
4520 }
4521 Ok(None)
4522 }
4523
4524 /// Pick an index seek for this WHERE, if any of the four apply:
4525 /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4526 ///
4527 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4528 /// frame reason on `try_from_shape_paths`: in a debug build a
4529 /// closure's locals belong to the enclosing frame, and this one is
4530 /// four seek attempts wide on a function that nests.
4531 #[inline(never)]
4532 fn pick_indexed_rows<'r>(
4533 &'r self,
4534 stmt: &SelectStatement,
4535 table: &'r spg_storage::Table,
4536 schema_cols: &[spg_storage::ColumnSchema],
4537 alias: &str,
4538 ctx: &crate::eval::EvalContext<'_>,
4539 seek_snapshot: &crate::Snapshot,
4540 ) -> Option<crate::index_access::Seeked<'r>> {
4541 stmt.where_.as_ref().and_then(|w| {
4542 // BTree / col=literal seek first — covers the v7.11.3 multi-
4543 // column AND case and the leading-column equality lookup.
4544 try_index_seek(
4545 w,
4546 schema_cols,
4547 self.active_catalog(),
4548 table,
4549 alias,
4550 seek_snapshot,
4551 ctx.mysql_dialect,
4552 )
4553 .or_else(|| {
4554 // v7.12.3 — GIN-accelerated `WHERE col @@
4555 // tsquery` when the column has a `USING gin`
4556 // index. Returns an over-approximate candidate
4557 // set; the WHERE re-eval loop below verifies
4558 // the full `@@` predicate per row.
4559 try_gin_seek(
4560 w,
4561 schema_cols,
4562 self.active_catalog(),
4563 table,
4564 alias,
4565 ctx,
4566 seek_snapshot,
4567 )
4568 .map(crate::index_access::Seeked::over_approximate)
4569 })
4570 .or_else(|| {
4571 // v7.15.0 — trigram-GIN-accelerated
4572 // `WHERE col LIKE / ILIKE '<pat>'` when the
4573 // column has a `gin_trgm_ops` GIN index.
4574 // Over-approximate candidate set; the WHERE
4575 // re-eval verifies the LIKE per row.
4576 try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4577 .map(crate::index_access::Seeked::over_approximate)
4578 })
4579 .or_else(|| {
4580 // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4581 // accelerated `WHERE col @> <jsonb_literal>`
4582 // when the column has a `USING gin` index. The
4583 // posting-list intersection returns an over-
4584 // approximate candidate set; the WHERE re-eval
4585 // verifies the full `@>` predicate per row.
4586 try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4587 .map(crate::index_access::Seeked::over_approximate)
4588 })
4589 })
4590 }
4591
4592 /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4593 /// the two `count(*)` short-circuits. Out-of-line for the frame
4594 /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4595 /// of them, and in a debug build their locals sit in the frame
4596 /// regardless.
4597 #[inline(never)]
4598 fn try_seek_fast_paths(
4599 &self,
4600 stmt: &SelectStatement,
4601 table: &spg_storage::Table,
4602 schema_cols: &[spg_storage::ColumnSchema],
4603 alias: &str,
4604 seek_snapshot: &crate::Snapshot,
4605 cancel: CancelToken<'_>,
4606 ) -> Result<Option<QueryResult>, EngineError> {
4607 if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4608 // NSW kNN dispatches against the hot-tier vector index only
4609 // (vector cells aren't promoted to cold segments), so wrap
4610 // the returned row indices as `Cow::Borrowed` for the
4611 // unified `materialise_in_order` shape.
4612 let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4613 .into_iter()
4614 .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4615 .collect();
4616 return materialise_in_order(stmt, schema_cols, alias, &ordered, self.speaks_mysql)
4617 .map(Some);
4618 }
4619
4620 // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4621 // the scan via the BTree iterator in the requested direction
4622 // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4623 // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4624 // the load-bearing consumer; this skips the materialise-every-
4625 // row + partial-sort tail entirely. Walker output is already
4626 // in ORDER BY order so `materialise_in_order` (no extra sort)
4627 // is the natural sink.
4628 if let Some(walked) = try_pk_walk_top_n(
4629 stmt,
4630 self.active_catalog(),
4631 table,
4632 schema_cols,
4633 alias,
4634 self,
4635 cancel,
4636 self.speaks_mysql,
4637 ) {
4638 return materialise_in_order(stmt, schema_cols, alias, &walked, self.speaks_mysql)
4639 .map(Some);
4640 }
4641
4642 // Index seek: if WHERE is `col = literal` (or commuted) and the
4643 // referenced column has an index, dispatch each locator through
4644 // the catalog (hot tier → borrow, cold tier → page-read +
4645 // decode) and iterate just those rows. Otherwise fall back to a
4646 // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4647 // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4648 // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4649 // we don't pay the row materialisation cost twice. Returns
4650 // a bare `Rows{count}` if the shape matches.
4651 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
4652 && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4653 {
4654 return Ok(Some(out));
4655 }
4656 // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4657 // locators directly, skipping row materialisation + WHERE re-eval.
4658 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
4659 && let Some(out) = self.try_count_star_indexed_range_fast(
4660 stmt,
4661 table,
4662 schema_cols,
4663 alias,
4664 seek_snapshot,
4665 )
4666 {
4667 return Ok(Some(out));
4668 }
4669 Ok(None)
4670 }
4671
4672 /// The two rewrites that must happen before the FROM clause is even
4673 /// looked at: a meta-view reference needs the catalog views
4674 /// materialised, and a windowed projection belongs to the window
4675 /// executor. Out-of-line for the frame reason on
4676 /// `try_from_shape_paths`.
4677 #[inline(never)]
4678 fn try_pre_from_paths(
4679 &self,
4680 stmt: &SelectStatement,
4681 cancel: CancelToken<'_>,
4682 ) -> Result<Option<QueryResult>, EngineError> {
4683 if !self.meta_views_materialised && select_references_meta_view(stmt) {
4684 return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4685 }
4686 // v4.12: window-function path. When the projection contains
4687 // any `name(args) OVER (...)` we route to the dedicated
4688 // executor — partition + sort + per-row window value before
4689 // the regular projection.
4690 if select_has_window(stmt) {
4691 // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4692 // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4693 // needs the aggregation done first, then windows over the grouped
4694 // rows. Rewrite to an aggregate derived subquery + outer window query
4695 // (which the window-over-derived path, D.13, executes). Only fires on
4696 // the currently-erroring agg+window+GROUP BY shape, so it can't
4697 // regress working window-only or aggregate-only queries.
4698 if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4699 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4700 }
4701 return self.exec_select_with_window(stmt, cancel).map(Some);
4702 }
4703 Ok(None)
4704 }
4705
4706 /// A projection naming `ctid` or another system column: the schema
4707 /// has to be widened with them before the scan. Out-of-line for the
4708 /// frame reason on `try_from_shape_paths`.
4709 #[inline(never)]
4710 fn try_ctid_projection(
4711 &self,
4712 stmt: &SelectStatement,
4713 primary: &spg_sql::ast::TableRef,
4714 table: &spg_storage::Table,
4715 schema_cols: &[spg_storage::ColumnSchema],
4716 alias: &str,
4717 cancel: CancelToken<'_>,
4718 ) -> Result<Option<QueryResult>, EngineError> {
4719 if references_ctid(stmt) {
4720 let snapshot = self.current_snapshot();
4721 let mut ext_cols = schema_cols.to_vec();
4722 for name in SYSTEM_COLUMNS {
4723 ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4724 }
4725 let table_oid =
4726 crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4727 .unwrap_or(0);
4728 let headers = table.headers();
4729 let rows: Vec<Row<'static>> = table
4730 .scan_visible(&snapshot)
4731 .map(|(i, r)| {
4732 let mut vals = r.values.clone();
4733 // One block, offsets from 1, as PG numbers them.
4734 vals.push(Value::Tid(0, i as u32 + 1));
4735 let h = headers.get(i);
4736 vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4737 vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4738 // SPG keeps no per-statement command ids; PG shows 0 for
4739 // every row a reader can see, which is every row here.
4740 vals.push(Value::Cid(0));
4741 vals.push(Value::Cid(0));
4742 vals.push(Value::BigInt(table_oid));
4743 Row::new(vals)
4744 })
4745 .collect();
4746 return self
4747 .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4748 .map(Some);
4749 }
4750 Ok(None)
4751 }
4752
4753 /// A sequence read as a one-row relation (`SELECT last_value FROM
4754 /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4755 /// the frame reason on `try_from_shape_paths`.
4756 #[inline(never)]
4757 fn try_sequence_relation(
4758 &self,
4759 stmt: &SelectStatement,
4760 primary: &spg_sql::ast::TableRef,
4761 cancel: CancelToken<'_>,
4762 ) -> Result<Option<QueryResult>, EngineError> {
4763 if self.active_catalog().get(&primary.name).is_none()
4764 && let Some(seq) = self.active_catalog().sequence(&primary.name)
4765 {
4766 let rows = alloc::vec![Row::new(alloc::vec![
4767 Value::BigInt(seq.last_value),
4768 Value::BigInt(0),
4769 Value::Bool(seq.is_called),
4770 ])];
4771 let schema_cols = alloc::vec![
4772 ColumnSchema::new("last_value", DataType::BigInt, false),
4773 ColumnSchema::new("log_cnt", DataType::BigInt, false),
4774 ColumnSchema::new("is_called", DataType::Bool, false),
4775 ];
4776 let alias = primary
4777 .alias
4778 .clone()
4779 .unwrap_or_else(|| primary.name.clone());
4780 return self
4781 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4782 .map(Some);
4783 }
4784 Ok(None)
4785 }
4786
4787 pub(crate) fn exec_bare_select_cancel(
4788 &self,
4789 stmt: &SelectStatement,
4790 cancel: CancelToken<'_>,
4791 ) -> Result<QueryResult, EngineError> {
4792 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4793 // is meaningless without an ORDER BY; PG raises a hard
4794 // error and SPG mirrors the surface so the same DDL/app
4795 // path behaves identically on cutover.
4796 check_with_ties_requires_order_by(stmt)?;
4797 // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4798 // PG rejects window calls there outright. Checked here rather than
4799 // on the window path: `HAVING row_number() OVER () = 1` has no
4800 // window in its projection at all.
4801 crate::window::reject_window_in_row_clauses(stmt)?;
4802 // v7.39 (round 232) — the ORDER BY legality rules (positional
4803 // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4804 // check: before anything scans.
4805 crate::orderby::check_order_by_legality(stmt)?;
4806 // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4807 // equivalent statement the regular executor handles (merged join
4808 // columns collapse to a single unqualified output column; NATURAL
4809 // gets its common-column ON synthesised). The rewrite clears the
4810 // flags, so this re-entrant call is a no-op on the second pass.
4811 if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4812 return self.exec_bare_select_cancel(&rewritten, cancel);
4813 }
4814 // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4815 // exactly the group keys, IS a DISTINCT and was paying for the
4816 // aggregate executor to find that out. Same placement and shape
4817 // as the desugar above; the rewrite clears `group_by`, so the
4818 // re-entry is a no-op on the second pass. See `baregroup` for
4819 // what the gate rules out.
4820 if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4821 return self.exec_bare_select_cancel(&rewritten, cancel);
4822 }
4823 // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4824 // operand in a security-barrier subquery, then re-enter (the wrapped
4825 // operands are no longer bare RLS tables, so this is a no-op on the
4826 // second pass).
4827 if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4828 return self.exec_bare_select_cancel(&rewritten, cancel);
4829 }
4830 // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4831 // AND the RLS USING predicate into a single-table SELECT's WHERE.
4832 // Superuser sessions and non-RLS tables get `None` (no clone, no
4833 // change). Applied inline (shadowing `stmt`) rather than via re-entry
4834 // so it can't re-inject on a recursive pass.
4835 let rls_stmt;
4836 let stmt = match self.rls_select_predicate(stmt)? {
4837 Some(pred) => {
4838 let mut s = stmt.clone();
4839 s.where_ = Some(match s.where_.take() {
4840 Some(existing) => spg_sql::ast::Expr::Binary {
4841 lhs: alloc::boxed::Box::new(existing),
4842 op: spg_sql::ast::BinOp::And,
4843 rhs: alloc::boxed::Box::new(pred),
4844 },
4845 None => pred,
4846 });
4847 rls_stmt = s;
4848 &rls_stmt
4849 }
4850 None => stmt,
4851 };
4852 // v7.16.2 — same meta-view dispatch as
4853 // `exec_select_cancel`, applied here too because
4854 // `subquery_replacement` enters this function directly
4855 // for Exists / ScalarSubquery / InSubquery resolution
4856 // (bypassing the top-level entry to avoid double
4857 // subquery walking). Without this dispatch the subquery
4858 // hits `__spg_info_columns` and reports TableNotFound.
4859 if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4860 return Ok(done);
4861 }
4862 // Constant SELECT (no FROM) — evaluate each item once against an
4863 // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4864 // `SELECT '7'::INT`. Column references will surface as
4865 // ColumnNotFound on eval since the schema is empty.
4866 let Some(from) = &stmt.from else {
4867 return self.exec_constant_select(stmt);
4868 };
4869 // Multi-table FROM (one or more joined peers) goes through the
4870 // nested-loop join executor. Single-table FROM stays on the
4871 // existing scan + index-seek path.
4872 if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4873 return Ok(done);
4874 }
4875 // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4876 // tested — eight ORDER BY shapes byte-identical spilled against
4877 // in-memory, with 103 runs opened to prove the spill ran — and it
4878 // loses on wall clock, which is a hard stop whatever the memory
4879 // buys. Measured round 865, same psql client both sides, same
4880 // machine, row counts verified, and both sides confirmed to be
4881 // doing an external merge rather than an indexed walk:
4882 //
4883 // PG18 178.7 - 187.0 ms Sort Method: external merge, 85 MB
4884 // SPG spilled 269.7 - 299.6 ms 33 spill files at peak
4885 //
4886 // Non-overlapping, about 1.55x. Re-enable by restoring the call
4887 // below once that closes; nothing else has to change, which is
4888 // the point of it being a separate path.
4889 //
4890 // if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4891 // return Ok(done);
4892 // }
4893 //
4894 // v7.37 (round 882) — this walk stays unhooked, but its streaming
4895 // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4896 // bail in `try_exec_joined_streaming`. Collecting the answer was
4897 // most of what this one cost: handing rows over as the merge
4898 // produces them holds peak to the budget plus one row, and the
4899 // wall clock lands inside PG18's range rather than 1.55x outside
4900 // it. Numbers in `extsort.rs`'s header.
4901 let primary = &from.primary;
4902 // v7.39 (round 244) — a sequence is selectable as a one-row relation
4903 // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4904 // read it). Synthesize PG's three columns.
4905 if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4906 return Ok(done);
4907 }
4908 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4909 StorageError::TableNotFound {
4910 name: primary.name.clone(),
4911 }
4912 })?;
4913 let schema_cols = &table.schema().columns;
4914 // The qualifier accepted on column refs is the alias (if any) else the
4915 // bare table name.
4916 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4917 // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4918 // system columns at all: `SELECT ctid FROM t` answered "column
4919 // \"ctid\" does not exist", which takes out the dedup idiom every
4920 // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4921 // GROUP BY key)`.
4922 //
4923 // The value comes from the row's position, which the scan already
4924 // yields; the column is appended to the schema and the rows only
4925 // when the statement asks for it, so nothing else pays for it. That
4926 // also routes the query down the general path, past the index fast
4927 // paths below — they hand back rows without positions, and a ctid
4928 // that was sometimes right would be worse than none.
4929 if let Some(done) =
4930 self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4931 {
4932 return Ok(done);
4933 }
4934 let ctx = self.ev_ctx(schema_cols, Some(alias));
4935
4936 // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4937 // WHERE and an NSW index on `col` skips the full scan. The
4938 // walk returns rows already in ascending-distance order, so
4939 // ORDER BY / LIMIT are honoured implicitly.
4940 // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4941 // and thread it into every index-seek fast path below. No-op
4942 // today (every hot header is committed-alive).
4943 let seek_snapshot = self.current_snapshot();
4944 if let Some(done) =
4945 self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4946 {
4947 return Ok(done);
4948 }
4949 // full scan over the hot tier (cold-tier rows are only reached
4950 // via index seek in v5.1 — full table scans against cold-tier
4951 // data ship in v5.2 with the freezer's per-segment scan API).
4952 let indexed_rows =
4953 self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4954
4955 // Aggregate path: filter rows first, then hand off to the
4956 // aggregate executor which does its own projection + ORDER BY.
4957 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
4958 return self.run_single_table_aggregate(
4959 stmt,
4960 table,
4961 schema_cols,
4962 alias,
4963 indexed_rows,
4964 cancel,
4965 );
4966 }
4967 self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4968 }
4969
4970 /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4971 /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4972 /// uncorrelated FROM-primary case is the simpler shape, used by
4973 /// e2e pins. Materialises the (key, value) pair stream into a
4974 /// synthetic two-column TEXT table, then routes through the
4975 /// regular projection / WHERE / ORDER BY pipeline.
4976 /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4977 /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4978 /// item into (rows, schema). `outer_doc` is `Some` only when this
4979 /// is a NESTED level being expanded against a parent row item's
4980 /// already-parsed sub-document; the top-level call parses the doc
4981 /// expr itself. Row/column paths reuse the existing jsonpath
4982 /// evaluator (`json::json_table_path`); coercion reuses
4983 /// `coerce_value` on the JSON scalar text, so a json string
4984 /// coerces to DATE by its content, matching PG.
4985 #[allow(clippy::type_complexity)]
4986 pub(crate) fn json_table_rows(
4987 &self,
4988 jt: &spg_sql::ast::JsonTable,
4989 outer_doc: Option<&crate::json::JsonValue>,
4990 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4991 // Column schema is static (independent of data): flatten the
4992 // COLUMNS tree in declaration order (NESTED contributes its
4993 // children inline, the PG output shape).
4994 let schema = json_table_schema(&jt.columns);
4995
4996 // PASSING variables → a single JsonValue object the jsonpath
4997 // engine reads `$name` from.
4998 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4999 let ctx = EvalContext::new(&empty_schema, None);
5000 let dummy = Row::new(alloc::vec::Vec::new());
5001 let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
5002 None
5003 } else {
5004 let mut entries = alloc::vec::Vec::new();
5005 for (name, e) in &jt.passing {
5006 let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
5007 entries.push((name.clone(), value_to_json_value(&v)));
5008 }
5009 Some(crate::json::JsonValue::Object(entries))
5010 };
5011
5012 // The document root: a NESTED level gets it from the parent;
5013 // the top level parses its doc expr.
5014 let root_owned;
5015 let root: &crate::json::JsonValue = match outer_doc {
5016 Some(d) => d,
5017 None => {
5018 let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
5019 let src = match &doc_val {
5020 Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
5021 Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
5022 other => {
5023 return Err(EngineError::Unsupported(alloc::format!(
5024 "JSON_TABLE document must be json/text, got {}",
5025 crate::conversions::pg_type_name_for_error_opt(other.data_type())
5026 )));
5027 }
5028 };
5029 root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
5030 &root_owned
5031 }
5032 };
5033
5034 let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
5035 .map_err(EngineError::Eval)?;
5036 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5037 for (idx, item) in items.iter().enumerate() {
5038 self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
5039 }
5040 Ok((rows, schema))
5041 }
5042
5043 /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
5044 /// Regular columns produce one value each; a NESTED column expands
5045 /// as an outer join (each nested match → one row sharing the
5046 /// parent cells; no nested match → one row with the nested cells
5047 /// NULL). Sibling NESTED at one level cross by concatenation of
5048 /// their independent expansions (PG's UNION-of-outer shape).
5049 fn json_table_emit_item(
5050 &self,
5051 jt: &spg_sql::ast::JsonTable,
5052 item: &crate::json::JsonValue,
5053 ordinality: usize,
5054 vars: Option<&crate::json::JsonValue>,
5055 out: &mut alloc::vec::Vec<Row<'static>>,
5056 ) -> Result<(), EngineError> {
5057 use spg_sql::ast::JsonTableColumn as C;
5058 // Parent cells (regular + ordinality), left-to-right; NESTED
5059 // columns contribute a run of child cells appended after.
5060 let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5061 let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
5062 alloc::vec::Vec::new();
5063 let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5064 for col in &jt.columns {
5065 match col {
5066 C::Ordinality { .. } => {
5067 parent_cells.push(Value::BigInt(ordinality as i64 + 1));
5068 }
5069 C::Regular { .. } => {
5070 parent_cells.push(self.json_table_column_value(col, item, vars)?);
5071 }
5072 C::Nested { path, columns } => {
5073 // Recurse: a nested JSON_TABLE over `item` filtered
5074 // by `path`, with the same PASSING vars.
5075 let sub = spg_sql::ast::JsonTable {
5076 doc: jt.doc.clone(), // unused (outer_doc provided)
5077 row_path: path.clone(),
5078 columns: columns.clone(),
5079 passing: alloc::vec::Vec::new(),
5080 };
5081 let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
5082 nested_widths.push(nschema.len());
5083 nested_runs.push(nrows);
5084 }
5085 }
5086 }
5087 if nested_runs.is_empty() {
5088 out.push(Row::new(parent_cells));
5089 return Ok(());
5090 }
5091 // PG sibling-NESTED semantics: each sibling expands
5092 // INDEPENDENTLY and the results CONCATENATE — a row from
5093 // sibling s fills only s's cells, every other sibling's cells
5094 // NULL. An empty sibling contributes ZERO rows (not a NULL
5095 // row). Only when EVERY sibling is empty does the parent still
5096 // emit one all-NULL row (the outer-join guarantee that a parent
5097 // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
5098 // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
5099 let before = out.len();
5100 for (s_idx, run) in nested_runs.iter().enumerate() {
5101 for nrow in run {
5102 let mut cells = parent_cells.clone();
5103 for (o_idx, w) in nested_widths.iter().enumerate() {
5104 if o_idx == s_idx {
5105 cells.extend(nrow.values.iter().cloned());
5106 } else {
5107 for _ in 0..*w {
5108 cells.push(Value::Null);
5109 }
5110 }
5111 }
5112 out.push(Row::new(cells));
5113 }
5114 }
5115 if out.len() == before {
5116 // Every sibling empty → one all-NULL nested row.
5117 let mut cells = parent_cells.clone();
5118 for w in &nested_widths {
5119 for _ in 0..*w {
5120 cells.push(Value::Null);
5121 }
5122 }
5123 out.push(Row::new(cells));
5124 }
5125 Ok(())
5126 }
5127
5128 /// v7.39 (round 205) — evaluate one Regular column against a row
5129 /// item: EXISTS → bool; else path → at most one value, coerced to
5130 /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
5131 fn json_table_column_value(
5132 &self,
5133 col: &spg_sql::ast::JsonTableColumn,
5134 item: &crate::json::JsonValue,
5135 vars: Option<&crate::json::JsonValue>,
5136 ) -> Result<Value<'static>, EngineError> {
5137 use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
5138 let C::Regular {
5139 name,
5140 ty,
5141 path,
5142 exists,
5143 format_json,
5144 wrapper,
5145 on_empty,
5146 on_error,
5147 } = col
5148 else {
5149 unreachable!("caller guards Regular");
5150 };
5151 let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5152 if *exists {
5153 return Ok(Value::Bool(!matches.is_empty()));
5154 }
5155 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5156 let ctx = EvalContext::new(&empty_schema, None);
5157 let dummy = Row::new(alloc::vec::Vec::new());
5158 let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5159 match b {
5160 B::Null => Ok(Some(Value::Null)),
5161 B::Error => Ok(None),
5162 B::Default(e) => Ok(Some(
5163 eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5164 )),
5165 }
5166 };
5167 // Empty match set → ON EMPTY.
5168 if matches.is_empty() {
5169 return match default_of(on_empty)? {
5170 Some(v) => coerce_json_table_default(v, *ty, name),
5171 None => Err(EngineError::Unsupported(alloc::format!(
5172 "no SQL/JSON item found for JSON_TABLE column {name:?}"
5173 ))),
5174 };
5175 }
5176 let first = &matches[0];
5177 // FORMAT JSON: return the PG-canonical json representation.
5178 // WITH WRAPPER wraps the whole match SET in an array (even a
5179 // single scalar → `[5]`); without it, the single match's json.
5180 if *format_json {
5181 let text = if *wrapper {
5182 crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5183 } else {
5184 first.canonical_json_text()
5185 };
5186 return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5187 }
5188 if first.is_json_null() {
5189 return Ok(Value::Null);
5190 }
5191 // Coerce the scalar text to the declared type; on failure → ON
5192 // ERROR (default NULL, DEFAULT expr, or raise).
5193 let dt = crate::conversions::column_type_to_data_type(*ty);
5194 let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5195 match crate::conversions::coerce_value(scalar, dt, name, 0) {
5196 Ok(v) => Ok(v),
5197 Err(e) => match default_of(on_error)? {
5198 Some(v) => coerce_json_table_default(v, *ty, name),
5199 None => Err(e),
5200 },
5201 }
5202 }
5203
5204 /// table function into (rows, default schema). Dispatch by name.
5205 pub(crate) fn table_fn_rows(
5206 &self,
5207 primary: &TableRef,
5208 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5209 let (fn_name, args) = primary
5210 .table_fn_call
5211 .as_deref()
5212 .expect("caller guards table_fn_call.is_some()");
5213 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5214 let ctx = EvalContext::new(&empty_schema, None);
5215 let dummy_row = Row::new(alloc::vec::Vec::new());
5216 let arg0: Option<Value<'static>> = match args.first() {
5217 Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5218 None => None,
5219 };
5220 match fn_name.as_str() {
5221 // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5222 // `…_recordset` (+ json_ variants). The row shape is the BASE
5223 // argument's declared type — a table's or a composite type's
5224 // column list — which only the catalog knows, so the parser hands
5225 // the raw arguments here rather than desugaring blind.
5226 "jsonb_populate_record"
5227 | "json_populate_record"
5228 | "jsonb_populate_recordset"
5229 | "json_populate_recordset" => {
5230 let type_name = match args.first() {
5231 Some(Expr::Cast {
5232 target: spg_sql::ast::CastTarget::Named(n),
5233 ..
5234 }) => n.clone(),
5235 _ => {
5236 return Err(EngineError::Unsupported(alloc::format!(
5237 "{fn_name}(): first argument must name a row type, \
5238 e.g. NULL::mytable"
5239 )));
5240 }
5241 };
5242 let cat = self.active_catalog();
5243 let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5244 t.schema().columns.clone()
5245 } else if let Some(c) = cat.composite_types().get(&type_name) {
5246 c.fields
5247 .iter()
5248 .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5249 .collect()
5250 } else {
5251 return Err(EngineError::Unsupported(alloc::format!(
5252 "type \"{type_name}\" does not exist"
5253 )));
5254 };
5255 let json_arg = match args.get(1) {
5256 Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5257 None => Value::Null,
5258 };
5259 // The set form iterates the JSON array; the scalar form is
5260 // the one-element case of the same walk.
5261 let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5262 crate::json::array_element_rows(&json_arg, false, fn_name)
5263 .map_err(EngineError::Eval)?
5264 .into_iter()
5265 .map(|s| s.map_or(Value::Null, Value::json))
5266 .collect()
5267 } else if matches!(json_arg, Value::Null) {
5268 alloc::vec::Vec::new()
5269 } else {
5270 alloc::vec![json_arg]
5271 };
5272 let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5273 for doc in &docs {
5274 let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5275 for c in &cols {
5276 // `->>` semantics: a missing key is NULL, present keys
5277 // arrive as text and cast to the declared column type.
5278 let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5279 .map_err(EngineError::Eval)?;
5280 let v = if matches!(raw, Value::Null) {
5281 Value::Null
5282 } else {
5283 crate::conversions::coerce_value(raw, c.ty, "", 0)
5284 .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5285 };
5286 vals.push(v);
5287 }
5288 rows.push(Row::new(vals));
5289 }
5290 Ok((rows, cols))
5291 }
5292 // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5293 // a text[] of 'name=value' reloptions/fdw options → one
5294 // (option_name, option_value) row per element. NULL or an
5295 // empty array yields zero rows (PG); an element without
5296 // '=' carries a NULL option_value, matching PG's split.
5297 "pg_options_to_table" => {
5298 let schema = alloc::vec![
5299 ColumnSchema::new("option_name", DataType::Text, true),
5300 ColumnSchema::new("option_value", DataType::Text, true),
5301 ];
5302 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5303 if let Some(Value::TextArray(items)) = arg0 {
5304 for item in items.into_iter().flatten() {
5305 let (name, value) = match item.split_once('=') {
5306 Some((n, v)) => (Value::text(n), Value::text(v)),
5307 None => (Value::text(item.as_str()), Value::Null),
5308 };
5309 rows.push(Row::new(alloc::vec![name, value]));
5310 }
5311 }
5312 Ok((rows, schema))
5313 }
5314 // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5315 // PG18's per-sequence state SRF, (last_value, is_called).
5316 // pg_dump reads it joined to pg_sequence for every dumped
5317 // sequence's setval line. The oid resolves through the
5318 // same relation_oid mapping seqrelid publishes.
5319 "pg_get_sequence_data" => {
5320 let schema = alloc::vec![
5321 ColumnSchema::new("last_value", DataType::BigInt, false),
5322 ColumnSchema::new("is_called", DataType::Bool, false),
5323 ];
5324 let want = match arg0 {
5325 Some(Value::Int(n)) => i64::from(n),
5326 Some(Value::BigInt(n)) => n,
5327 _ => {
5328 return Err(EngineError::Unsupported(
5329 "pg_get_sequence_data(): argument must be a sequence oid".into(),
5330 ));
5331 }
5332 };
5333 let cat = self.active_catalog();
5334 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5335 for (name, def) in cat.sequences_all() {
5336 if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5337 rows.push(Row::new(alloc::vec![
5338 Value::BigInt(def.last_value),
5339 Value::Bool(def.is_called),
5340 ]));
5341 break;
5342 }
5343 }
5344 Ok((rows, schema))
5345 }
5346 "pg_partition_tree" => {
5347 let cols = alloc::vec![
5348 ColumnSchema::new("relid".to_string(), DataType::Text, true),
5349 ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5350 ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5351 ColumnSchema::new("level".to_string(), DataType::Int, true),
5352 ];
5353 let Some(Value::Text(name)) = &arg0 else {
5354 // NULL (or missing) argument → zero rows (PG).
5355 return Ok((alloc::vec::Vec::new(), cols));
5356 };
5357 let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5358 if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5359 return Err(EngineError::Unsupported(alloc::format!(
5360 "relation \"{name}\" does not exist"
5361 )));
5362 }
5363 let rows = entries
5364 .into_iter()
5365 .map(|(relid, parent, isleaf, level)| {
5366 Row::new(alloc::vec![
5367 Value::text(relid),
5368 parent.map_or(Value::Null, Value::text),
5369 Value::Bool(isleaf),
5370 #[allow(clippy::cast_possible_truncation)]
5371 Value::Int(level as i32),
5372 ])
5373 })
5374 .collect();
5375 Ok((rows, cols))
5376 }
5377 "pg_partition_ancestors" => {
5378 let cols =
5379 alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5380 let Some(Value::Text(name)) = &arg0 else {
5381 return Ok((alloc::vec::Vec::new(), cols));
5382 };
5383 let cat = self.active_catalog();
5384 if cat.get(name.as_ref()).is_none() {
5385 return Err(EngineError::Unsupported(alloc::format!(
5386 "relation \"{name}\" does not exist"
5387 )));
5388 }
5389 // A relation outside any partition tree yields no rows (PG).
5390 let in_tree = cat
5391 .get(name.as_ref())
5392 .is_some_and(|t| t.schema().partition_role.is_some());
5393 let rows = if in_tree {
5394 crate::partition_walks::ancestors_of(cat, name.as_ref())
5395 .into_iter()
5396 .map(|n| Row::new(alloc::vec![Value::text(n)]))
5397 .collect()
5398 } else {
5399 alloc::vec::Vec::new()
5400 };
5401 Ok((rows, cols))
5402 }
5403 // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5404 // saw, what each token was called, which dictionary took it
5405 // and what came out. It is a projection of the same tokenizer
5406 // and the same map the indexer uses, so it cannot describe a
5407 // pipeline other than the one that runs.
5408 "ts_debug" => {
5409 use crate::fts::{TokenType, TsDict};
5410 let cols = alloc::vec![
5411 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5412 ColumnSchema::new("description".to_string(), DataType::Text, false),
5413 ColumnSchema::new("token".to_string(), DataType::Text, false),
5414 ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5415 ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5416 ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5417 ];
5418 // PG's one-arg form uses the session configuration; the
5419 // two-arg form names one.
5420 let (cfg_name, text) = match (&arg0, args.get(1)) {
5421 (Some(Value::Text(c)), Some(t)) => {
5422 let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5423 (c.to_string(), crate::eval::value_to_text(&v))
5424 }
5425 (Some(v), None) => (
5426 alloc::string::String::from("english"),
5427 crate::eval::value_to_text(v),
5428 ),
5429 _ => return Ok((alloc::vec::Vec::new(), cols)),
5430 };
5431 let english = match cfg_name
5432 .trim()
5433 .trim_start_matches("pg_catalog.")
5434 .to_ascii_lowercase()
5435 .as_str()
5436 {
5437 "english" => true,
5438 "simple" => false,
5439 other => {
5440 return Err(EngineError::Unsupported(alloc::format!(
5441 "text search configuration \"{other}\" does not exist"
5442 )));
5443 }
5444 };
5445 let rows = crate::fts::tokenize_typed(&text)
5446 .into_iter()
5447 .map(|tok| {
5448 let dict = tok.ty.dictionary(english);
5449 let dname = dict.map(|d| match d {
5450 TsDict::Simple => "simple",
5451 TsDict::EnglishStem => "english_stem",
5452 });
5453 let folded = tok.text.to_lowercase();
5454 let lexemes = dict.map(|d| match d {
5455 TsDict::Simple => alloc::vec![Some(folded.clone())],
5456 TsDict::EnglishStem => {
5457 if crate::fts::is_english_stopword(&folded) {
5458 alloc::vec::Vec::new()
5459 } else {
5460 alloc::vec![Some(crate::fts::porter_stem(&folded))]
5461 }
5462 }
5463 });
5464 Row::new(alloc::vec![
5465 Value::text(tok.ty.alias()),
5466 Value::text(tok.ty.description()),
5467 Value::text(tok.text),
5468 Value::TextArray(
5469 dname
5470 .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5471 .unwrap_or_default(),
5472 ),
5473 dname.map_or(Value::Null, Value::text),
5474 lexemes.map_or(Value::Null, Value::TextArray),
5475 ])
5476 })
5477 .collect();
5478 let _ = TokenType::AsciiWord;
5479 Ok((rows, cols))
5480 }
5481 // v7.39 (round 651) — `ts_token_type('default')`, the list the
5482 // parser actually produces. It is a projection of the
5483 // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5484 // read, so the three cannot disagree about what a token is.
5485 "ts_token_type" => {
5486 use crate::fts::TokenType as T;
5487 let cols = alloc::vec![
5488 ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5489 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5490 ColumnSchema::new("description".to_string(), DataType::Text, false),
5491 ];
5492 // PG takes the parser by name or oid; SPG has the one.
5493 if let Some(Value::Text(p)) = &arg0
5494 && !p.eq_ignore_ascii_case("default")
5495 && !p.eq_ignore_ascii_case("pg_catalog.default")
5496 {
5497 return Err(EngineError::Unsupported(alloc::format!(
5498 "text search parser \"{p}\" does not exist"
5499 )));
5500 }
5501 const TYPES: &[T] = &[
5502 T::AsciiWord,
5503 T::Word,
5504 T::NumWord,
5505 T::Email,
5506 T::Url,
5507 T::Host,
5508 T::SFloat,
5509 T::Version,
5510 T::HwordNumPart,
5511 T::HwordPart,
5512 T::HwordAsciiPart,
5513 T::Blank,
5514 T::Tag,
5515 T::Protocol,
5516 T::NumHword,
5517 T::AsciiHword,
5518 T::Hword,
5519 T::UrlPath,
5520 T::File,
5521 T::Float,
5522 T::Int,
5523 T::Uint,
5524 T::Entity,
5525 ];
5526 let rows = TYPES
5527 .iter()
5528 .map(|t| {
5529 Row::new(alloc::vec![
5530 Value::Int(*t as i32),
5531 Value::text(t.alias()),
5532 Value::text(t.description()),
5533 ])
5534 })
5535 .collect();
5536 Ok((rows, cols))
5537 }
5538 // v7.39 (read01 round 65) — a set-returning USER function in FROM
5539 // (`FROM rows_of(2)`). Its body runs through the real executor, like
5540 // every other function body since round 63.
5541 other => {
5542 if !self.active_catalog().functions_named(other).is_empty() {
5543 return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5544 }
5545 Err(EngineError::Unsupported(alloc::format!(
5546 "table function {other}() is not supported in FROM"
5547 )))
5548 }
5549 }
5550 }
5551
5552 /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5553 /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5554 /// are bound into it as literals and it goes through the read path, so the
5555 /// rows it yields are exactly the rows a hand-written query would see.
5556 ///
5557 /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5558 /// text)` names them, and a `SETOF <scalar>` yields a single column named
5559 /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5560 /// shows.
5561 fn exec_setof_user_function(
5562 &self,
5563 name: &str,
5564 args: &[spg_sql::ast::Expr],
5565 // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5566 // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5567 alias: Option<&str>,
5568 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5569 // The call's arguments belong to the ENCLOSING query, so they are
5570 // evaluated here and the body sees values.
5571 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5572 let arg_ctx = self.ev_ctx(&empty, None);
5573 let dummy = Row::new(alloc::vec::Vec::new());
5574 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5575 for a in args {
5576 vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5577 }
5578 self.setof_rows_of(name, &vals, alias)
5579 }
5580
5581 /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5582 /// arguments. Shared by the FROM position and the target-list expansion, so
5583 /// a function cannot behave differently depending on where it is called.
5584 pub(crate) fn setof_rows_of(
5585 &self,
5586 name: &str,
5587 arg_values: &[Value<'static>],
5588 alias: Option<&str>,
5589 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5590 let cat = self.active_catalog();
5591 let overloads = cat.functions_named(name);
5592 let def = overloads
5593 .iter()
5594 .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5595 .ok_or_else(|| {
5596 EngineError::Unsupported(alloc::format!(
5597 "function {name} does not exist with {} argument(s)",
5598 arg_values.len()
5599 ))
5600 })?;
5601 let declared = def.returns.trim().to_string();
5602 let upper = declared.to_ascii_uppercase();
5603 if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5604 return Err(EngineError::Unsupported(alloc::format!(
5605 "function {name}() does not return a set — it cannot be used in FROM"
5606 )));
5607 }
5608
5609 let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5610 // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5611 // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5612 if def.language.eq_ignore_ascii_case("plpgsql") {
5613 let out_rows = self
5614 .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5615 .map_err(EngineError::Eval)?;
5616 let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5617 let rows = out_rows.into_iter().map(Row::new).collect();
5618 return Ok((rows, cols));
5619 }
5620 let body = def.body.trim().trim_end_matches(';');
5621 let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5622 EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5623 })?;
5624 let spg_sql::ast::Statement::Select(body_select) = stmt else {
5625 return Err(EngineError::Unsupported(alloc::format!(
5626 "function {name}(): a set-returning body must be a SELECT"
5627 )));
5628 };
5629 let arg_names = spg_storage::function_arg_names(&def.args_repr);
5630 let bound = crate::eval::bind_user_fn_args(
5631 self.active_catalog(),
5632 &body_select,
5633 &arg_names,
5634 arg_values,
5635 )
5636 .map_err(EngineError::Eval)?;
5637 let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5638 let QueryResult::Rows { columns, rows } = out else {
5639 return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5640 };
5641 // Name the columns from the DECLARED shape — the same rule the plpgsql
5642 // path above uses, so a body's language cannot change the row shape.
5643 let cols = setof_column_shape_from(&declared, name, alias, &columns);
5644 Ok((rows, cols))
5645 }
5646
5647 fn exec_select_jsonb_each_text(
5648 &self,
5649 stmt: &SelectStatement,
5650 primary: &TableRef,
5651 cancel: CancelToken<'_>,
5652 ) -> Result<QueryResult, EngineError> {
5653 let (each_fn, arg_expr) = primary
5654 .jsonb_each_text_arg
5655 .as_ref()
5656 .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5657 .expect("caller guards jsonb_each_text_arg.is_some()");
5658 // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5659 // forms keep JSON rendering in the value column (JSON null
5660 // stays jsonb 'null', strings keep their quotes).
5661 let as_text = each_fn.ends_with("_text");
5662 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5663 let ctx = EvalContext::new(&empty_schema, None);
5664 let dummy_row = Row::new(alloc::vec::Vec::new());
5665 let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5666 let pairs =
5667 crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5668 let rows: alloc::vec::Vec<Row<'static>> = pairs
5669 .into_iter()
5670 .map(|(k, v)| {
5671 let key_val = Value::text(k);
5672 let value_val = match v {
5673 Some(s) if as_text => Value::text(s),
5674 Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5675 None => Value::Null,
5676 };
5677 Row::new(alloc::vec![key_val, value_val])
5678 })
5679 .collect();
5680 let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5681 let value_dtype = if as_text {
5682 spg_storage::DataType::Text
5683 } else {
5684 spg_storage::DataType::Json
5685 };
5686 let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5687 let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5688 let mut schema_cols = alloc::vec![key_col, value_col];
5689 // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5690 // LATERAL-position form of the same call already honours it.
5691 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5692 if let Some(col) = schema_cols.get_mut(i) {
5693 col.name = new_name.clone();
5694 }
5695 }
5696 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5697 // `EvalContext::new` drops it and every catalog-dependent cast
5698 // (regclass / enum / composite / domain) silently degrades.
5699 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5700 // WHERE.
5701 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5702 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5703 for row in rows {
5704 cancel.check()?;
5705 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5706 if matches!(v, Value::Bool(true)) {
5707 out.push(row);
5708 }
5709 }
5710 out
5711 } else {
5712 rows
5713 };
5714 // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5715 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
5716 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5717 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5718 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5719 .map_err(|err| match err {
5720 EngineError::Eval(ev) => ev,
5721 other => eval::EvalError::TypeMismatch {
5722 detail: alloc::format!("{other}"),
5723 },
5724 })
5725 };
5726 // v7.39 (round 656) — hand the rows over as they are rather than
5727 // collecting a second vector of `RowRef` wrappers. Note this is
5728 // a set-returning-function path, NOT the relational scan: the
5729 // measured O(rows) cost lived in `run_single_table_aggregate`,
5730 // and converting these four first was a miss that cost a full
5731 // round — every test stayed green and the number did not move.
5732 let agg = aggregate::run(
5733 stmt,
5734 crate::join::AggRows::Owned(&filtered),
5735 &schema_cols,
5736 Some(&alias),
5737 Some(&agg_correlated),
5738 self.parallel_runner.0.as_deref(),
5739 Some(self.active_catalog()),
5740 Some(self),
5741 )?;
5742 return self.finish_agg_result(agg, stmt, cancel);
5743 }
5744 // Projection.
5745 let projection = build_projection(
5746 &stmt.items,
5747 &schema_cols,
5748 &alias,
5749 self.speaks_mysql,
5750 Some(self.active_catalog()),
5751 )?;
5752 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5753 alloc::vec::Vec::with_capacity(filtered.len());
5754 for row in &filtered {
5755 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5756 for p in &projection {
5757 let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5758 vals.push(v);
5759 }
5760 projected_rows.push(Row::new(vals));
5761 }
5762 let columns: alloc::vec::Vec<ColumnSchema> = projection
5763 .iter()
5764 // v7.39 (read01 round 54) — keep the column's enum identity through
5765 // the projection (it lives outside the DataType lattice), or a
5766 // derived table / UNION / windowed result forgets it and any outer
5767 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5768 .map(|p| p.to_column_schema())
5769 .collect();
5770 // ORDER BY.
5771 if !stmt.order_by.is_empty() {
5772 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5773 .iter()
5774 .enumerate()
5775 .map(|(i, r)| -> Result<_, EngineError> {
5776 let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5777 .order_by
5778 .iter()
5779 .map(|ob| {
5780 eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5781 })
5782 .collect();
5783 Ok((i, keys?))
5784 })
5785 .collect::<Result<_, _>>()?;
5786 indexed.sort_by(|a, b| {
5787 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5788 let o = &stmt.order_by[idx];
5789 let cmp = order_by_value_cmp_in(
5790 o.desc,
5791 o.nulls_first,
5792 ka,
5793 kb,
5794 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5795 );
5796 if cmp != core::cmp::Ordering::Equal {
5797 return cmp;
5798 }
5799 }
5800 core::cmp::Ordering::Equal
5801 });
5802 projected_rows = indexed
5803 .into_iter()
5804 .map(|(i, _)| projected_rows[i].clone())
5805 .collect();
5806 }
5807 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5808 if stmt.distinct {
5809 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
5810 // spec folds EVERY text position, so a column declared
5811 // `COLLATE utf8mb4_bin` had its values merged here exactly the
5812 // way 3b494b6e fixed on the main scan path. The projection is
5813 // already in scope at each of these sites, so the mask needs no
5814 // new plumbing -- it was simply never asked for.
5815 projected_rows = dedup_rows(
5816 projected_rows,
5817 FoldSpec::of_masks(
5818 scan_ctx.mysql_dialect,
5819 &fold_mask(&projection),
5820 &pad_mask(&projection),
5821 ),
5822 );
5823 }
5824 if let Some(offset) = stmt.offset_literal() {
5825 let off = (offset as usize).min(projected_rows.len());
5826 projected_rows.drain(..off);
5827 }
5828 if let Some(limit) = stmt.limit_literal() {
5829 projected_rows.truncate(limit as usize);
5830 }
5831 Ok(QueryResult::Rows {
5832 columns,
5833 rows: projected_rows,
5834 })
5835 }
5836
5837 /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5838 /// ( SELECT … ) alias` in primary position. The inner SELECT
5839 /// materialises once through the regular bare-select executor
5840 /// (UNION tails included), then the outer WHERE / aggregate /
5841 /// projection / ORDER BY / LIMIT pipeline runs over the
5842 /// synthetic table — the same post-materialisation shape as
5843 /// exec_select_jsonb_each_text, generalised to N columns.
5844 fn exec_select_derived(
5845 &self,
5846 stmt: &SelectStatement,
5847 primary: &TableRef,
5848 cancel: CancelToken<'_>,
5849 ) -> Result<QueryResult, EngineError> {
5850 let inner = primary
5851 .lateral_subquery
5852 .as_deref()
5853 .expect("caller guards lateral_subquery.is_some()");
5854 // exec_select_cancel is the union-aware wrapper — the inner
5855 // SELECT may carry UNION tails on stmt.unions.
5856 let QueryResult::Rows {
5857 columns: inner_cols,
5858 rows,
5859 } = self.exec_select_cancel(inner, cancel)?
5860 else {
5861 return Err(EngineError::Unsupported(
5862 "derived table subquery must return rows".into(),
5863 ));
5864 };
5865 let alias = primary
5866 .alias
5867 .clone()
5868 .unwrap_or_else(|| primary.name.clone());
5869 // `AS t(a, b)` renames the materialised columns positionally
5870 // (extra inner columns keep their own names, PG behaviour).
5871 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5872 // v7.39 (read01 round 78) — a column-alias list longer than the item is
5873 // the error PG reports; SPG used to let the extra names through and then
5874 // fail two layers downstream with "column not found: <the extra name>".
5875 let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5876 if primary.unnest_column_aliases.len() > n_out {
5877 return Err(EngineError::Unsupported(alloc::format!(
5878 "table \"{alias}\" has {n_out} columns available but {} columns specified",
5879 primary.unnest_column_aliases.len()
5880 )));
5881 }
5882 if primary.scalar_fn_item && schema_cols.len() == 1 {
5883 schema_cols[0].scalar_row_source = true;
5884 }
5885 // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5886 // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5887 // The column-alias list, if given, names it like any other column.
5888 let mut rows = rows;
5889 if primary.with_ordinality {
5890 schema_cols.push(ColumnSchema::new(
5891 "ordinality".to_string(),
5892 DataType::BigInt,
5893 false,
5894 ));
5895 rows = rows
5896 .into_iter()
5897 .enumerate()
5898 .map(|(i, r)| {
5899 let mut v = r.values;
5900 #[allow(clippy::cast_possible_wrap)]
5901 v.push(Value::BigInt(i as i64 + 1));
5902 Row::new(v)
5903 })
5904 .collect();
5905 }
5906 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5907 if let Some(col) = schema_cols.get_mut(i) {
5908 col.name = new_name.clone();
5909 }
5910 }
5911 self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5912 }
5913
5914 /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5915 /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5916 /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5917 /// derived-table executor and the FROM-position table functions.
5918 fn exec_select_over_rows(
5919 &self,
5920 stmt: &SelectStatement,
5921 rows: alloc::vec::Vec<Row<'static>>,
5922 schema_cols: alloc::vec::Vec<ColumnSchema>,
5923 alias: &str,
5924 cancel: CancelToken<'_>,
5925 ) -> Result<QueryResult, EngineError> {
5926 let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5927 // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5928 // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5929 // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5930 // (the same path the aggregate branch uses); the old plain eval_expr let
5931 // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5932 let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5933 // WHERE.
5934 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5935 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5936 for row in rows {
5937 cancel.check()?;
5938 let v = self.eval_expr_with_correlated(
5939 w,
5940 &row,
5941 &scan_ctx,
5942 cancel,
5943 Some(&mut corr_memo.borrow_mut()),
5944 )?;
5945 if matches!(v, Value::Bool(true)) {
5946 out.push(row);
5947 }
5948 }
5949 out
5950 } else {
5951 rows
5952 };
5953 // Aggregate dispatch.
5954 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
5955 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5956 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5957 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5958 .map_err(|err| match err {
5959 EngineError::Eval(ev) => ev,
5960 other => eval::EvalError::TypeMismatch {
5961 detail: alloc::format!("{other}"),
5962 },
5963 })
5964 };
5965 // v7.39 (round 656) — hand the rows over as they are rather than
5966 // collecting a second vector of `RowRef` wrappers. Note this is
5967 // a set-returning-function path, NOT the relational scan: the
5968 // measured O(rows) cost lived in `run_single_table_aggregate`,
5969 // and converting these four first was a miss that cost a full
5970 // round — every test stayed green and the number did not move.
5971 let agg = aggregate::run(
5972 stmt,
5973 crate::join::AggRows::Owned(&filtered),
5974 &schema_cols,
5975 Some(alias),
5976 Some(&agg_correlated),
5977 self.parallel_runner.0.as_deref(),
5978 Some(self.active_catalog()),
5979 Some(self),
5980 )?;
5981 return self.finish_agg_result(agg, stmt, cancel);
5982 }
5983 // Projection.
5984 let projection = build_projection(
5985 &stmt.items,
5986 &schema_cols,
5987 alias,
5988 self.speaks_mysql,
5989 Some(self.active_catalog()),
5990 )?;
5991 // v7.39 (round 621) — a target-list SRF expands here too. This tail
5992 // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5993 // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5994 // answered `function unnest(integer[]) does not exist` for a query PG
5995 // answers.
5996 let srf_idxs = self.srf_target_idxs(&projection);
5997 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5998 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5999 alloc::vec::Vec::with_capacity(filtered.len());
6000 if !srf_idxs.is_empty() {
6001 let (rows, src) =
6002 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
6003 projected_rows = rows;
6004 src_of_row = src;
6005 } else {
6006 for row in &filtered {
6007 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
6008 for p in &projection {
6009 let v = self.eval_expr_with_correlated(
6010 &p.expr,
6011 row,
6012 &scan_ctx,
6013 cancel,
6014 Some(&mut corr_memo.borrow_mut()),
6015 )?;
6016 vals.push(v);
6017 }
6018 projected_rows.push(Row::new(vals));
6019 }
6020 }
6021 let columns: alloc::vec::Vec<ColumnSchema> = projection
6022 .iter()
6023 // v7.39 (read01 round 54) — keep the column's enum identity through
6024 // the projection (it lives outside the DataType lattice), or a
6025 // derived table / UNION / windowed result forgets it and any outer
6026 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
6027 .map(|p| p.to_column_schema())
6028 .collect();
6029 // ORDER BY over the source rows (same shape as the other
6030 // synthetic-table executors).
6031 // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
6032 // OUTPUT column. Evaluated as an expression, as it was here, the literal
6033 // `1` is just the constant 1: the same sort key for every row, so the
6034 // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
6035 // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
6036 // landing on this executor) came back in input order.
6037 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6038 if !order_by.is_empty() {
6039 // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
6040 // SRF makes more of them than there were inputs.
6041 let out_cols = if srf_idxs.is_empty() {
6042 alloc::vec![None; order_by.len()]
6043 } else {
6044 srf_order_output_cols(&order_by, &projection)
6045 };
6046 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
6047 .iter()
6048 .enumerate()
6049 .map(|(k, out)| -> Result<_, EngineError> {
6050 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
6051 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
6052 .iter()
6053 .zip(out_cols.iter())
6054 .map(|(ob, oc)| {
6055 // v7.39 (read01 round 54) — this path builds its
6056 // sort keys itself instead of going through
6057 // `build_order_keys`, so it skipped the enum-ordinal
6058 // substitution: an OUTER `ORDER BY <enum col>` over
6059 // a DERIVED TABLE sorted by the label TEXT, not by
6060 // member order. Silently wrong rows, not an error.
6061 let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
6062 Ok(
6063 match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
6064 Some(ord) => Value::Float(ord),
6065 None => v,
6066 },
6067 )
6068 })
6069 .collect();
6070 Ok((k, keys?))
6071 })
6072 .collect::<Result<_, _>>()?;
6073 indexed.sort_by(|a, b| {
6074 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
6075 let o = &stmt.order_by[idx];
6076 let cmp = order_by_value_cmp_in(
6077 o.desc,
6078 o.nulls_first,
6079 ka,
6080 kb,
6081 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
6082 );
6083 if cmp != core::cmp::Ordering::Equal {
6084 return cmp;
6085 }
6086 }
6087 core::cmp::Ordering::Equal
6088 });
6089 projected_rows = indexed
6090 .into_iter()
6091 .map(|(i, _)| projected_rows[i].clone())
6092 .collect();
6093 }
6094 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
6095 if stmt.distinct {
6096 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
6097 // spec folds EVERY text position, so a column declared
6098 // `COLLATE utf8mb4_bin` had its values merged here exactly the
6099 // way 3b494b6e fixed on the main scan path. The projection is
6100 // already in scope at each of these sites, so the mask needs no
6101 // new plumbing -- it was simply never asked for.
6102 projected_rows = dedup_rows(
6103 projected_rows,
6104 FoldSpec::of_masks(
6105 scan_ctx.mysql_dialect,
6106 &fold_mask(&projection),
6107 &pad_mask(&projection),
6108 ),
6109 );
6110 }
6111 if let Some(offset) = stmt.offset_literal() {
6112 let off = (offset as usize).min(projected_rows.len());
6113 projected_rows.drain(..off);
6114 }
6115 if let Some(limit) = stmt.limit_literal() {
6116 projected_rows.truncate(limit as usize);
6117 }
6118 Ok(QueryResult::Rows {
6119 columns,
6120 rows: projected_rows,
6121 })
6122 }
6123
6124 /// Constant `SELECT` with no FROM: evaluate each projection item
6125 /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
6126 fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
6127 let empty_schema: Vec<ColumnSchema> = Vec::new();
6128 let ctx = self.ev_ctx(&empty_schema, None);
6129 // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
6130 // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
6131 // `SELECT string_agg('x',',')` → x). Before this it fell through to the
6132 // scalar projection, where the aggregate name looked like an unknown
6133 // function. The WHERE filters that one row, so `… WHERE false` leaves
6134 // the aggregate zero input rows (`count(*)` → 0).
6135 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
6136 let dummy = Row::new(Vec::new());
6137 let passes = match &stmt.where_ {
6138 Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
6139 None => true,
6140 };
6141 let rows: Vec<RowRef<'_>> = if passes {
6142 alloc::vec![RowRef::Owned(&dummy)]
6143 } else {
6144 Vec::new()
6145 };
6146 let agg = aggregate::run(
6147 stmt,
6148 crate::join::AggRows::Refs(&rows),
6149 &empty_schema,
6150 None,
6151 None,
6152 self.parallel_runner.0.as_deref(),
6153 Some(self.active_catalog()),
6154 Some(self),
6155 )?;
6156 return self.finish_agg_result(agg, stmt, CancelToken::none());
6157 }
6158 let projection = build_projection(
6159 &stmt.items,
6160 &empty_schema,
6161 "",
6162 self.speaks_mysql,
6163 Some(self.active_catalog()),
6164 )?;
6165 // `SELECT … WHERE cond` with no FROM — the one conceptual
6166 // row survives only when the condition is true (previously
6167 // the WHERE was silently ignored: `SELECT 1 WHERE false`
6168 // returned a row).
6169 let dummy_row = Row::new(Vec::new());
6170 if let Some(w) = &stmt.where_ {
6171 let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
6172 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6173 let columns: Vec<ColumnSchema> = projection
6174 .into_iter()
6175 .map(|p| p.to_column_schema())
6176 .collect();
6177 return Ok(QueryResult::Rows {
6178 columns,
6179 rows: Vec::new(),
6180 });
6181 }
6182 }
6183 // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6184 // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6185 // desugar to unnest) expands here: one output row per SRF row, sibling
6186 // scalar columns repeated. unnest / array_elements / path_query reach a
6187 // real FROM via the parser rewrite and never land here.
6188 // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6189 let srf_idxs = self.srf_target_idxs(&projection);
6190 if !srf_idxs.is_empty() {
6191 let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6192 let columns: Vec<ColumnSchema> = projection
6193 .into_iter()
6194 .map(|p| p.to_column_schema())
6195 .collect();
6196 // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6197 // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6198 // to. This returned straight out of the expansion, so
6199 // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6200 // input order — the sort was not wrong, it never ran. (There is
6201 // exactly one conceptual input row here, which is why the ordinary
6202 // scan pipeline is not on this path at all.)
6203 if !stmt.order_by.is_empty() {
6204 let synth_ctx =
6205 EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6206 let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6207 .order_by
6208 .iter()
6209 .map(|o| {
6210 let mut o = o.clone();
6211 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6212 && *n >= 1
6213 && let Ok(idx) = usize::try_from(*n - 1)
6214 && idx < columns.len()
6215 {
6216 o.expr = Expr::Column(spg_sql::ast::ColumnName {
6217 qualifier: None,
6218 name: columns[idx].name.clone(),
6219 });
6220 }
6221 o
6222 })
6223 .collect();
6224 let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6225 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6226 for r in rows {
6227 // v7.39.12 — a correlated subquery in ORDER BY is resolved
6228 // for this row before the key is built; see
6229 // `Engine::order_by_resolved_for_row`.
6230 let per_row = self.order_by_resolved_for_row(
6231 &resolved,
6232 &r,
6233 &synth_ctx,
6234 CancelToken::none(),
6235 )?;
6236 let keys =
6237 build_order_keys(per_row.as_deref().unwrap_or(&resolved), &r, &synth_ctx)?;
6238 tagged.push((keys, r));
6239 }
6240 sort_by_keys(&mut tagged, &descs, self.session_parallel_workers());
6241 rows = tagged.into_iter().map(|(_, r)| r).collect();
6242 }
6243 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6244 return Ok(QueryResult::Rows { columns, rows });
6245 }
6246 let mut values = Vec::with_capacity(projection.len());
6247 for p in &projection {
6248 values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6249 }
6250 let columns: Vec<ColumnSchema> = projection
6251 .into_iter()
6252 .map(|p| p.to_column_schema())
6253 .collect();
6254 // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6255 // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6256 // returns none. (The SRF and aggregate arms above already applied
6257 // them; this tail was the one that didn't.)
6258 let mut rows = alloc::vec![Row::new(values)];
6259 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6260 Ok(QueryResult::Rows { columns, rows })
6261 }
6262
6263 /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6264 /// circuit. Catches
6265 /// SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6266 /// BEFORE `resolve_select_subqueries` materialises the inner result
6267 /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6268 /// values into a `HashSet<i64>` directly, then probes A.pk per
6269 /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6270 /// (~150 µs / query at INSUBQ benchmark scale).
6271 pub(crate) fn try_count_star_pk_in_subquery_fast(
6272 &self,
6273 stmt: &SelectStatement,
6274 cancel: CancelToken<'_>,
6275 ) -> Result<Option<QueryResult>, EngineError> {
6276 use spg_sql::ast::SelectItem;
6277 if stmt.distinct
6278 || stmt.limit_with_ties
6279 || stmt.group_by.is_some()
6280 || stmt.having.is_some()
6281 || !stmt.unions.is_empty()
6282 || !stmt.order_by.is_empty()
6283 || stmt.limit.is_some()
6284 || stmt.offset.is_some()
6285 || stmt.items.len() != 1
6286 {
6287 return Ok(None);
6288 }
6289 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6290 return Ok(None);
6291 };
6292 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6293 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6294 if !is_count_star {
6295 return Ok(None);
6296 }
6297 let Some(from) = stmt.from.as_ref() else {
6298 return Ok(None);
6299 };
6300 if !from.joins.is_empty()
6301 || from.primary.lateral_subquery.is_some()
6302 || from.primary.unnest_expr.is_some()
6303 || from.primary.generate_series_args.is_some()
6304 || from.primary.table_fn_call.is_some()
6305 || from.primary.as_of_segment.is_some()
6306 {
6307 return Ok(None);
6308 }
6309 let Some(where_expr) = stmt.where_.as_ref() else {
6310 return Ok(None);
6311 };
6312 // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6313 // negated=false; no other predicates.
6314 let Expr::InSubquery {
6315 expr: col_expr,
6316 subquery,
6317 negated: false,
6318 } = where_expr
6319 else {
6320 return Ok(None);
6321 };
6322 let Expr::Column(c) = col_expr.as_ref() else {
6323 return Ok(None);
6324 };
6325 let outer_alias = from
6326 .primary
6327 .alias
6328 .as_deref()
6329 .unwrap_or(from.primary.name.as_str());
6330 if let Some(q) = c.qualifier.as_deref()
6331 && !q.eq_ignore_ascii_case(outer_alias)
6332 {
6333 return Ok(None);
6334 }
6335 // Outer column must be a single-column PK on integer family.
6336 let catalog = self.active_catalog();
6337 let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6338 return Ok(None);
6339 };
6340 let outer_schema = outer_table.schema();
6341 let Some(outer_pos) = outer_schema
6342 .columns
6343 .iter()
6344 .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6345 else {
6346 return Ok(None);
6347 };
6348 if !matches!(
6349 outer_schema.columns[outer_pos].ty,
6350 spg_storage::DataType::BigInt
6351 | spg_storage::DataType::Int
6352 | spg_storage::DataType::SmallInt
6353 ) {
6354 return Ok(None);
6355 }
6356 if !outer_schema
6357 .uniqueness_constraints
6358 .iter()
6359 .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6360 {
6361 return Ok(None);
6362 }
6363 let Some(idx) = outer_table.index_on(outer_pos) else {
6364 return Ok(None);
6365 };
6366 // Inner must be uncorrelated. The cheap-correlation pre-check
6367 // exists upstream; here we just attempt the bare exec.
6368 if crate::subquery::select_is_correlated(subquery) {
6369 return Ok(None);
6370 }
6371 let mut inner = (**subquery).clone();
6372 self.resolve_select_subqueries(&mut inner, cancel)?;
6373 let r = match self.exec_bare_select_cancel(&inner, cancel) {
6374 Ok(r) => r,
6375 Err(_) => return Ok(None),
6376 };
6377 let QueryResult::Rows { columns, rows, .. } = r else {
6378 return Ok(None);
6379 };
6380 if columns.len() != 1 {
6381 return Ok(None);
6382 }
6383 // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6384 // subquery projects a column known to be UNIQUE/PK on its table
6385 // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6386 // in `tbl.uniqueness_constraints`), survivor values are
6387 // guaranteed distinct and the per-survivor `HashSet::insert`
6388 // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6389 //
6390 // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6391 // projection that is a bare Column ref, table-column lookup in
6392 // catalog confirms the column appears as a unique constraint's
6393 // sole member. UNIQUE NOT NULL is required — a nullable unique
6394 // column may have multiple NULLs, but NULLs are already skipped
6395 // above (`Value::Null => continue`), so a UNIQUE-only column is
6396 // still safe to dedup-skip.
6397 let inner_unique = (|| -> bool {
6398 if inner.distinct
6399 || inner.group_by.is_some()
6400 || !inner.unions.is_empty()
6401 || inner.having.is_some()
6402 || inner.items.len() != 1
6403 {
6404 return false;
6405 }
6406 let Some(inner_from) = inner.from.as_ref() else {
6407 return false;
6408 };
6409 if !inner_from.joins.is_empty()
6410 || inner_from.primary.lateral_subquery.is_some()
6411 || inner_from.primary.unnest_expr.is_some()
6412 || inner_from.primary.generate_series_args.is_some()
6413 || inner_from.primary.table_fn_call.is_some()
6414 {
6415 return false;
6416 }
6417 let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6418 return false;
6419 };
6420 let Expr::Column(pc) = proj else {
6421 return false;
6422 };
6423 let inner_alias = inner_from
6424 .primary
6425 .alias
6426 .as_deref()
6427 .unwrap_or(inner_from.primary.name.as_str());
6428 if let Some(q) = pc.qualifier.as_deref()
6429 && !q.eq_ignore_ascii_case(inner_alias)
6430 {
6431 return false;
6432 }
6433 let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6434 return false;
6435 };
6436 let isch = inner_table.schema();
6437 let Some(ipos) = isch
6438 .columns
6439 .iter()
6440 .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6441 else {
6442 return false;
6443 };
6444 isch.uniqueness_constraints
6445 .iter()
6446 .any(|u| u.columns.as_slice() == [ipos])
6447 })();
6448 // Collect inner i64 values directly into a HashSet, then probe.
6449 let mut count: i64 = 0;
6450 let mut probed = if inner_unique {
6451 hashbrown::HashSet::<i64>::new()
6452 } else {
6453 hashbrown::HashSet::<i64>::with_capacity(rows.len())
6454 };
6455 for row in &rows {
6456 let v = row.values.first().cloned().unwrap_or(Value::Null);
6457 let n = match v {
6458 Value::BigInt(n) => n,
6459 Value::Int(n) => i64::from(n),
6460 Value::SmallInt(n) => i64::from(n),
6461 Value::Null => continue,
6462 _ => return Ok(None),
6463 };
6464 // De-duplicate inner key set so a duplicate inner value
6465 // doesn't double-count the same outer row. Skipped when
6466 // the inner projection is statically unique.
6467 if !inner_unique && !probed.insert(n) {
6468 continue;
6469 }
6470 // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6471 // the `IndexKey::from_value` enum-dispatch and the per-call
6472 // `IndexKey` wrapper construction. The outer column is
6473 // already gated to integer-family above, so an i64 key
6474 // always corresponds to a valid PK lookup.
6475 if !idx.lookup_eq_i64(n).is_empty() {
6476 count += 1;
6477 }
6478 }
6479 let columns_out = alloc::vec![ColumnSchema::new(
6480 "count".to_string(),
6481 spg_storage::DataType::BigInt,
6482 false,
6483 )];
6484 let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6485 Ok(Some(QueryResult::Rows {
6486 columns: columns_out,
6487 rows: rows_out,
6488 }))
6489 }
6490
6491 /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6492 /// SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6493 /// (the post-subquery-replacement shape of the INSUBQ probe
6494 /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6495 /// The general aggregate path materialises every seeked row into
6496 /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6497 /// For COUNT(*) we only care how many keys hit; iterate the list
6498 /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6499 /// row materialisation, the aggregate state machine, and the per-
6500 /// row WHERE re-eval (the seek already filtered by the same list).
6501 /// Returns `None` when the shape doesn't match.
6502 fn try_count_star_pk_in_list_fast(
6503 &self,
6504 stmt: &SelectStatement,
6505 table: &spg_storage::Table,
6506 schema_cols: &[ColumnSchema],
6507 alias: &str,
6508 ) -> Option<QueryResult> {
6509 use spg_sql::ast::{ColumnName, SelectItem};
6510 // Gates on the SELECT shape.
6511 if stmt.distinct
6512 || stmt.limit_with_ties
6513 || stmt.group_by.is_some()
6514 || stmt.having.is_some()
6515 || !stmt.unions.is_empty()
6516 || !stmt.order_by.is_empty()
6517 || stmt.limit.is_some()
6518 || stmt.offset.is_some()
6519 || stmt.items.len() != 1
6520 {
6521 return None;
6522 }
6523 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6524 return None;
6525 };
6526 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6527 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6528 if !is_count_star {
6529 return None;
6530 }
6531 // WHERE must be `<col> IN (literal list)` with no other
6532 // conjuncts (the seek result is a true subset of the row
6533 // population for this predicate).
6534 let where_expr = stmt.where_.as_ref()?;
6535 let Expr::InList {
6536 expr: col_expr,
6537 list,
6538 negated: false,
6539 } = where_expr
6540 else {
6541 return None;
6542 };
6543 let Expr::Column(c) = col_expr.as_ref() else {
6544 return None;
6545 };
6546 if let Some(q) = c.qualifier.as_deref()
6547 && !q.eq_ignore_ascii_case(alias)
6548 {
6549 return None;
6550 }
6551 let col_pos = schema_cols
6552 .iter()
6553 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6554 // The column must be a single-column PK on an integer family
6555 // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6556 // so the antiset stays collision-free under `HashSet<i64>`.
6557 let schema = table.schema();
6558 if !matches!(
6559 schema.columns[col_pos].ty,
6560 spg_storage::DataType::BigInt
6561 | spg_storage::DataType::Int
6562 | spg_storage::DataType::SmallInt
6563 ) {
6564 return None;
6565 }
6566 if !schema
6567 .uniqueness_constraints
6568 .iter()
6569 .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6570 {
6571 return None;
6572 }
6573 let idx = table.index_on(col_pos)?;
6574 // Tally non-empty seek results across all literal values.
6575 let mut count: i64 = 0;
6576 for lit in list {
6577 let Expr::Literal(l) = lit else {
6578 return None;
6579 };
6580 // r1039 — through the shared resolver, so a literal spelled
6581 // in another type ('5' against an integer PK) is read as the
6582 // column's before it becomes a key. This tally answers from
6583 // the index alone, so a key in the wrong space would return a
6584 // COUNT of zero rather than fall back to a scan.
6585 let col = schema.columns.get(col_pos)?;
6586 let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6587 let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6588 if !idx.lookup_eq(&key).is_empty() {
6589 count += 1;
6590 }
6591 }
6592 let columns = alloc::vec![ColumnSchema::new(
6593 "count".to_string(),
6594 spg_storage::DataType::BigInt,
6595 false,
6596 )];
6597 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6598 let _ = ColumnName {
6599 qualifier: None,
6600 name: String::new(),
6601 };
6602 Some(QueryResult::Rows { columns, rows })
6603 }
6604
6605 /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6606 /// BETWEEN a AND b` on an indexed column. The index range walk yields
6607 /// exactly the matching (visible) rows, so we count locators directly —
6608 /// skipping the row materialisation, the aggregate state machine, and the
6609 /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6610 /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6611 /// when the shape doesn't match.
6612 fn try_count_star_indexed_range_fast(
6613 &self,
6614 stmt: &SelectStatement,
6615 table: &spg_storage::Table,
6616 schema_cols: &[ColumnSchema],
6617 alias: &str,
6618 snapshot: &spg_storage::snapshot::Snapshot,
6619 ) -> Option<QueryResult> {
6620 use spg_sql::ast::SelectItem;
6621 if stmt.distinct
6622 || stmt.limit_with_ties
6623 || stmt.group_by.is_some()
6624 || stmt.having.is_some()
6625 || !stmt.unions.is_empty()
6626 || !stmt.order_by.is_empty()
6627 || stmt.limit.is_some()
6628 || stmt.offset.is_some()
6629 || stmt.items.len() != 1
6630 {
6631 return None;
6632 }
6633 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6634 return None;
6635 };
6636 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6637 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6638 if !is_count_star {
6639 return None;
6640 }
6641 let where_expr = stmt.where_.as_ref()?;
6642 let count = crate::index_access::try_range_count(
6643 where_expr,
6644 schema_cols,
6645 table,
6646 alias,
6647 snapshot,
6648 self.speaks_mysql,
6649 )?;
6650 let columns = alloc::vec![ColumnSchema::new(
6651 "count".to_string(),
6652 spg_storage::DataType::BigInt,
6653 false,
6654 )];
6655 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6656 Some(QueryResult::Rows { columns, rows })
6657 }
6658
6659 /// Single-table aggregate path: filter the (optionally index-seeked)
6660 /// rows, then hand off to the aggregate executor which does its own
6661 /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6662 fn run_single_table_aggregate<'a>(
6663 &self,
6664 stmt: &SelectStatement,
6665 table: &'a spg_storage::Table,
6666 schema_cols: &'a [ColumnSchema],
6667 alias: &str,
6668 indexed_rows: Option<crate::index_access::Seeked<'a>>,
6669 cancel: CancelToken<'_>,
6670 ) -> Result<QueryResult, EngineError> {
6671 // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6672 // REPEATABLE (see run_single_table_scan). Aggregates
6673 // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6674 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6675 let ctx = self
6676 .ev_ctx(schema_cols, Some(alias))
6677 .with_sample_rng(&sample_cell);
6678 // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6679 // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6680 // and every abandoned buffer on the way stays resident: RSS is a
6681 // high-water mark, so the intermediates are paid for even though
6682 // they are freed. Round 656 measured the scan at 17 bytes/row
6683 // where the survivor list itself only needs 8.
6684 let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6685 Vec::with_capacity(table.rows().len())
6686 } else {
6687 // With a WHERE, the row count is an UPPER bound and reserving it
6688 // is the worse trade: `… WHERE id = 5` over 50M rows would take
6689 // 400 MB of pointers to hold one survivor. Let it grow.
6690 Vec::new()
6691 };
6692 // v6.2.6 — Memoize: per-query LRU cache for correlated
6693 // scalar subqueries. Fresh per row-loop entry so each
6694 // SELECT execution gets an isolated cache.
6695 let mut memo = memoize::MemoizeCache::new();
6696 // v7.37 (perf) — single-table aggregate's WHERE filter
6697 // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6698 // correlated`) per row, even for subquery-free WHEREs that
6699 // the single-table SCAN path has compiled since v7.32
6700 // (perf knife D). The asymmetry meant a fold-to-filter
6701 // rewrite (joinfold) that swapped a JOIN for a single-table
6702 // aggregate over a compiled WHERE saw the tree-walker
6703 // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6704 // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6705 // step. Compile once if eligible; fall back to the walker
6706 // for subquery-bearing or non-compilable WHEREs.
6707 let compiled_where: Option<eval::CompiledExpr> = stmt
6708 .where_
6709 .as_ref()
6710 .filter(|w| eval::fully_compilable(w))
6711 .map(|w| {
6712 // v7.38.8 — the scan filter runs the cheap half of its
6713 // conjunction first. Called from HERE and not from
6714 // `eval::compiled`, deliberately: the row loop lives in
6715 // that file, and adding a function to it cost this
6716 // query 11 % through layout alone while doing no work
6717 // for it. See `crate::qualorder`.
6718 match crate::qualorder::reordered(w) {
6719 Some(r) => eval::compile_expr(&r, &ctx),
6720 None => eval::compile_expr(w, &ctx),
6721 }
6722 });
6723 let mut eval_stack: Vec<Value<'static>> = Vec::new();
6724 let mut row_passes_where = |row: &Row<'static>,
6725 eval_stack: &mut Vec<Value<'static>>,
6726 memo: &mut memoize::MemoizeCache|
6727 -> Result<bool, EngineError> {
6728 match (&compiled_where, &stmt.where_) {
6729 (Some(cw), _) => {
6730 // v7.39 (round 479) — the predicate wants a bool, not a
6731 // Value. The owned entry ended in `Value::into_owned`
6732 // and the caller then dropped it, once per row; round
6733 // 478's profile put that pair above the comparison
6734 // itself.
6735 Ok(eval::compiled::eval_compiled_pred(
6736 cw,
6737 row,
6738 &ctx,
6739 eval_stack,
6740 ctx.mysql_dialect,
6741 )
6742 .map_err(EngineError::Eval)?)
6743 }
6744 (None, Some(w)) => {
6745 let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6746 Ok(crate::eval::predicate_is_true(
6747 &cond,
6748 "WHERE",
6749 ctx.mysql_dialect,
6750 )?)
6751 }
6752 (None, None) => Ok(true),
6753 }
6754 };
6755 if let Some(seeked) = &indexed_rows {
6756 // v7.38.19 — an EXACT seek has already applied the whole
6757 // predicate, so asking again is asking the index's question
6758 // a second time, once per row.
6759 //
6760 // Profiled on `count(*) FROM events WHERE project_id = 3`
6761 // over 200,000 rows: `try_index_seek` 1,814 leaf samples and
6762 // `binop::compare` 1,633 — and `compare`'s first arm is
6763 // `(Int, Int) => a.cmp(b)`, so it was never that a
6764 // comparison is expensive. It was that 25,000 of them were
6765 // re-deciding what the walk had decided. The same query with
6766 // `GROUP BY project_id` bolted on ran in half the time,
6767 // doing strictly more work, because that path reached the
6768 // rows differently.
6769 //
6770 // `exact` is false for every arm that has not proven it —
6771 // the GIN, trigram and jsonb walks, an `AND` whose other
6772 // conjuncts went unapplied, a collated key, a type whose key
6773 // cannot name it. See `index_access::Seeked`.
6774 if seeked.exact {
6775 filtered.extend(seeked.rows.iter().map(Cow::as_ref));
6776 } else {
6777 for cow in &seeked.rows {
6778 let row = cow.as_ref();
6779 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6780 continue;
6781 }
6782 filtered.push(row);
6783 }
6784 }
6785 }
6786 // v7.36 (cold-tier coverage) — single-table aggregate's
6787 // non-indexed full scan was hot-only and silently lost cold
6788 // rows on COUNT/SUM/etc. Materialise cold rows once into
6789 // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6790 // shape stays unchanged; the cold rows live until the end of
6791 // the aggregate run.
6792 let cold_rows_storage = if indexed_rows.is_none() {
6793 self.iter_cold_rows_of_table(table)
6794 } else {
6795 Vec::new()
6796 };
6797 if indexed_rows.is_none() {
6798 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6799 // single-table aggregate full-scan path. Mirrors the gate on
6800 // `run_single_table_scan`: this is a user-query result path,
6801 // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6802 // reader's snapshot cannot see (e.g. tombstoned versions),
6803 // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6804 // under the default gate-off: every hot row is frozen or
6805 // committed-and-alive, so `is_row_visible` returns true.
6806 // Cold-tier rows are frozen (visible) by definition — left
6807 // ungated, matching the plain-scan path.
6808 let scan_snapshot = self.current_snapshot();
6809 // v7.39 (pg_stat knife B) — this full-scan branch walks
6810 // headers directly (serial and sharded alike); count the
6811 // sequential scan here.
6812 table.note_seq_scan();
6813 // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6814 // filter dominate the pre-aggregate wall time on big
6815 // scans (P1's ground truth: accumulation is only ~17%).
6816 // Shard THAT work when the host injected an executor and
6817 // the WHERE is compiled (the compiled evaluator is pure
6818 // over &row; the tree-walker fallback can hit correlated
6819 // subqueries and stays serial). Shards return surviving
6820 // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6821 // 'static bound — and the main thread only dereferences.
6822 let n = table.row_count();
6823 let par = self.parallel_runner.0.as_deref().filter(|_| {
6824 n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6825 });
6826 // v7.38.11 — ask the BRIN summary first. When it prunes,
6827 // the work left is a few thousand rows and sharding it
6828 // costs more than it saves, so the serial pruned loop below
6829 // takes it; the shard machinery is left exactly as it was
6830 // rather than taught about slots.
6831 let brin_slots = stmt
6832 .where_
6833 .as_ref()
6834 .and_then(|w| crate::brin::candidate_slots(w, table));
6835 let brin_prunes = brin_slots
6836 .as_ref()
6837 .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6838 if let Some(r) = par
6839 && !brin_prunes
6840 {
6841 let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6842 let chunk = n.div_ceil(n_shards);
6843 type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6844 let cw = &compiled_where;
6845 let snap_ref = &scan_snapshot;
6846 let results = r.run_shards(n_shards, &|s| {
6847 let lo = s * chunk;
6848 let hi = ((s + 1) * chunk).min(n);
6849 let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6850 // EvalContext carries Cells (sampler / row counters)
6851 // and is !Sync — each shard builds its own from the
6852 // same Sync inputs. The compiled WHERE is gated to
6853 // the pure-scalar whitelist, which reads none of the
6854 // session state the engine-built ctx would add
6855 // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6856 // sampled scans never take this branch).
6857 let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6858 let mut stack: Vec<Value<'static>> = Vec::new();
6859 let out: ShardOut = (|| {
6860 for i in lo..hi {
6861 if !table.is_row_visible(i, snap_ref) {
6862 continue;
6863 }
6864 let row = &table.rows()[i];
6865 // v7.39 (round 480) — the parallel full-scan
6866 // shard is the path the aggregate benchmark
6867 // actually takes, and it was still on the OWNED
6868 // entry: round 480's profile attributed 68.7 %
6869 // of `drop_glue<Value>` to this closure, which
6870 // is why round 479's fix to the indexed path
6871 // barely moved the total.
6872 //
6873 // The `matches!(…, Value::Bool(true))` form was
6874 // also a narrower reading than the rest of the
6875 // engine uses — `predicate_is_true` is what
6876 // handles NULL and MySQL truthiness — so the
6877 // bool entry fixes the shape as well as the cost.
6878 let pass = match cw {
6879 Some(c) => eval::compiled::eval_compiled_pred(
6880 c,
6881 row,
6882 &shard_ctx,
6883 &mut stack,
6884 shard_ctx.mysql_dialect,
6885 )
6886 .map_err(EngineError::Eval)?,
6887 None => true,
6888 };
6889 if pass {
6890 keep.push(i);
6891 }
6892 }
6893 Ok(keep)
6894 })();
6895 alloc::boxed::Box::new(out)
6896 });
6897 // v7.39 (round 567) — `rows()` is a 32-way trie, so
6898 // indexing it is four dependent loads and a scan that
6899 // reads every row paid them every row. A profile of
6900 // `SELECT sum(id)` over 500k rows put 37.8% of the
6901 // connection thread's CPU on THIS ONE LINE. The cursor
6902 // holds the leaf, making that one descent per 32.
6903 let mut rows_cur = table.rows().run_cursor();
6904 for boxed in results {
6905 let shard = boxed
6906 .downcast::<ShardOut>()
6907 .expect("runner echoes the closure's box");
6908 for i in (*shard)? {
6909 if let Some(row) = rows_cur.get(i) {
6910 filtered.push(row);
6911 }
6912 }
6913 }
6914 } else {
6915 let mut rows_cur = table.rows().run_cursor();
6916 // v7.38.11 — the slots the BRIN summary could not rule
6917 // out. The predicate still runs on every row that
6918 // survives: the summary decides what to SKIP, never
6919 // what to return.
6920 let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6921 for range in ranges {
6922 for i in range {
6923 if !table.is_row_visible(i, &scan_snapshot) {
6924 continue;
6925 }
6926 let Some(row) = rows_cur.get(i) else { continue };
6927 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6928 continue;
6929 }
6930 filtered.push(row);
6931 }
6932 }
6933 }
6934 for row in &cold_rows_storage {
6935 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6936 continue;
6937 }
6938 filtered.push(row);
6939 }
6940 }
6941 // v7.29 — a per-query memo so correlated scalar
6942 // subqueries batch-evaluate once (group map) instead of
6943 // executing per group.
6944 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6945 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6946 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6947 .map_err(|err| match err {
6948 EngineError::Eval(ev) => ev,
6949 other => eval::EvalError::TypeMismatch {
6950 detail: alloc::format!("{other}"),
6951 },
6952 })
6953 };
6954 // v7.39 (round 656) — the plain relational scan. This collect() was
6955 // the measured defect: one 64-byte `RowRef` per surviving row to
6956 // wrap an 8-byte pointer `filtered` already holds. Scalar
6957 // aggregates measured ~81 bytes/row of working memory because of
6958 // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6959 // one number. `AggRows::Ptrs` reads the pointers directly.
6960 let agg = aggregate::run(
6961 stmt,
6962 crate::join::AggRows::Ptrs(&filtered),
6963 schema_cols,
6964 Some(alias),
6965 Some(&agg_correlated),
6966 self.parallel_runner.0.as_deref(),
6967 Some(self.active_catalog()),
6968 Some(self),
6969 )?;
6970 self.finish_agg_result(agg, stmt, cancel)
6971 }
6972
6973 /// Single-table scan + projection path: WHERE filter (compiled when
6974 /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6975 /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6976 fn run_single_table_scan<'a>(
6977 &self,
6978 stmt: &SelectStatement,
6979 table: &'a spg_storage::Table,
6980 schema_cols: &'a [ColumnSchema],
6981 alias: &str,
6982 indexed_rows: Option<crate::index_access::Seeked<'a>>,
6983 cancel: CancelToken<'_>,
6984 ) -> Result<QueryResult, EngineError> {
6985 // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6986 // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6987 // deterministic `__tsm_fract(seed)` draws share one scan-local
6988 // state (isolated from the global random() PRNG); a fresh cell per
6989 // scan makes a repeat / rescan reproduce the same sample. Unused
6990 // and cheap when the query carries no sample.
6991 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6992 let ctx = self
6993 .ev_ctx(schema_cols, Some(alias))
6994 .with_sample_rng(&sample_cell);
6995 let projection = build_projection(
6996 &stmt.items,
6997 schema_cols,
6998 alias,
6999 self.speaks_mysql,
7000 Some(self.active_catalog()),
7001 )?;
7002 // v7.19 P5 — single-table SELECT path for SRF
7003 // `SELECT unnest(arr) FROM t` shape. Detect a top-level
7004 // unnest in the projection list. When present, the
7005 // per-row processor emits one output row per array
7006 // element (broadcasting non-SRF projections from the
7007 // same input row). Empty / NULL arrays emit zero rows
7008 // for that input — PG semantics.
7009 // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
7010 let srf_idxs = self.srf_target_idxs(&projection);
7011 let srf_position = srf_idxs.first().copied();
7012 // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
7013 let mut srf_plan = if srf_position.is_some() {
7014 Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
7015 } else {
7016 None
7017 };
7018
7019 // Materialise the filter pass into `(order_key, projected_row)`
7020 // tuples. The order key is `None` when there's no ORDER BY clause.
7021 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
7022 // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
7023 // output row to the per-query byte budget as it is built, so a
7024 // fat single-table scan / sort REJECTS with QueryBytesExceeded
7025 // at ~the ceiling instead of materialising the whole table and
7026 // only noticing at the final enforce_row_limit check. Without
7027 // this, N concurrent fat scans peak at N×table and OOM the host.
7028 // `max_query_bytes = None` (the embedded default) = no ceiling,
7029 // so existing unbudgeted behaviour is byte-identical.
7030 let mut budget = ByteBudget::new(self.max_query_bytes);
7031 // v6.2.6 — Memoize per-row WHERE eval shares one cache.
7032 let mut memo = memoize::MemoizeCache::new();
7033 // v7.32 (perf knife D) — subquery-free WHERE compiles once;
7034 // the row loop then runs a flat step program instead of a
7035 // tree interpretation per row.
7036 let compiled_where: Option<eval::CompiledExpr> = stmt
7037 .where_
7038 .as_ref()
7039 .filter(|w| eval::fully_compilable(w))
7040 .map(|w| {
7041 // v7.38.8 — the scan filter runs the cheap half of its
7042 // conjunction first. Called from HERE and not from
7043 // `eval::compiled`, deliberately: the row loop lives in
7044 // that file, and adding a function to it cost this
7045 // query 11 % through layout alone while doing no work
7046 // for it. See `crate::qualorder`.
7047 match crate::qualorder::reordered(w) {
7048 Some(r) => eval::compile_expr(&r, &ctx),
7049 None => eval::compile_expr(w, &ctx),
7050 }
7051 });
7052 let mut eval_stack: Vec<Value<'static>> = Vec::new();
7053 // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
7054 // SELECT-item scalar subquery for the PK-probe fast path. The
7055 // analysis (gate checks + catalog lookups) takes ~500 ns; doing
7056 // it once per query instead of once per row × 100 rows saves
7057 // ~50 µs and lets the per-row evaluation reduce to a single
7058 // index probe + outer-column read.
7059 let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
7060 .iter()
7061 .map(|p| {
7062 if let Expr::ScalarSubquery(inner) = &p.expr {
7063 self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
7064 } else {
7065 None
7066 }
7067 })
7068 .collect();
7069 let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
7070 // v7.39 (round 487) — a projection item that is a bare column
7071 // reference binds its position ONCE per query.
7072 //
7073 // Per row it used to walk `eval_expr_with_correlated` (a memo
7074 // lookup for "does this have a subquery", then an un-memoised
7075 // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
7076 // then `resolve_column`, which finds the column by scanning the
7077 // schema and comparing NAMES. On `SELECT g FROM h` that chain was
7078 // 19 % of self time for what is ultimately one cell read.
7079 //
7080 // `compile_column_pos` is the Step VM's resolver, already
7081 // `pub(crate)` and already reused by the aggregate's bind-once
7082 // path: it mirrors `resolve_column`'s happy layers and returns
7083 // None for anything that would reach an error, an ambiguity, or a
7084 // miss, so those still go the interpreter's way and keep its
7085 // exact message. A composite column is excluded for the same
7086 // reason `compile_into` excludes it — it must be rehydrated from
7087 // stored JSON, which is not a cell read.
7088 let proj_direct = bind_direct_columns(&projection, &ctx);
7089 let any_proj_direct = proj_direct.iter().any(Option::is_some);
7090 // v7.39 (round 605) — a projection item that cannot depend on the row
7091 // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
7092 // allocations a row against one for a plain column, `'abc' || 'def'`
7093 // six and `upper('abc')` five, all of them producing the same value
7094 // 50,000 times. An item that fails to evaluate is left alone, so its
7095 // error still comes from the row loop in the interpreter's wording.
7096 let proj_const: Vec<Option<Value<'static>>> = projection
7097 .iter()
7098 .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
7099 .collect();
7100 let any_proj_const = proj_const.iter().any(Option::is_some);
7101 crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
7102 // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
7103 // projection. Statement prep (`resolve_order_by_position`) can only map
7104 // `ORDER BY 1` onto the first SELECT item when that item is an
7105 // expression; a `*` is not one, so the literal survived to here and was
7106 // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
7107 // at all. The parser rewrites `SELECT unnest(a) x` into
7108 // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
7109 // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
7110 // back in input order. The projection is built by now, so the Nth output
7111 // column is known — resolve against it.
7112 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
7113 // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
7114 // EXPANDED rows, so a key naming a select-list item reads that item.
7115 let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
7116 srf_order_output_cols(&order_by, &projection)
7117 } else {
7118 Vec::new()
7119 };
7120 let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
7121 // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
7122 // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
7123 // Hoisted above the closure so the projection-eval path can
7124 // gate `memo` passing on it: the SELECT-item correlated-scalar
7125 // batch path scans the FULL inner table once (~5 ms for 12.5 k
7126 // rows) and is only a win when N outer rows is large; for small
7127 // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
7128 let early_cap: Option<usize> = if order_by.is_empty()
7129 && !stmt.distinct
7130 && !stmt.limit_with_ties
7131 && srf_position.is_none()
7132 && stmt.where_.is_none()
7133 {
7134 stmt.limit_literal()
7135 .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
7136 } else {
7137 None
7138 };
7139 // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
7140 // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
7141 // full-sort by the test gate) keep only the running top-`keep`
7142 // rows in memory instead of materialising every projected row,
7143 // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
7144 // space, not O(rows). `None` = accumulate everything (the prior
7145 // behaviour). The final `partial_sort_tagged(keep)` below still
7146 // runs and produces the identical rows.
7147 // v7.39 (round 683) — the declared collation for each ORDER BY
7148 // position, resolved once and carried beside `descs` for the same
7149 // reason `descs` is carried: it is per key position, not per row.
7150 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
7151 let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
7152 && !stmt.distinct
7153 && !stmt.limit_with_ties
7154 && srf_position.is_none()
7155 && !self.env_cfg().disable_topk
7156 {
7157 stmt.limit_literal().and_then(|l| {
7158 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
7159 (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
7160 })
7161 } else {
7162 None
7163 };
7164 // v7.38.19 — when the sort column is one the projection already
7165 // carries, build no key at all and sort by reading it.
7166 //
7167 // Restricted to the FULL sort: a top-N compares against a stored
7168 // boundary key and `WITH TIES` extends past the limit through the
7169 // keys, both of which need one to exist. DISTINCT keys on them
7170 // too, and an SRF's keys come from the EXPANDED row.
7171 // A COLLATION does not rule it out, but it has to be one that
7172 // orders these values the way bytes do -- decided on the values
7173 // themselves, further down, once they exist.
7174 let sort_by_output: Option<Vec<usize>> = if stmt.distinct
7175 || stmt.limit_with_ties
7176 || srf_position.is_some()
7177 || topk_stream.is_some()
7178 {
7179 None
7180 } else {
7181 order_by_output_cols_if_identical(&order_by, &projection, schema_cols)
7182 };
7183 // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
7184 // kept rows in `tagged`. Probing on the PROJECTED row as soon as
7185 // it is built means a duplicate costs neither a build_order_keys
7186 // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
7187 // a tagged slot, and the sort below runs over u survivors, not
7188 // n input rows — PG's hash-distinct-then-sort plan shape.
7189 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
7190 hashbrown::HashMap::new();
7191 let distinct_hb = hashbrown::DefaultHashBuilder::default();
7192 // v7.38.13 — which output positions must NOT fold. Built once per
7193 // scan from the projection, which carries the source column's
7194 // byte-wise-ness; see `FoldSpec`.
7195 let distinct_mask = fold_mask(&projection);
7196 // v7.39 (round 485) — one projection buffer for the whole scan
7197 // rather than a fresh `Vec` per input row. A row that survives
7198 // the DISTINCT probe takes the buffer with it (`mem::take`) and
7199 // the next row allocates a new one; a row that duplicates an
7200 // earlier one leaves the buffer — and its capacity — in place.
7201 // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
7202 // projected rows are duplicates, so that is 49 900 allocate /
7203 // free pairs the scan no longer performs. Shapes where every row
7204 // survives (plain projection, `DISTINCT` over a unique column)
7205 // allocate exactly as often as before.
7206 let mut proj_buf: Vec<Value<'static>> = Vec::new();
7207 // v7.39 (round 571) — buffers handed back by the top-N trim.
7208 // Round 485 made the scan share ONE projection buffer, but a
7209 // surviving row takes it (`mem::take`) and without DISTINCT
7210 // almost every row survives, so the next one starts from zero
7211 // capacity and allocates. The trim drops `keep` rows at a time
7212 // and their buffers come back here instead of being freed.
7213 let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
7214 let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
7215 // v7.39 (round 581) — the worst row the accumulator is currently
7216 // keeping. Anything that loses to it cannot reach the answer, so
7217 // it is dropped before its projection is ever built.
7218 let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
7219 // v7.38.20 — the boundary's own leading eight bytes, so a losing
7220 // row can be turned away before a key is built for it. Kept
7221 // beside the boundary and refreshed with it; `None` whenever the
7222 // boundary's first key is not one this can read, which sends
7223 // every row down the ordinary path.
7224 // v7.38.21 — and whether those bytes may be trusted under the
7225 // collation in force, which is the boundary's own text to answer.
7226 let mut topk_boundary_prefix: Option<(crate::orderby::PrefixKind, u64, bool)> = None;
7227 // v7.39 (round 582) — resolve each ORDER BY column once, not
7228 // once per row. See `order_by_bound_positions`.
7229 let order_bound =
7230 crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
7231 // v7.39.12 — a correlated scalar subquery in ORDER BY is
7232 // resolved for the row before its key is built.
7233 //
7234 // Uncorrelated subqueries are replaced by a literal before
7235 // execution; a correlated one cannot be, so it reached the
7236 // per-row evaluator — the one place that cannot run a subquery
7237 // — and the statement raised "subquery reached row eval".
7238 // Reported by sentori against 7.39.11; see
7239 // `Engine::order_by_resolved_for_row`.
7240 //
7241 // The `any` runs once, here, so an ordinary ORDER BY pays one
7242 // bool per row and nothing else.
7243 let order_has_subquery = order_by
7244 .iter()
7245 .any(|o| crate::subquery::expr_has_subquery(&o.expr));
7246 let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
7247 // v7.39 (round 581) — and it stops asking when the answer is
7248 // always "keep".
7249 //
7250 // The check earns its place only on rows it rejects. Over
7251 // ascending ids, `ORDER BY id DESC` never rejects one — every
7252 // row beats the current worst — so the comparison is pure
7253 // overhead there, measured at +5.5% in three batches out of
7254 // three. After a window of rows it looks at what it has
7255 // actually rejected and switches itself off if the shape is not
7256 // paying. The answers do not depend on it either way.
7257 // v7.38.21 — resolved once per query, not per row.
7258 //
7259 // No collation at all is the case v7.38.20 shipped. A DECLARED
7260 // one may still be answered by bytes, and which collations those
7261 // are is `Collated::ascii_byte_order`'s to say — the same
7262 // allowlist `byte_order_answers_the_collation` consults, so the
7263 // two cannot come to disagree about a collation. What that
7264 // allowlist requires of the TEXT is checked per row and on the
7265 // boundary, because a streaming top-N has no batch to check.
7266 let boundary_no_collation = order_colls.iter().all(Option::is_none);
7267 let boundary_collations_permit = boundary_no_collation
7268 || order_colls
7269 .iter()
7270 .flatten()
7271 .all(crate::collate::Collated::ascii_byte_order);
7272 const BOUNDARY_WINDOW: u32 = 8192;
7273 let mut boundary_checks: u32 = 0;
7274 let mut boundary_rejects: u32 = 0;
7275 let mut boundary_check_on = true;
7276 // Inline the per-row work in a closure so the indexed and full-
7277 // scan branches share the body.
7278 // v7.38.19 — `check_where` is per CALL SITE, not per closure: the
7279 // full-scan loops below must apply the predicate, and the
7280 // indexed loop must not when the seek already did. A captured
7281 // flag would have to be right for both.
7282 let mut process_row = |row: &Row<'static>,
7283 loop_idx: usize,
7284 check_where: bool|
7285 -> Result<(), EngineError> {
7286 if loop_idx.is_multiple_of(256) {
7287 cancel.check()?;
7288 }
7289 if !check_where {
7290 // The seek answered the whole predicate. See
7291 // `index_access::Seeked`.
7292 } else if let Some(cw) = &compiled_where {
7293 let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7294 .map_err(EngineError::Eval)?;
7295 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7296 return Ok(());
7297 }
7298 } else if let Some(where_expr) = &stmt.where_ {
7299 let cond =
7300 self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7301 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7302 return Ok(());
7303 }
7304 }
7305 // Under DISTINCT the keys are built AFTER the dup probe
7306 // (survivors only); the non-distinct order is unchanged.
7307 // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7308 // row further down, and building them here would evaluate the
7309 // ORDER BY against the INPUT row: a key naming the SRF's own
7310 // output became a scalar call to it, which is where
7311 // "function unnest(integer[]) does not exist" came from.
7312 let order_keys = if order_by.is_empty()
7313 || stmt.distinct
7314 || srf_position.is_some()
7315 // v7.38.19 — the branch below builds whatever key it
7316 // needs from the projected values, collation included,
7317 // so nothing has to be built here for it.
7318 //
7319 // A draft that skipped them here but still let the
7320 // COLLATED case fall through to the key-based sort put a
7321 // mixed column back in INSERT order: every key empty,
7322 // every row equal, a stable sort faithfully preserving
7323 // nothing. The rule is one decision, not two.
7324 || sort_by_output.is_some()
7325 {
7326 Vec::new()
7327 } else {
7328 // v7.38.20 — turn a decisively losing row away before
7329 // its key is built. Only the FIRST key is read, and only
7330 // its leading eight bytes; a tie there decides nothing
7331 // and falls through to the full path below.
7332 //
7333 // ASC only: under DESC the boundary is the largest kept
7334 // key and the comparison flips, which this deliberately
7335 // does not try to express — a second direction in a
7336 // fast-path predicate is how one of them ends up wrong.
7337 if boundary_check_on
7338 && let Some((_, descs)) = &topk_stream
7339 && !descs.first().copied().unwrap_or(false)
7340 && order_by.len() == 1
7341 && boundary_collations_permit
7342 && let Some((bkind, bp, boundary_is_ascii)) = topk_boundary_prefix
7343 && let Some((rkind, rp, row_is_ascii)) =
7344 crate::orderby::first_key_prefix(&order_bound, row)
7345 && bkind == rkind
7346 && (boundary_no_collation || (boundary_is_ascii && row_is_ascii))
7347 && rp > bp
7348 {
7349 boundary_checks += 1;
7350 boundary_rejects += 1;
7351 if boundary_checks == BOUNDARY_WINDOW {
7352 boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7353 }
7354 return Ok(());
7355 }
7356 let mut buf = key_pool.pop().unwrap_or_default();
7357 if order_has_subquery {
7358 // A substituted literal is no longer a bound column.
7359 let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7360 crate::orderby::build_order_keys_bound(
7361 per_row.as_deref().unwrap_or(&order_by),
7362 &unbound,
7363 &order_colls,
7364 row,
7365 &ctx,
7366 &mut buf,
7367 )?;
7368 } else {
7369 crate::orderby::build_order_keys_bound(
7370 &order_by,
7371 &order_bound,
7372 &order_colls,
7373 row,
7374 &ctx,
7375 &mut buf,
7376 )?;
7377 }
7378 // v7.39 (round 581) — reject before projecting.
7379 //
7380 // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7381 // 50 distinct `g` decides nearly every row on the FIRST
7382 // key, and PG answers it FASTER than the single-key form
7383 // (7.4 ms against 10.4) because a rejected row costs it
7384 // one comparison. SPG built both keys AND the projected
7385 // row for all 500k before throwing them away. The keys
7386 // are needed to compare; the projection is not.
7387 if boundary_check_on
7388 && let Some((_, descs)) = &topk_stream
7389 && let Some(b) = &topk_boundary
7390 {
7391 boundary_checks += 1;
7392 let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7393 == core::cmp::Ordering::Greater;
7394 if loses {
7395 boundary_rejects += 1;
7396 }
7397 if boundary_checks == BOUNDARY_WINDOW {
7398 // Keep asking only if it has been rejecting at
7399 // least a quarter of what it saw.
7400 boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7401 }
7402 if loses {
7403 buf.clear();
7404 key_pool.push(buf);
7405 return Ok(());
7406 }
7407 }
7408 buf
7409 };
7410 if srf_position.is_some() {
7411 let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7412 for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7413 if stmt.distinct {
7414 let bucket = seen_distinct
7415 .entry(norm_hash_row(
7416 &out,
7417 &distinct_hb,
7418 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7419 ))
7420 .or_default();
7421 if bucket.iter().any(|i| {
7422 row_eq_norm(
7423 &tagged[i].1,
7424 &out,
7425 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7426 )
7427 }) {
7428 continue;
7429 }
7430 bucket.push(tagged.len());
7431 }
7432 budget.charge(approx_row_bytes(&out))?;
7433 // The keys come from THIS expanded row: a key naming a
7434 // select-list item reads its value, anything else is
7435 // still evaluated against the input row.
7436 let keys = if order_by.is_empty() {
7437 Vec::new()
7438 } else {
7439 let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7440 for (k, ob) in order_by.iter().enumerate() {
7441 kv.push(match srf_order_cols.get(k).copied().flatten() {
7442 Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7443 None => eval::eval_expr(&ob.expr, row, &ctx)
7444 .map_err(EngineError::Eval)?,
7445 });
7446 }
7447 // Packed by the same code every other ORDER BY uses,
7448 // so DESC / NULLS FIRST / the MySQL rule are not
7449 // restated here.
7450 let key_row = Row::new(kv);
7451 let mut buf = Vec::new();
7452 crate::orderby::build_order_keys_bound(
7453 &order_by,
7454 &srf_key_bound,
7455 &order_colls,
7456 &key_row,
7457 &ctx,
7458 &mut buf,
7459 )?;
7460 buf
7461 };
7462 tagged.push((keys, out));
7463 }
7464 } else {
7465 let values = &mut proj_buf;
7466 values.clear();
7467 values.reserve(projection.len());
7468 for (i, p) in projection.iter().enumerate() {
7469 // v7.37.x (docker-fair SCALARSQ attack) — pre-
7470 // analysed PK-probe fast path. The per-row work is
7471 // a read of outer.col from the row plus an index
7472 // probe — no Expr clone, no walker, no
7473 // `eval_expr_with_correlated` framework.
7474 if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7475 values.push(self.probe_with_pk_fast_path(fp, row));
7476 continue;
7477 }
7478 // v7.39 (round 605) — the same value every row.
7479 if any_proj_const && let Some(v) = &proj_const[i] {
7480 values.push(v.clone());
7481 continue;
7482 }
7483 // v7.39 (round 487) — bound column: read the cell.
7484 // This is `rehydrate_cell`'s body for a non-composite
7485 // column, which is what the whole chain below reduces
7486 // to once the name has been resolved.
7487 if any_proj_direct && let Some(pos) = proj_direct[i] {
7488 crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7489 values.push(row.values[pos].clone().into_owned());
7490 continue;
7491 }
7492 // v7.24 (round-16 B) — correlated-aware.
7493 // v7.37.x (docker-fair SCALARSQ attack) — share the
7494 // per-row memo with projection. Required for the
7495 // batch-evaluated correlated-scalar path to fire on
7496 // SELECT-item scalar subqueries; otherwise each row
7497 // re-executes the inner.
7498 //
7499 // Skip the memo when the outer row count is small
7500 // (early-limited): the batch path scans the FULL
7501 // inner table to build a GroupMap (~5 ms for a
7502 // 12.5 k-row inner), while per-row execution with a
7503 // PK index seek is ~5 µs per call — much cheaper for
7504 // N ≤ ~1000 outer rows.
7505 let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7506 let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7507 values.push(
7508 self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7509 );
7510 }
7511 crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7512 if stmt.distinct {
7513 let bucket = seen_distinct
7514 .entry(norm_hash_values(
7515 &proj_buf,
7516 &distinct_hb,
7517 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7518 ))
7519 .or_default();
7520 if bucket.iter().any(|i| {
7521 values_eq_norm(
7522 &tagged[i].1.values,
7523 &proj_buf,
7524 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7525 )
7526 }) {
7527 crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7528 return Ok(());
7529 }
7530 bucket.push(tagged.len());
7531 }
7532 let out = Row::new(core::mem::replace(
7533 &mut proj_buf,
7534 proj_pool.pop().unwrap_or_default(),
7535 ));
7536 let order_keys = if stmt.distinct && !order_by.is_empty() {
7537 // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7538 // the bound-cell path precisely so an ORDER BY key that
7539 // names a column is READ instead of evaluated, and the
7540 // non-DISTINCT branch above has passed it ever since;
7541 // this branch never did, so `SELECT DISTINCT k .. ORDER
7542 // BY k` resolved "k" by string for every surviving row.
7543 let mut buf = key_pool.pop().unwrap_or_default();
7544 if order_has_subquery {
7545 // A substituted literal is no longer a bound column.
7546 let per_row =
7547 self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7548 crate::orderby::build_order_keys_bound(
7549 per_row.as_deref().unwrap_or(&order_by),
7550 &unbound,
7551 &order_colls,
7552 row,
7553 &ctx,
7554 &mut buf,
7555 )?;
7556 } else {
7557 crate::orderby::build_order_keys_bound(
7558 &order_by,
7559 &order_bound,
7560 &order_colls,
7561 row,
7562 &ctx,
7563 &mut buf,
7564 )?;
7565 }
7566 buf
7567 } else {
7568 order_keys
7569 };
7570 budget.charge(approx_row_bytes(&out))?;
7571 tagged.push((order_keys, out));
7572 }
7573 // Streaming top-N: bound the accumulator to O(keep) rows.
7574 if let Some((k, descs)) = &topk_stream {
7575 crate::orderby::topk_trim_recycling(
7576 &mut tagged,
7577 *k,
7578 descs,
7579 &mut proj_pool,
7580 &mut key_pool,
7581 &mut topk_boundary,
7582 );
7583 // The prefix follows the boundary it summarises.
7584 topk_boundary_prefix = topk_boundary
7585 .as_ref()
7586 .and_then(|b| b.first())
7587 .and_then(crate::orderby::order_key_prefix);
7588 }
7589 Ok(())
7590 };
7591 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7592 // load-bearing full-scan path. This is the primary single-table
7593 // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7594 // in-place writers retain dead/old versions, an ungated scan
7595 // here would return them, so the gate must land BEFORE the
7596 // writers flip (see the plan's activation-order rule). A no-op
7597 // today: every hot row is frozen or committed-and-alive under
7598 // the reader's snapshot, so `is_row_visible` returns true for
7599 // all of them (verified by the full e2e suite staying green).
7600 let scan_snapshot = self.current_snapshot();
7601 let mut emitted: usize = 0;
7602 if let Some(seeked) = &indexed_rows {
7603 let recheck = !seeked.exact;
7604 for (loop_idx, cow) in seeked.rows.iter().enumerate() {
7605 if let Some(cap) = early_cap
7606 && emitted >= cap
7607 {
7608 break;
7609 }
7610 process_row(cow.as_ref(), loop_idx, recheck)?;
7611 emitted = emitted.saturating_add(1);
7612 }
7613 } else {
7614 // v7.39 (round 570) — the row store is a 32-way trie, so
7615 // indexing it is four dependent loads. Round 567 measured
7616 // -18% on the aggregate scan from holding the leaf between
7617 // rows; this is the same loop for the projecting scan.
7618 let mut rows_cur = table.rows().run_cursor();
7619 // v7.38.11 — see the aggregate scan above: a BRIN index on a
7620 // column this WHERE bounds says which slots cannot match.
7621 let brin_slots = stmt
7622 .where_
7623 .as_ref()
7624 .and_then(|w| crate::brin::candidate_slots(w, table))
7625 .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7626 for i in brin_slots.into_iter().flatten() {
7627 if let Some(cap) = early_cap
7628 && emitted >= cap
7629 {
7630 break;
7631 }
7632 // Skip rows this snapshot cannot see (invisible rows do
7633 // not count toward the LIMIT).
7634 if !table.is_row_visible(i, &scan_snapshot) {
7635 continue;
7636 }
7637 let Some(row) = rows_cur.get(i) else { continue };
7638 process_row(row, i, true)?;
7639 emitted = emitted.saturating_add(1);
7640 }
7641 // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7642 // rows into the same loop. The full-scan path here is the
7643 // load-bearing single-table SELECT executor, and pre-
7644 // 7.35.1 it only walked `table.rows()` (hot), so any
7645 // `SELECT … FROM t` against a table with cold segments
7646 // silently returned a subset.
7647 let cold_rows = self.iter_cold_rows_of_table(table);
7648 for (offset, row) in cold_rows.iter().enumerate() {
7649 if let Some(cap) = early_cap
7650 && emitted >= cap
7651 {
7652 break;
7653 }
7654 process_row(row, table.row_count() + offset, true)?;
7655 emitted = emitted.saturating_add(1);
7656 }
7657 }
7658
7659 // (DISTINCT already de-duped STREAMING inside process_row, so the
7660 // sort below only sees the u survivors and the partial-sort
7661 // budget applies to DISTINCT too.)
7662 if !order_by.is_empty() {
7663 // Partial-sort fast path: when LIMIT is small relative to
7664 // the row count, select_nth_unstable + sort just the
7665 // prefix is O(n + k log k) instead of O(n log n).
7666 // WITH TIES needs the full sort so the tie extension can
7667 // scan past `limit` to find rows that share the last-kept
7668 // row's key.
7669 let keep = if stmt.limit_with_ties
7670 // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7671 // forces the full-sort fallback by suppressing the
7672 // partial-sort `keep` budget. See
7673 // `xtests/sigil/test-mode-gucs.md`.
7674 || self.env_cfg().disable_topk
7675 {
7676 None
7677 } else {
7678 stmt.limit_literal()
7679 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7680 };
7681 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7682 if let Some(cols) = &sort_by_output {
7683 // No keys were built; the sort reads the projected row.
7684 // The comparator is the value-level one the window
7685 // functions and the key path both defer to, so DESC,
7686 // NULLS placement, the MySQL fold and the collation are
7687 // not restated here.
7688 let terms: Vec<(usize, bool, Option<bool>)> = cols
7689 .iter()
7690 .zip(order_by.iter())
7691 .map(|(c, o)| (*c, o.desc, o.nulls_first))
7692 .collect();
7693 let mysql = ctx.mysql_dialect;
7694 // v7.38.19 — sort a PERMUTATION carrying the first eight
7695 // bytes, not the rows.
7696 //
7697 // The elements above are `(Vec<OrderKey>, Row)`, 48 bytes,
7698 // and driftsort moves them ~n log n times: 7.4 M moves at
7699 // 400,000 rows. Worse, every comparison chases three
7700 // dependent loads PER SIDE to reach the byte it wants --
7701 // the row's `Vec`, the `Value`, then the string's own
7702 // buffer -- and a profile of this sort put 35% of its
7703 // working samples in the sort machinery around that.
7704 //
7705 // A `(u64, u32)` is 16 bytes and the comparison reads it
7706 // straight out of the array. The u64 is the first eight
7707 // bytes big-endian, zero-padded, which ORDERS THE SAME as
7708 // the string: if two differ inside those bytes they differ
7709 // at the same index either way, and a string shorter than
7710 // eight pads with zeros exactly where `[u8]`'s own
7711 // comparison runs out. Equal prefixes fall through to the
7712 // full comparator, so nothing rests on the padding being
7713 // clever.
7714 //
7715 // The tail-break on the index is what keeps the sort
7716 // STABLE, which `sort_by` was giving for free and an
7717 // unstable sort over a permutation would not.
7718 // v7.38.19 — three ways to sort these rows, and which
7719 // one is right turns on the values, which is why it is
7720 // decided here rather than at plan time.
7721 //
7722 // * the collation orders these values the way bytes do
7723 // -- take the eight-byte key below
7724 // * it does not, but there IS a collation -- build its
7725 // sort key once per row and order the permutation on
7726 // those, which is what the key path did, done from
7727 // the projected value instead of during the scan
7728 // * no collation at all -- the eight-byte key again
7729 //
7730 // The middle case is the one a draft got wrong by
7731 // leaving the rows to a key path whose keys it had just
7732 // skipped building.
7733 let mut keep_sorted = false;
7734 let bytes_answer = byte_order_answers_the_collation(&tagged, &terms, &order_colls);
7735 if !bytes_answer && let Some(coll) = order_colls.first().and_then(Option::as_ref) {
7736 let (first_col, first_desc, _) = terms[0];
7737 let mut order: Vec<(Vec<u8>, u32)> = Vec::with_capacity(tagged.len());
7738 for (i, row) in tagged.iter().enumerate() {
7739 let k = match row.1.values.get(first_col) {
7740 Some(Value::Text(t)) => coll.sort_key_of(t).unwrap_or_else(|| {
7741 let mut v = Vec::with_capacity(t.len() + 1);
7742 v.push(0);
7743 v.extend_from_slice(t.as_bytes());
7744 v
7745 }),
7746 _ => Vec::new(),
7747 };
7748 order.push((k, u32::try_from(i).unwrap_or(u32::MAX)));
7749 }
7750 // v7.40.4 — collated sort keys across threads. This
7751 // comparator ends on the row index like every other
7752 // one here, so it is a strict total order and the
7753 // split cannot reach a different answer; see
7754 // `crate::parsort`. It is also the path a customer on
7755 // a locale collation actually runs, which is why the
7756 // module moves its elements rather than copying them:
7757 // an ICU sort key is a `Vec<u8>`.
7758 let order = crate::parsort::sort_total(
7759 order,
7760 self.session_parallel_workers(),
7761 &|(ka, ia): &(Vec<u8>, u32), (kb, ib): &(Vec<u8>, u32)| {
7762 let c = ka.cmp(kb);
7763 let c = if first_desc { c.reverse() } else { c };
7764 if c != core::cmp::Ordering::Equal {
7765 return c;
7766 }
7767 row_cmp_by_index(&tagged, &terms, &order_colls, mysql, *ia, *ib)
7768 .then_with(|| ia.cmp(ib))
7769 },
7770 );
7771 let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7772 core::mem::take(&mut tagged).into_iter().map(Some).collect();
7773 tagged = order
7774 .iter()
7775 .map(|&(_, i)| {
7776 slots[i as usize]
7777 .take()
7778 .expect("the permutation names each row once")
7779 })
7780 .collect();
7781 keep_sorted = true;
7782 }
7783 // v7.38.20 — a key that does NOT discriminate is still
7784 // worth sorting on, as long as the runs it leaves are
7785 // handled once instead of n log n times.
7786 //
7787 // `text (26 values)` is two hundred identical characters
7788 // drawn from twenty-six letters, so every eight-byte
7789 // prefix inside a letter is the same and 15,384 rows tie
7790 // on it. A comparison sort then asks ~7.4 M questions of
7791 // which nearly all are a two-hundred-byte `memcmp`
7792 // answering EQUAL: profiled, 30% of the working samples
7793 // sat in `memcmp` and 37% in the sort machinery.
7794 //
7795 // Sorting the integer keys is cheap. What each run needs
7796 // afterwards is ONE pass: if every value in it is equal,
7797 // input order already IS the stable answer, and proving
7798 // that costs n-1 comparisons rather than n log n. Only a
7799 // run that is not all-equal gets sorted.
7800 //
7801 // Single-term only. With a second ORDER BY column an
7802 // all-equal first term does not settle the row order --
7803 // the later terms still speak -- and the shortcut would
7804 // drop them.
7805 let all_keys = if keep_sorted {
7806 None
7807 } else {
7808 sort_keys_of(&tagged, terms[0].0)
7809 };
7810 let (worth_it, key_exact) = match all_keys.as_ref() {
7811 Some(PrefixKeys::Narrow(k, e)) => (*e || key_discriminates(k), *e),
7812 Some(PrefixKeys::Wide(k, e)) => (*e || key_discriminates(k), *e),
7813 None => (false, false),
7814 };
7815 let low_card = !keep_sorted && terms.len() == 1 && !key_exact && !worth_it;
7816 let keyed = all_keys.filter(|_| worth_it || low_card);
7817 if keep_sorted {
7818 // The collated permutation above already placed every
7819 // row. A draft let the byte-order fallback run after
7820 // it and undo the whole thing.
7821 } else if let Some(keys) = keyed {
7822 let exact = key_exact;
7823 let (first_col, first_desc, _) = terms[0];
7824 let row_cmp = |ia: u32, ib: u32| -> core::cmp::Ordering {
7825 let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
7826 for (col, desc, nf) in &terms {
7827 let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col))
7828 else {
7829 continue;
7830 };
7831 let ord = match (va, vb) {
7832 (Value::Text(x), Value::Text(y)) if !mysql => {
7833 let c = crate::orderby::str_cmp_prefix_first(x, y);
7834 if *desc { c.reverse() } else { c }
7835 }
7836 _ => {
7837 crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7838 }
7839 };
7840 if ord != core::cmp::Ordering::Equal {
7841 return ord;
7842 }
7843 }
7844 core::cmp::Ordering::Equal
7845 };
7846 // v7.40.2 — the uniform check reads a COMPACT column,
7847 // not the rows.
7848 //
7849 // `low_card` sorts the prefix keys and then proves,
7850 // once per run, whether every value in it is equal.
7851 // That proof is n-1 comparisons, and each one used to
7852 // walk `order[i] -> tagged[i] -> values -> Value ->
7853 // &str -> bytes`: four dependent reads, the first of
7854 // them scattered across a 400,000-element array of fat
7855 // rows. This file's own note at `key_discriminates`
7856 // warned about that random read; here is its price.
7857 //
7858 // Measured, 400,000 rows, twenty-six distinct values,
7859 // server-reported, against the same binary with
7860 // `low_card` forced off (md5-witnessed, order digests
7861 // identical):
7862 //
7863 // 8-byte values 54.98 ms on 54.91 ms off
7864 // 200-byte values 89.64 ms on 163.18 ms off
7865 //
7866 // So the path earns 1.82x and is not in question. What
7867 // the 8-byte and 200-byte cells say together is where
7868 // the rest goes: the extra 192 bytes a row cost
7869 // 34.6 ms, which is 80 MB compared at 2.3 GB/s — an
7870 // order of magnitude under this machine's memory
7871 // bandwidth, because the cost is the misses and not
7872 // the compare.
7873 //
7874 // Collecting the column first is one sequential pass
7875 // over `tagged` and leaves the comparison two reads:
7876 // a 16-byte slice header, then its bytes.
7877 // Built ONLY for the branch that uses it. `same_value`
7878 // is called from the `low_card` run walk and nowhere
7879 // else, so an exact key -- every value inside sixteen
7880 // bytes -- never asks the question. The first version
7881 // built the column unconditionally and charged 2.1 ms
7882 // to a shape that never reads it:
7883 //
7884 // 8-byte values 56.45 -> 58.55 ms (a tax)
7885 // 200-byte values 97.18 -> 84.70 ms (the point)
7886 let col_strs: Option<Vec<&str>> = if low_card {
7887 tagged
7888 .iter()
7889 .map(|t| match t.1.values.get(first_col) {
7890 Some(Value::Text(x)) => Some(x.as_ref()),
7891 _ => None,
7892 })
7893 .collect()
7894 } else {
7895 None
7896 };
7897 let same_value = |ia: u32, ib: u32| -> bool {
7898 col_strs.as_ref().map_or_else(
7899 || {
7900 tagged[ia as usize].1.values.get(first_col)
7901 == tagged[ib as usize].1.values.get(first_col)
7902 },
7903 |c| c[ia as usize] == c[ib as usize],
7904 )
7905 };
7906 let how = PrefixSort {
7907 first_desc,
7908 low_card,
7909 exact,
7910 single_term: terms.len() == 1,
7911 workers: self.session_parallel_workers(),
7912 };
7913 let order: Vec<u32> = match keys {
7914 PrefixKeys::Narrow(v, _) => {
7915 sort_prefix_permutation(v, &how, &row_cmp, &same_value)
7916 }
7917 PrefixKeys::Wide(v, _) => {
7918 sort_prefix_permutation(v, &how, &row_cmp, &same_value)
7919 }
7920 };
7921 let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7922 core::mem::take(&mut tagged).into_iter().map(Some).collect();
7923 tagged = order
7924 .iter()
7925 .map(|&i| {
7926 slots[i as usize]
7927 .take()
7928 .expect("the permutation names each row once")
7929 })
7930 .collect();
7931 } else {
7932 tagged.sort_by(|a, b| {
7933 for (i, (col, desc, nf)) in terms.iter().enumerate() {
7934 let va = a.1.values.get(*col);
7935 let vb = b.1.values.get(*col);
7936 let (Some(va), Some(vb)) = (va, vb) else {
7937 continue;
7938 };
7939 let _ = i;
7940 // v7.38.19 — two non-NULL strings, no MySQL fold, is
7941 // where a text sort spends every one of its ~7 M
7942 // comparisons, and the shared comparator cannot be
7943 // inlined into this loop: it carries NULL placement,
7944 // the fold, the NUMERIC bignum gate and the float
7945 // total order. Answering that one pair here is the
7946 // same answer by the same route — `value_cmp`'s
7947 // leading same-variant arm is `x.cmp(y)`, and the
7948 // raw comparator's last act is this reverse.
7949 let ord = match (va, vb) {
7950 (Value::Text(x), Value::Text(y)) if !mysql => {
7951 let c = crate::orderby::str_cmp_prefix_first(x, y);
7952 if *desc { c.reverse() } else { c }
7953 }
7954 _ => {
7955 crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7956 }
7957 };
7958 if ord != core::cmp::Ordering::Equal {
7959 return ord;
7960 }
7961 }
7962 core::cmp::Ordering::Equal
7963 });
7964 }
7965 } else {
7966 crate::orderby::partial_sort_tagged_in(
7967 &mut tagged,
7968 keep,
7969 &descs,
7970 &order_colls,
7971 self.session_parallel_workers(),
7972 );
7973 }
7974 }
7975
7976 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7977 // past the truncated tail through every row that shares the
7978 // last-kept row's ORDER BY key. The tie check uses the
7979 // already-computed `(order_keys, row)` pairs so it matches
7980 // the sort comparator exactly. DISTINCT + WITH TIES falls
7981 // through to the no-ties path (PG also disallows their
7982 // combination; SPG silently drops the tie extension here so
7983 // the customer doesn't see a hard error mid-query — the
7984 // user-visible result is still correct, just narrower).
7985 let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7986 apply_offset_and_limit_tagged(
7987 &mut tagged,
7988 stmt.offset_literal(),
7989 stmt.limit_literal(),
7990 true,
7991 );
7992 tagged.into_iter().map(|(_, r)| r).collect()
7993 } else {
7994 // DISTINCT already de-duped pre-sort above.
7995 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7996 apply_offset_and_limit(
7997 &mut output_rows,
7998 stmt.offset_literal(),
7999 stmt.limit_literal(),
8000 );
8001 output_rows
8002 };
8003
8004 let columns: Vec<ColumnSchema> = projection
8005 .into_iter()
8006 .map(|p| p.to_column_schema())
8007 .collect();
8008
8009 Ok(QueryResult::Rows {
8010 columns,
8011 rows: output_rows,
8012 })
8013 }
8014
8015 /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
8016 /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
8017 /// select items for the surviving rows only — PG's Result-above-
8018 /// Limit shape, where SubPlan loops equal the OUTPUT row count
8019 /// (50) instead of the group count (24k).
8020 fn finish_agg_result(
8021 &self,
8022 mut agg: aggregate::AggResult,
8023 stmt: &SelectStatement,
8024 cancel: CancelToken<'_>,
8025 ) -> Result<QueryResult, EngineError> {
8026 apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
8027 if !agg.deferred.is_empty() {
8028 apply_offset_and_limit(
8029 &mut agg.synth_rows,
8030 stmt.offset_literal(),
8031 stmt.limit_literal(),
8032 );
8033 let ctx = EvalContext::new(&agg.synth_schema, None);
8034 let mut memo = memoize::MemoizeCache::default();
8035 // v7.32 (architecture v2 P3) — keyed index-probe seeding.
8036 // Deferred subqueries are referenced only by surviving
8037 // select-list rows (≤ LIMIT), so their correlation keys are
8038 // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
8039 // each batchable subquery's group map over just those keys
8040 // via per-key index seek; the per-row splice loop below then
8041 // reuses the seeded map. A join-shaped or un-indexed inner
8042 // falls through to the all-keys batch inside the call (built
8043 // eagerly here instead of lazily on row 0 — same cost), so
8044 // it still pays the full scan, never the 715 ms per-row
8045 // direct eval; its index-nested-loop probe is the next
8046 // knife. Genuinely non-batchable shapes return None and are
8047 // left unseeded for the loop's per-row resolver, as before.
8048 for (_, expr) in &agg.deferred {
8049 let mut subs: Vec<&SelectStatement> = Vec::new();
8050 collect_scalar_subqueries(expr, &mut subs);
8051 for sub in subs {
8052 let repr = alloc::format!("{sub}");
8053 if memo.group_maps.contains_key(&repr) {
8054 continue;
8055 }
8056 if let Some(gm) = self.try_batch_correlated_scalar(
8057 sub,
8058 Some((&agg.synth_rows, &ctx)),
8059 cancel,
8060 )? {
8061 memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
8062 }
8063 }
8064 }
8065 for (ri, srow) in agg.synth_rows.iter().enumerate() {
8066 cancel.check()?;
8067 for (col, expr) in &agg.deferred {
8068 let v =
8069 self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
8070 if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
8071 *cell = v;
8072 }
8073 }
8074 }
8075 }
8076 Ok(QueryResult::Rows {
8077 columns: agg.columns,
8078 rows: agg.rows,
8079 })
8080 }
8081
8082 /// v7.37 — streaming projection for the joined-non-aggregate
8083 /// shape (multi-table FROM, all projection items bound, no
8084 /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
8085 /// UNION). Walks the deferred join survivors and emits
8086 /// `&[&Value]` borrowed straight out of the source tables — no
8087 /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
8088 /// on the mailrs `PROJ` shape (about 4 ms saved).
8089 ///
8090 /// Returns `Ok(None)` when the shape doesn't qualify; the caller
8091 /// then falls back to the materialising path.
8092 /// v7.37 (round 831) — stream a joinless SELECT straight off the
8093 /// stored table, one row at a time, without ever building a row set.
8094 ///
8095 /// Returns `Ok(None)` for anything this cannot serve, and the caller
8096 /// falls through to the deferred-join path exactly as before: a
8097 /// missing table, or a cold tier whose hydration the fallback handles.
8098 /// Sort a single-table scan through the external sorter, so the
8099 /// answer's size is bounded by `work_mem` and not by the input.
8100 ///
8101 /// Sorting held every row twice — the scan's `Vec<Row>` and the
8102 /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
8103 /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
8104 /// enough ORDER BY took the server down, which is a liveness
8105 /// problem before it is a performance one.
8106 ///
8107 /// A SEPARATE walk rather than a change to `run_single_table_scan`,
8108 /// following what round 831 did for the joinless shape. That
8109 /// function is 552 lines whose projection loop is entangled with
8110 /// DISTINCT (which indexes back into the tagged vector) and with
8111 /// streaming top-N (whose boundary moves as the scan runs); both
8112 /// assume the projection has already happened when a row is
8113 /// pushed, which is exactly what spilling has to defer. Two earlier
8114 /// attempts tried to rework that loop and were reverted. Here the
8115 /// existing path is untouched and this one only claims shapes it
8116 /// can serve, so a decline costs nothing.
8117 ///
8118 /// Records are SOURCE rows, not projected ones: `finish` re-derives
8119 /// keys from what it decodes, and an ORDER BY key need not be in
8120 /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
8121 fn try_spill_sorted_scan(
8122 &self,
8123 stmt: &SelectStatement,
8124 from: &FromClause,
8125 cancel: CancelToken<'_>,
8126 ) -> Result<Option<QueryResult>, EngineError> {
8127 // Shapes this walk does not serve. Each one either needs the
8128 // whole tagged vector addressable (DISTINCT probes back into
8129 // it, WITH TIES re-reads its tail) or is already bounded
8130 // without spilling (a LIMIT makes the partial sort O(keep)).
8131 if !self.can_spill()
8132 || stmt.order_by.is_empty()
8133 || stmt.distinct
8134 || stmt.limit_with_ties
8135 || stmt.limit_literal().is_some()
8136 || !from.joins.is_empty()
8137 || from.primary.lateral_subquery.is_some()
8138 || from.primary.unnest_expr.is_some()
8139 || from.primary.generate_series_args.is_some()
8140 || select_has_window(stmt)
8141 {
8142 return Ok(None);
8143 }
8144 // A parent's rows are its children's. These walks scan the named
8145 // relation alone, so a partitioned or inherited parent comes back
8146 // short — and silently: the corpus caught `SELECT id FROM pr
8147 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8148 // parent's own rows instead of the partitions'. `ONLY` is exactly
8149 // the case that does not fan out, so it stays, which is the test
8150 // the FROM-clause fan-out itself makes.
8151 if !from.primary.only
8152 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8153 {
8154 return Ok(None);
8155 }
8156 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8157 return Ok(None);
8158 };
8159 // Cold-tier rows live outside `rows()`; this walk would drop
8160 // them silently, the same reason round 831's walk declines.
8161 if table.has_cold_rows_fast() {
8162 return Ok(None);
8163 }
8164
8165 let alias = from
8166 .primary
8167 .alias
8168 .as_deref()
8169 .unwrap_or(from.primary.name.as_str());
8170 let cols = table.schema().columns.clone();
8171 let sess = self.dml_session();
8172 let ctx = EvalContext::new(&cols, Some(alias))
8173 .with_catalog(self.active_catalog())
8174 .with_session(&sess);
8175 let projection = build_projection(
8176 &stmt.items,
8177 &cols,
8178 alias,
8179 self.speaks_mysql,
8180 Some(self.active_catalog()),
8181 )?;
8182 let order_by = stmt.order_by.clone();
8183 // The same one-shot resolution the general path does (round
8184 // 582): each ORDER BY column is bound once, not once per row.
8185 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8186 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8187 // Resolved BEFORE the scan, because it now decides what the sort
8188 // STORES and not just what it decodes (round 995).
8189 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8190
8191 // v7.38.22 — resolved HERE, because this path did not resolve
8192 // them at all.
8193 //
8194 // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8195 // "en_US.utf8"` in BYTE order on this path — and swallowed an
8196 // unknown collation name rather than raising — because the sorter
8197 // below compared with an empty collation slice. The materialising
8198 // path honoured both. Which answer a query got depended on which
8199 // path the planner took, and this is the path a plain single-table
8200 // SELECT takes.
8201 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
8202 // v7.39.12 — a correlated scalar subquery in ORDER BY is
8203 // resolved for the row before its key is built.
8204 //
8205 // Uncorrelated subqueries are replaced by a literal before
8206 // execution; a correlated one cannot be, so it reached the
8207 // per-row evaluator — the one place that cannot run a subquery
8208 // — and the statement raised "subquery reached row eval".
8209 // Reported by sentori against 7.39.11; see
8210 // `Engine::order_by_resolved_for_row`.
8211 //
8212 // The `any` runs once, here, so an ordinary ORDER BY pays one
8213 // bool per row and nothing else.
8214 let order_has_subquery = order_by
8215 .iter()
8216 .any(|o| crate::subquery::expr_has_subquery(&o.expr));
8217 let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
8218 let mut sorter = crate::extsort::ExternalSorter::new(
8219 self.temp_run_factory,
8220 self.session_work_mem_bytes(),
8221 cols.clone(),
8222 &descs,
8223 &order_colls,
8224 )
8225 .with_stats(&self.spill_stats)
8226 .with_workers(self.session_parallel_workers())
8227 .with_pruned(&needed);
8228 let snapshot = self.current_snapshot();
8229 // One key buffer for the whole scan: `push` drains it and leaves
8230 // the capacity behind.
8231 let mut keys: Vec<OrderKey> = Vec::new();
8232 // r1024 — compile the predicate once for the scan.
8233 //
8234 // These two sorted-spill scans are the paths a single-table SELECT
8235 // with an ORDER BY takes, and they were the last row-returning ones
8236 // still walking the expression tree per row. r1023 did the
8237 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8238 // exactly this shape.
8239 //
8240 // Found from the profile's CALL TREE rather than its leaves. The
8241 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8242 // 261, `mod_op` 178 — and two attempts at reasoning out which
8243 // function asked for it were both wrong. The tree names the caller
8244 // chain, and it named this one.
8245 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8246 .where_
8247 .as_ref()
8248 .filter(|w| crate::eval::fully_compilable(w))
8249 .map(|w| crate::eval::compile_expr(w, &ctx));
8250 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8251 for (i, row) in table.scan_visible_from(0, &snapshot) {
8252 if i.is_multiple_of(256) {
8253 cancel.check()?;
8254 }
8255 if let Some(c) = &compiled_where {
8256 if !crate::eval::compiled::eval_compiled_pred(
8257 c,
8258 row,
8259 &ctx,
8260 &mut eval_stack,
8261 ctx.mysql_dialect,
8262 )? {
8263 continue;
8264 }
8265 } else if let Some(w) = &stmt.where_ {
8266 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8267 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8268 continue;
8269 }
8270 }
8271 keys.clear();
8272 // The same collations the sorter compares with, and the
8273 // re-derivation below is handed the same ones. `finish`'s
8274 // contract is that a key comes back the way it was pushed;
8275 // a collation is part of the way it was pushed.
8276 if order_has_subquery {
8277 // A substituted literal is no longer a bound column.
8278 let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
8279 crate::orderby::build_order_keys_bound(
8280 per_row.as_deref().unwrap_or(&order_by),
8281 &unbound,
8282 &order_colls,
8283 row,
8284 &ctx,
8285 &mut keys,
8286 )?;
8287 } else {
8288 crate::orderby::build_order_keys_bound(
8289 &order_by,
8290 &order_bound,
8291 &order_colls,
8292 row,
8293 &ctx,
8294 &mut keys,
8295 )?;
8296 }
8297 sorter.push(&mut keys, row)?;
8298 }
8299
8300 let key_ctx = &ctx;
8301 let rows = sorter.finish(
8302 |src, buf| {
8303 crate::orderby::build_order_keys_rederived(
8304 &order_by,
8305 &order_bound,
8306 &order_colls,
8307 src,
8308 key_ctx,
8309 buf,
8310 )
8311 },
8312 |src| {
8313 let mut values = Vec::with_capacity(projection.len());
8314 for p in &projection {
8315 values.push(
8316 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8317 );
8318 }
8319 Ok(Row::new(values))
8320 },
8321 )?;
8322
8323 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8324 Ok(Some(QueryResult::Rows { columns, rows }))
8325 }
8326
8327 /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
8328 /// handing each row to the consumer instead of collecting the answer.
8329 ///
8330 /// That walk bounds the SORT and then returns `QueryResult::Rows`,
8331 /// which holds every output row. Measured at `work_mem = 4 MB` over
8332 /// 200-byte rows, RSS above the server's own baseline while the
8333 /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
8334 /// at 400k — linear — while the spill underneath worked correctly
8335 /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
8336 /// removes each file, so a count taken afterwards reads 0 whatever
8337 /// happened, and an earlier reading of "no spill at all" was that
8338 /// blind witness). The growth is the collected result, not the sort.
8339 ///
8340 /// Emitting makes peak the budget, one buffer per run and a single
8341 /// row — the state a merge already holds at every step. It also
8342 /// frees each projected row as the next is built rather than
8343 /// accumulating them, which is where the time is: a profile of the
8344 /// collecting walk put the allocator at 586 samples, more than every
8345 /// sort comparison combined (420), against 19 for `push` itself.
8346 /// v7.37 (round 923) — which of a sort record's columns the output half
8347 /// reads. The record is the SOURCE row (round 836), so a narrow projection
8348 /// decoded every column: skipping one 200-byte text halves a decode
8349 /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
8350 ///
8351 /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
8352 /// column reads NULL. Answers only when every projection item is a bare
8353 /// column reference AND every ORDER BY key is a bound column; anything
8354 /// else returns empty, decoding everything as before.
8355 /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
8356 /// drops references from expression kinds it does not enumerate.
8357 ///
8358 /// ORDER BY columns are included — the merge re-derives keys from the
8359 /// decoded row on the spilled path, so pruning one would sort NULLs.
8360 pub(crate) fn sort_record_columns_needed(
8361 items: &[SelectItem],
8362 order_bound: &[Option<usize>],
8363 arity: usize,
8364 ctx: &EvalContext,
8365 ) -> Vec<bool> {
8366 let all_bare = items.iter().all(|i| {
8367 matches!(
8368 i,
8369 SelectItem::Expr {
8370 expr: Expr::Column(_),
8371 ..
8372 }
8373 )
8374 });
8375 if !all_bare || order_bound.iter().any(Option::is_none) {
8376 return Vec::new();
8377 }
8378 let mut mask = alloc::vec![false; arity];
8379 for item in items {
8380 if let SelectItem::Expr {
8381 expr: Expr::Column(c),
8382 ..
8383 } = item
8384 {
8385 match crate::eval::find_column_pos(c, ctx) {
8386 Some(p) if p < arity => mask[p] = true,
8387 _ => return Vec::new(),
8388 }
8389 }
8390 }
8391 for p in order_bound.iter().flatten() {
8392 if *p < arity {
8393 mask[*p] = true;
8394 } else {
8395 return Vec::new();
8396 }
8397 }
8398 mask
8399 }
8400
8401 /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
8402 /// of sorting.
8403 ///
8404 /// PG serves such an ordering from the index and never sorts. We sorted:
8405 /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
8406 /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
8407 /// the sorter's own round trip — `ExternalSorter::finish_each` →
8408 /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
8409 /// Every row is encoded into the sorter's arena and decoded back out,
8410 /// for an order the index already holds.
8411 ///
8412 /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
8413 /// because it was built for top-N. This is the unbounded sibling.
8414 ///
8415 /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
8416 /// from a btree, so walking one would silently drop those rows. That is
8417 /// exactly the defect r1020 fixed on the top-N path, where it had
8418 /// shipped.
8419 /// r1044 — the index this statement's ORDER BY can be WALKED on,
8420 /// instead of sorted, or `None`.
8421 ///
8422 /// Extracted so `EXPLAIN` can ask the same question the executor
8423 /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
8424 /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
8425 /// while the executor walked the primary key — 34.9 ms against
8426 /// 147.0 for the same query ordered by an unindexed column, so the
8427 /// walk was plainly running. Round 551 fixed a different case of
8428 /// this and wrote the reason down: EXPLAIN is the first thing any
8429 /// performance question opens, and an instrument that misnames the
8430 /// access path is worse than one that says nothing.
8431 ///
8432 /// The gate is here once. Two copies of it is how the plan and the
8433 /// executor come to disagree again.
8434 /// v7.39.13 — the shape refusals both ordered-walk gates make.
8435 ///
8436 /// One list, because two of them would be two answers to "can this
8437 /// statement walk an index", and a walk that runs where EXPLAIN says
8438 /// it does not is the defect r1044 exists to prevent.
8439 /// v7.39.13 — `WHERE lead = <literal> ORDER BY next [DESC] LIMIT n`
8440 /// behind an index on `(lead, next, …)`: one seek to the key prefix,
8441 /// then n steps inside it.
8442 ///
8443 /// Sentori's busiest read, and the one shape they have reported
8444 /// unchanged for three versions: `WHERE project_id = ? ORDER BY
8445 /// received_at DESC LIMIT 20`. PostgreSQL 18 answers it with
8446 /// `Limit -> Index Scan`; SPG planned `Sort -> Seq Scan` and sorted
8447 /// the table to return twenty rows, roughly 250x behind.
8448 ///
8449 /// The ordered walk that existed could only start at an index's
8450 /// LEADING column, so an index on `(project_id, received_at)` could
8451 /// serve `ORDER BY project_id` and nothing else. What was missing is
8452 /// below it: a tree walk bounded by a key prefix, which
8453 /// `Index::iter_prefix_desc` now provides.
8454 ///
8455 /// The equality conjunct only NARROWS the walk — the statement's own
8456 /// `WHERE` still runs per row — so picking the wrong conjunct can
8457 /// cost time and cannot change an answer.
8458 pub(crate) fn index_prefix_walk_target(
8459 &self,
8460 stmt: &SelectStatement,
8461 from: &FromClause,
8462 ) -> Option<(String, usize, alloc::vec::Vec<spg_storage::IndexKey>)> {
8463 if self.walk_shape_refused(stmt, from) {
8464 return None;
8465 }
8466 // One ORDER BY term for now: a second one would have to be the
8467 // next key column again, and the tree walks one direction.
8468 if stmt.order_by.len() != 1 || stmt.distinct {
8469 return None;
8470 }
8471 let table = self.active_catalog().get(&from.primary.name)?;
8472 let alias = from
8473 .primary
8474 .alias
8475 .as_deref()
8476 .unwrap_or(from.primary.name.as_str());
8477 let cols = &table.schema().columns;
8478 let order = &stmt.order_by[0];
8479 let Expr::Column(oc) = &order.expr else {
8480 return None;
8481 };
8482 if let Some(q) = &oc.qualifier
8483 && !q.eq_ignore_ascii_case(alias)
8484 {
8485 return None;
8486 }
8487 let order_pos = cols
8488 .iter()
8489 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8490 // The walk comes out in the tree's order, so it may only take an
8491 // ORDER BY whose order that IS — the same question the leading-
8492 // column gate asks, for the same reason.
8493 let order_col = cols.get(order_pos)?;
8494 if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8495 && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8496 {
8497 return None;
8498 }
8499 // A NULL key is not in the tree, and this walk has no separate
8500 // pass for those rows the way the leading-column one does.
8501 if order_col.nullable {
8502 return None;
8503 }
8504 let where_ = stmt.where_.as_ref()?;
8505 for index in table.indices() {
8506 if !matches!(index.kind, spg_storage::IndexKind::BTreeMulti(_))
8507 || index.expression.is_some()
8508 || index.partial_predicate.is_some()
8509 {
8510 continue;
8511 }
8512 // The ORDER BY column must be the key component that follows
8513 // the equality-bound prefix.
8514 if index.extra_column_positions.first() != Some(&order_pos) {
8515 continue;
8516 }
8517 let lead_pos = index.column_position;
8518 let lead_col = cols.get(lead_pos)?;
8519 // The prefix is compared with the tree's own ordering, so the
8520 // leading column has to be one the tree orders bytewise too.
8521 if crate::index_access::collated_column(lead_col, table.db_collation()).is_none()
8522 && !crate::collate::column_key_is_bytewise(lead_col, self.speaks_mysql)
8523 {
8524 continue;
8525 }
8526 let Some(key) = self.eq_literal_key_for(where_, lead_pos, cols, alias) else {
8527 continue;
8528 };
8529 return Some((index.name.clone(), order_pos, alloc::vec![key]));
8530 }
8531 None
8532 }
8533
8534 /// The index key a top-level `AND` conjunct binds `col_pos` to, when
8535 /// one of them is `col = <literal>` (either way round).
8536 ///
8537 /// Only literals: a column reference or a function would have to be
8538 /// evaluated per row, and this runs once for the whole statement.
8539 fn eq_literal_key_for(
8540 &self,
8541 where_: &Expr,
8542 col_pos: usize,
8543 cols: &[ColumnSchema],
8544 alias: &str,
8545 ) -> Option<spg_storage::IndexKey> {
8546 let col = cols.get(col_pos)?;
8547 let mut found: Option<spg_storage::IndexKey> = None;
8548 let mut stack: alloc::vec::Vec<&Expr> = alloc::vec![where_];
8549 while let Some(e) = stack.pop() {
8550 match e {
8551 Expr::Binary {
8552 lhs,
8553 op: spg_sql::ast::BinOp::And,
8554 rhs,
8555 } => {
8556 stack.push(lhs);
8557 stack.push(rhs);
8558 }
8559 Expr::Binary {
8560 lhs,
8561 op: spg_sql::ast::BinOp::Eq,
8562 rhs,
8563 } => {
8564 let names_col = |x: &Expr| match x {
8565 Expr::Column(c) => {
8566 c.name.eq_ignore_ascii_case(&col.name)
8567 && c.qualifier
8568 .as_ref()
8569 .is_none_or(|q| q.eq_ignore_ascii_case(alias))
8570 }
8571 _ => false,
8572 };
8573 let lit = if names_col(lhs) {
8574 Some(&**rhs)
8575 } else if names_col(rhs) {
8576 Some(&**lhs)
8577 } else {
8578 None
8579 };
8580 // v7.39.13 — a BARE literal means whatever the
8581 // COLUMN says it means, and
8582 // `literal_as_column_value` is the one place that
8583 // decision is made. Asking
8584 // `literal_expr_to_value` instead made this the
8585 // fifth copy of it, and it read every string
8586 // literal as text: `WHERE k = '\x07'` on a `bytea`
8587 // column built no key at all, so the walk declined
8588 // and the plan went back to sorting the table —
8589 // while the EQUALITY seek beside it, which does ask
8590 // the one funnel, used the very same index.
8591 //
8592 // Anything that is not a bare literal — a cast, a
8593 // negation — already carries its own type, and
8594 // `from_value_for_column` decides whether that type
8595 // keys for this column.
8596 let v = match lit {
8597 Some(Expr::Literal(l)) => {
8598 crate::index_access::literal_as_column_value(l, col, col_pos)
8599 }
8600 Some(other) => {
8601 crate::conversions::literal_expr_to_value(other.clone()).ok()
8602 }
8603 None => None,
8604 };
8605 if let Some(v) = v
8606 && !v.is_null()
8607 && let Some(k) = spg_storage::IndexKey::from_value_for_column(&v, col.ty)
8608 {
8609 found = Some(k);
8610 }
8611 }
8612 _ => {}
8613 }
8614 }
8615 found
8616 }
8617
8618 fn walk_shape_refused(&self, stmt: &SelectStatement, from: &FromClause) -> bool {
8619 // A non-literal count is refused: `LIMIT $1` is rewritten to a
8620 // literal by `resolve_limit_exprs` before dispatch, so anything
8621 // still carrying a placeholder here has not been through it.
8622 let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8623 matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8624 };
8625 if stmt.order_by.is_empty()
8626 || !stmt.distinct_on.is_empty()
8627 || stmt.limit_with_ties
8628 || !literal_count(&stmt.limit)
8629 || !literal_count(&stmt.offset)
8630 || stmt.having.is_some()
8631 || stmt.group_by.is_some()
8632 || !stmt.unions.is_empty()
8633 || !from.joins.is_empty()
8634 || from.primary.lateral_subquery.is_some()
8635 || from.primary.unnest_expr.is_some()
8636 || from.primary.as_of_segment.is_some()
8637 || from.primary.generate_series_args.is_some()
8638 || select_has_window(stmt)
8639 || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
8640 {
8641 return true;
8642 }
8643 if stmt
8644 .items
8645 .iter()
8646 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8647 {
8648 return true;
8649 }
8650 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8651 return true;
8652 };
8653 if table.has_cold_rows_fast() {
8654 return true;
8655 }
8656 !from.primary.only
8657 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8658 }
8659
8660 pub(crate) fn index_order_walk_target(
8661 &self,
8662 stmt: &SelectStatement,
8663 from: &FromClause,
8664 ) -> Option<(String, usize)> {
8665 // v7.39.11 — LIMIT / OFFSET join the walk instead of refusing it.
8666 //
8667 // Reported by sentori against 7.39.10 and measured on their own
8668 // busiest read: "the most recent N events for this project",
8669 // backed by an index on exactly that ordering. PostgreSQL 18
8670 // answered it with `Limit -> Index Scan`; SPG with
8671 // `Limit -> Sort -> Seq Scan`, 4.998 ms against 0.021 — the
8672 // whole table sorted to return twenty rows.
8673 //
8674 // The walk was built for this shape — `iter_desc`'s own doc says
8675 // "the ORDER BY <indexed col> DESC + LIMIT N executor path" —
8676 // and then the gate refused every statement that had a LIMIT, so
8677 // the one query it was written for could never reach it. The
8678 // capability was here; the routing was not.
8679 //
8680 // A non-literal count is refused: `LIMIT $1` is rewritten to a
8681 // literal by `resolve_limit_exprs` before dispatch, so anything
8682 // still carrying a placeholder here has not been through it.
8683 let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8684 matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8685 };
8686 if self.walk_shape_refused(stmt, from) {
8687 return None;
8688 }
8689 let table = self.active_catalog().get(&from.primary.name)?;
8690 let alias = from
8691 .primary
8692 .alias
8693 .as_deref()
8694 .unwrap_or(from.primary.name.as_str());
8695 let cols = &table.schema().columns;
8696 let order = &stmt.order_by[0];
8697 let Expr::Column(oc) = &order.expr else {
8698 return None;
8699 };
8700 if let Some(q) = &oc.qualifier
8701 && !q.eq_ignore_ascii_case(alias)
8702 {
8703 return None;
8704 }
8705 let order_pos = cols
8706 .iter()
8707 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8708 // r1047 — DISTINCT joins the walk when the projection IS the
8709 // order column, and only then. The index's keys are canonical
8710 // (r1039: representation equality is value equality — the
8711 // property every seek already depends on), so one key is one
8712 // distinct value and the walk can emit the first passing row of
8713 // each key group instead of hashing every row. On the release
8714 // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
8715 // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
8716 // with an ablation floor of 14.8, because the hash must
8717 // normalize and probe ALL the rows; the walk visits each key
8718 // once. A wider projection makes DISTINCT about the whole tuple,
8719 // not the key, so anything else still declines.
8720 if stmt.distinct {
8721 let only_the_order_column = stmt.items.len() == 1
8722 && match &stmt.items[0] {
8723 SelectItem::Expr {
8724 expr: Expr::Column(c),
8725 ..
8726 } => {
8727 c.name.eq_ignore_ascii_case(&oc.name)
8728 && match &c.qualifier {
8729 Some(q) => q.eq_ignore_ascii_case(alias),
8730 None => true,
8731 }
8732 }
8733 _ => false,
8734 };
8735 if !only_the_order_column {
8736 return None;
8737 }
8738 }
8739 // r1046 — a nullable key no longer refuses the walk; it changes
8740 // what the walk has to do. A NULL key is not in the btree, so
8741 // walking alone would silently drop those rows — the r1020
8742 // defect, which shipped once. The walk emits them separately, at
8743 // the end SQL puts them.
8744 //
8745 // Refusing was costing every nullable indexed column a 3.4x:
8746 // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
8747 // 72.0 ms with the column nullable and 20.2 with the same data
8748 // under NOT NULL. `NOT NULL` is not the default, so that was the
8749 // common case paying for the uncommon one.
8750 // v7.39.11 — the walk comes out in the tree's order, so it may
8751 // only take an ORDER BY whose order that IS.
8752 //
8753 // The B-tree walks in BYTE order unless the column's keys are
8754 // ICU sort keys. `try_pk_walk_top_n` has asked this since
8755 // v7.38.18; this gate never did, and the answer changed when an
8756 // index appeared. Measured on `alpha / Beta / GAMMA / delta`
8757 // over a MySQL-dialect session, `SELECT t FROM s ORDER BY t`:
8758 //
8759 // no index alpha Beta delta GAMMA (MySQL's own order)
8760 // indexed Beta GAMMA alpha delta (bytes)
8761 //
8762 // No row is wrong and nothing raises; only the order changes,
8763 // and it changes because an index exists. Ordering is the one
8764 // thing a walk contributes, so when it is the wrong ordering
8765 // there is nothing left to keep.
8766 let order_col = cols.get(order_pos)?;
8767 if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8768 && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8769 {
8770 return None;
8771 }
8772 // v7.39.11 — a composite B-tree LEADING on the ORDER BY column
8773 // walks it too, which is what `try_pk_walk_top_n` has always
8774 // done and what this gate did not know.
8775 //
8776 // Keys sort by the whole tuple, so the leading component comes
8777 // out in order — `Index::iter_asc` says so, and the materialising
8778 // top-N walk has relied on it since v7.38.1. The consequence of
8779 // the two gates disagreeing was the thing r1044 exists to
8780 // prevent: measured on a table indexed `(a, b)`, `SELECT a FROM
8781 // m ORDER BY a LIMIT 2` planned as `Limit -> Sort -> Seq Scan`
8782 // while the executor plainly walked the index — a projection
8783 // that divides by zero on the last row in key order returned two
8784 // rows instead of raising. EXPLAIN is the first thing any
8785 // performance question opens, and an instrument that misnames
8786 // the access path is worse than one that says nothing.
8787 let index = table
8788 .index_on(order_pos)
8789 .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8790 .or_else(|| {
8791 table.indices().iter().find(|i| {
8792 matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8793 && i.column_position == order_pos
8794 })
8795 })?;
8796 if index.expression.is_some() || index.partial_predicate.is_some() {
8797 return None;
8798 }
8799 // v7.39.11 — more than one ORDER BY term walks when the index
8800 // holds exactly that ordering.
8801 //
8802 // Keys sort by the whole tuple, so `iter_asc` over a composite
8803 // B-tree IS `ORDER BY a, b` — the walk needs no new machinery,
8804 // only permission. Reported by sentori against 7.39.10:
8805 // `ORDER BY a, b LIMIT 10` planned as `Seq Scan -> Sort` here
8806 // against an `Incremental Sort` over an index scan on
8807 // PostgreSQL 18, on a table indexed for it.
8808 //
8809 // Three things have to hold, and each of them is the tree's
8810 // limitation rather than a conservative choice:
8811 //
8812 // * the terms are the index's key columns, in its order, from
8813 // the leading one — a suffix or a permutation is a different
8814 // ordering;
8815 // * every term runs the same direction, because the tree is
8816 // walked one way for all of them. `(a, b DESC)` is what
8817 // PostgreSQL serves from an index whose SECOND key is
8818 // descending, and SPG's tree does not scan per column;
8819 // * every key column is NOT NULL. A NULL key is not in the
8820 // tree at all, and the separate pass that emits those rows
8821 // (r1046) knows how to place them for ONE column, not for a
8822 // tuple.
8823 if stmt.order_by.len() > 1 {
8824 let keys: Vec<usize> = core::iter::once(index.column_position)
8825 .chain(index.extra_column_positions.iter().copied())
8826 .collect();
8827 if stmt.order_by.len() > keys.len() {
8828 return None;
8829 }
8830 let desc = stmt.order_by[0].desc;
8831 for (term, &key_pos) in stmt.order_by.iter().zip(keys.iter()) {
8832 if term.desc != desc {
8833 return None;
8834 }
8835 let Expr::Column(c) = &term.expr else {
8836 return None;
8837 };
8838 if let Some(q) = &c.qualifier
8839 && !q.eq_ignore_ascii_case(alias)
8840 {
8841 return None;
8842 }
8843 let pos = cols
8844 .iter()
8845 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
8846 if pos != key_pos {
8847 return None;
8848 }
8849 let col = cols.get(pos)?;
8850 if col.nullable {
8851 return None;
8852 }
8853 if crate::index_access::collated_column(col, table.db_collation()).is_none()
8854 && !crate::collate::column_key_is_bytewise(col, self.speaks_mysql)
8855 {
8856 return None;
8857 }
8858 }
8859 }
8860 Some((index.name.clone(), order_pos))
8861 }
8862
8863 fn try_index_order_stream<F>(
8864 &self,
8865 stmt: &SelectStatement,
8866 from: &FromClause,
8867 cancel: CancelToken<'_>,
8868 emit: &mut F,
8869 ) -> Result<Option<usize>, EngineError>
8870 where
8871 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8872 {
8873 // r1044 — the shape gate lives in `index_order_walk_target`, so
8874 // `EXPLAIN` answers the same question. What stays here is the
8875 // part that RAISES (an illegal ORDER BY has to keep erroring
8876 // from where it did) and the bindings the walk needs.
8877 crate::orderby::check_order_by_legality(stmt)?;
8878 crate::orderby::check_order_by_positions(stmt)?;
8879 crate::window::reject_window_in_row_clauses(stmt)?;
8880 // v7.39.13 — the prefix walk first: it serves a shape the
8881 // leading-column walk cannot, and refuses everything that one
8882 // takes.
8883 let (order_pos, prefix) = match self.index_prefix_walk_target(stmt, from) {
8884 Some((_, pos, keys)) => (pos, Some(keys)),
8885 None => match self.index_order_walk_target(stmt, from) {
8886 Some((_, pos)) => (pos, None),
8887 None => return Ok(None),
8888 },
8889 };
8890 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8891 return Ok(None);
8892 };
8893 let alias = from
8894 .primary
8895 .alias
8896 .as_deref()
8897 .unwrap_or(from.primary.name.as_str());
8898 let cols = table.schema().columns.clone();
8899 let order = &stmt.order_by[0];
8900 // v7.39.11 — the same lookup the gate made; see
8901 // `index_order_walk_target`.
8902 let Some(index) = (if prefix.is_some() {
8903 // The prefix planner named an index whose FIRST extra key
8904 // column is the order column; the lookup below looks for one
8905 // whose LEADING column is, and would find the wrong tree.
8906 table.indices().iter().find(|i| {
8907 matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8908 && i.extra_column_positions.first() == Some(&order_pos)
8909 && i.expression.is_none()
8910 && i.partial_predicate.is_none()
8911 })
8912 } else {
8913 table
8914 .index_on(order_pos)
8915 .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8916 .or_else(|| {
8917 table.indices().iter().find(|i| {
8918 matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8919 && i.column_position == order_pos
8920 })
8921 })
8922 }) else {
8923 return Ok(None);
8924 };
8925
8926 let sess = self.dml_session();
8927 let ctx = EvalContext::new(&cols, Some(alias))
8928 .with_catalog(self.active_catalog())
8929 .with_session(&sess);
8930 let projection = build_projection(
8931 &stmt.items,
8932 &cols,
8933 alias,
8934 self.speaks_mysql,
8935 Some(self.active_catalog()),
8936 )?;
8937 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8938 emit(crate::StreamItem::Header(&columns))?;
8939 let bound_pos: Vec<Option<usize>> = projection
8940 .iter()
8941 .map(|p| match &p.expr {
8942 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8943 Ok(Some(pos)) => Some(pos),
8944 _ => None,
8945 },
8946 _ => None,
8947 })
8948 .collect();
8949
8950 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8951 .where_
8952 .as_ref()
8953 .filter(|w| crate::eval::fully_compilable(w))
8954 .map(|w| crate::eval::compile_expr(w, &ctx));
8955 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8956 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8957 let snapshot = self.current_snapshot();
8958
8959 // A btree holds one locator per row VERSION, so a row whose key was
8960 // updated can sit under two keys and a dead one can sit beside its
8961 // replacement. The visibility gate drops the dead; `seen` drops a
8962 // live row that the walk reaches twice, which would otherwise be a
8963 // duplicated output row rather than a slow one.
8964 let mut emitted_rows = alloc::vec![false; table.rows().len()];
8965
8966 // r1046 — the rows the index cannot hold.
8967 //
8968 // A NULL key is not in the btree, so the walk below never reaches
8969 // those rows; they are emitted here, at the end SQL puts them.
8970 // PG's default is NULLS LAST ascending and NULLS FIRST
8971 // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
8972 // the same rule `order_by_value_cmp_raw` applies to the sort this
8973 // replaces, so the two orders agree.
8974 //
8975 // Finding them costs one pass over the column. That pass is why
8976 // this is still worth doing: the sort it replaces encodes and
8977 // decodes every row, and the walk plus the pass measured 72.0 ms
8978 // down to about 22 on 400,000 rows.
8979 let nulls_first = order.nulls_first.unwrap_or(order.desc);
8980 // r1047 — under DISTINCT the walk emits the FIRST passing row of
8981 // each key group and skips the rest; the gate admits DISTINCT
8982 // only when the projection is the order column itself, so one
8983 // canonical key is one output row. NULL is one distinct value,
8984 // so the NULL pass stops at its first emit too.
8985 let distinct = stmt.distinct;
8986 let mut count = 0usize;
8987 let mut visited = 0usize;
8988 // v7.39.11 — OFFSET and LIMIT, applied as the walk goes.
8989 //
8990 // Both count PASSING rows, so a skipped row still has to run the
8991 // predicate and the projection — `stream_filter_project` is
8992 // `stream_project_row` without the emit, which is exactly that.
8993 // Stopping at `remaining == 0` is the whole point: twenty rows
8994 // off the end of an index instead of a sorted table.
8995 let mut to_skip = stmt.offset_literal().unwrap_or(0) as usize;
8996 let mut remaining: Option<usize> = stmt.limit_literal().map(|l| l as usize);
8997 let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
8998 eval_stack: &mut Vec<Value<'static>>,
8999 values: &mut Vec<Value<'static>>,
9000 visited: &mut usize,
9001 to_skip: &mut usize,
9002 remaining: &mut Option<usize>,
9003 emit: &mut F|
9004 -> Result<usize, EngineError> {
9005 if !cols[order_pos].nullable {
9006 return Ok(0);
9007 }
9008 // v7.39.11 — nothing to emit once the LIMIT is met, and
9009 // finding that out must not cost a scan.
9010 //
9011 // This pass looks for NULL-keyed rows by walking the whole
9012 // heap, because they are not in the tree. That is the price
9013 // r1046 measured and accepted for an UNBOUNDED order. With
9014 // a LIMIT the walk above has usually already produced every
9015 // row the caller asked for, and scanning 400,000 rows to
9016 // add none of them is the whole cost of the query: the
9017 // release sweep's `SELECT pad FROM t ORDER BY n LIMIT 10`
9018 // over a nullable indexed NUMERIC went 0.237 ms at 50,000
9019 // rows and 2.251 at 400,000 — linear, against PostgreSQL's
9020 // 0.155 and 0.182 — the moment this gate started accepting
9021 // LIMIT. The `remaining` check below sits after the
9022 // per-row filters, so it could never be reached.
9023 if *remaining == Some(0) {
9024 return Ok(0);
9025 }
9026 let mut n = 0usize;
9027 for (ri, row) in table.rows().iter().enumerate() {
9028 if !matches!(row.values.get(order_pos), Some(Value::Null)) {
9029 continue;
9030 }
9031 if emitted_rows.get(ri).copied().unwrap_or(true) {
9032 continue;
9033 }
9034 if !table.is_row_visible(ri, &snapshot) {
9035 continue;
9036 }
9037 *visited += 1;
9038 if visited.is_multiple_of(256) {
9039 cancel.check()?;
9040 }
9041 emitted_rows[ri] = true;
9042 if *remaining == Some(0) {
9043 break;
9044 }
9045 let passed = if *to_skip > 0 {
9046 let p = Self::stream_filter_project(
9047 row,
9048 stmt.where_.as_ref(),
9049 compiled_where.as_ref(),
9050 eval_stack,
9051 &projection,
9052 &bound_pos,
9053 &ctx,
9054 values,
9055 )?;
9056 if p {
9057 *to_skip -= 1;
9058 }
9059 false
9060 } else {
9061 Self::stream_project_row(
9062 row,
9063 stmt.where_.as_ref(),
9064 compiled_where.as_ref(),
9065 eval_stack,
9066 &projection,
9067 &bound_pos,
9068 &ctx,
9069 values,
9070 emit,
9071 )?
9072 };
9073 if passed {
9074 n += 1;
9075 if let Some(r) = remaining.as_mut() {
9076 *r -= 1;
9077 if *r == 0 {
9078 break;
9079 }
9080 }
9081 if distinct {
9082 break;
9083 }
9084 }
9085 }
9086 Ok(n)
9087 };
9088
9089 if nulls_first {
9090 count += emit_null_rows(
9091 &mut emitted_rows,
9092 &mut eval_stack,
9093 &mut values,
9094 &mut visited,
9095 &mut to_skip,
9096 &mut remaining,
9097 emit,
9098 )?;
9099 }
9100
9101 // v7.39.13 — a prefix walk when the statement binds the index's
9102 // leading column, the whole tree otherwise. The key is not read
9103 // by the loop, so the two shapes meet as posting lists.
9104 let walker: alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> =
9105 match prefix.as_ref().and_then(|p| {
9106 if order.desc {
9107 index.iter_prefix_desc(p).map(
9108 |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9109 alloc::boxed::Box::new(it.map(|(_, l)| l))
9110 },
9111 )
9112 } else {
9113 index.iter_prefix_asc(p).map(
9114 |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9115 alloc::boxed::Box::new(it.map(|(_, l)| l))
9116 },
9117 )
9118 }
9119 }) {
9120 Some(it) => it,
9121 None if order.desc => alloc::boxed::Box::new(index.iter_desc().map(|(_, l)| l)),
9122 None => alloc::boxed::Box::new(index.iter_asc().map(|(_, l)| l)),
9123 };
9124 'walk: for locators in walker {
9125 if remaining == Some(0) {
9126 break;
9127 }
9128 for loc in locators {
9129 let spg_storage::RowLocator::Hot(ri) = *loc else {
9130 continue;
9131 };
9132 if emitted_rows.get(ri).copied().unwrap_or(true) {
9133 continue;
9134 }
9135 if !table.is_row_visible(ri, &snapshot) {
9136 continue;
9137 }
9138 let Some(row) = table.rows().get(ri) else {
9139 continue;
9140 };
9141 visited += 1;
9142 if visited.is_multiple_of(256) {
9143 cancel.check()?;
9144 }
9145 emitted_rows[ri] = true;
9146 // v7.39.11 — a skipped row still runs the predicate and
9147 // the projection, because OFFSET counts rows that PASS;
9148 // it just does not reach the client.
9149 let passed = if to_skip > 0 {
9150 let p = Self::stream_filter_project(
9151 row,
9152 stmt.where_.as_ref(),
9153 compiled_where.as_ref(),
9154 &mut eval_stack,
9155 &projection,
9156 &bound_pos,
9157 &ctx,
9158 &mut values,
9159 )?;
9160 if p {
9161 to_skip -= 1;
9162 }
9163 false
9164 } else {
9165 Self::stream_project_row(
9166 row,
9167 stmt.where_.as_ref(),
9168 compiled_where.as_ref(),
9169 &mut eval_stack,
9170 &projection,
9171 &bound_pos,
9172 &ctx,
9173 &mut values,
9174 emit,
9175 )?
9176 };
9177 if passed {
9178 count += 1;
9179 if let Some(r) = remaining.as_mut() {
9180 *r -= 1;
9181 if *r == 0 {
9182 break 'walk;
9183 }
9184 }
9185 // One row per key group: the rest are the same value.
9186 if distinct {
9187 break;
9188 }
9189 }
9190 }
9191 }
9192
9193 if !nulls_first {
9194 count += emit_null_rows(
9195 &mut emitted_rows,
9196 &mut eval_stack,
9197 &mut values,
9198 &mut visited,
9199 &mut to_skip,
9200 &mut remaining,
9201 emit,
9202 )?;
9203 }
9204 Ok(Some(count))
9205 }
9206
9207 /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
9208 /// building an `OrderKey` vector per row.
9209 ///
9210 /// The row-returning sorted scan allocates twice per row: one
9211 /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
9212 /// projection. Counted over 400 k rows (r1030,
9213 /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
9214 /// allocations and 208 MB of traffic for an answer of four hundred
9215 /// thousand integers.
9216 ///
9217 /// The key half is pure ceremony on this shape.
9218 /// `sort_tagged_by_inline_int_key` already sorts indices rather than
9219 /// rows, so the per-row vector is built, has one integer taken out of
9220 /// it, and is then dragged through the permutation — it exists to carry
9221 /// a number the row's column already held. This lane carries the number
9222 /// instead, in a fixed-size array that lives inside the buffer element
9223 /// and allocates nothing. Same idea as the predicate VM's integer lane.
9224 ///
9225 /// Declines to `None` for anything it does not cover, and every caller
9226 /// falls through to the general path, so the gate list is the
9227 /// specification.
9228 ///
9229 /// Ties: equal keys keep scan order, as the stable sort on the general
9230 /// path does. Rows that tie on every ORDER BY term are entitled to any
9231 /// order among themselves either way — see `STABILITY.md`.
9232 fn try_int_key_sorted_stream<F>(
9233 &self,
9234 stmt: &SelectStatement,
9235 from: &FromClause,
9236 cancel: CancelToken<'_>,
9237 emit: &mut F,
9238 ) -> Result<Option<usize>, EngineError>
9239 where
9240 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9241 {
9242 /// Sort terms this lane carries inline. Four covers every ORDER BY
9243 /// in the endpoint sweep and in the dogfood corpus; wider ones fall
9244 /// through rather than growing the buffer element for everybody.
9245 const MAX_KEYS: usize = 4;
9246
9247 if stmt.order_by.is_empty()
9248 || stmt.order_by.len() > MAX_KEYS
9249 // v7.38.14 — DISTINCT is admitted when the projected set is
9250 // exactly the ORDER BY set, and only then. This lane sorts, and
9251 // when the sort key determines the projected row every duplicate
9252 // lands ADJACENT to its twin -- so the de-duplication is a
9253 // comparison with the previous row rather than a hash table, and
9254 // the reason this lane declined DISTINCT disappears with it. The
9255 // seen-set it could not offer held indices into a materialised
9256 // vector; there is no seen-set now.
9257 //
9258 // The gate is as narrow as the bare-GROUP-BY rewrite's for the
9259 // same reason: `ORDER BY a` over a projection of `a, b` does NOT
9260 // place duplicates of the PAIR adjacent, so set EQUALITY, never
9261 // overlap.
9262 || (stmt.distinct && !Self::distinct_is_adjacent_after_sort(stmt))
9263 || stmt.limit_with_ties
9264 || stmt.limit.is_some()
9265 || stmt.offset.is_some()
9266 || stmt.having.is_some()
9267 || stmt.group_by.is_some()
9268 || !stmt.unions.is_empty()
9269 || !from.joins.is_empty()
9270 || from.primary.lateral_subquery.is_some()
9271 || from.primary.unnest_expr.is_some()
9272 || from.primary.as_of_segment.is_some()
9273 || from.primary.generate_series_args.is_some()
9274 || select_has_window(stmt)
9275 || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
9276 {
9277 return Ok(None);
9278 }
9279 if stmt
9280 .items
9281 .iter()
9282 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9283 {
9284 return Ok(None);
9285 }
9286 crate::orderby::check_order_by_legality(stmt)?;
9287 crate::orderby::check_order_by_positions(stmt)?;
9288 crate::window::reject_window_in_row_clauses(stmt)?;
9289 let Some(table) = self.active_catalog().get(&from.primary.name) else {
9290 return Ok(None);
9291 };
9292 if table.has_cold_rows_fast() {
9293 return Ok(None);
9294 }
9295 if !from.primary.only
9296 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9297 {
9298 return Ok(None);
9299 }
9300 let alias = from
9301 .primary
9302 .alias
9303 .as_deref()
9304 .unwrap_or(from.primary.name.as_str());
9305 let cols = table.schema().columns.clone();
9306
9307 // Every ORDER BY term must be a NOT NULL integer column of this
9308 // table. NOT NULL is what lets the key be a bare integer: with
9309 // NULLs the lane would have to carry their ordering too, and
9310 // getting that subtly wrong is the r1020 defect.
9311 let mut key_pos = [0usize; MAX_KEYS];
9312 let mut descs = [false; MAX_KEYS];
9313 // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
9314 // which the AST records as `None`; `unwrap_or(desc)` is how the
9315 // rest of the engine resolves it.
9316 let mut nulls_first = [false; MAX_KEYS];
9317 let n_keys = stmt.order_by.len();
9318 for (slot, order) in stmt.order_by.iter().enumerate() {
9319 let Expr::Column(oc) = &order.expr else {
9320 return Ok(None);
9321 };
9322 if let Some(q) = &oc.qualifier
9323 && !q.eq_ignore_ascii_case(alias)
9324 {
9325 return Ok(None);
9326 }
9327 let Some(pos) = cols
9328 .iter()
9329 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
9330 else {
9331 return Ok(None);
9332 };
9333 if !matches!(
9334 cols[pos].ty,
9335 spg_storage::DataType::SmallInt
9336 | spg_storage::DataType::Int
9337 | spg_storage::DataType::BigInt
9338 ) {
9339 return Ok(None);
9340 }
9341 key_pos[slot] = pos;
9342 descs[slot] = order.desc;
9343 nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
9344 }
9345
9346 let sess = self.dml_session();
9347 let ctx = EvalContext::new(&cols, Some(alias))
9348 .with_catalog(self.active_catalog())
9349 .with_session(&sess);
9350 let projection = build_projection(
9351 &stmt.items,
9352 &cols,
9353 alias,
9354 self.speaks_mysql,
9355 Some(self.active_catalog()),
9356 )?;
9357 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9358 let bound_pos: Vec<Option<usize>> = projection
9359 .iter()
9360 .map(|p| match &p.expr {
9361 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9362 Ok(Some(pos)) => Some(pos),
9363 _ => None,
9364 },
9365 _ => None,
9366 })
9367 .collect();
9368 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9369 .where_
9370 .as_ref()
9371 .filter(|w| crate::eval::fully_compilable(w))
9372 .map(|w| crate::eval::compile_expr(w, &ctx));
9373
9374 // The same first-observable point the materialising planner fires,
9375 // placed after the gates so it fires exactly once: this lane runs
9376 // BEFORE that planner and would otherwise be a hole in the
9377 // panic-isolation and cancellation-race coverage rather than a
9378 // faster path through it.
9379 crate::injection_point!("planner_first_row_fetch", &stmt.from);
9380
9381 let mut eval_stack: Vec<Value<'static>> = Vec::new();
9382 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9383 let mut budget = ByteBudget::new(self.max_query_bytes);
9384 let snapshot = self.current_snapshot();
9385 // Keys, a NULL bit per key slot, and the row. The bitmask keeps
9386 // the element small: a nullable key still costs one bit rather
9387 // than a second array.
9388 let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
9389
9390 for (ri, row) in table.rows().iter().enumerate() {
9391 if ri.is_multiple_of(256) {
9392 cancel.check()?;
9393 }
9394 if !table.is_row_visible(ri, &snapshot) {
9395 continue;
9396 }
9397 // The key comes from the STORED row, before projection: an
9398 // ORDER BY column need not appear in the select list.
9399 let mut keys = [0i64; MAX_KEYS];
9400 let mut nulls = 0u8;
9401 let mut keyed = true;
9402 for slot in 0..n_keys {
9403 match row.values.get(key_pos[slot]) {
9404 Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
9405 Some(Value::Int(v)) => keys[slot] = i64::from(*v),
9406 Some(Value::BigInt(v)) => keys[slot] = *v,
9407 Some(Value::Null) | None => nulls |= 1 << slot,
9408 // An integer column holding something else is a row
9409 // this lane cannot order; hand the whole query back
9410 // rather than guess at it.
9411 _ => {
9412 keyed = false;
9413 break;
9414 }
9415 }
9416 }
9417 if !keyed {
9418 return Ok(None);
9419 }
9420 if !Self::stream_filter_project(
9421 row,
9422 stmt.where_.as_ref(),
9423 compiled_where.as_ref(),
9424 &mut eval_stack,
9425 &projection,
9426 &bound_pos,
9427 &ctx,
9428 &mut values,
9429 )? {
9430 continue;
9431 }
9432 budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
9433 sorted.push((keys, nulls, core::mem::take(&mut values)));
9434 values.reserve(projection.len());
9435 }
9436
9437 sorted.sort_by(|a, b| {
9438 use core::cmp::Ordering;
9439 for slot in 0..n_keys {
9440 let bit = 1u8 << slot;
9441 let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
9442 (true, true) => Ordering::Equal,
9443 // Where the NULLs go is already decided — `nulls_first`
9444 // resolved DESC's default when it was read. Reversing
9445 // this for DESC as well would apply the direction
9446 // twice and put them at the wrong end.
9447 (true, false) => {
9448 if nulls_first[slot] {
9449 Ordering::Less
9450 } else {
9451 Ordering::Greater
9452 }
9453 }
9454 (false, true) => {
9455 if nulls_first[slot] {
9456 Ordering::Greater
9457 } else {
9458 Ordering::Less
9459 }
9460 }
9461 (false, false) => {
9462 let o = a.0[slot].cmp(&b.0[slot]);
9463 if descs[slot] { o.reverse() } else { o }
9464 }
9465 };
9466 if ord != Ordering::Equal {
9467 return ord;
9468 }
9469 }
9470 Ordering::Equal
9471 });
9472
9473 emit(crate::StreamItem::Header(&columns))?;
9474 // v7.38.14 — DISTINCT, de-duplicated against the PREVIOUS row.
9475 //
9476 // The gate above only admits DISTINCT when the sort key determines
9477 // the projected row, so every duplicate is adjacent to its twin by
9478 // the time this loop runs and one comparison replaces a hash table
9479 // of every row seen. Equality is `values_eq_norm` with the same mask
9480 // the materialising path builds -- deliberately the same function,
9481 // because a de-duplication that disagreed with the one on the other
9482 // path would make the answer depend on which lane a query took.
9483 //
9484 // A query that did not ask for DISTINCT pays one already-false bool
9485 // test per row: the short-circuit means the comparison never runs
9486 // and `prev` is never written.
9487 let dedup_mask = fold_mask(&projection);
9488 let fold = FoldSpec::of(self.speaks_mysql, &dedup_mask);
9489 let mut count = 0usize;
9490 let mut prev: Option<&[Value<'static>]> = None;
9491 for (_, _, vals) in &sorted {
9492 if stmt.distinct
9493 && let Some(p) = prev
9494 && values_eq_norm(p, vals, fold)
9495 {
9496 continue;
9497 }
9498 emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
9499 count += 1;
9500 if stmt.distinct {
9501 prev = Some(vals);
9502 }
9503 }
9504 Ok(Some(count))
9505 }
9506
9507 /// v7.38.14 — would sorting place every duplicate next to its twin?
9508 ///
9509 /// True when the projected expressions and the ORDER BY expressions are the
9510 /// same SET. Then the sort key determines the projected row, so equal rows
9511 /// are adjacent afterwards and an adjacent comparison de-duplicates exactly
9512 /// as a hash would -- and, because both sort paths are stable, the survivor
9513 /// is the first-seen row, which is the one the hash keeps too.
9514 ///
9515 /// A wildcard's expansion is not known here, so it is not a set this can
9516 /// compare; an ordinal ORDER BY names a select-list position rather than a
9517 /// value and is left alone.
9518 fn distinct_is_adjacent_after_sort(stmt: &SelectStatement) -> bool {
9519 if stmt.order_by.is_empty() || !stmt.distinct_on.is_empty() {
9520 return false;
9521 }
9522 let mut projected: alloc::vec::Vec<&Expr> =
9523 alloc::vec::Vec::with_capacity(stmt.items.len());
9524 for item in &stmt.items {
9525 match item {
9526 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return false,
9527 SelectItem::Expr { expr, .. } => projected.push(expr),
9528 }
9529 }
9530 if projected.is_empty() {
9531 return false;
9532 }
9533 let keys: alloc::vec::Vec<&Expr> = stmt.order_by.iter().map(|o| &o.expr).collect();
9534 if keys
9535 .iter()
9536 .any(|k| matches!(k, Expr::Literal(spg_sql::ast::Literal::Integer(_))))
9537 {
9538 return false;
9539 }
9540 projected.iter().all(|p| keys.contains(p)) && keys.iter().all(|k| projected.contains(k))
9541 }
9542
9543 fn try_spill_sorted_stream<F>(
9544 &self,
9545 stmt: &SelectStatement,
9546 from: &FromClause,
9547 cancel: CancelToken<'_>,
9548 emit: &mut F,
9549 ) -> Result<Option<usize>, EngineError>
9550 where
9551 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9552 {
9553 // The shapes `try_spill_sorted_scan` declines, plus the ones the
9554 // streaming executor does not carry (a LIMIT is already bounded
9555 // by a partial sort; the rest need the answer addressable).
9556 if !self.can_spill()
9557 || stmt.order_by.is_empty()
9558 || stmt.distinct
9559 || stmt.limit_with_ties
9560 || stmt.limit.is_some()
9561 || stmt.offset.is_some()
9562 || stmt.having.is_some()
9563 || stmt.group_by.is_some()
9564 || !stmt.unions.is_empty()
9565 || !from.joins.is_empty()
9566 || from.primary.lateral_subquery.is_some()
9567 || from.primary.unnest_expr.is_some()
9568 || from.primary.as_of_segment.is_some()
9569 || from.primary.generate_series_args.is_some()
9570 || select_has_window(stmt)
9571 || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
9572 {
9573 return Ok(None);
9574 }
9575 if stmt
9576 .items
9577 .iter()
9578 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9579 {
9580 return Ok(None);
9581 }
9582 // Everything `exec_bare_select_cancel` does before it scans runs
9583 // BELOW this path, so a statement claimed here skips it. Three of
9584 // those were missed on the way in and each was caught by a
9585 // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
9586 // ORDER BY 2` sorted happily instead of raising 42P10), the
9587 // cancellation check by another, the partition fan-out by the
9588 // differential corpus. What is reconciled, item by item: with-ties
9589 // needs ORDER BY (gated above), USING/NATURAL and RLS join
9590 // rewrites (joins gated above), the single-table RLS predicate
9591 // (the dispatcher declines a policy-subject table before this is
9592 // reached), the meta-view dispatch (those names are not in the
9593 // catalog, so the lookup below declines). These three are calls,
9594 // so the message and SQLSTATE are the ones the fall-back gives —
9595 // `select_has_window` above reads the select list and ORDER BY but
9596 // not WHERE, which is the case the third one covers.
9597 crate::orderby::check_order_by_legality(stmt)?;
9598 crate::orderby::check_order_by_positions(stmt)?;
9599 crate::window::reject_window_in_row_clauses(stmt)?;
9600 // A parent's rows are its children's. These walks scan the named
9601 // relation alone, so a partitioned or inherited parent comes back
9602 // short — and silently: the corpus caught `SELECT id FROM pr
9603 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
9604 // parent's own rows instead of the partitions'. `ONLY` is exactly
9605 // the case that does not fan out, so it stays, which is the test
9606 // the FROM-clause fan-out itself makes.
9607 if !from.primary.only
9608 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9609 {
9610 return Ok(None);
9611 }
9612 let Some(table) = self.active_catalog().get(&from.primary.name) else {
9613 return Ok(None);
9614 };
9615 // Cold-tier rows live outside `rows()`; this walk would drop
9616 // them silently, the same reason round 831's walk declines.
9617 if table.has_cold_rows_fast() {
9618 return Ok(None);
9619 }
9620
9621 let alias = from
9622 .primary
9623 .alias
9624 .as_deref()
9625 .unwrap_or(from.primary.name.as_str());
9626 let cols = table.schema().columns.clone();
9627 let sess = self.dml_session();
9628 let ctx = EvalContext::new(&cols, Some(alias))
9629 .with_catalog(self.active_catalog())
9630 .with_session(&sess);
9631 let projection = build_projection(
9632 &stmt.items,
9633 &cols,
9634 alias,
9635 self.speaks_mysql,
9636 Some(self.active_catalog()),
9637 )?;
9638 let order_by = stmt.order_by.clone();
9639 // The same one-shot resolution the general path does (round
9640 // 582): each ORDER BY column is bound once, not once per row.
9641 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
9642 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
9643 // Resolved BEFORE the scan, because it now decides what the sort
9644 // STORES and not just what it decodes (round 995).
9645 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
9646
9647 // v7.38.22 — resolved HERE, because this path did not resolve
9648 // them at all.
9649 //
9650 // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
9651 // "en_US.utf8"` in BYTE order on this path — and swallowed an
9652 // unknown collation name rather than raising — because the sorter
9653 // below compared with an empty collation slice. The materialising
9654 // path honoured both. Which answer a query got depended on which
9655 // path the planner took, and this is the path a plain single-table
9656 // SELECT takes.
9657 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
9658 // v7.39.12 — a correlated scalar subquery in ORDER BY is
9659 // resolved for the row before its key is built.
9660 //
9661 // Uncorrelated subqueries are replaced by a literal before
9662 // execution; a correlated one cannot be, so it reached the
9663 // per-row evaluator — the one place that cannot run a subquery
9664 // — and the statement raised "subquery reached row eval".
9665 // Reported by sentori against 7.39.11; see
9666 // `Engine::order_by_resolved_for_row`.
9667 //
9668 // The `any` runs once, here, so an ordinary ORDER BY pays one
9669 // bool per row and nothing else.
9670 let order_has_subquery = order_by
9671 .iter()
9672 .any(|o| crate::subquery::expr_has_subquery(&o.expr));
9673 let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
9674 let mut sorter = crate::extsort::ExternalSorter::new(
9675 self.temp_run_factory,
9676 self.session_work_mem_bytes(),
9677 cols.clone(),
9678 &descs,
9679 &order_colls,
9680 )
9681 .with_stats(&self.spill_stats)
9682 .with_workers(self.session_parallel_workers())
9683 .with_pruned(&needed);
9684 let snapshot = self.current_snapshot();
9685 // One key buffer for the whole scan: `push` drains it and leaves
9686 // the capacity behind.
9687 let mut keys: Vec<OrderKey> = Vec::new();
9688 // r1024 — compile the predicate once for the scan.
9689 //
9690 // These two sorted-spill scans are the paths a single-table SELECT
9691 // with an ORDER BY takes, and they were the last row-returning ones
9692 // still walking the expression tree per row. r1023 did the
9693 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
9694 // exactly this shape.
9695 //
9696 // Found from the profile's CALL TREE rather than its leaves. The
9697 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
9698 // 261, `mod_op` 178 — and two attempts at reasoning out which
9699 // function asked for it were both wrong. The tree names the caller
9700 // chain, and it named this one.
9701 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9702 .where_
9703 .as_ref()
9704 .filter(|w| crate::eval::fully_compilable(w))
9705 .map(|w| crate::eval::compile_expr(w, &ctx));
9706 let mut eval_stack: Vec<Value<'static>> = Vec::new();
9707 for (i, row) in table.scan_visible_from(0, &snapshot) {
9708 if i.is_multiple_of(256) {
9709 cancel.check()?;
9710 }
9711 if let Some(c) = &compiled_where {
9712 if !crate::eval::compiled::eval_compiled_pred(
9713 c,
9714 row,
9715 &ctx,
9716 &mut eval_stack,
9717 ctx.mysql_dialect,
9718 )? {
9719 continue;
9720 }
9721 } else if let Some(w) = &stmt.where_ {
9722 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
9723 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9724 continue;
9725 }
9726 }
9727 keys.clear();
9728 // The same collations the sorter compares with, and the
9729 // re-derivation below is handed the same ones. `finish`'s
9730 // contract is that a key comes back the way it was pushed;
9731 // a collation is part of the way it was pushed.
9732 if order_has_subquery {
9733 // A substituted literal is no longer a bound column.
9734 let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
9735 crate::orderby::build_order_keys_bound(
9736 per_row.as_deref().unwrap_or(&order_by),
9737 &unbound,
9738 &order_colls,
9739 row,
9740 &ctx,
9741 &mut keys,
9742 )?;
9743 } else {
9744 crate::orderby::build_order_keys_bound(
9745 &order_by,
9746 &order_bound,
9747 &order_colls,
9748 row,
9749 &ctx,
9750 &mut keys,
9751 )?;
9752 }
9753 sorter.push(&mut keys, row)?;
9754 }
9755
9756 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9757 emit(crate::StreamItem::Header(&columns))?;
9758
9759 let key_ctx = &ctx;
9760 let mut emitted_since_check = 0usize;
9761 let n = sorter.finish_each(
9762 |src, buf| {
9763 crate::orderby::build_order_keys_rederived(
9764 &order_by,
9765 &order_bound,
9766 &order_colls,
9767 src,
9768 key_ctx,
9769 buf,
9770 )
9771 },
9772 |src, values| {
9773 for p in &projection {
9774 values.push(
9775 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
9776 );
9777 }
9778 Ok(())
9779 },
9780 |cells| {
9781 // The merge is the long half of a big sort, and the scan's
9782 // check above stops running once it ends: a cancelled
9783 // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
9784 // anyway. Same stride as the scan.
9785 emitted_since_check += 1;
9786 if emitted_since_check >= 256 {
9787 emitted_since_check = 0;
9788 cancel.check()?;
9789 }
9790 emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
9791 },
9792 )?;
9793 Ok(Some(n))
9794 }
9795
9796 /// One row of the single-table streaming walk: the WHERE test, the
9797 /// projection, the emit. Returns whether a row was emitted.
9798 ///
9799 /// v7.39 (round 970) — factored out because the walk now has two ways
9800 /// to reach a row, the sequential scan and an index seek's candidate
9801 /// positions, and both must do IDENTICALLY this. A copy in each is how
9802 /// two paths for one job drift; this file already carries the cost of
9803 /// that lesson twice (rounds 823 and 961, both resolvers).
9804 ///
9805 /// `#[inline]` so the scan loop keeps the shape round 957 measured it
9806 /// in — a shared hot path pays for a new abstraction whether or not it
9807 /// uses it, and this one is on the scan.
9808 #[inline]
9809 #[allow(clippy::too_many_arguments)]
9810 fn stream_filter_project(
9811 row: &spg_storage::Row<'static>,
9812 where_: Option<&Expr>,
9813 // r1023 — the same WHERE, compiled once by the caller. `None` means
9814 // the expression did not qualify and `where_` is evaluated as before.
9815 compiled_where: Option<&crate::eval::CompiledExpr>,
9816 eval_stack: &mut Vec<Value<'static>>,
9817 projection: &[ProjectedItem],
9818 bound_pos: &[Option<usize>],
9819 ctx: &crate::eval::EvalContext<'_>,
9820 values: &mut Vec<Value<'static>>,
9821 ) -> Result<bool, EngineError> {
9822 // r1023 — this scan ran its predicate through the TREE INTERPRETER,
9823 // once per row, and it was the only row-returning path that did.
9824 // The aggregate path, `table_access`, and the PK walker all compile
9825 // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
9826 // server's live samples were `eval_expr` 99, `apply_binary` 81,
9827 // `mod_op` 29 — the interpreter, not delivery.
9828 //
9829 // The arithmetic accounted for it exactly. Over the wire, the same
9830 // filter costs 6.375 ms returning rows and 0.679 ms counting them;
9831 // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
9832 // which is what an interpreted predicate costs against the compiled
9833 // lane's 11.7. It was named "delivery after a filter" before this
9834 // profile, and it was never delivery.
9835 if let Some(c) = compiled_where {
9836 if !crate::eval::compiled::eval_compiled_pred(
9837 c,
9838 row,
9839 ctx,
9840 eval_stack,
9841 ctx.mysql_dialect,
9842 )? {
9843 return Ok(false);
9844 }
9845 } else if let Some(w) = where_ {
9846 let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
9847 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9848 return Ok(false);
9849 }
9850 }
9851 values.clear();
9852 for (p, bound) in projection.iter().zip(bound_pos) {
9853 values.push(match bound {
9854 Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
9855 None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
9856 });
9857 }
9858 Ok(true)
9859 }
9860
9861 /// The same filter and projection, then emit. Split from
9862 /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
9863 /// before it can emit them — a sort — runs the identical predicate and
9864 /// projection rather than a second copy of them.
9865 #[allow(clippy::too_many_arguments)]
9866 fn stream_project_row<F>(
9867 row: &spg_storage::Row<'static>,
9868 where_: Option<&Expr>,
9869 compiled_where: Option<&crate::eval::CompiledExpr>,
9870 eval_stack: &mut Vec<Value<'static>>,
9871 projection: &[ProjectedItem],
9872 bound_pos: &[Option<usize>],
9873 ctx: &crate::eval::EvalContext<'_>,
9874 values: &mut Vec<Value<'static>>,
9875 emit: &mut F,
9876 ) -> Result<bool, EngineError>
9877 where
9878 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9879 {
9880 if !Self::stream_filter_project(
9881 row,
9882 where_,
9883 compiled_where,
9884 eval_stack,
9885 projection,
9886 bound_pos,
9887 ctx,
9888 values,
9889 )? {
9890 return Ok(false);
9891 }
9892 emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
9893 Ok(true)
9894 }
9895
9896 fn try_stream_single_table<F>(
9897 &self,
9898 stmt: &SelectStatement,
9899 from: &FromClause,
9900 cancel: CancelToken<'_>,
9901 emit: &mut F,
9902 ) -> Result<Option<usize>, EngineError>
9903 where
9904 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9905 {
9906 let Some(table) = self.active_catalog().get(&from.primary.name) else {
9907 return Ok(None);
9908 };
9909 // Cold-tier rows live outside `rows()`; the materialising fallback
9910 // covers both tiers and this walk would silently drop them.
9911 if table.has_cold_rows_fast() {
9912 return Ok(None);
9913 }
9914 let alias = from
9915 .primary
9916 .alias
9917 .as_deref()
9918 .unwrap_or(from.primary.name.as_str());
9919 let cols = table.schema().columns.clone();
9920 let sess = self.dml_session();
9921 let ctx = EvalContext::new(&cols, Some(alias))
9922 .with_catalog(self.active_catalog())
9923 .with_session(&sess);
9924 let projection = build_projection(
9925 &stmt.items,
9926 &cols,
9927 alias,
9928 self.speaks_mysql,
9929 Some(self.active_catalog()),
9930 )?;
9931
9932 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9933 emit(crate::StreamItem::Header(&columns))?;
9934
9935 // v7.37 (round 957) — resolve each bare-column projection ONCE
9936 // instead of once per row. `find_column_pos`-style resolution is a
9937 // linear walk of the schema comparing column-name strings, and the
9938 // row loop below ran it for every cell of every row: measured at
9939 // 400k rows, binding it out of the loop took `SELECT pad` from
9940 // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
9941 //
9942 // ORDER BY has bound its keys this way since round 582
9943 // (`order_by_bound_positions`); the projection never did.
9944 //
9945 // `locate_column` is the same resolution `resolve_column` performs,
9946 // returning the site instead of the value, so the two cannot drift
9947 // apart the way a second hand-written resolver would. Anything it
9948 // declines — an expression, a whole-row reference, a name that does
9949 // not resolve — binds to `None` and takes the general path below,
9950 // errors included, so an empty table still reports nothing rather
9951 // than raising at bind time.
9952 let bound_pos: Vec<Option<usize>> = projection
9953 .iter()
9954 .map(|p| match &p.expr {
9955 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9956 Ok(Some(pos)) => Some(pos),
9957 _ => None,
9958 },
9959 _ => None,
9960 })
9961 .collect();
9962
9963 // One snapshot for the whole scan, as the materialising path takes.
9964 let snapshot = self.current_snapshot();
9965
9966 // v7.39 (round 970) — ask the indices BEFORE walking the table.
9967 //
9968 // This walk had no index step at all, and it is preferred over the
9969 // materialising path, which does have one (`pick_indexed_rows` ->
9970 // `try_index_seek`). So a primary-key point lookup — the commonest
9971 // statement there is — read every row: measured on 500k rows,
9972 // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
9973 // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
9974 // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
9975 //
9976 // The control that named it: `... OFFSET 0` — semantically the same
9977 // query — answered in 0.159 ms, because OFFSET is one of the shape
9978 // gates that declines this walk and sends the statement to the path
9979 // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
9980 // no semantics in common; what they share is making this function
9981 // stand down.
9982 //
9983 // The seek only NARROWS: every candidate still goes through the
9984 // full WHERE below, exactly as the mutation paths use it, so a
9985 // partial index match cannot change an answer. Positions come back
9986 // already visibility-filtered and already capped at a quarter of the
9987 // table (round 490), so a seek can never cost more than the scan it
9988 // replaces, and `None` means "walk the table" as before.
9989 //
9990 // Sorted because the scan would have produced table order and the
9991 // index produces key order. Without an ORDER BY neither is promised,
9992 // but a walk that silently reorders its answer when an index happens
9993 // to exist is a difference nobody asked for.
9994 let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
9995 crate::index_access::try_index_seek_positions(
9996 w,
9997 &cols,
9998 table,
9999 alias,
10000 &snapshot,
10001 self.speaks_mysql,
10002 )
10003 });
10004
10005 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
10006 // r1023 — compile the predicate once for the whole scan. Same gate
10007 // every other path uses: `fully_compilable` or keep the interpreter,
10008 // so a shape the VM cannot take answers exactly as it did before.
10009 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
10010 .where_
10011 .as_ref()
10012 .filter(|w| crate::eval::fully_compilable(w))
10013 .map(|w| crate::eval::compile_expr(w, &ctx));
10014 let mut eval_stack: Vec<Value<'static>> = Vec::new();
10015 let mut count: usize = 0;
10016 match seek_positions {
10017 Some(mut positions) => {
10018 positions.sort_unstable();
10019 for (n, pos) in positions.into_iter().enumerate() {
10020 if n.is_multiple_of(256) {
10021 cancel.check()?;
10022 }
10023 let Some(row) = table.rows().get(pos) else {
10024 continue;
10025 };
10026 if Self::stream_project_row(
10027 row,
10028 stmt.where_.as_ref(),
10029 compiled_where.as_ref(),
10030 &mut eval_stack,
10031 &projection,
10032 &bound_pos,
10033 &ctx,
10034 &mut values,
10035 emit,
10036 )? {
10037 count += 1;
10038 }
10039 }
10040 }
10041 None => {
10042 // v7.38.11 — the streaming scan is the path a client
10043 // reaches over the wire, so it is the one that has to
10044 // ask the BRIN summary which slots can be skipped. The
10045 // predicate still runs on every row that survives.
10046 let slots = stmt
10047 .where_
10048 .as_ref()
10049 .and_then(|w| crate::brin::candidate_slots(w, table))
10050 .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
10051 for (i, row) in table.scan_visible_slots(slots, &snapshot) {
10052 if i.is_multiple_of(256) {
10053 cancel.check()?;
10054 }
10055 if Self::stream_project_row(
10056 row,
10057 stmt.where_.as_ref(),
10058 compiled_where.as_ref(),
10059 &mut eval_stack,
10060 &projection,
10061 &bound_pos,
10062 &ctx,
10063 &mut values,
10064 emit,
10065 )? {
10066 count += 1;
10067 }
10068 }
10069 }
10070 }
10071 Ok(Some(count))
10072 }
10073
10074 pub(crate) fn try_exec_joined_streaming<F>(
10075 &self,
10076 stmt: &SelectStatement,
10077 cancel: CancelToken<'_>,
10078 emit: &mut F,
10079 ) -> Result<Option<usize>, EngineError>
10080 where
10081 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
10082 {
10083 // Shape gates — keep the streamable surface narrow on
10084 // purpose. The fall-back path still handles everything else.
10085 let Some(from) = &stmt.from else {
10086 return Ok(None);
10087 };
10088 // v7.37 (round 830) — decline anything a row-security policy binds
10089 // for this session. Policies are injected in
10090 // `exec_bare_select_cancel`, below this path, so a statement claimed
10091 // here would read the table unfiltered: measured, `SELECT val FROM
10092 // sec` returned all three rows to a session whose policy allows two,
10093 // while `SELECT upper(val) FROM sec` — declined by the shape gates
10094 // and so materialised — returned the correct two.
10095 //
10096 // Declining sends it to the path that enforces. Teaching this one to
10097 // inject the predicate itself would keep the streaming benefit for
10098 // RLS tables and is the better end state; it is not what a
10099 // correctness fix should carry, and the fall-back is exactly as
10100 // correct, only slower.
10101 if self.select_reads_policy_subject_table(stmt) {
10102 return Ok(None);
10103 }
10104 // r1058 — a WITH list this path never materialises: the CTE
10105 // name would be resolved as a physical relation and error
10106 // ("relation \"big\" does not exist" over the extended
10107 // protocol, caught by the perm-runner's wire legs). The
10108 // materialising fallback owns CTE execution.
10109 if !stmt.ctes.is_empty() {
10110 return Ok(None);
10111 }
10112 // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
10113 // tables` and kin) exist only as synth arms on the
10114 // materialising path; claiming one here errored "relation
10115 // does not exist" over the extended protocol for a query the
10116 // simple protocol answered. Prefix test only — a genuinely
10117 // missing relation must keep erroring in-path.
10118 if from.primary.name.starts_with("__spg_")
10119 || from
10120 .joins
10121 .iter()
10122 .any(|j| j.table.name.starts_with("__spg_"))
10123 {
10124 return Ok(None);
10125 }
10126 // r1058 — decline partitioned / inheritance parents, same
10127 // shape of bug as the RLS decline above: this path scans the
10128 // named table's own (empty) heap, so `SELECT id, region FROM
10129 // cust` on a partition parent streamed ZERO rows over the wire
10130 // while COUNT(*) — an aggregate, materialised below — said 3.
10131 // Caught by the perm-runner's server permutations; the
10132 // materialising fallback expands children correctly.
10133 if crate::partition::has_children(self.active_catalog(), &from.primary.name)
10134 || from
10135 .joins
10136 .iter()
10137 .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
10138 {
10139 return Ok(None);
10140 }
10141 // v7.39 (round 790) — single-table SELECTs stream too. This
10142 // gate said "joins only" because the path was written for
10143 // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
10144 // fell to the materialising fallback, which builds the whole
10145 // `Vec<Row<'static>>` and only then iterates it. Measured on
10146 // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
10147 // reached through a one-row JOIN — 2.6x, purely for lacking a
10148 // join. The deferred-join structure handles one source as the
10149 // degenerate stride-1 case, so the walk below is unchanged.
10150 let _single_table = from.joins.is_empty();
10151 // An ORDER BY that the bounded sort can serve streams; everything
10152 // else still falls to the materialising fallback below.
10153 // r1025 — an ordering the index already holds needs no sort at all.
10154 // Tried before the spill sort, which is the path it replaces.
10155 if !stmt.order_by.is_empty()
10156 && from.joins.is_empty()
10157 && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
10158 {
10159 return Ok(Some(n));
10160 }
10161 if !stmt.order_by.is_empty()
10162 && from.joins.is_empty()
10163 && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
10164 {
10165 return Ok(Some(n));
10166 }
10167 // r1031 — integer keys carried inline instead of an `OrderKey`
10168 // vector per row. Tried AFTER the spill sort on purpose: this lane
10169 // buffers the whole answer, so anything the spill path would take
10170 // must keep taking it rather than be turned back into an in-memory
10171 // sort that answers with a budget error.
10172 if !stmt.order_by.is_empty()
10173 && from.joins.is_empty()
10174 && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
10175 {
10176 return Ok(Some(n));
10177 }
10178 if !stmt.order_by.is_empty()
10179 || stmt.limit.is_some()
10180 || stmt.offset.is_some()
10181 || stmt.having.is_some()
10182 || stmt.group_by.is_some()
10183 || stmt.distinct
10184 || !stmt.unions.is_empty()
10185 || stmt.limit_with_ties
10186 {
10187 return Ok(None);
10188 }
10189 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
10190 return Ok(None);
10191 }
10192 // No window / SRF on the streaming path.
10193 if select_has_window(stmt) {
10194 return Ok(None);
10195 }
10196 if stmt
10197 .items
10198 .iter()
10199 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
10200 {
10201 return Ok(None);
10202 }
10203 // v7.37 (round 831) — a joinless FROM over a plain stored table
10204 // never needs the deferred structure, and building one costs the
10205 // whole table. `materialise_table_ref_filtered` clones every row
10206 // into a `Vec<Row<'static>>` before anything is filtered or
10207 // projected, so peak cost tracks the TABLE, not the result:
10208 // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
10209 // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
10210 // projection saving nothing, while an arithmetic projection — which
10211 // the shape gates decline, so it materialises through the ordinary
10212 // executor — cost +21 MB.
10213 //
10214 // Scanning in batches and releasing each one is what `cursor_fill`
10215 // already does for a lazy cursor, and it is the same walk: resume
10216 // from a slot, take visible rows, evaluate, hand them over, drop
10217 // them. Round 800's finding stands and is why this reads rows OUT
10218 // rather than seeding the join by index — touching the stored
10219 // `PersistentVec` in place makes the whole table resident, which is
10220 // worse than the copy. Each batch is copied, then freed.
10221 if from.joins.is_empty()
10222 && from.primary.unnest_expr.is_none()
10223 && from.primary.lateral_subquery.is_none()
10224 && from.primary.as_of_segment.is_none()
10225 && from.primary.generate_series_args.is_none()
10226 && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
10227 {
10228 return Ok(Some(n));
10229 }
10230 // Build the deferred join under the regular byte budget.
10231 let mut budget = ByteBudget::new(self.max_query_bytes);
10232 let deferred = {
10233 let mut needed = alloc::collections::BTreeSet::new();
10234 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10235 self.build_joined_filtered_rows(
10236 from,
10237 stmt.where_.as_ref(),
10238 cancel,
10239 if prunable { Some(&needed) } else { None },
10240 &mut budget,
10241 )?
10242 };
10243 let combined_schema = &deferred.combined_schema;
10244 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10245 // `::regclass` / enum cast in a joined projection or HAVING needs it.
10246 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10247 // the same predicate the unjoined shape carries.
10248 let joined_sess = self.dml_session();
10249 // v7.38.18 — and the DIALECT. This context carried the catalog and
10250 // the session and not the one field that decides how text
10251 // compares, so a joined row was evaluated in PostgreSQL
10252 // semantics inside a MySQL session.
10253 //
10254 // It showed up only where the two sides had DIFFERENT text types:
10255 // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10256 // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10257 // were fine and the same comparison inside one table was fine.
10258 // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10259 // so the wrong semantics were invisible until a CHAR's padding
10260 // had to be stripped and PostgreSQL's arm does not strip it.
10261 //
10262 // `with_engine` is what sets it; the next line already reaches
10263 // for `self.backslash_escapes`, so the dialect was in hand.
10264 let ctx = EvalContext::new(combined_schema, None)
10265 .with_catalog(self.active_catalog())
10266 .with_engine(self)
10267 .with_session(&joined_sess);
10268 let projection = build_projection(
10269 &stmt.items,
10270 combined_schema,
10271 "",
10272 self.speaks_mysql,
10273 Some(self.active_catalog()),
10274 )?;
10275 // Every projection item must be a bound qualified column —
10276 // anything that needs `eval_expr_with_correlated` keeps the
10277 // materialising path.
10278 let bound_pos = |e: &Expr| -> Option<usize> {
10279 match e {
10280 // v7.39 (round 822) — an UNQUALIFIED column resolves here
10281 // too. The `qualifier.is_some()` guard this replaces meant
10282 // `SELECT pad FROM big` — the commonest projection there is
10283 // — never reached the streaming walk: it fell out at this
10284 // gate and re-ran on the materialising path, after the
10285 // deferred join structure had already been built and paid
10286 // for. Measured (round 821, statement_timeout=120 over 400k
10287 // rows): `big.pad` and `b.pad` streamed and cancelled at
10288 // ~65k rows in 0.14 s, while bare `pad` ran to completion in
10289 // 0.80 s with the timeout never consulted. `find_column_pos`
10290 // has always handled the unqualified case (it falls through
10291 // to a by-name match), so the guard narrowed the gate for no
10292 // reason it recorded.
10293 Expr::Column(c) => eval::find_column_pos(c, &ctx),
10294 _ => None,
10295 }
10296 };
10297 let proj_decomposed: Vec<(usize, usize)> = {
10298 let mut out = Vec::with_capacity(projection.len());
10299 for p in &projection {
10300 let Some(abs) = bound_pos(&p.expr) else {
10301 return Ok(None);
10302 };
10303 let Some(k) = deferred
10304 .offsets
10305 .partition_point(|&o| o <= abs)
10306 .checked_sub(1)
10307 else {
10308 return Ok(None);
10309 };
10310 out.push((k, abs - deferred.offsets[k]));
10311 }
10312 out
10313 };
10314 // Emit columns once.
10315 let columns: Vec<ColumnSchema> = projection
10316 .iter()
10317 // v7.39 (read01 round 54) — keep the column's enum identity through
10318 // the projection (it lives outside the DataType lattice), or a
10319 // derived table / UNION / windowed result forgets it and any outer
10320 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
10321 .map(|p| p.to_column_schema())
10322 .collect();
10323 emit(crate::StreamItem::Header(&columns))?;
10324 let sources_ref = &deferred.sources;
10325 let stride = deferred.stride;
10326 let survivors_ref = &deferred.survivors;
10327 let n_surv = if stride == 0 {
10328 0
10329 } else {
10330 survivors_ref.len() / stride
10331 };
10332 // Reused per-row cell-ref scratch — pushes are zero-alloc
10333 // after the first row.
10334 let null_value = Value::Null;
10335 let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
10336 let mut count: usize = 0;
10337 for surv_i in 0..n_surv {
10338 if surv_i.is_multiple_of(256) {
10339 cancel.check()?;
10340 }
10341 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10342 cell_refs.clear();
10343 for &(k, col_in_src) in &proj_decomposed {
10344 let ri = tuple[k];
10345 let v: &Value = if ri == usize::MAX {
10346 &null_value
10347 } else {
10348 sources_ref[k]
10349 .get(ri)
10350 .and_then(|r| r.values.get(col_in_src))
10351 .unwrap_or(&null_value)
10352 };
10353 cell_refs.push(v);
10354 }
10355 emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
10356 count += 1;
10357 }
10358 Ok(Some(count))
10359 }
10360
10361 fn exec_joined_select(
10362 &self,
10363 stmt: &SelectStatement,
10364 from: &FromClause,
10365 cancel: CancelToken<'_>,
10366 ) -> Result<QueryResult, EngineError> {
10367 // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
10368 // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
10369 // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
10370 // FROM B WHERE B.k = A.k)` into
10371 // SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
10372 // WHERE B.k IS NULL
10373 // The general join executor builds a hash, probes every outer
10374 // tuple, materialises (left_padded_with_null) for every miss,
10375 // then runs the aggregate over the result set. For COUNT(*) we
10376 // only need the count — skip the tuple materialisation. Build
10377 // a HashSet of B's unique join values, scan A's PK index, and
10378 // increment the counter on each miss. PG's Merge Anti-Join
10379 // does roughly this; ours becomes a simple HashSet probe.
10380 if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
10381 return Ok(out);
10382 }
10383 // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
10384 // When ORDER BY is on an indexed primary column, walking the
10385 // btree in the requested direction lets the streamer break
10386 // after `LIMIT + OFFSET` survivors without ever materialising
10387 // the rest of the join — the 80 ms `mailrs_prod_not_exists`
10388 // plateau is exactly this shape.
10389 if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
10390 return Ok(out);
10391 }
10392 // v7.30.3 (mailrs round-26) — the bounded single-join path
10393 // first; peak memory scales with LIMIT instead of the table.
10394 if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
10395 return Ok(out);
10396 }
10397 // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
10398 // WHERE materialisation to the shared helper so the LATERAL
10399 // / UNNEST / regular-catalog paths route through one place.
10400 // (`build_joined_filtered_rows` carries LATERAL support as
10401 // of Phase 3.P0-41.) Downstream we still handle aggregate /
10402 // projection / ORDER BY / DISTINCT / LIMIT inline because
10403 // those depend on the SelectStatement's items list.
10404 let mut budget = ByteBudget::new(self.max_query_bytes);
10405 let deferred = {
10406 let mut needed = alloc::collections::BTreeSet::new();
10407 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10408 self.build_joined_filtered_rows(
10409 from,
10410 stmt.where_.as_ref(),
10411 cancel,
10412 if prunable { Some(&needed) } else { None },
10413 &mut budget,
10414 )?
10415 };
10416 let combined_schema = &deferred.combined_schema;
10417 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10418 // `::regclass` / enum cast in a joined projection or HAVING needs it.
10419 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10420 // the same predicate the unjoined shape carries.
10421 let joined_sess = self.dml_session();
10422 // v7.38.18 — and the DIALECT. This context carried the catalog and
10423 // the session and not the one field that decides how text
10424 // compares, so a joined row was evaluated in PostgreSQL
10425 // semantics inside a MySQL session.
10426 //
10427 // It showed up only where the two sides had DIFFERENT text types:
10428 // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10429 // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10430 // were fine and the same comparison inside one table was fine.
10431 // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10432 // so the wrong semantics were invisible until a CHAR's padding
10433 // had to be stripped and PostgreSQL's arm does not strip it.
10434 //
10435 // `with_engine` is what sets it; the next line already reaches
10436 // for `self.backslash_escapes`, so the dialect was in hand.
10437 let ctx = EvalContext::new(combined_schema, None)
10438 .with_catalog(self.active_catalog())
10439 .with_engine(self)
10440 .with_session(&joined_sess);
10441 // Aggregate path: handle GROUP BY / aggregate calls over the
10442 // joined+filtered rows.
10443 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
10444 // v7.32 (P4 borrow channel, increment 2) — borrow each
10445 // surviving join tuple as a RowRef::Tuple; the aggregate
10446 // engine reads source cells by reference (bound fast path =
10447 // zero clone) instead of consuming materialised combined
10448 // Rows. This is where the +211k materialise_tuple_vals
10449 // clones disappear for the join+aggregate shape.
10450 let refs = deferred.row_refs();
10451 // v7.29 — a per-query memo so correlated scalar
10452 // subqueries batch-evaluate once (group map) instead of
10453 // executing per group.
10454 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
10455 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
10456 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
10457 .map_err(|err| match err {
10458 EngineError::Eval(ev) => ev,
10459 other => eval::EvalError::TypeMismatch {
10460 detail: alloc::format!("{other}"),
10461 },
10462 })
10463 };
10464 let agg = aggregate::run(
10465 stmt,
10466 crate::join::AggRows::Refs(&refs),
10467 combined_schema,
10468 None,
10469 Some(&agg_correlated),
10470 self.parallel_runner.0.as_deref(),
10471 Some(self.active_catalog()),
10472 Some(self),
10473 )?;
10474 return self.finish_agg_result(agg, stmt, cancel);
10475 }
10476
10477 let projection = build_projection(
10478 &stmt.items,
10479 combined_schema,
10480 "",
10481 self.speaks_mysql,
10482 Some(self.active_catalog()),
10483 )?;
10484 // v7.39 (round 734) — a set-returning projection over a JOIN.
10485 // This executor's projection loop treats every item as a scalar,
10486 // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
10487 // "function unnest(integer[]) does not exist" where PG expands
10488 // it. The row-set executor already carries the full SRF pipeline
10489 // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
10490 // sharding): materialise the joined survivors and hand over. The
10491 // WHERE is cleared — the join already applied it, and combined
10492 // columns resolve identically in both executors.
10493 if !self.srf_target_idxs(&projection).is_empty() {
10494 let refs = deferred.row_refs();
10495 let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
10496 let mut s2 = stmt.clone();
10497 s2.where_ = None;
10498 let schema = combined_schema.clone();
10499 return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
10500 }
10501 // v7.33 (P4 borrow channel, increment 3) — project directly off
10502 // the deferred row-index tuples instead of materialising an
10503 // intermediate combined Row per survivor. A bound qualified
10504 // column is read by reference (`RowRef::get` → `tuple_value`) and
10505 // cloned ONCE into the output row; the old `materialise()` (a full
10506 // combined Row plus a source→intermediate clone per referenced
10507 // cell, for every survivor) is gone. A row materialises on demand
10508 // only when a projection or ORDER BY expression needs the eval
10509 // path (subquery / function / arithmetic / unqualified column).
10510 // Same bind-once classification the aggregate input fast path uses
10511 // (`accumulate_groups`), reading the same `tuple_value` mapping the
10512 // differential gate already covers.
10513 let refs = deferred.row_refs();
10514 let bound_pos = |e: &Expr| -> Option<usize> {
10515 match e {
10516 Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
10517 _ => None,
10518 }
10519 };
10520 let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
10521 let all_proj_bound = proj_pos.iter().all(Option::is_some);
10522 // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
10523 // pre-decompose each bound projection position into
10524 // `(source_k, col_in_source)` so the per-row column read
10525 // skips the per-cell `tuple_value` partition_point + slice
10526 // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
10527 // calls) that walk dominated; this version reaches into
10528 // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
10529 let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
10530 .iter()
10531 .map(|p| {
10532 p.and_then(|abs| {
10533 let k = deferred
10534 .offsets
10535 .partition_point(|&o| o <= abs)
10536 .checked_sub(1)?;
10537 Some((k, abs - deferred.offsets[k]))
10538 })
10539 })
10540 .collect();
10541 // v7.39 (round 962) — which projection items are whole-row
10542 // references, and to which join source. The test is
10543 // `locate_column` declining the name, which is the SAME resolver
10544 // the evaluation path uses, so this cannot drift from it: a real
10545 // column carrying an alias's name resolves to a position and is
10546 // not reported here. The source index comes from the alias
10547 // prefix, the way the combined schema names its columns.
10548 let whole_row_src: Vec<Option<usize>> = projection
10549 .iter()
10550 .map(|p| {
10551 let Expr::Column(c) = &p.expr else {
10552 return None;
10553 };
10554 if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
10555 return None;
10556 }
10557 let prefix = alloc::format!("{name}.", name = c.name);
10558 let abs = deferred
10559 .combined_schema
10560 .iter()
10561 .position(|s| s.name.starts_with(&prefix))?;
10562 deferred
10563 .offsets
10564 .partition_point(|&o| o <= abs)
10565 .checked_sub(1)
10566 })
10567 .collect();
10568 // ORDER BY (when present) still evaluates against a materialised
10569 // Row — keep the order-key encoder correct rather than fork it.
10570 let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
10571 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
10572 let mut proj_memo = memoize::MemoizeCache::default();
10573 let sources_ref = &deferred.sources;
10574 let stride = deferred.stride;
10575 let survivors_ref = &deferred.survivors;
10576 let n_surv = survivors_ref.len() / stride.max(1);
10577 // v7.38 (read01 B8) — streaming top-N budget (see the sibling
10578 // single-table path). Bounds this JOIN projection's accumulator
10579 // to O(keep) for `ORDER BY … LIMIT k`.
10580 let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
10581 && !stmt.distinct
10582 && !stmt.limit_with_ties
10583 && !self.env_cfg().disable_topk
10584 {
10585 stmt.limit_literal().and_then(|l| {
10586 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
10587 (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
10588 })
10589 } else {
10590 None
10591 };
10592 // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
10593 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
10594 hashbrown::HashMap::new();
10595 let distinct_hb = hashbrown::DefaultHashBuilder::default();
10596 // v7.38.13 — which output positions must NOT fold. Built once per
10597 // scan from the projection, which carries the source column's
10598 // byte-wise-ness; see `FoldSpec`.
10599 let distinct_mask = fold_mask(&projection);
10600 for surv_i in 0..n_surv {
10601 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10602 let row = &refs[surv_i];
10603 let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
10604 Some(row.as_row())
10605 } else {
10606 None
10607 };
10608 let mut values = Vec::with_capacity(projection.len());
10609 for (i, p) in projection.iter().enumerate() {
10610 if let Some((k, col_in_src)) = proj_decomposed[i] {
10611 // v7.36 — direct (source_k, col) lookup, no
10612 // partition_point. tuple[k] is the row index in
10613 // sources[k]; LEFT-NULL slots are `usize::MAX`.
10614 let ri = tuple[k];
10615 let v: Value<'static> = if ri == usize::MAX {
10616 Value::Null
10617 } else {
10618 sources_ref[k]
10619 .get(ri)
10620 .and_then(|r| r.values.get(col_in_src))
10621 .cloned()
10622 .map(Value::into_owned)
10623 .unwrap_or(Value::Null)
10624 };
10625 values.push(v);
10626 } else if let Some(pos) = proj_pos[i] {
10627 // Bound but couldn't decompose (shouldn't normally
10628 // happen — keep as a safe path).
10629 values.push(
10630 row.get(pos)
10631 .cloned()
10632 .map(Value::into_owned)
10633 .unwrap_or(Value::Null),
10634 );
10635 } else if let Some(k) = whole_row_src[i]
10636 && tuple[k] == usize::MAX
10637 {
10638 // v7.39 (round 962) — a whole-row reference to a side
10639 // an OUTER join null-extended is NULL, not a
10640 // composite whose fields are all NULL. PG18.4 answers
10641 // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
10642 // an empty cell; round 961 answered `(,)`.
10643 //
10644 // The evaluator below cannot tell the two apart: it
10645 // reads the MATERIALISED combined row, where a
10646 // null-extended side is indistinguishable from a real
10647 // row whose every column is NULL — and that row is
10648 // `(,)` in PG too, so guessing by "all fields NULL"
10649 // would trade one wrong answer for another. The
10650 // tuple, which is still in hand here, does know:
10651 // `usize::MAX` is the sentinel the join writes for
10652 // exactly this.
10653 values.push(Value::Null);
10654 } else {
10655 // Eval path — `materialised` is Some whenever any
10656 // projection item is non-bound (need_eval_row true).
10657 // v7.24 (round-16 B) — select-list subqueries under a
10658 // JOIN go through the correlated-aware evaluator too.
10659 let mrow = materialised.as_deref().expect("materialised for eval");
10660 values.push(self.eval_expr_with_correlated(
10661 &p.expr,
10662 mrow,
10663 &ctx,
10664 cancel,
10665 Some(&mut proj_memo),
10666 )?);
10667 }
10668 }
10669 let out_row = Row::new(values);
10670 // v7.37.16 — streaming DISTINCT (see the scan-path twin):
10671 // probe on the projected row; duplicates skip the
10672 // build_order_keys eval and never enter `tagged`.
10673 if stmt.distinct {
10674 let bucket = seen_distinct
10675 .entry(norm_hash_row(
10676 &out_row,
10677 &distinct_hb,
10678 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10679 ))
10680 .or_default();
10681 if bucket.iter().any(|i| {
10682 row_eq_norm(
10683 &tagged[i].1,
10684 &out_row,
10685 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10686 )
10687 }) {
10688 continue;
10689 }
10690 bucket.push(tagged.len());
10691 }
10692 let order_keys = if stmt.order_by.is_empty() {
10693 Vec::new()
10694 } else {
10695 let mrow = materialised.as_deref().expect("materialised for order by");
10696 build_order_keys(&stmt.order_by, mrow, &ctx)?
10697 };
10698 budget.charge(approx_row_bytes(&out_row))?;
10699 tagged.push((order_keys, out_row));
10700 if let Some((k, descs)) = &topk_stream {
10701 topk_trim(&mut tagged, *k, descs);
10702 }
10703 }
10704 if !stmt.order_by.is_empty() {
10705 // v7.38 元机制 D acceptor — see other call site above.
10706 let keep = if self.env_cfg().disable_topk {
10707 None
10708 } else {
10709 stmt.limit_literal()
10710 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
10711 };
10712 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
10713 // v7.39 (round 688) — the join's ORDER BY resolves its keys
10714 // against `ctx`, which is built from `build_combined_schema`, so
10715 // this is where a declared collation reaches the sort. There was
10716 // exactly ONE resolver call in the engine before this — the
10717 // single-table scan's — which is why every other shape sorted by
10718 // bytes no matter what the schemas carried.
10719 let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
10720 crate::orderby::partial_sort_tagged_in(
10721 &mut tagged,
10722 keep,
10723 &descs,
10724 &colls,
10725 self.session_parallel_workers(),
10726 );
10727 }
10728 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
10729 apply_offset_and_limit(
10730 &mut output_rows,
10731 stmt.offset_literal(),
10732 stmt.limit_literal(),
10733 );
10734 let columns: Vec<ColumnSchema> = projection
10735 .into_iter()
10736 .map(|p| p.to_column_schema())
10737 .collect();
10738 Ok(QueryResult::Rows {
10739 columns,
10740 rows: output_rows,
10741 })
10742 }
10743}
10744
10745impl Engine {
10746 /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
10747 /// by id, decodes each row body against the table's current
10748 /// schema, applies the SELECT's projection + optional WHERE +
10749 /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
10750 /// / ORDER BY are unsupported on this path (STABILITY carve-
10751 /// out); operators wanting them should restore the segment
10752 /// into a regular table first.
10753 fn exec_select_as_of_segment(
10754 &self,
10755 stmt: &SelectStatement,
10756 from: &spg_sql::ast::FromClause,
10757 segment_id: u32,
10758 ) -> Result<QueryResult, EngineError> {
10759 // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
10760 // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
10761 if !from.joins.is_empty()
10762 || stmt.group_by.is_some()
10763 || stmt.having.is_some()
10764 || !stmt.unions.is_empty()
10765 || !stmt.order_by.is_empty()
10766 || stmt.offset.is_some()
10767 || stmt.distinct
10768 || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
10769 {
10770 return Err(EngineError::Unsupported(
10771 "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
10772 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
10773 .into(),
10774 ));
10775 }
10776 let table = self
10777 .active_catalog()
10778 .get(&from.primary.name)
10779 .ok_or_else(|| StorageError::TableNotFound {
10780 name: from.primary.name.clone(),
10781 })?;
10782 let schema = table.schema().clone();
10783 let schema_cols = &schema.columns;
10784 let alias = from
10785 .primary
10786 .alias
10787 .as_deref()
10788 .unwrap_or(from.primary.name.as_str());
10789 let ctx = self.ev_ctx(schema_cols, Some(alias));
10790 let seg = self
10791 .active_catalog()
10792 .cold_segment(segment_id)
10793 .ok_or_else(|| {
10794 EngineError::Unsupported(alloc::format!(
10795 "AS OF SEGMENT: cold segment {segment_id} not registered"
10796 ))
10797 })?;
10798 let mut out_rows: Vec<Row<'static>> = Vec::new();
10799 let mut limit_remaining: Option<usize> =
10800 stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
10801 for (_key, body) in seg.scan() {
10802 let (row, _consumed) =
10803 spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
10804 .map_err(EngineError::Storage)?;
10805 if let Some(where_expr) = &stmt.where_ {
10806 let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
10807 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
10808 continue;
10809 }
10810 }
10811 // Projection.
10812 let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
10813 out_rows.push(projected);
10814 if let Some(rem) = limit_remaining.as_mut() {
10815 if *rem == 0 {
10816 out_rows.pop();
10817 break;
10818 }
10819 *rem -= 1;
10820 }
10821 }
10822 // Output column schema: derive from SELECT items.
10823 let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
10824 Ok(QueryResult::Rows {
10825 columns,
10826 rows: out_rows,
10827 })
10828 }
10829
10830 /// v6.10.2 — simple-path WHERE eval that doesn't go through
10831 /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
10832 /// scan paths predicate against a snapshot frozen segment, no
10833 /// cross-row state.
10834 fn eval_expr_simple(
10835 &self,
10836 expr: &Expr,
10837 row: &Row<'static>,
10838 ctx: &EvalContext,
10839 ) -> Result<Value<'static>, EngineError> {
10840 let cancel = CancelToken::none();
10841 self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
10842 }
10843}
10844
10845// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
10846
10847/// One row-producing projection: an expression to evaluate, the resulting
10848/// column's user-visible name, its inferred type, and nullability.
10849#[derive(Debug, Clone)]
10850pub(crate) struct ProjectedItem {
10851 pub(crate) expr: Expr,
10852 pub(crate) output_name: String,
10853 pub(crate) ty: DataType,
10854 pub(crate) nullable: bool,
10855 /// v7.39 (read01 round 54) — a projected enum column keeps its enum
10856 /// identity. Enum-ness lives outside the DataType lattice (the value is a
10857 /// Text), so a projection that dropped this made the RESULT schema forget
10858 /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
10859 /// that schema, silently fell back to TEXT order instead of member order.
10860 pub(crate) user_enum_type: Option<String>,
10861 /// v7.39 (round 425) — a projected MySQL temporal column keeps its
10862 /// declared fractional-seconds precision, so the renderer can pad to
10863 /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
10864 /// a whole second). Like `user_enum_type` this lives outside the
10865 /// DataType lattice, so a projection that dropped it made the RESULT
10866 /// schema forget how wide the fraction should print.
10867 pub(crate) mysql_fsp: Option<u8>,
10868 /// v7.39 (round 688) — and its declared collation, the third thing to
10869 /// live outside the DataType lattice and the third to be lost the same
10870 /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
10871 /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
10872 /// projection rebuilt the output column and the ORDER BY resolves
10873 /// against THAT schema.
10874 pub(crate) collation_name: Option<String>,
10875 /// v7.38.13 — and whether this position must NOT fold when DISTINCT
10876 /// de-dups it. The fourth thing to live outside the DataType lattice
10877 /// and the fourth to be lost the same way: a column declared
10878 /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
10879 /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
10880 /// returns two.
10881 ///
10882 /// A BOOL rather than the `Collation` enum on purpose. The enum's
10883 /// storage default is `Binary`, but the FOLD default under MySQL is
10884 /// case-insensitive — carrying the enum would silently mean
10885 /// "exempt" for every projected expression that is not a column.
10886 /// This field states the question it answers.
10887 pub(crate) fold_exempt: bool,
10888 /// v7.38.18 — does this column's collation make trailing spaces
10889 /// insignificant? A separate question from `fold_exempt`:
10890 /// `utf8mb4_bin` is fold-exempt AND pads, `utf8mb4_0900_ai_ci`
10891 /// folds and does not. Read off the same column, at the same
10892 /// place, so the two masks cannot drift apart.
10893 pub(crate) pads: bool,
10894}
10895
10896impl ProjectedItem {
10897 /// v7.38.14 — the output column this projected item describes.
10898 ///
10899 /// There were TWENTY-ONE places converting a `ProjectedItem` into a
10900 /// `ColumnSchema`, each written as `ColumnSchema::new(..)` followed by a
10901 /// hand-picked list of attributes to copy after it, and the lists did not
10902 /// agree: six carried enum identity, the collation NAME and MySQL fsp; ten
10903 /// carried the first and last but not the name; five carried nothing at
10904 /// all. Not one carried `collation`, the enum every MySQL text comparison
10905 /// actually reads.
10906 ///
10907 /// That is how a declared collation vanished between a subquery and the
10908 /// query that selects from it: the inner SELECT's output schema claimed
10909 /// `ColumnSchema::new`'s default, which is `Binary` — a value downstream
10910 /// reads as "byte-wise ON PURPOSE" rather than as "unknown", so the loss
10911 /// presents as a deliberate declaration.
10912 ///
10913 /// One conversion, so a field added to either type has one place to be
10914 /// remembered instead of twenty-one.
10915 pub(crate) fn to_column_schema(&self) -> ColumnSchema {
10916 let mut c = ColumnSchema::new(self.output_name.clone(), self.ty, self.nullable);
10917 c.user_enum_type.clone_from(&self.user_enum_type);
10918 c.collation_name.clone_from(&self.collation_name);
10919 c.mysql_fsp = self.mysql_fsp;
10920 // `fold_exempt` is the projection's answer to the same question
10921 // `ColumnSchema::collation` answers downstream, and it was computed
10922 // from the source column. Keeping the two in step here is what stops
10923 // a de-duplication site further on from asking the schema and being
10924 // told the opposite of what the projection knew.
10925 c.collation = if self.fold_exempt {
10926 spg_storage::Collation::Binary
10927 } else {
10928 spg_storage::Collation::CaseInsensitive
10929 };
10930 c
10931 }
10932}
10933
10934/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
10935/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
10936/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
10937/// the spec's "two NULLs are not distinct"; the second is a tolerated
10938/// quirk for v1 (no NaN literals are reachable from the SQL surface).
10939/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
10940fn expr_is_aggregate_call(e: &Expr) -> bool {
10941 match e {
10942 Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
10943 Expr::AggregateOrdered { .. } => true,
10944 _ => false,
10945 }
10946}
10947
10948/// Collect distinct top-level aggregate call expressions (dedup by value). Does
10949/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
10950/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
10951/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
10952/// than today — never a regression on a working query).
10953fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
10954 if expr_is_aggregate_call(e) {
10955 if !out.iter().any(|x| x == e) {
10956 out.push(e.clone());
10957 }
10958 return;
10959 }
10960 match e {
10961 Expr::Binary { lhs, rhs, .. } => {
10962 collect_agg_exprs(lhs, out);
10963 collect_agg_exprs(rhs, out);
10964 }
10965 Expr::Unary { expr, .. }
10966 | Expr::Cast { expr, .. }
10967 | Expr::IsNull { expr, .. }
10968 | Expr::BoolTest { expr, .. }
10969 | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
10970 Expr::FunctionCall { args, .. } => {
10971 for a in args {
10972 collect_agg_exprs(a, out);
10973 }
10974 }
10975 Expr::Like { expr, pattern, .. } => {
10976 collect_agg_exprs(expr, out);
10977 collect_agg_exprs(pattern, out);
10978 }
10979 Expr::Extract { source, .. } => collect_agg_exprs(source, out),
10980 Expr::WindowFunction {
10981 args,
10982 partition_by,
10983 order_by,
10984 ..
10985 } => {
10986 for a in args {
10987 collect_agg_exprs(a, out);
10988 }
10989 for p in partition_by {
10990 collect_agg_exprs(p, out);
10991 }
10992 for (o, _, _) in order_by {
10993 collect_agg_exprs(o, out);
10994 }
10995 }
10996 _ => {}
10997 }
10998}
10999
11000/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
11001fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
11002 if expr_is_aggregate_call(e) {
11003 if let Some(idx) = aggs.iter().position(|x| x == e) {
11004 *e = Expr::Column(ColumnName {
11005 qualifier: None,
11006 name: alloc::format!("__agg{idx}"),
11007 });
11008 }
11009 return;
11010 }
11011 match e {
11012 Expr::Binary { lhs, rhs, .. } => {
11013 replace_agg_exprs(lhs, aggs);
11014 replace_agg_exprs(rhs, aggs);
11015 }
11016 Expr::Unary { expr, .. }
11017 | Expr::Cast { expr, .. }
11018 | Expr::IsNull { expr, .. }
11019 | Expr::BoolTest { expr, .. }
11020 | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
11021 Expr::FunctionCall { args, .. } => {
11022 for a in args {
11023 replace_agg_exprs(a, aggs);
11024 }
11025 }
11026 Expr::Like { expr, pattern, .. } => {
11027 replace_agg_exprs(expr, aggs);
11028 replace_agg_exprs(pattern, aggs);
11029 }
11030 Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
11031 Expr::WindowFunction {
11032 args,
11033 partition_by,
11034 order_by,
11035 ..
11036 } => {
11037 for a in args {
11038 replace_agg_exprs(a, aggs);
11039 }
11040 for p in partition_by {
11041 replace_agg_exprs(p, aggs);
11042 }
11043 for (o, _, _) in order_by {
11044 replace_agg_exprs(o, aggs);
11045 }
11046 }
11047 _ => {}
11048 }
11049}
11050
11051/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
11052/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
11053/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
11054/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
11055/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
11056/// Returns None outside the bounded subset (leaves current behaviour). Only fires
11057/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
11058/// window-only / aggregate-only queries.
11059fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
11060 if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
11061 return None;
11062 }
11063 // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
11064 if !stmt.unions.is_empty() {
11065 return None;
11066 }
11067 let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
11068 if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
11069 return None;
11070 }
11071 stmt.from.as_ref()?;
11072 // Collect the aggregate calls to hoist from projection + outer ORDER BY.
11073 let mut aggs: Vec<Expr> = Vec::new();
11074 for item in &stmt.items {
11075 if let SelectItem::Expr { expr, .. } = item {
11076 collect_agg_exprs(expr, &mut aggs);
11077 }
11078 }
11079 for ob in &stmt.order_by {
11080 collect_agg_exprs(&ob.expr, &mut aggs);
11081 }
11082 // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
11083 let mut inner_items: Vec<SelectItem> = Vec::new();
11084 for g in &group_cols {
11085 inner_items.push(SelectItem::Expr {
11086 expr: g.clone(),
11087 alias: None,
11088 });
11089 }
11090 for (i, a) in aggs.iter().enumerate() {
11091 inner_items.push(SelectItem::Expr {
11092 expr: a.clone(),
11093 alias: Some(alloc::format!("__agg{i}")),
11094 });
11095 }
11096 let inner = SelectStatement {
11097 items: inner_items,
11098 distinct: false,
11099 distinct_on: Vec::new(),
11100 unions: Vec::new(),
11101 order_by: Vec::new(),
11102 limit: None,
11103 offset: None,
11104 limit_with_ties: false,
11105 window_check_exprs: Vec::new(),
11106 ..stmt.clone()
11107 };
11108 let derived = TableRef {
11109 name: "__aggwin".into(),
11110 alias: Some("__aggwin".into()),
11111 only: false,
11112 as_of_segment: None,
11113 unnest_expr: None,
11114 unnest_column_aliases: Vec::new(),
11115 with_ordinality: false,
11116 generate_series_args: None,
11117 lateral_subquery: Some(alloc::boxed::Box::new(inner)),
11118 jsonb_each_text_arg: None,
11119 table_fn_call: None,
11120 rows_from: None,
11121 json_table: None,
11122 scalar_fn_item: false,
11123 };
11124 // Outer window query over the derived rows: aggregates → __aggN column refs.
11125 let mut outer_items = stmt.items.clone();
11126 for item in &mut outer_items {
11127 if let SelectItem::Expr { expr, alias } = item {
11128 // Preserve PG's column label for a bare aggregate projection.
11129 if alias.is_none()
11130 && let Expr::FunctionCall { name, .. } = expr
11131 && crate::aggregate::is_aggregate_name(name)
11132 {
11133 *alias = Some(name.to_ascii_lowercase());
11134 }
11135 replace_agg_exprs(expr, &aggs);
11136 }
11137 }
11138 let mut outer_order = stmt.order_by.clone();
11139 for ob in &mut outer_order {
11140 replace_agg_exprs(&mut ob.expr, &aggs);
11141 }
11142 let mut outer_distinct_on = stmt.distinct_on.clone();
11143 for e in &mut outer_distinct_on {
11144 replace_agg_exprs(e, &aggs);
11145 }
11146 Some(SelectStatement {
11147 locking: None,
11148 ctes: Vec::new(),
11149 distinct: stmt.distinct,
11150 distinct_on: outer_distinct_on,
11151 items: outer_items,
11152 from: Some(FromClause {
11153 primary: derived,
11154 joins: Vec::new(),
11155 }),
11156 where_: None,
11157 group_by: None,
11158 group_by_all: false,
11159 having: None,
11160 unions: Vec::new(),
11161 order_by: outer_order,
11162 limit: stmt.limit.clone(),
11163 offset: stmt.offset.clone(),
11164 limit_with_ties: stmt.limit_with_ties,
11165 window_check_exprs: Vec::new(),
11166 })
11167}
11168
11169/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
11170/// membership.
11171///
11172/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
11173/// there?", and all four answered by scanning the whole right side once per
11174/// left row. The cost was (left rows x right rows), which is why
11175/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
11176/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
11177/// row that does not pays for all of it. Over 100k left rows, raising the
11178/// right side from 100 to 10,000 took 35 ms to 2848.
11179///
11180/// This is the shape round 485 already solved for DISTINCT, and it reuses
11181/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
11182/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
11183/// every bucket with the exact comparator, so a collision costs time and
11184/// never an answer.
11185struct PeerIndex<'r> {
11186 bh: hashbrown::DefaultHashBuilder,
11187 buckets: hashbrown::HashMap<u64, Vec<usize>>,
11188 rows: &'r [Row<'static>],
11189 fold: FoldSpec<'r>,
11190}
11191
11192impl<'r> PeerIndex<'r> {
11193 fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
11194 // ONE hasher for the whole pass: the default builder is seeded per
11195 // instance, so a fresh one per row would put equal rows in different
11196 // buckets.
11197 let bh = hashbrown::DefaultHashBuilder::default();
11198 let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
11199 hashbrown::HashMap::with_capacity(rows.len());
11200 for (i, r) in rows.iter().enumerate() {
11201 buckets
11202 .entry(norm_hash_row(r, &bh, fold))
11203 .or_default()
11204 .push(i);
11205 }
11206 Self {
11207 bh,
11208 buckets,
11209 rows,
11210 fold,
11211 }
11212 }
11213
11214 fn contains(&self, r: &Row<'static>) -> bool {
11215 let h = norm_hash_row(r, &self.bh, self.fold);
11216 self.buckets
11217 .get(&h)
11218 .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
11219 }
11220
11221 /// Remove ONE occurrence, so the multiset forms cancel row for row the
11222 /// way the pool they replaced did.
11223 fn take_one(&mut self, r: &Row<'static>) -> bool {
11224 let h = norm_hash_row(r, &self.bh, self.fold);
11225 let Some(b) = self.buckets.get_mut(&h) else {
11226 return false;
11227 };
11228 let Some(pos) = b
11229 .iter()
11230 .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
11231 else {
11232 return false;
11233 };
11234 b.swap_remove(pos);
11235 true
11236 }
11237}
11238
11239pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
11240 dedup_by_row(rows, |r| r, fold)
11241}
11242
11243/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
11244/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
11245/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
11246/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
11247/// order is preserved, and correctness needs only the one-way guarantee
11248/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
11249/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
11250fn dedup_by_row<T>(
11251 items: Vec<T>,
11252 row_of: impl Fn(&T) -> &Row<'static>,
11253 fold: FoldSpec<'_>,
11254) -> Vec<T> {
11255 if items.len() <= 32 {
11256 let mut out: Vec<T> = Vec::with_capacity(items.len());
11257 for it in items {
11258 if !out
11259 .iter()
11260 .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
11261 {
11262 out.push(it);
11263 }
11264 }
11265 return out;
11266 }
11267 // ONE BuildHasher instance for the whole pass — the default builder
11268 // is randomly seeded PER INSTANCE, so a fresh one per row would give
11269 // equal rows different hashes and never dedup.
11270 let bh = hashbrown::DefaultHashBuilder::default();
11271 let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
11272 let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
11273 hashbrown::HashMap::with_capacity(items.len());
11274 for it in items {
11275 let h = norm_hash_row(row_of(&it), &bh, fold);
11276 let bucket = buckets.entry(h).or_default();
11277 if !bucket
11278 .iter()
11279 .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
11280 {
11281 bucket.push(out.len());
11282 out.push(it);
11283 }
11284 }
11285 out
11286}
11287
11288/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
11289/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
11290/// rows may collide (buckets are re-checked with the exact comparator).
11291///
11292/// Domain design mirrors `value_cmp`'s equivalence classes:
11293/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
11294/// shares one domain: a value that is an integer fitting i64 hashes the
11295/// i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
11296/// anything else hashes the f64 approximation computed by THE SAME
11297/// formula the value_cmp float arms use (`numeric_to_f64`), so
11298/// `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
11299/// hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
11300/// Known un-closable corner: an integer in [2^53, 2^63) can compare
11301/// Equal to a float via value_cmp's lossy f64 arm while hashing in the
11302/// exact-i64 domain — mixed int/float rows at that magnitude may miss a
11303/// dedup (PG itself compares int8↔float8 in the lossy float8 domain).
11304/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
11305/// compares them blank-insensitively; plain Text pairs that differ only
11306/// in trailing blanks merely collide and are separated exactly).
11307/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
11308/// hash their fields under a distinct tag.
11309/// - Everything value_cmp falls back to debug-format ordering for
11310/// (Json, arrays, vectors, geometry, ranges, …) shares one constant
11311/// bucket — degrades to the exact linear scan, never wrong.
11312fn norm_hash_row(
11313 row: &Row<'static>,
11314 bh: &hashbrown::DefaultHashBuilder,
11315 fold: FoldSpec<'_>,
11316) -> u64 {
11317 norm_hash_values(&row.values, bh, fold)
11318}
11319
11320/// v7.39 (round 485) — the same hash over a bare value slice, so the
11321/// DISTINCT probe can run against a reused buffer instead of demanding a
11322/// `Row` that has to be allocated first (see `values_eq_norm`).
11323fn norm_hash_values(
11324 values: &[Value<'static>],
11325 bh: &hashbrown::DefaultHashBuilder,
11326 fold: FoldSpec<'_>,
11327) -> u64 {
11328 use core::hash::{BuildHasher, Hash, Hasher};
11329 let mut h = bh.build_hasher();
11330 for (i, v) in values.iter().enumerate() {
11331 // v7.39 (round 410) — hash the folded key when the MySQL collation
11332 // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
11333 // `'A'` vs `'a '`) share a hash bucket.
11334 //
11335 // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
11336 // byte-wise column that folded here while the comparator did not
11337 // would scatter equal rows across buckets and stop de-duplicating
11338 // at all; the hash and the comparator have to read the same mask.
11339 if fold.folds(i)
11340 && let Some(folded) = mysql_dedup_fold(v, fold.pads_at(i))
11341 {
11342 folded.hash(&mut h);
11343 continue;
11344 }
11345 norm_hash_value(v, &mut h);
11346 }
11347 h.finish()
11348}
11349
11350/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
11351///
11352/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
11353const fn pow10_i128(p: u16) -> Option<i128> {
11354 const P: [i128; 39] = {
11355 let mut t = [1i128; 39];
11356 let mut i = 1;
11357 while i < 39 {
11358 t[i] = t[i - 1] * 10;
11359 i += 1;
11360 }
11361 t
11362 };
11363 if (p as usize) < P.len() {
11364 Some(P[p as usize])
11365 } else {
11366 None
11367 }
11368}
11369
11370fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
11371 const TAG_NULL: u8 = 0;
11372 const TAG_BOOL: u8 = 1;
11373 const TAG_NUM_I64: u8 = 2;
11374 const TAG_NUM_F64: u8 = 3;
11375 const TAG_TEXT: u8 = 4;
11376 const TAG_DATE: u8 = 6;
11377 const TAG_TIME: u8 = 7;
11378 const TAG_TIMESTAMP: u8 = 8;
11379 const TAG_TIMETZ: u8 = 10;
11380 const TAG_UUID: u8 = 11;
11381 const TAG_MONEY: u8 = 12;
11382 const TAG_BYTES: u8 = 13;
11383 const TAG_INTERVAL: u8 = 14;
11384 const TAG_CHAR1: u8 = 15;
11385 const TAG_OPAQUE: u8 = 255;
11386 // One shared writer for the numeric family: an integer value
11387 // representable as i64 goes exact (round-trip probe — no_std, so no
11388 // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
11389 // through 0i64, folding it into 0.0 as value_cmp requires.
11390 let num_f64 = |h: &mut H, x: f64| {
11391 if x.is_nan() {
11392 h.write_u8(TAG_NUM_F64);
11393 h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
11394 return;
11395 }
11396 const TWO63: f64 = 9_223_372_036_854_775_808.0;
11397 if (-TWO63..TWO63).contains(&x) {
11398 #[allow(clippy::cast_possible_truncation)]
11399 let n = x as i64;
11400 #[allow(clippy::cast_precision_loss)]
11401 if (n as f64) == x {
11402 h.write_u8(TAG_NUM_I64);
11403 h.write_i64(n);
11404 return;
11405 }
11406 }
11407 h.write_u8(TAG_NUM_F64);
11408 h.write_u64(x.to_bits());
11409 };
11410 match v {
11411 Value::Null => h.write_u8(TAG_NULL),
11412 Value::Bool(b) => {
11413 h.write_u8(TAG_BOOL);
11414 h.write_u8(u8::from(*b));
11415 }
11416 Value::SmallInt(n) => {
11417 h.write_u8(TAG_NUM_I64);
11418 h.write_i64(i64::from(*n));
11419 }
11420 Value::Int(n) => {
11421 h.write_u8(TAG_NUM_I64);
11422 h.write_i64(i64::from(*n));
11423 }
11424 Value::BigInt(n) => {
11425 h.write_u8(TAG_NUM_I64);
11426 h.write_i64(*n);
11427 }
11428 Value::Float(x) => num_f64(h, *x),
11429 Value::Numeric {
11430 scaled,
11431 scale,
11432 kind,
11433 } => match kind {
11434 spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
11435 spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
11436 spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
11437 spg_storage::NumericKind::Finite => {
11438 // Reduce trailing fractional zeros so 1.50 and 1.5 share a
11439 // representation, then: exact integers fitting i64 go to the
11440 // i64 domain; everything else uses numeric_to_f64 — the SAME
11441 // formula value_cmp's Numeric↔Float arm compares with.
11442 // r1044 — the reduction is required (`1.5` and `1.50` are
11443 // one value and must land in one bucket) and it used to
11444 // walk one digit at a time. That is O(scale), and scale
11445 // is not small in practice: `n / 100` on a NUMERIC
11446 // column stores `9.1900000000000000`, scale 16, so the
11447 // loop ran fourteen times PER ROW.
11448 //
11449 // Priced by ablation rather than guessed at — removing
11450 // the loop entirely took `SELECT DISTINCT n FROM t ORDER
11451 // BY n` over 400,000 rows from 52 ms to 14.8, against
11452 // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
11453 // tried first moved it not at all, which is why this one
11454 // was measured before it was written.
11455 //
11456 // Binary search over the same powers finds the whole
11457 // run of trailing zeros in at most six tests and one
11458 // division, instead of one test and one division per
11459 // digit.
11460 let (mut s, mut sc) = (*scaled, *scale);
11461 if sc > 0 && s != 0 {
11462 let mut lo: u16 = 0;
11463 let mut hi: u16 = sc;
11464 while lo < hi {
11465 let mid = (lo + hi).div_ceil(2);
11466 match pow10_i128(mid) {
11467 Some(p) if s % p == 0 => lo = mid,
11468 _ => hi = mid - 1,
11469 }
11470 }
11471 if lo > 0 {
11472 if let Some(p) = pow10_i128(lo) {
11473 s /= p;
11474 sc -= lo;
11475 }
11476 }
11477 }
11478 if sc == 0 {
11479 if let Ok(n) = i64::try_from(s) {
11480 h.write_u8(TAG_NUM_I64);
11481 h.write_i64(n);
11482 } else {
11483 num_f64(h, crate::orderby::numeric_to_f64(s, 0));
11484 }
11485 } else {
11486 num_f64(h, crate::orderby::numeric_to_f64(s, sc));
11487 }
11488 }
11489 },
11490 // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
11491 // value that also fits i128 reuses the Numeric path above so
11492 // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
11493 // any i128-representable value — constant bucket is safe.
11494 Value::NumericBig(b) => match b.to_i128() {
11495 Some(s) => norm_hash_value(
11496 &Value::Numeric {
11497 scaled: s,
11498 scale: b.scale(),
11499 kind: spg_storage::NumericKind::Finite,
11500 },
11501 h,
11502 ),
11503 None => h.write_u8(TAG_OPAQUE),
11504 },
11505 // value_cmp compares Text↔BpChar blank-insensitively (both sides
11506 // trimmed), so both hash the trimmed bytes. Text pairs differing
11507 // only in trailing blanks collide and are split exactly in-bucket.
11508 Value::Text(s) | Value::BpChar(s) => {
11509 h.write_u8(TAG_TEXT);
11510 h.write(s.trim_end_matches(' ').as_bytes());
11511 }
11512 Value::Char1(c) => {
11513 h.write_u8(TAG_CHAR1);
11514 h.write_u8(*c);
11515 }
11516 Value::Date(d) => {
11517 h.write_u8(TAG_DATE);
11518 h.write_i32(*d);
11519 }
11520 Value::Time(t) => {
11521 h.write_u8(TAG_TIME);
11522 h.write_i64(*t);
11523 }
11524 Value::Timestamp(t) => {
11525 h.write_u8(TAG_TIMESTAMP);
11526 h.write_i64(*t);
11527 }
11528 Value::TimeTz { us, offset_secs } => {
11529 h.write_u8(TAG_TIMETZ);
11530 h.write_i64(*us);
11531 h.write_i32(*offset_secs);
11532 }
11533 Value::Uuid(u) => {
11534 h.write_u8(TAG_UUID);
11535 h.write(u);
11536 }
11537 Value::Money(c) => {
11538 h.write_u8(TAG_MONEY);
11539 h.write_i64(*c);
11540 }
11541 Value::Bytes(b) => {
11542 h.write_u8(TAG_BYTES);
11543 h.write(b.as_ref());
11544 }
11545 Value::Interval {
11546 months,
11547 days,
11548 micros,
11549 kind,
11550 } => {
11551 h.write_u8(TAG_INTERVAL);
11552 h.write_i32(*months);
11553 h.write_i32(*days);
11554 h.write_i64(*micros);
11555 }
11556 // v7.37.16 — REAL joined the numeric value_cmp family (widened
11557 // to f64, same formulas as the arms), so it hashes in the shared
11558 // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
11559 // f32→f64 is exact, so equal-under-cmp implies equal bits here.
11560 Value::Real(x) => num_f64(h, f64::from(*x)),
11561 // Json (structural equality), vector families (float rendering),
11562 // arrays / geometry / net / ranges / composites (debug-format
11563 // fallback): one constant bucket — exact linear within.
11564 _ => h.write_u8(TAG_OPAQUE),
11565 }
11566}
11567
11568/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
11569/// treats numerically-equal exact values as one regardless of type or scale
11570/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
11571/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
11572/// `Row` `==` would keep them distinct.
11573/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
11574/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
11575/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
11576/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
11577/// the folded comparison key for a text value, None for anything else (which
11578/// keeps the byte-exact `value_cmp` path).
11579fn mysql_dedup_fold(v: &Value, pads: bool) -> Option<String> {
11580 match v {
11581 // v7.38.17 — CHAR's trailing spaces are padding; TEXT's are
11582 // data. The comment above named `utf8mb4_uca1400_ai_ci`, which
11583 // is MariaDB's default and PAD SPACE. SPG advertises MySQL 8.0,
11584 // whose default is NO PAD, so `'alpha'` and `'alpha '` are two
11585 // rows to a `SELECT DISTINCT` and one to `count(DISTINCT)` was
11586 // the same question answered twice.
11587 // v7.38.18 — a CHAR's padding is the TYPE's and never counts; a
11588 // TEXT's is the collation's, which `pads` carries per position.
11589 Value::BpChar(s) => Some(spg_storage::mysql_compare_fold_char(s)),
11590 Value::Text(s) if pads => Some(spg_storage::mysql_compare_fold_char(s)),
11591 Value::Text(s) => Some(spg_storage::mysql_compare_fold(s)),
11592 _ => None,
11593 }
11594}
11595
11596/// v7.39 (round 485) — how many projected rows the single-table scan
11597/// builds, and how many of those the DISTINCT probe throws away again.
11598///
11599/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
11600/// 21 % of all samples in malloc/free called straight from the scan
11601/// closure. The closure's one per-row allocation is the projected
11602/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
11603/// instructions later — but "most" is a guess until it is a number, so
11604/// these count it. (Round 480 was spent acting on an inference about a
11605/// branch that turned out never to run.)
11606/// v7.39 (round 488) — reachability counters for round 487's projection
11607/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
11608/// and a never-called-function probe rules out code layout — so the
11609/// question is whether that shape reaches this code at all, which is a
11610/// number, not an inference.
11611pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11612pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11613
11614pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11615pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
11616 core::sync::atomic::AtomicU64::new(0);
11617
11618/// v7.38.13 — how DISTINCT must compare one row of output.
11619///
11620/// The MySQL default collation folds case and trailing spaces when it
11621/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
11622/// must not fold — `e2e_mysql_collate_binary_round370` calls the
11623/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
11624/// one when the schema asked to keep them apart", and names DISTINCT as
11625/// one of the sites that has to honour it.
11626///
11627/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
11628/// value in a MySQL session, because a bool cannot see a column. The
11629/// GROUP BY path consults the schema and was right all along; the test
11630/// only ever exercised that spelling, so the DISTINCT hole was never
11631/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
11632///
11633/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
11634/// which is what a caller with no schema to offer gets.
11635#[derive(Clone, Copy)]
11636pub(crate) struct FoldSpec<'c> {
11637 mysql: bool,
11638 binary: &'c [bool],
11639 /// v7.38.18 — the padding mask, in lockstep with `binary`. Read the
11640 /// note on `folds`: a hash and its comparator must consult the same
11641 /// masks or equal rows scatter across buckets.
11642 pads: &'c [bool],
11643}
11644
11645impl<'c> FoldSpec<'c> {
11646 /// No column information — every Text position folds under MySQL.
11647 pub(crate) const fn dialect(mysql: bool) -> Self {
11648 Self {
11649 mysql,
11650 binary: &[],
11651 pads: &[],
11652 }
11653 }
11654
11655 /// The mask read off the output columns.
11656 pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
11657 Self {
11658 mysql,
11659 binary,
11660 pads: &[],
11661 }
11662 }
11663
11664 /// The masks read off the output columns — fold-exemption AND
11665 /// padding, which are different questions about the same collation.
11666 pub(crate) fn of_masks(mysql: bool, binary: &'c [bool], pads: &'c [bool]) -> Self {
11667 Self {
11668 mysql,
11669 binary,
11670 pads,
11671 }
11672 }
11673
11674 /// Does position `i` treat trailing spaces as insignificant?
11675 #[inline]
11676 fn pads_at(&self, i: usize) -> bool {
11677 self.pads.get(i).copied().unwrap_or(false)
11678 }
11679
11680 /// Does position `i` fold?
11681 #[inline]
11682 fn folds(&self, i: usize) -> bool {
11683 self.mysql && !self.binary.get(i).copied().unwrap_or(false)
11684 }
11685}
11686
11687/// The fold-exempt mask for a projection.
11688///
11689/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
11690/// projection rebuilds that schema through `ColumnSchema::new`, whose
11691/// collation default is `Binary` — a mask built from it would mark
11692/// EVERY column byte-wise and stop DISTINCT folding at all.
11693/// The padding mask for a projection, read off the same items as
11694/// [`fold_mask`] so the two cannot come from different places.
11695pub(crate) fn pad_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11696 projection.iter().map(|p| p.pads).collect()
11697}
11698
11699pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11700 projection.iter().map(|p| p.fold_exempt).collect()
11701}
11702
11703/// v7.38.14 — the same mask, from an OUTPUT SCHEMA instead of a
11704/// projection.
11705///
11706/// Some de-duplication sites hold `Vec<ColumnSchema>` and never see the
11707/// `ProjectedItem`s it came from. `ProjectedItem::fold_exempt` is built
11708/// from exactly this test (`select.rs`, `build_projection`), so the two
11709/// must keep answering identically -- a site that decided "byte-wise" one
11710/// way while its neighbour decided the other is how the answer came to
11711/// depend on which executor ran the query.
11712///
11713/// The direction matters: `Collation::Binary` is `ColumnSchema::new`'s
11714/// DEFAULT, so a schema rebuilt without carrying the field reads as
11715/// "byte-wise on purpose" here. That is a real trap and it has caught
11716/// five fields so far; it is why S4 of this release exists.
11717/// v7.38.18 — the padding mask from output columns, the sibling of
11718/// [`fold_mask_of_columns`]. Whether a column folds and whether it
11719/// pads are different questions about the same collation.
11720pub(crate) fn pad_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11721 columns
11722 .iter()
11723 .map(|c| crate::collate::pads_space(c.collation_name.as_deref()))
11724 .collect()
11725}
11726
11727pub(crate) fn fold_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11728 columns
11729 .iter()
11730 .map(|c| matches!(c.collation, spg_storage::Collation::Binary))
11731 .collect()
11732}
11733
11734pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
11735 values_eq_norm(&a.values, &b.values, fold)
11736}
11737
11738/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
11739/// DISTINCT probe can compare a reused projection buffer against a kept
11740/// row without building a `Row` for it.
11741pub(crate) fn values_eq_norm(
11742 a: &[Value<'static>],
11743 b: &[Value<'static>],
11744 fold: FoldSpec<'_>,
11745) -> bool {
11746 a.len() == b.len()
11747 && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
11748 if fold.folds(i)
11749 && let (Some(fx), Some(fy)) = (
11750 mysql_dedup_fold(x, fold.pads_at(i)),
11751 mysql_dedup_fold(y, fold.pads_at(i)),
11752 )
11753 {
11754 return fx == fy;
11755 }
11756 crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
11757 })
11758}
11759
11760/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
11761/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
11762/// order via the byte values; vectors are not sortable.
11763pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
11764 // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
11765 // so values sharing a ≥6-byte common prefix (`product_001` vs
11766 // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
11767 // order by their exact bytes instead of the old lossy f64 coarse key.
11768 // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
11769 // matches PG's default C / binary text collation. Every other type
11770 // keeps the lossless-enough `f64` fast path below.
11771 if let Value::Text(s) = v {
11772 return Ok(OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())));
11773 }
11774 // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
11775 // byte order (PG bpcharcmp under C collation), so mixed-pad values of
11776 // the same logical string order equal.
11777 if let Value::BpChar(s) = v {
11778 return Ok(OrderKey::Text(crate::orderby::CompactText::new(
11779 s.trim_end_matches(' '),
11780 )));
11781 }
11782 // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
11783 // carry the parsed value and compare it structurally (see
11784 // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
11785 if let Value::Json(s) = v {
11786 return Ok(match crate::json::parse(s) {
11787 Ok(jv) => OrderKey::Json(jv),
11788 Err(_) => OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())),
11789 });
11790 }
11791 // v7.37 — byte-orderable types PG sorts byte-wise but that have no
11792 // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
11793 // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
11794 // matching PG's network ordering.
11795 match v {
11796 Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
11797 // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
11798 Value::NumericBig(b) => {
11799 return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11800 spg_storage::NumericKey::from_big(b),
11801 )));
11802 }
11803 Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
11804 Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11805 Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11806 Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
11807 Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
11808 let mut key = alloc::vec::Vec::with_capacity(18);
11809 key.push(*family);
11810 key.extend_from_slice(addr);
11811 key.push(*bits);
11812 return Ok(OrderKey::Bytes(key));
11813 }
11814 _ => {}
11815 }
11816 // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
11817 // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
11818 // own OrderKey so integer arrays sort numerically; a NULL element rides to
11819 // the end via the +INF sentinel.
11820 let inf = || OrderKey::NullBig;
11821 let arr = match v {
11822 Value::IntArray(a) => Some(
11823 a.iter()
11824 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11825 .collect(),
11826 ),
11827 Value::SmallIntArray(a) => Some(
11828 a.iter()
11829 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11830 .collect(),
11831 ),
11832 Value::BigIntArray(a) => Some(
11833 a.iter()
11834 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11835 .collect(),
11836 ),
11837 Value::BoolArray(a) => Some(
11838 a.iter()
11839 .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
11840 .collect(),
11841 ),
11842 Value::TextArray(a) => Some(
11843 a.iter()
11844 .map(|o| {
11845 o.as_ref()
11846 .map_or_else(inf, |s| OrderKey::Text(crate::orderby::CompactText::new(s)))
11847 })
11848 .collect(),
11849 ),
11850 #[allow(clippy::cast_precision_loss)]
11851 Value::FloatArray(a) => Some(
11852 a.iter()
11853 .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
11854 .collect(),
11855 ),
11856 // r1040 — array elements take the same exact key their scalar
11857 // form does; an f64 projection here would order `{0.1}` against
11858 // `{0.1000000000000000001}` by luck.
11859 Value::NumericArray(a) => Some(
11860 a.iter()
11861 .map(|o| {
11862 o.map_or_else(inf, |(m, s)| {
11863 OrderKey::Numeric(alloc::boxed::Box::new(
11864 spg_storage::NumericKey::from_numeric(
11865 m,
11866 s,
11867 spg_storage::NumericKind::Finite,
11868 ),
11869 ))
11870 })
11871 })
11872 .collect(),
11873 ),
11874 Value::DateArray(a) => Some(
11875 a.iter()
11876 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11877 .collect(),
11878 ),
11879 _ => None,
11880 };
11881 if let Some(elements) = arr {
11882 return Ok(OrderKey::Array(elements));
11883 }
11884 // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
11885 // right, which is exactly the lexicographic element order an Array key
11886 // already gives: `(2,'b') < (9,'a')` because the leading field decides.
11887 if let Value::Composite(fields) = v {
11888 let elements = fields
11889 .iter()
11890 .map(|(_, fv)| value_to_order_key(fv))
11891 .collect::<Result<alloc::vec::Vec<_>, _>>()?;
11892 return Ok(OrderKey::Array(elements));
11893 }
11894 // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
11895 // Projecting these to f64 (the historic path) silently collapses BigInt /
11896 // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
11897 // the wrong order for large ids and microsecond timestamps.
11898 match v {
11899 Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11900 Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
11901 Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11902 // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
11903 // integer (days / micros / cents / calendar year); TIMETZ by the
11904 // UTC-equivalent micros (local wall - offset) so the same physical
11905 // instant in different zones sorts equal.
11906 Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
11907 Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
11908 Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
11909 Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
11910 // v7.39.13 — the UTC instant is only HALF the key.
11911 //
11912 // This ordered by the instant alone, so the values that share
11913 // one were called equal and a stable sort then returned them in
11914 // insertion order — an answer, not a tie-break. Measured on
11915 // PostgreSQL 18.6 against this engine, six rows, one column:
11916 //
11917 // ```text
11918 // PG 18.6 SPG 7.39.12
11919 // 07:00:00+01 07:00:00+01
11920 // 06:59:59+00 06:59:59+00
11921 // 09:00:00+02 07:00:00+00 <- the four that share
11922 // 07:00:00+00 02:00:00-05 07:00 UTC, in the
11923 // 02:00:00-05 09:00:00+02 order they were written
11924 // 01:00:00-06 01:00:00-06
11925 // ```
11926 //
11927 // PostgreSQL breaks the tie by OFFSET DESCENDING, and
11928 // `'07:00:00+00' = '02:00:00-05'` is FALSE there. Shifting the
11929 // instant left by 32 bits leaves room for the offset underneath
11930 // it — `i128` holds both exactly, where `i64` could not — and
11931 // `compare` in `eval::binop` orders the same pair the same way,
11932 // from the same measurement.
11933 Value::TimeTz { us, offset_secs } => {
11934 return Ok(OrderKey::Int(i128::from(spg_storage::timetz_sort_key(
11935 *us,
11936 *offset_secs,
11937 ))));
11938 }
11939 Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
11940 _ => {}
11941 }
11942 let num = match v {
11943 // Callers without NULLS FIRST/LAST context (array elements,
11944 // histogram sampling) put NULL last, as before.
11945 Value::Null => return Ok(OrderKey::NullBig),
11946 // v7.17.0 Phase 3.P0-38 — range ordering is not supported
11947 // in v7.17.0 (needs lex-then-inclusivity tiebreak).
11948 Value::Range { .. } => {
11949 return Err(EngineError::Unsupported(
11950 "ORDER BY of a range value is not supported in v7.17.0".into(),
11951 ));
11952 }
11953 // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
11954 Value::Hstore(_) => {
11955 return Err(EngineError::Unsupported(
11956 "ORDER BY of a hstore value is not supported".into(),
11957 ));
11958 }
11959 // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
11960 Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
11961 return Err(EngineError::Unsupported(
11962 "ORDER BY of a 2D array is not supported in v7.17.0".into(),
11963 ));
11964 }
11965 // r1039/r1040 — the exact canonical key, not an f64 projection.
11966 //
11967 // r1039 fixed the three specials, which carry a canonical zero in
11968 // `scaled` and so all sorted as the number 0. The projection
11969 // itself was the rest of the defect: "precision losses here only
11970 // matter for tie-breaks well past 15 significant digits" was the
11971 // comment, and the measurement disagreed — f64 called
11972 // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
11973 // returned them in insertion order. Three of ten values came back
11974 // in the wrong place against PG18.4.
11975 Value::Numeric {
11976 scaled,
11977 scale,
11978 kind,
11979 } => {
11980 return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11981 spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
11982 )));
11983 }
11984 Value::Float(x) => *x,
11985 // v7.37.16 — REAL sorts by its exact f64 widening (it had no
11986 // arm and fell through to the unsupported error).
11987 Value::Real(x) => f64::from(*x),
11988 Value::Bool(b) => {
11989 if *b {
11990 1.0
11991 } else {
11992 0.0
11993 }
11994 }
11995 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
11996 return Err(EngineError::Unsupported(
11997 "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
11998 ));
11999 }
12000 // v7.37 — PG orders INTERVAL by its total time, treating a month as
12001 // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
12002 // f64 is exact for any interval under ~285 years, and only ORDER BY
12003 // tie-breaks past that magnitude lose precision. Matches the
12004 // min/max(interval) comparator in aggregate.rs.
12005 #[allow(clippy::cast_precision_loss)]
12006 Value::Interval {
12007 months,
12008 days,
12009 micros,
12010 kind,
12011 } => {
12012 let total = i128::from(*months) * 30 * 86_400_000_000
12013 + i128::from(*days) * 86_400_000_000
12014 + i128::from(*micros);
12015 total as f64
12016 }
12017 Value::Json(_) => {
12018 return Err(EngineError::Unsupported(
12019 "ORDER BY of a JSON value is not supported — cast the document to text first"
12020 .into(),
12021 ));
12022 }
12023 // v7.5.0 — Value is #[non_exhaustive]; future variants need
12024 // an explicit ORDER BY mapping. Surface as Unsupported until
12025 // engine support is added.
12026 _ => {
12027 return Err(EngineError::Unsupported(
12028 "ORDER BY of this value type is not supported".into(),
12029 ));
12030 }
12031 };
12032 Ok(OrderKey::Num(num))
12033}
12034
12035/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
12036/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
12037/// `EngineError` so the projection-build path keeps `UnknownQualifier`
12038/// vs `ColumnNotFound` distinct.
12039/// PG's name for the physical row identity. It is reserved there — no table
12040/// can have a column called this — which is what lets `*` skip it by name.
12041pub(crate) const CTID_COLUMN: &str = "ctid";
12042
12043/// v7.39 (round 512) — PG's system columns, in the order they are appended.
12044/// All six are reserved names there, which is what lets `*` skip them and
12045/// lets a scan tell them from a user column without a flag.
12046pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
12047
12048/// Is this name one of them?
12049pub(crate) fn is_system_column(name: &str) -> bool {
12050 SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
12051}
12052
12053/// Where the scan's appended system columns begin, if this schema carries
12054/// them: the trailing six, named in order. A catalog view with a column of
12055/// its own called `xmin` does not match, which is the point.
12056fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
12057 let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
12058 cols[start..]
12059 .iter()
12060 .zip(SYSTEM_COLUMNS)
12061 .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
12062 .then_some(start)
12063}
12064
12065/// v7.39 (round 540) — which positions `*` must skip.
12066///
12067/// The rule stays round 512's — the synthetic columns are the trailing
12068/// six of a relation's block, matched by POSITION so a genuine `xmin`
12069/// column is not lost — but a JOINED schema names its columns
12070/// `alias.column` and lays the peers out end to end, so a peer's six sit
12071/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
12072/// "trailing six" test back on the block it was written for.
12073fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
12074 let mut skip = alloc::vec![false; cols.len()];
12075 fn qualifier(n: &str) -> Option<&str> {
12076 n.rsplit_once('.').map(|(q, _)| q)
12077 }
12078 fn bare(n: &str) -> &str {
12079 n.rsplit('.').next().unwrap_or(n)
12080 }
12081 let mut i = 0;
12082 while i < cols.len() {
12083 let q = qualifier(&cols[i].name);
12084 let mut end = i;
12085 while end < cols.len() && qualifier(&cols[end].name) == q {
12086 end += 1;
12087 }
12088 if let Some(start) = (end - i)
12089 .checked_sub(SYSTEM_COLUMNS.len())
12090 .map(|off| i + off)
12091 && cols[start..end]
12092 .iter()
12093 .zip(SYSTEM_COLUMNS)
12094 .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
12095 {
12096 for s in skip.iter_mut().take(end).skip(start) {
12097 *s = true;
12098 }
12099 }
12100 i = end;
12101 }
12102 skip
12103}
12104
12105/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
12106/// read? Only then is the column materialised.
12107pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
12108 let mut found = false;
12109 crate::expr_analysis::visit_expr_columns_and_subqueries(
12110 e,
12111 &mut |c| {
12112 if is_system_column(&c.name) {
12113 found = true;
12114 }
12115 },
12116 &mut |_| {},
12117 );
12118 found
12119}
12120
12121fn references_ctid(stmt: &SelectStatement) -> bool {
12122 let in_expr = expr_references_ctid;
12123 stmt.items.iter().any(|i| match i {
12124 SelectItem::Expr { expr, .. } => in_expr(expr),
12125 _ => false,
12126 }) || stmt.where_.as_ref().is_some_and(in_expr)
12127 || stmt.order_by.iter().any(|o| in_expr(&o.expr))
12128 || stmt
12129 .group_by
12130 .as_ref()
12131 .is_some_and(|g| g.iter().any(in_expr))
12132 || stmt.having.as_ref().is_some_and(in_expr)
12133}
12134
12135/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
12136/// is a name the projection has to TYPE before any row exists.
12137///
12138/// Evaluation has answered this since round T9 (`resolve_column` builds a
12139/// `Value::Composite` of every column), but the typing side below had no
12140/// such branch and raised `column "t" does not exist` first — so the
12141/// feature was unreachable through a projection. Measured against PG18.4:
12142/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
12143///
12144/// The type is `Jsonb` + a composite marker, which is exactly how a
12145/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
12146/// the value travels as a `Value::Composite` and renders in the canonical
12147/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
12148/// so the marker names the alias and no rehydration keys off it — the
12149/// value arrives already built.
12150fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
12151 let mut s = ColumnSchema::new(
12152 alloc::string::String::from(alias),
12153 spg_storage::DataType::Jsonb,
12154 true,
12155 );
12156 s.user_composite_type = Some(alloc::string::String::from(alias));
12157 s
12158}
12159
12160/// v7.39.3 — `mysql` makes the name comparison case-INSENSITIVE, which
12161/// is MySQL's rule for column names (measured on 9.7.2: `mycol`,
12162/// `MYCOL` and a backquoted `MyCol` all resolve the same column).
12163///
12164/// SPG compared byte for byte and its lexer folds an UNQUOTED
12165/// identifier, so a table restored from a `mysqldump` — where every
12166/// identifier is backquoted and keeps its case — had every mixed-case
12167/// column unreachable from ordinary unquoted SQL. Same "two spellings,
12168/// two things" defect v7.39.1 closed for relation names.
12169pub(crate) fn resolve_projection_column<'a>(
12170 c: &ColumnName,
12171 schema_cols: &'a [ColumnSchema],
12172 table_alias: &str,
12173 mysql: bool,
12174) -> Result<Cow<'a, ColumnSchema>, EngineError> {
12175 let same = |a: &str, b: &str| {
12176 if mysql {
12177 a.eq_ignore_ascii_case(b)
12178 } else {
12179 a == b
12180 }
12181 };
12182 if let Some(q) = &c.qualifier {
12183 let composite = alloc::format!("{q}.{name}", name = c.name);
12184 if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &composite)) {
12185 return Ok(Cow::Borrowed(s));
12186 }
12187 // Single-table case: the qualifier may equal the active alias —
12188 // then look for the bare column name.
12189 if same(q, table_alias)
12190 && let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name))
12191 {
12192 return Ok(Cow::Borrowed(s));
12193 }
12194 // For multi-table schemas the qualifier is unknown only if no
12195 // column bears the "<q>." prefix. For single-table, the alias
12196 // mismatch alone is enough.
12197 let prefix = alloc::format!("{q}.");
12198 let qualifier_known =
12199 same(q, table_alias) || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
12200 if !qualifier_known {
12201 return Err(EngineError::Eval(EvalError::UnknownQualifier {
12202 qualifier: q.clone(),
12203 column: c.name.clone(),
12204 }));
12205 }
12206 return Err(EngineError::Eval(EvalError::ColumnNotFound {
12207 name: c.name.clone(),
12208 }));
12209 }
12210 if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name)) {
12211 return Ok(Cow::Borrowed(s));
12212 }
12213 let suffix = alloc::format!(".{name}", name = c.name);
12214 let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
12215 let first = matches.next();
12216 let extra = matches.next();
12217 match (first, extra) {
12218 (Some(s), None) => Ok(Cow::Borrowed(s)),
12219 (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
12220 detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
12221 })),
12222 // The whole-row reference, checked LAST so a real column carrying
12223 // the alias's name still wins — the same precedence
12224 // `resolve_column` applies on the evaluation side.
12225 //
12226 // Two schema shapes reach here. A single-table (or subquery, or
12227 // CTE) scan carries its alias and bare column names, so the name
12228 // has to equal the alias. A JOIN's combined schema carries no
12229 // alias at all and qualifies every column `alias.col`, so the
12230 // alias is identified by the prefix instead — which is exactly
12231 // how `whole_row_composite` picks the fields out on the
12232 // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
12233 // answers `(7,z)` on PG18.4 and errored here until this arm
12234 // covered the joined shape too.
12235 _ if !table_alias.is_empty() && c.name == table_alias => {
12236 Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
12237 }
12238 _ if table_alias.is_empty() && {
12239 let prefix = alloc::format!("{name}.", name = c.name);
12240 schema_cols.iter().any(|s| s.name.starts_with(&prefix))
12241 } =>
12242 {
12243 Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
12244 }
12245 _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
12246 name: c.name.clone(),
12247 })),
12248 }
12249}
12250
12251/// v7.40.0 — a column the grouping-set rewrite injected purely to sort
12252/// on, and which must not reach the client. Two families: `__grp_ord_*`
12253/// carries a branch's `grouping()` mask (round 135), `__grp_key_*`
12254/// carries a key the rollup orders by that the query did not project —
12255/// without it `SELECT SUM(qty) … GROUP BY qty WITH ROLLUP` answered
12256/// `column "qty" does not exist`, because a UNION's ORDER BY can only
12257/// name output columns.
12258fn is_synthetic_group_col(name: &str) -> bool {
12259 name.starts_with("__grp_ord_") || name.starts_with("__grp_key_")
12260}
12261
12262/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
12263/// parser to carry per-branch GROUPING() masks into a grouping-set query's
12264/// ORDER BY. They must never reach the output. No-op unless such a column is
12265/// present, so the common path is untouched.
12266/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
12267///
12268/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
12269/// a `LIMIT 2` that should have answered two groups answered one.
12270fn apply_deferred_limit(
12271 rows: alloc::vec::Vec<Row<'static>>,
12272 deferred: &(
12273 Option<spg_sql::ast::LimitExpr>,
12274 Option<spg_sql::ast::LimitExpr>,
12275 ),
12276) -> alloc::vec::Vec<Row<'static>> {
12277 let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
12278 Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
12279 _ => None,
12280 };
12281 let mut rows = rows;
12282 if let Some(off) = count(&deferred.1) {
12283 rows = rows.split_off(off.min(rows.len()));
12284 }
12285 if let Some(lim) = count(&deferred.0) {
12286 rows.truncate(lim);
12287 }
12288 rows
12289}
12290
12291fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
12292 let QueryResult::Rows { columns, rows } = result else {
12293 return result;
12294 };
12295 if !columns.iter().any(|c| is_synthetic_group_col(&c.name)) {
12296 return QueryResult::Rows { columns, rows };
12297 }
12298 let keep: Vec<usize> = columns
12299 .iter()
12300 .enumerate()
12301 .filter(|(_, c)| !is_synthetic_group_col(&c.name))
12302 .map(|(i, _)| i)
12303 .collect();
12304 let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
12305 let new_rows: Vec<Row<'static>> = rows
12306 .into_iter()
12307 .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
12308 .collect();
12309 QueryResult::Rows {
12310 columns: new_cols,
12311 rows: new_rows,
12312 }
12313}
12314
12315/// v7.39 (round 487) — bind every projection item that is a bare column
12316/// reference to its position, once per query.
12317///
12318/// `#[inline(never)]` and out of line on purpose. Round 486 established
12319/// that adding code inside these scan bodies moves neighbouring hot
12320/// functions around under fat LTO: the first version of this had the loop
12321/// inline in `run_single_table_scan` and four aggregate shapes that never
12322/// touch that function — `full_agg`, `join_agg`, `group_500k`,
12323/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
12324/// the same machine. Keeping it out of line kept them still.
12325#[inline(never)]
12326fn bind_direct_columns(
12327 projection: &[ProjectedItem],
12328 ctx: &eval::EvalContext<'_>,
12329) -> Vec<Option<usize>> {
12330 projection
12331 .iter()
12332 .map(|p| match &p.expr {
12333 Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
12334 // Same exclusion `compile_into` makes: a composite column
12335 // has to be rehydrated from stored JSON, which is not a
12336 // cell read.
12337 ctx.columns
12338 .get(*pos)
12339 .is_none_or(|sc| sc.user_composite_type.is_none())
12340 }),
12341 _ => None,
12342 })
12343 .collect()
12344}
12345
12346/// v7.39 (round 505) — the name an un-aliased projected expression reports.
12347///
12348/// PG18 names a call for its function and everything else `?column?`;
12349/// measured with `\gdesc`. SPG used to print the parsed expression back
12350/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
12351/// name-keyed row access found nothing under `upper`.
12352///
12353/// The MySQL half is NOT this rule and is deliberately left alone here:
12354/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
12355/// which needs the parser to hand over spans the AST does not carry yet.
12356/// Until it does, a MySQL session keeps the printed form — closer to what
12357/// MariaDB answers than `?column?` would be.
12358pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
12359 if mysql {
12360 return expr.to_string();
12361 }
12362 spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
12363}
12364
12365pub(crate) fn build_projection(
12366 items: &[SelectItem],
12367 schema_cols: &[ColumnSchema],
12368 table_alias: &str,
12369 mysql: bool,
12370 cat: Option<&Catalog>,
12371) -> Result<Vec<ProjectedItem>, EngineError> {
12372 build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0, cat)
12373}
12374
12375/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
12376/// invisible to `*`.
12377///
12378/// The windowed-SELECT path appends a synthetic `__win_N` column per window
12379/// function so the rewritten projection can reference the computed values as
12380/// ordinary columns. `*` then expanded them too, and
12381/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
12382/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
12383/// silent one: the row simply had one more field than the client asked for.
12384///
12385/// Hidden by POSITION rather than by name, for the reason round 512 recorded
12386/// about the system columns: a name test looks safe until a real column
12387/// happens to carry the name. These are appended last, so the count is what
12388/// identifies them.
12389pub(crate) fn build_projection_hiding_tail(
12390 items: &[SelectItem],
12391 schema_cols: &[ColumnSchema],
12392 table_alias: &str,
12393 mysql: bool,
12394 hidden_tail: usize,
12395 // v7.38.19 — the catalog, so a user-defined function's DECLARED
12396 // return type reaches the projection. Without it `describe_expr`
12397 // cannot type `f_sql()` and the column falls back to text, which is
12398 // what psql reads to decide alignment: `SELECT 7::bigint, f_sql()`
12399 // right-aligned one cell and left-aligned the other while both held
12400 // a bigint. Reported by sentori against 7.38.18 (their §2.2), who
12401 // also established that the EXECUTOR was never confused -- CTAS off
12402 // the same expression gives a bigint column, and arithmetic on it
12403 // works. Only the type travelling in the RowDescription was wrong.
12404 cat: Option<&Catalog>,
12405) -> Result<Vec<ProjectedItem>, EngineError> {
12406 let visible = schema_cols.len().saturating_sub(hidden_tail);
12407 // v7.39 (round 462) — a join's combined schema qualifies every column
12408 // `alias.col` so the deferred-join cell lookups resolve by composite
12409 // name. That is an internal convention, and `*` was handing it to the
12410 // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
12411 // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
12412 // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
12413 // already learned this for `q.*`; plain `*` never got the same rule.
12414 //
12415 // The signal is the schema itself, not the call site: only a combined
12416 // join schema arrives with no table alias AND every column qualified.
12417 // A single-table schema carries its alias, an empty schema has nothing
12418 // to strip, and a synthetic schema's names carry no dot.
12419 let joined_schema = table_alias.is_empty()
12420 && !schema_cols.is_empty()
12421 && schema_cols.iter().all(|c| c.name.contains('.'));
12422 let bare_name = |name: &str| -> String {
12423 if !joined_schema {
12424 return name.to_string();
12425 }
12426 match name.split_once('.') {
12427 Some((_, rest)) if !rest.is_empty() => rest.to_string(),
12428 _ => name.to_string(),
12429 }
12430 };
12431 let mut out = Vec::new();
12432 for item in items {
12433 match item {
12434 SelectItem::Wildcard => {
12435 // v7.39 (round 511) — `*` never expands a system column, as
12436 // PG's does not. They join the schema only when the statement
12437 // asked for them, so this matters for the mixed shape
12438 // `SELECT *, ctid FROM t`.
12439 //
12440 // v7.39 (round 512) — by POSITION, not by name. Matching on
12441 // the name alone looked safe because PG reserves them, and it
12442 // is not: `pg_replication_slots` genuinely has a column called
12443 // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
12444 // Only the trailing six, in the order the scan appends them,
12445 // are the synthetic ones.
12446 let sys_skip = synthetic_system_positions(schema_cols);
12447 for (idx, col) in schema_cols.iter().enumerate() {
12448 if sys_skip[idx] || idx >= visible {
12449 continue;
12450 }
12451 out.push(ProjectedItem {
12452 expr: Expr::Column(ColumnName {
12453 qualifier: None,
12454 name: col.name.clone(),
12455 }),
12456 output_name: bare_name(&col.name),
12457 ty: col.ty,
12458 nullable: col.nullable,
12459 user_enum_type: col.user_enum_type.clone(),
12460 mysql_fsp: col.mysql_fsp,
12461 collation_name: col.collation_name.clone(),
12462 fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12463 pads: crate::collate::pads_space(col.collation_name.as_deref()),
12464 });
12465 }
12466 }
12467 // v7.39 (round 128) — `q.*` expands to every column belonging to
12468 // the qualifier `q`. Single-table schemas carry bare column names
12469 // reachable via `table_alias`; a join's combined schema carries
12470 // `alias.col` names, so a column belongs to `q` when its name has
12471 // the `q.` prefix. PG labels the expanded columns by their bare
12472 // name, so the `alias.` prefix is stripped from the output name.
12473 SelectItem::QualifiedWildcard(q) => {
12474 let prefix = alloc::format!("{q}.");
12475 let single_table = !table_alias.is_empty() && q == table_alias;
12476 let mut matched = 0usize;
12477 for col in &schema_cols[..visible] {
12478 let belongs =
12479 col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
12480 if !belongs {
12481 continue;
12482 }
12483 matched += 1;
12484 let output_name = col
12485 .name
12486 .strip_prefix(&prefix)
12487 .unwrap_or(&col.name)
12488 .to_string();
12489 out.push(ProjectedItem {
12490 expr: Expr::Column(ColumnName {
12491 qualifier: None,
12492 name: col.name.clone(),
12493 }),
12494 output_name,
12495 ty: col.ty,
12496 nullable: col.nullable,
12497 user_enum_type: col.user_enum_type.clone(),
12498 mysql_fsp: col.mysql_fsp,
12499 collation_name: col.collation_name.clone(),
12500 fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12501 pads: crate::collate::pads_space(col.collation_name.as_deref()),
12502 });
12503 }
12504 if matched == 0 {
12505 // `q.*` names no column, so the reference IS the star.
12506 return Err(EngineError::Eval(EvalError::UnknownQualifier {
12507 qualifier: q.clone(),
12508 column: alloc::string::String::from("*"),
12509 }));
12510 }
12511 }
12512 SelectItem::Expr { expr, alias } => {
12513 // Plain column ref keeps full schema info (real type +
12514 // nullability). For compound expressions try the
12515 // describe-side function-return-type table first
12516 // (e.g. `SELECT now()` → Timestamptz, `SELECT
12517 // concat(…)` → Text). Falls back to nullable Text
12518 // for shapes the describe path can't resolve.
12519 if let Expr::Column(c) = expr {
12520 let sch = resolve_projection_column(c, schema_cols, table_alias, mysql)?;
12521 let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
12522 out.push(ProjectedItem {
12523 expr: expr.clone(),
12524 output_name,
12525 ty: sch.ty,
12526 nullable: sch.nullable,
12527 // v7.39 (read01 round 54) — a bare enum column keeps
12528 // its enum identity through the projection.
12529 user_enum_type: sch.user_enum_type.clone(),
12530 mysql_fsp: sch.mysql_fsp,
12531 collation_name: sch.collation_name.clone(),
12532 // v7.38.13 — and its byte-wise-ness. This is the
12533 // site `SELECT DISTINCT t FROM t` arrives at.
12534 fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
12535 pads: crate::collate::pads_space(sch.collation_name.as_deref()),
12536 });
12537 } else if let Some(shape) = describe::describe_expr_in(expr, schema_cols, cat) {
12538 let output_name = alias
12539 .clone()
12540 .unwrap_or_else(|| default_output_name(expr, mysql));
12541 out.push(ProjectedItem {
12542 expr: expr.clone(),
12543 // v7.38.18 — a projected EXPRESSION has no column collation
12544 // to read, so it takes the session default, which is MySQL
12545 // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12546 pads: false,
12547 output_name,
12548 ty: shape.ty,
12549 // v7.39 (round 258) — a projected EXPRESSION keeps its
12550 // enum identity too, not just a bare column. `FROM
12551 // (VALUES ('happy'::mood), …) t(m)` lowers to constant
12552 // SELECTs, so the derived column arrived here as a cast
12553 // and lost the enum — making the outer ORDER BY / min /
12554 // max / array_agg sort by the label's TEXT.
12555 nullable: shape.nullable,
12556 user_enum_type: None,
12557 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12558 // A bare column reference keeps its collation; any
12559 // other expression produces a new value and has none.
12560 collation_name: match expr {
12561 Expr::Column(c) => schema_cols
12562 .iter()
12563 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12564 .and_then(|sc| sc.collation_name.clone()),
12565 _ => None,
12566 },
12567 fold_exempt: match expr {
12568 Expr::Column(c) => schema_cols
12569 .iter()
12570 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12571 .is_some_and(|sc| {
12572 matches!(sc.collation, spg_storage::Collation::Binary)
12573 }),
12574 // Not a column: no declared collation to honour,
12575 // so the session default applies and it folds.
12576 _ => false,
12577 },
12578 });
12579 } else {
12580 let output_name = alias
12581 .clone()
12582 .unwrap_or_else(|| default_output_name(expr, mysql));
12583 out.push(ProjectedItem {
12584 expr: expr.clone(),
12585 // v7.38.18 — a projected EXPRESSION has no column collation
12586 // to read, so it takes the session default, which is MySQL
12587 // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12588 pads: false,
12589 output_name,
12590 // A user ENUM has no DataType of its own, so
12591 // `describe_expr` cannot type `'ok'::mood` and the
12592 // item lands HERE, defaulting to text — which is why
12593 // pg_typeof answered `text` and a derived table sorted
12594 // enum values by their label.
12595 ty: DataType::Text,
12596 nullable: true,
12597 user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
12598 .map(alloc::string::String::from),
12599 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12600 collation_name: match expr {
12601 Expr::Column(c) => schema_cols
12602 .iter()
12603 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12604 .and_then(|sc| sc.collation_name.clone()),
12605 _ => None,
12606 },
12607 fold_exempt: match expr {
12608 Expr::Column(c) => schema_cols
12609 .iter()
12610 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12611 .is_some_and(|sc| {
12612 matches!(sc.collation, spg_storage::Collation::Binary)
12613 }),
12614 // Not a column: no declared collation to honour,
12615 // so the session default applies and it folds.
12616 _ => false,
12617 },
12618 });
12619 }
12620 }
12621 }
12622 }
12623 Ok(out)
12624}
12625
12626// ---- v4.12 window-function helpers ----
12627// The (partition-key, order-key, original-index) tuple shape used
12628// across these helpers is intrinsic to the planner. Factoring it
12629// into a typedef adds indirection without making the code clearer,
12630// so several lints are allowed inline on the affected functions
12631// rather than module-wide.
12632
12633/// v4.22: pick more specific column types from observed rows when
12634/// the projection builder defaulted to Text (the v1.x behavior for
12635/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
12636/// land an Int column in the CTE storage table rather than failing
12637/// the insert with "expected TEXT, got INT".
12638pub(crate) fn infer_column_types(
12639 columns: &[ColumnSchema],
12640 rows: &[Row<'static>],
12641) -> Vec<ColumnSchema> {
12642 let mut out = columns.to_vec();
12643 for (col_idx, col) in out.iter_mut().enumerate() {
12644 if col.ty != DataType::Text {
12645 continue;
12646 }
12647 let mut inferred: Option<DataType> = None;
12648 let mut all_null = true;
12649 for row in rows {
12650 let Some(v) = row.values.get(col_idx) else {
12651 continue;
12652 };
12653 let ty = match v {
12654 Value::Null => continue,
12655 Value::SmallInt(_) => DataType::SmallInt,
12656 Value::Int(_) => DataType::Int,
12657 Value::BigInt(_) => DataType::BigInt,
12658 Value::Float(_) => DataType::Float,
12659 Value::Bool(_) => DataType::Bool,
12660 Value::Vector(_) => DataType::Vector {
12661 dim: 0,
12662 encoding: VecEncoding::F32,
12663 },
12664 // v7.38 (read01 U16) — carry array values through with an
12665 // array type so a recursive CTE that projects an array
12666 // (e.g. a SEARCH/CYCLE ord / path column) types the working
12667 // column as an array, not Text.
12668 Value::TextArray(_) => DataType::TextArray,
12669 Value::IntArray(_) => DataType::IntArray,
12670 Value::BigIntArray(_) => DataType::BigIntArray,
12671 Value::SmallIntArray(_) => DataType::SmallIntArray,
12672 Value::FloatArray(_) => DataType::FloatArray,
12673 Value::BoolArray(_) => DataType::BoolArray,
12674 // v7.39 (GUC knife 2) — an interval projection describes
12675 // as INTERVAL (typed drivers read the RowDescription OID).
12676 Value::Interval { .. } => DataType::Interval,
12677 _ => DataType::Text,
12678 };
12679 all_null = false;
12680 inferred = Some(match inferred {
12681 None => ty,
12682 Some(prev) if prev == ty => prev,
12683 Some(_) => DataType::Text,
12684 });
12685 }
12686 if let Some(t) = inferred {
12687 col.ty = t;
12688 col.nullable = true;
12689 } else if all_null {
12690 col.nullable = true;
12691 }
12692 }
12693 out
12694}
12695
12696/// Numeric widening rank for UNION type resolution (higher = wider).
12697fn numeric_rank(t: DataType) -> Option<u8> {
12698 match t {
12699 DataType::SmallInt => Some(1),
12700 DataType::Int => Some(2),
12701 DataType::BigInt => Some(3),
12702 DataType::Numeric { .. } => Some(4),
12703 DataType::Float => Some(5),
12704 _ => None,
12705 }
12706}
12707
12708/// Resolve the common result type for a UNION / VALUES column from the
12709/// set of concrete (non-NULL) branch types, following the safe subset
12710/// of PG's type resolution:
12711/// * all-numeric → the widest numeric (int ∪ bigint → bigint, … ∪
12712/// numeric → numeric, … ∪ float → float);
12713/// * DATE ∪ TIMESTAMP → TIMESTAMP;
12714/// * exactly one concrete non-TEXT type mixed with TEXT literals →
12715/// that concrete type (the TEXT cells get parsed into it).
12716/// Returns `None` for anything ambiguous, so the caller leaves the
12717/// column untouched rather than risk a wrong or failing coercion.
12718fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
12719 // NB: types are collected from RUNTIME values, which are coarser
12720 // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
12721 // a single-concrete-type fast path must NOT overwrite the column
12722 // type — it would downgrade tstz to ts. NULL-only unification (PG:
12723 // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
12724 // row's pg_typeof) needs schema-level resolution — recorded, not
12725 // attempted here.
12726 if types.len() < 2 {
12727 return None;
12728 }
12729 if types.iter().all(|t| numeric_rank(*t).is_some()) {
12730 return types
12731 .iter()
12732 .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
12733 .copied();
12734 }
12735 let non_text: Vec<&DataType> = types
12736 .iter()
12737 .filter(|t| !matches!(t, DataType::Text))
12738 .collect();
12739 // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
12740 // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
12741 // if any is timestamp the result is timestamp (ts ∪ date). All values are
12742 // the same UTC-micros instant, so widening date/ts to tstz is lossless.
12743 if non_text.iter().all(|t| {
12744 matches!(
12745 t,
12746 DataType::Date | DataType::Timestamp | DataType::Timestamptz
12747 )
12748 }) && non_text
12749 .iter()
12750 .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
12751 {
12752 if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
12753 return Some(DataType::Timestamptz);
12754 }
12755 return Some(DataType::Timestamp);
12756 }
12757 // A single concrete non-TEXT type mixed with TEXT literals.
12758 if non_text.len() == 1 {
12759 return Some(*non_text[0]);
12760 }
12761 // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
12762 // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
12763 // text): resolve the concrete set first (PG treats the unknown-
12764 // typed string literals as castable to whatever the knowns
12765 // resolve to), then the TEXT cells parse into that target — the
12766 // caller's coercion dry-run still abandons the column if any
12767 // literal doesn't parse.
12768 if !non_text.is_empty() && non_text.len() < types.len() {
12769 let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
12770 return resolve_union_common_type(&concrete);
12771 }
12772 None
12773}
12774
12775/// Coerce every cell of a UNION / VALUES result column to one common
12776/// type (see [`resolve_union_common_type`]). Conservative: a column
12777/// whose branches already agree, or whose types don't resolve, or where
12778/// any cell fails to coerce, is left exactly as it was — this never
12779/// turns a previously-working query into an error.
12780fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
12781 for col_idx in 0..columns.len() {
12782 let mut seen: Vec<DataType> = Vec::new();
12783 for row in rows.iter() {
12784 if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
12785 if !seen.contains(&dt) {
12786 seen.push(dt);
12787 }
12788 }
12789 }
12790 // v7.37.16 — a single concrete runtime type under a TEXT-typed
12791 // column means the column type came off a NULL (or unknown-text)
12792 // branch: NULL literals describe as TEXT (`L::Null → Text`), so
12793 // `VALUES (NULL),(1.5)` left the column "text" while every
12794 // non-NULL cell is numeric. Adopt the concrete type — schema
12795 // only, no cell changes. tstz-safe by construction: a real
12796 // timestamptz column's schema type is Timestamptz, not Text, so
12797 // the coarser runtime type (Value::Timestamp) can't downgrade it
12798 // through this arm; and a real text column's non-NULL cells are
12799 // Text, which keeps seen == [Text] and skips it.
12800 if seen.len() == 1
12801 && matches!(columns[col_idx].ty, DataType::Text)
12802 && !matches!(seen[0], DataType::Text)
12803 {
12804 columns[col_idx].ty = seen[0];
12805 continue;
12806 }
12807 let Some(target) = resolve_union_common_type(&seen) else {
12808 continue;
12809 };
12810 // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
12811 // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
12812 // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
12813 // existing numeric cell untouched and only promote integers (to scale 0)
12814 // rather than rescaling everything to the widest scale.
12815 let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
12816 // Dry-run the coercion; abandon the whole column if any fails.
12817 let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
12818 let mut ok = true;
12819 for row in rows.iter() {
12820 match row.values.get(col_idx) {
12821 Some(Value::Numeric { .. }) if scale_preserving_numeric => {
12822 coerced.push(Some(row.values[col_idx].clone()));
12823 }
12824 Some(v) => {
12825 let cell_target = if scale_preserving_numeric {
12826 DataType::Numeric {
12827 precision: 0,
12828 scale: 0,
12829 }
12830 } else {
12831 target
12832 };
12833 match crate::conversions::coerce_value(
12834 v.clone(),
12835 cell_target,
12836 &columns[col_idx].name,
12837 col_idx,
12838 ) {
12839 Ok(cv) => coerced.push(Some(cv)),
12840 Err(_) => {
12841 ok = false;
12842 break;
12843 }
12844 }
12845 }
12846 None => coerced.push(None),
12847 }
12848 }
12849 if !ok {
12850 continue;
12851 }
12852 for (row, cv) in rows.iter_mut().zip(coerced) {
12853 if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
12854 *slot = nv;
12855 }
12856 }
12857 columns[col_idx].ty = target;
12858 }
12859}
12860
12861/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
12862/// dedup inside the recursive iteration. Crude but deterministic
12863/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
12864fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
12865 let mut out = Vec::new();
12866 for v in &row.values {
12867 // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
12868 // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
12869 // like PG (and like GROUP BY, which already normalizes). The old
12870 // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
12871 // the exact-decimal family through one scale-stripped canonical form.
12872 match v {
12873 Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12874 Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12875 Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12876 Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
12877 other => {
12878 let s = alloc::format!("{other:?}|");
12879 out.extend_from_slice(s.as_bytes());
12880 }
12881 }
12882 }
12883 out
12884}
12885
12886/// Append a scale-independent canonical key for an exact-decimal value: strip
12887/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
12888/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
12889fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
12890 while scale > 0 && scaled % 10 == 0 {
12891 scaled /= 10;
12892 scale -= 1;
12893 }
12894 let s = alloc::format!("\u{1}{scaled}e-{scale}|");
12895 out.extend_from_slice(s.as_bytes());
12896}
12897
12898/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
12899/// (uncorrelated; outer refs were substituted upstream), then zip
12900/// them in parallel, NULL-padding shorter arrays to the longest
12901/// (PG's ROWS FROM shorthand). Shared by the primary-position
12902/// executor and the join-position materialiser, which both detect
12903/// the parser's `__unnest_zip` marker call.
12904pub(crate) fn unnest_zip_rows(
12905 args: &[Expr],
12906) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
12907 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12908 let ctx = EvalContext::new(&empty_schema, None);
12909 let dummy_row = Row::new(alloc::vec::Vec::new());
12910 let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
12911 let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
12912 alloc::vec::Vec::with_capacity(args.len());
12913 for a in args {
12914 let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
12915 // v7.39.13 — the element menu the rest of the workspace already
12916 // has, not a third copy of a shortened one.
12917 //
12918 // This arm listed Text, Int and BigInt and refused everything
12919 // else, so `unnest(uuid[], text[])` raised while
12920 // `unnest(uuid[])` — a different path — did not. A shipped
12921 // endpoint of a customer's returned 500 on every call because
12922 // of it. `array_elements` and `array_element_type` are the two
12923 // halves of the menu that `array_element_at`'s own comment
12924 // describes: "previously only matched Text/Int/BigInt arrays
12925 // and errored on every other element type". Same sentence,
12926 // third arm.
12927 let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = if matches!(v, Value::Null) {
12928 (DataType::Text, alloc::vec::Vec::new())
12929 } else if let Some(items) = crate::eval::values::array_elements(&v) {
12930 let dt = v
12931 .data_type()
12932 .and_then(crate::describe::array_element_type)
12933 .unwrap_or(DataType::Text);
12934 (dt, items)
12935 } else {
12936 return Err(EngineError::Unsupported(alloc::format!(
12937 "unnest() expects array arguments, got {}",
12938 crate::conversions::pg_type_name_for_error_opt(v.data_type())
12939 )));
12940 };
12941 dtypes.push(dt);
12942 columns.push(items);
12943 }
12944 let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
12945 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
12946 for i in 0..max_len {
12947 let vals: alloc::vec::Vec<Value<'static>> = columns
12948 .iter()
12949 .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
12950 .collect();
12951 rows.push(Row::new(vals));
12952 }
12953 Ok((dtypes, rows))
12954}
12955
12956/// Detect the parser's multi-arg unnest marker on an unnest_expr.
12957pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
12958 match expr {
12959 Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
12960 _ => None,
12961 }
12962}
12963
12964/// Evaluate generate_series arguments (uncorrelated — outer refs
12965/// were substituted upstream where applicable) and build the row
12966/// stream. Dispatches on the start value's shape and rejects
12967/// mixed-shape calls early (e.g. start = timestamp, stop =
12968/// integer) so the caller gets a clean error rather than a panic.
12969/// Shared by the primary-position executor and the join-position
12970/// materialiser.
12971pub(crate) fn generate_series_rows(
12972 args: &[Expr],
12973 cancel: &CancelToken<'_>,
12974) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12975 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12976 let ctx = EvalContext::new(&empty_schema, None);
12977 let dummy_row = Row::new(alloc::vec::Vec::new());
12978 let mut arg_values: alloc::vec::Vec<Value<'static>> =
12979 alloc::vec::Vec::with_capacity(args.len());
12980 for a in args {
12981 arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
12982 }
12983 generate_series_from_values(arg_values, args, cancel)
12984}
12985
12986/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
12987/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
12988/// full integer / numeric / timestamp overload set with the FROM-clause path.
12989/// Before this split the target-list arm reimplemented only the integer case,
12990/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
12991/// NULL for the timestamp column instead of the series. `arg_values` are the
12992/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
12993/// timestamp type resolution (it inspects the argument expressions' types).
12994pub(crate) fn generate_series_from_values(
12995 mut arg_values: alloc::vec::Vec<Value<'static>>,
12996 args: &[Expr],
12997 cancel: &CancelToken<'_>,
12998) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12999 // PG: a NULL bound or step yields zero rows (also keeps the
13000 // NULL-padded lateral probe alive — schema without data).
13001 if arg_values.iter().any(|v| matches!(v, Value::Null)) {
13002 return Ok((DataType::BigInt, alloc::vec::Vec::new()));
13003 }
13004 // PG resolves `generate_series(date, date, interval)` to the
13005 // timestamp/timestamptz overload by implicitly casting each date
13006 // bound up to a timestamp at midnight (verified vs live PG18.4:
13007 // date args yield rows anchored at 00:00:00). SPG's TZ-naive
13008 // timestamp model renders the same instants, so fold any Date
13009 // bound to its midnight Timestamp (canonical `days *
13010 // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
13011 // the shape match so the existing timestamp arm drives the walk.
13012 // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
13013 // `generate_series(date, date, interval)` has no date overload, and among
13014 // the two candidates PG prefers the timestamptz one (timestamptz is the
13015 // preferred type of the datetime category), so the column comes back
13016 // `timestamp with time zone` — the rows render with a `+00` offset. A
13017 // timestamptz bound obviously lands there too. Only genuinely
13018 // timestamp-typed bounds keep the TZ-naive result type.
13019 let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13020 let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
13021 || args.iter().any(|a| {
13022 crate::describe::describe_expr(a, &empty_cols)
13023 .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
13024 });
13025 for v in &mut arg_values {
13026 if let Value::Date(d) = *v {
13027 *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
13028 }
13029 }
13030 match arg_values.as_slice() {
13031 [Value::Timestamp(start), Value::Timestamp(stop), step] => {
13032 let interval_step = match step {
13033 Value::Interval { .. } => step.clone(),
13034 // v7.38 (read01) — PG resolves an unknown-type string step
13035 // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
13036 // a bare text step by parsing it the same way `::interval` does.
13037 Value::Text(s) => crate::conversions::coerce_value(
13038 Value::text(s.as_ref()),
13039 DataType::Interval,
13040 "",
13041 0,
13042 )
13043 .map_err(|_| {
13044 EngineError::Unsupported(alloc::format!(
13045 "generate_series(timestamp, timestamp, …): \
13046 could not parse step {s:?} as INTERVAL"
13047 ))
13048 })?,
13049 other => {
13050 return Err(EngineError::Unsupported(alloc::format!(
13051 "generate_series(timestamp, timestamp, …): \
13052 step must be INTERVAL, got {}",
13053 crate::conversions::pg_type_name_for_error_opt(other.data_type())
13054 )));
13055 }
13056 };
13057 let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
13058 Ok((
13059 if tz {
13060 DataType::Timestamptz
13061 } else {
13062 DataType::Timestamp
13063 },
13064 rows,
13065 ))
13066 }
13067 [start, stop, step]
13068 if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
13069 {
13070 let s = value_to_i64(start);
13071 let e = value_to_i64(stop);
13072 let st = value_to_i64(step);
13073 // PG types the series by the argument type: int4 args → int4
13074 // elements, int8 (bigint) args → int8. Any BigInt operand widens.
13075 let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
13076 let rows = generate_series_integers(s, e, st, wide, cancel)?;
13077 Ok((
13078 if wide {
13079 DataType::BigInt
13080 } else {
13081 DataType::Int
13082 },
13083 rows,
13084 ))
13085 }
13086 [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
13087 let s = value_to_i64(start);
13088 let e = value_to_i64(stop);
13089 let wide = value_is_bigint(start) || value_is_bigint(stop);
13090 let rows = generate_series_integers(s, e, 1, wide, cancel)?;
13091 Ok((
13092 if wide {
13093 DataType::BigInt
13094 } else {
13095 DataType::Int
13096 },
13097 rows,
13098 ))
13099 }
13100 // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
13101 // series in exact numeric arithmetic; NaN / infinity bounds and a
13102 // zero step get dedicated wordings, and a mixed int/numeric call
13103 // resolves here via the implicit int→numeric cast.
13104 [_, _] | [_, _, _]
13105 if arg_values
13106 .iter()
13107 .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
13108 && arg_values.iter().all(|v| {
13109 matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
13110 }) =>
13111 {
13112 use spg_storage::NumericKind as K;
13113 let words: [(&str, &str); 3] = [
13114 (
13115 "start value cannot be NaN",
13116 "start value cannot be infinity",
13117 ),
13118 ("stop value cannot be NaN", "stop value cannot be infinity"),
13119 ("step size cannot be NaN", "step size cannot be infinity"),
13120 ];
13121 for (i, v) in arg_values.iter().enumerate() {
13122 if let Value::Numeric { kind, .. } = v {
13123 if *kind != K::Finite {
13124 let (nan_w, inf_w) = words[i];
13125 return Err(EngineError::Unsupported(
13126 if *kind == K::NaN { nan_w } else { inf_w }.into(),
13127 ));
13128 }
13129 }
13130 }
13131 let big =
13132 |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
13133 let start = big(&arg_values[0]);
13134 let stop = big(&arg_values[1]);
13135 let step = if arg_values.len() == 3 {
13136 big(&arg_values[2])
13137 } else {
13138 spg_storage::bignum::BigNumeric::from_i128(1, 0)
13139 };
13140 if step.is_zero() {
13141 return Err(EngineError::Unsupported(
13142 "step size cannot equal zero".into(),
13143 ));
13144 }
13145 let descending = step.parts().0;
13146 let mut rows = alloc::vec::Vec::new();
13147 let mut cur = start;
13148 const MAX_ROWS: usize = 10_000_000;
13149 loop {
13150 cancel.check()?;
13151 let c = cur.cmp(&stop);
13152 if descending {
13153 if c == core::cmp::Ordering::Less {
13154 break;
13155 }
13156 } else if c == core::cmp::Ordering::Greater {
13157 break;
13158 }
13159 if rows.len() >= MAX_ROWS {
13160 return Err(EngineError::Unsupported(alloc::format!(
13161 "generate_series() result exceeds {MAX_ROWS} rows"
13162 )));
13163 }
13164 rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
13165 cur.clone()
13166 )]));
13167 cur = cur.add(&step);
13168 }
13169 Ok((
13170 DataType::Numeric {
13171 precision: 0,
13172 scale: 0,
13173 },
13174 rows,
13175 ))
13176 }
13177 _ => Err(EngineError::Unsupported(alloc::format!(
13178 "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
13179 argument shapes; got {}",
13180 arg_values
13181 .iter()
13182 .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
13183 .collect::<alloc::vec::Vec<_>>()
13184 .join(", ")
13185 ))),
13186 }
13187}
13188
13189/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
13190/// Step direction follows the sign: positive step iterates upward
13191/// (stops when current > stop); negative iterates downward; zero
13192/// errors. Caller-facing row stream is `BigInt`-typed so a single
13193/// projection schema covers SmallInt / Int / BigInt callers.
13194fn generate_series_integers(
13195 start: i64,
13196 stop: i64,
13197 step: i64,
13198 wide: bool,
13199 cancel: &CancelToken<'_>,
13200) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13201 if step == 0 {
13202 return Err(EngineError::Unsupported(
13203 "step size cannot equal zero".into(),
13204 ));
13205 }
13206 let mut out = alloc::vec::Vec::new();
13207 let mut cur = start;
13208 // Hard cap to keep a runaway call from eating all memory. PG
13209 // has no such cap but does honour query timeout; SPG's cancel
13210 // token will fire too — this is a defense-in-depth backstop.
13211 const MAX_ROWS: usize = 10_000_000;
13212 loop {
13213 cancel.check()?;
13214 if step > 0 && cur > stop {
13215 break;
13216 }
13217 if step < 0 && cur < stop {
13218 break;
13219 }
13220 out.push(Row::new(alloc::vec![if wide {
13221 Value::BigInt(cur)
13222 } else {
13223 Value::Int(cur as i32)
13224 }]));
13225 if out.len() > MAX_ROWS {
13226 return Err(EngineError::Unsupported(alloc::format!(
13227 "generate_series(): exceeded {MAX_ROWS} rows; \
13228 narrow start/stop or use a larger step"
13229 )));
13230 }
13231 cur = match cur.checked_add(step) {
13232 Some(n) => n,
13233 None => break,
13234 };
13235 }
13236 Ok(out)
13237}
13238
13239/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
13240/// `Value::Interval { months, micros }` per the caller's guard;
13241/// each iteration adds the interval via `apply_binary_interval`
13242/// so month-shifting handles short-month rollover (PG semantics).
13243fn generate_series_timestamps(
13244 start: i64,
13245 stop: i64,
13246 step: Value,
13247 cancel: &CancelToken<'_>,
13248) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13249 let (months, days, micros) = match &step {
13250 Value::Interval {
13251 months,
13252 days,
13253 micros,
13254 kind,
13255 } => (*months, *days, *micros),
13256 _ => unreachable!("caller guards step.is_interval"),
13257 };
13258 if months == 0 && days == 0 && micros == 0 {
13259 return Err(EngineError::Unsupported(
13260 "generate_series(): INTERVAL step cannot be zero".into(),
13261 ));
13262 }
13263 let ascending = months > 0 || days > 0 || micros > 0;
13264 let mut out = alloc::vec::Vec::new();
13265 let mut cur = Value::Timestamp(start);
13266 const MAX_ROWS: usize = 10_000_000;
13267 loop {
13268 cancel.check()?;
13269 let cur_t = match cur {
13270 Value::Timestamp(t) => t,
13271 _ => unreachable!("loop invariant: cur is Timestamp"),
13272 };
13273 if ascending && cur_t > stop {
13274 break;
13275 }
13276 if !ascending && cur_t < stop {
13277 break;
13278 }
13279 out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
13280 if out.len() > MAX_ROWS {
13281 return Err(EngineError::Unsupported(alloc::format!(
13282 "generate_series(): exceeded {MAX_ROWS} rows; \
13283 narrow start/stop or use a larger step"
13284 )));
13285 }
13286 let next = eval::apply_binary_interval(
13287 spg_sql::ast::BinOp::Add,
13288 &cur,
13289 &Value::Interval {
13290 months,
13291 days,
13292 micros,
13293 kind: spg_storage::IntervalKind::Finite,
13294 },
13295 )
13296 .map_err(EngineError::Eval)?;
13297 cur = match next {
13298 Some(v) => v,
13299 None => break,
13300 };
13301 }
13302 Ok(out)
13303}
13304
13305/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
13306/// WITH TIES` requires an `ORDER BY`. Without one, there's no
13307/// way to identify "ties" deterministically, so PG errors at
13308/// plan time. SPG mirrors that surface so the same DDL / app
13309/// behaviour holds on cutover.
13310fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
13311 if stmt.limit_with_ties && stmt.order_by.is_empty() {
13312 return Err(EngineError::Unsupported(alloc::string::String::from(
13313 "WITH TIES cannot be specified without ORDER BY clause",
13314 )));
13315 }
13316 Ok(())
13317}
13318
13319/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
13320/// (case-insensitive). Used by `exec_select_cancel`'s
13321/// projection loop to detect Set-Returning-Function rows that
13322/// need per-row expansion. Only the top-level call counts —
13323/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
13324/// projection's perspective; it would surface as an "unknown
13325/// function" mismatch downstream, which is what we want
13326/// (multi-SRF / nested SRF is documented carve-out for v7.19).
13327fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
13328 top_level_srf_kind(expr).is_some()
13329}
13330
13331/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
13332/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
13333/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
13334/// source row.
13335#[derive(Clone, Copy, PartialEq, Eq)]
13336pub(crate) enum SrfKind {
13337 Unnest,
13338 /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
13339 /// list. It used to be handled ONLY by the parser's lift into FROM, so a
13340 /// second one in the same list came back as "unknown function".
13341 GenerateSeries,
13342 GenerateSubscripts,
13343 /// `_text` variants unwrap scalars to their lexeme; the plain forms render
13344 /// every value as compact JSON text.
13345 ArrayElements {
13346 as_text: bool,
13347 },
13348 PathQuery,
13349 RegexpMatches,
13350 Each {
13351 as_text: bool,
13352 },
13353 ObjectKeys,
13354}
13355
13356/// Case-insensitive match against any of `names`.
13357fn name_is(name: &str, names: &[&str]) -> bool {
13358 names.iter().any(|n| name.eq_ignore_ascii_case(n))
13359}
13360
13361pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
13362 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13363 return None;
13364 };
13365 let n = args.len();
13366 // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
13367 // SELECT list (it returned an array there before) and shares the unnest
13368 // expansion machinery.
13369 if n == 1 && name.eq_ignore_ascii_case("unnest") {
13370 return Some(SrfKind::Unnest);
13371 }
13372 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
13373 return Some(SrfKind::GenerateSeries);
13374 }
13375 if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
13376 return Some(SrfKind::GenerateSubscripts);
13377 }
13378 // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
13379 // per element / match in the SELECT list; they collapsed to a single row
13380 // (a TextArray, or an "unknown function" error for `each`) before.
13381 if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
13382 return Some(SrfKind::ArrayElements { as_text: false });
13383 }
13384 if n == 1
13385 && name_is(
13386 name,
13387 &["jsonb_array_elements_text", "json_array_elements_text"],
13388 )
13389 {
13390 return Some(SrfKind::ArrayElements { as_text: true });
13391 }
13392 // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
13393 if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
13394 return Some(SrfKind::PathQuery);
13395 }
13396 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
13397 return Some(SrfKind::RegexpMatches);
13398 }
13399 if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
13400 return Some(SrfKind::Each { as_text: false });
13401 }
13402 if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
13403 return Some(SrfKind::Each { as_text: true });
13404 }
13405 if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
13406 return Some(SrfKind::ObjectKeys);
13407 }
13408 None
13409}
13410
13411/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
13412/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
13413/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
13414/// rows, as in PG).
13415pub(crate) fn top_level_srf_output(
13416 expr: &spg_sql::ast::Expr,
13417 row: &Row<'static>,
13418 ctx: &EvalContext<'_>,
13419) -> Result<Vec<Value<'static>>, EngineError> {
13420 let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
13421 (top_level_srf_kind(expr), expr)
13422 else {
13423 return Err(EngineError::Unsupported(
13424 "expected a SELECT-list SRF call".into(),
13425 ));
13426 };
13427 match kind {
13428 SrfKind::Unnest => {
13429 // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
13430 // the elements DIRECTLY: the old path built the whole
13431 // Value::Array (one eval + a clone per element) only for
13432 // array_value_to_elements to clone every element back out.
13433 // Any other argument shape (a column, a function result)
13434 // keeps the build-then-split path.
13435 if let spg_sql::ast::Expr::Array(items) = &args[0] {
13436 return items
13437 .iter()
13438 .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
13439 .collect();
13440 }
13441 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13442 array_value_to_elements(&arr)
13443 }
13444 SrfKind::GenerateSeries => {
13445 // v7.39 (read01 round 96) — evaluate the args against the actual
13446 // row, then hand off to the shared core so the numeric and
13447 // timestamp/timestamptz overloads work here too (this arm used to
13448 // handle only integers, silently NULLing a temporal/numeric series
13449 // when it shared a target list with another SRF).
13450 let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
13451 for a in args {
13452 arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13453 }
13454 let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
13455 Ok(rows
13456 .into_iter()
13457 .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
13458 .collect())
13459 }
13460 SrfKind::GenerateSubscripts => {
13461 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13462 let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13463 if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
13464 return Ok(Vec::new());
13465 }
13466 let len = array_value_to_elements(&arr)?.len();
13467 Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
13468 }
13469 // One Value per array element (`_text` → text / SQL NULL, plain → the
13470 // element's compact JSON text) — the element list the FROM-clause form
13471 // materialises.
13472 SrfKind::ArrayElements { as_text } => {
13473 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13474 if matches!(arg, Value::Null) {
13475 return Ok(Vec::new());
13476 }
13477 let items =
13478 crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13479 Ok(items
13480 .into_iter()
13481 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13482 .collect())
13483 }
13484 // The scalar form already yields a TextArray of the keys (or errors on
13485 // a non-object, like PG); expand it into rows.
13486 SrfKind::ObjectKeys => {
13487 let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
13488 array_value_to_elements(&v)
13489 }
13490 // One row per match, each a text[] of the pattern's capture groups.
13491 SrfKind::RegexpMatches => {
13492 let vals: Vec<Value<'static>> = args
13493 .iter()
13494 .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
13495 .collect::<Result<_, _>>()?;
13496 crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
13497 }
13498 // One composite `(key, value)` row per object member (plain → jsonb
13499 // value, `_text` → text / SQL NULL).
13500 SrfKind::Each { as_text } => {
13501 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13502 if matches!(arg, Value::Null) {
13503 return Ok(Vec::new());
13504 }
13505 let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13506 Ok(pairs
13507 .into_iter()
13508 .map(|(k, v)| {
13509 let val = if as_text {
13510 v.map(Value::text).unwrap_or(Value::Null)
13511 } else {
13512 v.map(Value::json).unwrap_or(Value::Null)
13513 };
13514 Value::Composite(alloc::vec![
13515 ("key".to_string(), Value::text(k)),
13516 ("value".to_string(), val),
13517 ])
13518 })
13519 .collect())
13520 }
13521 // One Value per matched JSON value.
13522 SrfKind::PathQuery => {
13523 let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13524 let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13525 // v7.39 — optional vars document (3rd arg).
13526 let vars = match args.get(2) {
13527 Some(a) => {
13528 let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
13529 crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
13530 }
13531 None => None,
13532 };
13533 match crate::json::path_query_vars(&doc, &path, vars.as_ref())
13534 .map_err(EngineError::Eval)?
13535 {
13536 Value::Null => Ok(Vec::new()),
13537 Value::TextArray(items) => Ok(items
13538 .into_iter()
13539 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13540 .collect()),
13541 other => Ok(alloc::vec![other]),
13542 }
13543 }
13544 }
13545}
13546
13547/// v7.19 P5 — turn an array-typed `Value` into the element list
13548/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
13549/// = (no rows)`). Non-array values fall through to a type-mismatch
13550/// error.
13551pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
13552 // v7.39 (round 236) — PG unnests a multidimensional array into its
13553 // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
13554 // rows). SPG stores 2-D arrays as their own variants, which fell
13555 // through to the type-mismatch arm below.
13556 if let Some(flat) = crate::eval::values::flatten_2d(v) {
13557 return array_value_to_elements(&flat);
13558 }
13559 // v7.39.11 — every array-family value, through the one element
13560 // menu. The arms below name int / bigint / text / json and stop, so
13561 // `SELECT unnest(ARRAY[1,2]::smallint[])` raised "expects an array
13562 // argument, got smallint[]" — the type it had just been given —
13563 // and so did every catalog vector. Found while closing sentori's
13564 // §4 against 7.39.10; the FROM-clause unnest has the same arm.
13565 if crate::eval::values::array_len(v).is_some() {
13566 if let Some(elems) = crate::eval::values::array_elements(v) {
13567 return Ok(elems);
13568 }
13569 }
13570 match v {
13571 Value::Null => Ok(Vec::new()),
13572 Value::TextArray(items) => Ok(items
13573 .iter()
13574 .map(|opt| {
13575 opt.as_ref()
13576 .map(|s| Value::text(s.clone()))
13577 .unwrap_or(Value::Null)
13578 })
13579 .collect()),
13580 Value::IntArray(items) => Ok(items
13581 .iter()
13582 .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
13583 .collect()),
13584 Value::BigIntArray(items) => Ok(items
13585 .iter()
13586 .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
13587 .collect()),
13588 // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
13589 // range per canonical span.
13590 Value::Multirange { kind, ranges } => Ok(ranges
13591 .iter()
13592 .map(|s| Value::Range {
13593 kind: *kind,
13594 lower: s.lower.clone(),
13595 upper: s.upper.clone(),
13596 lower_inc: s.lower_inc,
13597 upper_inc: s.upper_inc,
13598 empty: false,
13599 })
13600 .collect()),
13601 other => Err(EngineError::Eval(EvalError::TypeMismatch {
13602 detail: alloc::format!(
13603 "unnest() expects an array argument, got {}",
13604 crate::conversions::pg_type_name_for_error_opt(other.data_type())
13605 ),
13606 })),
13607 }
13608}
13609
13610impl Engine {
13611 /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
13612 /// the SELECT's FROM / JOIN graph, re-parse each view's body
13613 /// source, and prepend it as a synthetic CTE on the
13614 /// returned SelectStatement. Returns `None` when no view
13615 /// references are found (caller proceeds with the original
13616 /// statement); returns `Some(rewritten)` otherwise (caller
13617 /// re-runs exec_select_cancel on the rewritten form so the
13618 /// regular CTE materialiser handles it).
13619 fn expand_views_in_select(
13620 &self,
13621 stmt: &SelectStatement,
13622 ) -> Result<Option<SelectStatement>, EngineError> {
13623 let cat = self.active_catalog();
13624 let mut referenced: Vec<String> = Vec::new();
13625 if let Some(from) = &stmt.from {
13626 collect_view_refs(&from.primary, cat, &mut referenced);
13627 for j in &from.joins {
13628 collect_view_refs(&j.table, cat, &mut referenced);
13629 }
13630 }
13631 // Don't expand a view name that's already shadowed by a
13632 // CTE on the same SELECT — the CTE wins per PG.
13633 referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
13634 if referenced.is_empty() {
13635 return Ok(None);
13636 }
13637 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
13638 for name in &referenced {
13639 let view = cat.view(name).ok_or_else(|| {
13640 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13641 "view {name:?} disappeared mid-expansion"
13642 )))
13643 })?;
13644 let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
13645 EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
13646 })?;
13647 let Statement::Select(body) = parsed else {
13648 return Err(EngineError::Unsupported(alloc::format!(
13649 "view {name:?} body is not a SELECT (catalog corruption)"
13650 )));
13651 };
13652 new_ctes.push(spg_sql::ast::Cte {
13653 name: name.clone(),
13654 body: spg_sql::ast::CteBody::Select(body),
13655 recursive: false,
13656 column_overrides: view.columns.clone(),
13657 search: None,
13658 cycle: None,
13659 });
13660 }
13661 let mut out = stmt.clone();
13662 // Prepend so view CTEs are visible to caller-supplied CTEs.
13663 new_ctes.extend(out.ctes);
13664 out.ctes = new_ctes;
13665 Ok(Some(out))
13666 }
13667
13668 /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
13669 /// any partition-parent table, rewrite the SELECT so each parent
13670 /// reference resolves to a CTE whose body is a `UNION ALL` over the
13671 /// children that pass the WHERE-derived partition-key range. Returns
13672 /// `None`(no rewrite needed)when no parent is referenced or all
13673 /// references are shadowed by a same-name CTE.
13674 ///
13675 /// Pruning vocabulary at v7.37.6-B:
13676 /// * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
13677 /// and `<key> BETWEEN literal AND literal`.
13678 /// * Anything outside that(OR / nested IN / function call on the
13679 /// key)defaults to "no pruning" — every child + DEFAULT lands
13680 /// in the UNION. Correctness is preserved; only the plan size
13681 /// widens.
13682 fn expand_partition_parents_in_select(
13683 &self,
13684 stmt: &SelectStatement,
13685 ) -> Result<Option<SelectStatement>, EngineError> {
13686 let cat = self.active_catalog();
13687 let Some(from) = &stmt.from else {
13688 return Ok(None);
13689 };
13690 let mut parent_refs: Vec<String> = Vec::new();
13691 collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
13692 for j in &from.joins {
13693 collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
13694 }
13695 // Drop names shadowed by a CTE on the same SELECT(PG semantics
13696 // — same as view expansion above).
13697 parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
13698 if parent_refs.is_empty() {
13699 return Ok(None);
13700 }
13701 // Synthesise a CTE name per parent so the existing
13702 // "CTE shadows a real table" guard doesn't fire (the parent
13703 // IS a real table in the catalog, unlike VIEW expansion's
13704 // case). The FROM-clause TableRef walker below rewrites
13705 // every parent reference to point at the synthetic CTE.
13706 let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
13707 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
13708 let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
13709 for parent_name in &parent_refs {
13710 // No children = no rewrite. The parent itself is a real
13711 // (empty-rows) table — the regular FROM-resolution path
13712 // will scan it and return 0 rows, matching the
13713 // "partition parent with no children" plan. Skipping the
13714 // CTE here also avoids `SELECT * FROM parent` re-entering
13715 // this rewrite on the synthetic body (infinite recursion).
13716 let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
13717 continue;
13718 };
13719 new_ctes.push(spg_sql::ast::Cte {
13720 name: synth_name(parent_name),
13721 body: spg_sql::ast::CteBody::Select(body),
13722 recursive: false,
13723 column_overrides: Vec::new(),
13724 search: None,
13725 cycle: None,
13726 });
13727 expanded_parents.push(parent_name.clone());
13728 }
13729 if expanded_parents.is_empty() {
13730 return Ok(None);
13731 }
13732 let mut out = stmt.clone();
13733 if let Some(from) = out.from.as_mut() {
13734 rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
13735 for j in &mut from.joins {
13736 rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
13737 }
13738 }
13739 new_ctes.extend(out.ctes);
13740 out.ctes = new_ctes;
13741 Ok(Some(out))
13742 }
13743
13744 /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
13745 /// Children include every overlap-hit `Range` plus(always)the
13746 /// `Default` child(if any). Returns `Ok(None)` when no children
13747 /// would survive — caller skips the CTE injection and lets the
13748 /// parent fall through to the regular(empty-rows)scan path,
13749 /// avoiding the infinite recursion that an empty-body CTE
13750 /// referencing the parent name would trigger.
13751 /// v7.37.16 (16.10) — public helper invoked from explain.rs to
13752 /// surface "which children survive the WHERE-clause prune" in
13753 /// EXPLAIN output. Returns `None` when `parent_name` isn't
13754 /// actually a partition parent; otherwise returns the list of
13755 /// children the planner would scan (same algorithm as
13756 /// [`Self::build_partition_parent_union_body`] but without the
13757 /// SQL re-parse).
13758 /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
13759 /// expression (the PG-shaped EXPLAIN's scan builder has no full
13760 /// SelectStatement in hand). Wraps the original by synthesising a
13761 /// minimal statement carrying just the predicate.
13762 pub(crate) fn explain_partition_kept_children_by_where(
13763 &self,
13764 parent_name: &str,
13765 where_: Option<&spg_sql::ast::Expr>,
13766 ) -> Option<Vec<alloc::string::String>> {
13767 let mut synth = SelectStatement::default();
13768 synth.where_ = where_.cloned();
13769 self.explain_partition_kept_children(parent_name, &synth)
13770 }
13771
13772 pub(crate) fn explain_partition_kept_children(
13773 &self,
13774 parent_name: &str,
13775 outer: &SelectStatement,
13776 ) -> Option<Vec<alloc::string::String>> {
13777 use spg_storage::PartitionRole;
13778 let cat = self.active_catalog();
13779 let parent = cat.get(parent_name)?;
13780 let (key_position, parent_kind) = match &parent.schema().partition_role {
13781 Some(PartitionRole::Parent {
13782 key_column_positions,
13783 kind,
13784 ..
13785 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13786 _ => return None,
13787 };
13788 let key_col_name = parent.schema().columns[key_position].name.clone();
13789 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13790 Some(expr) => extract_key_range(expr, &key_col_name),
13791 None => (None, None),
13792 };
13793 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13794 Some(expr) => extract_key_eq_value(expr, &key_col_name),
13795 None => None,
13796 };
13797 let children = crate::partition::children_of_parent(cat, parent_name);
13798 let mut kept: Vec<alloc::string::String> = Vec::new();
13799 let mut default_child: Option<alloc::string::String> = None;
13800 for child_name in &children {
13801 let Some(child) = cat.get(child_name) else {
13802 continue;
13803 };
13804 match &child.schema().partition_role {
13805 Some(PartitionRole::Range { lower, upper, .. }) => {
13806 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13807 kept.push(child_name.clone());
13808 }
13809 }
13810 Some(PartitionRole::List { values, .. }) => match &eq_value {
13811 Some(v) => {
13812 if values.iter().any(|b| b.equals_value(v)) {
13813 kept.push(child_name.clone());
13814 }
13815 }
13816 None => kept.push(child_name.clone()),
13817 },
13818 Some(PartitionRole::Hash {
13819 modulus, remainder, ..
13820 }) => match &eq_value {
13821 Some(v) => {
13822 let h = crate::partition::pg_compatible_hash(v);
13823 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13824 kept.push(child_name.clone());
13825 }
13826 }
13827 None => kept.push(child_name.clone()),
13828 },
13829 Some(PartitionRole::Default { .. }) => {
13830 default_child = Some(child_name.clone());
13831 }
13832 _ => {}
13833 }
13834 }
13835 let _ = parent_kind;
13836 if let Some(d) = default_child {
13837 if kept.is_empty() || eq_value.is_none() {
13838 kept.push(d);
13839 }
13840 }
13841 Some(kept)
13842 }
13843
13844 fn build_partition_parent_union_body(
13845 &self,
13846 parent_name: &str,
13847 outer: &SelectStatement,
13848 ) -> Result<Option<SelectStatement>, EngineError> {
13849 use spg_storage::PartitionRole;
13850 let cat = self.active_catalog();
13851 let parent = cat.get(parent_name).ok_or_else(|| {
13852 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13853 "partition parent {parent_name:?} disappeared mid-expansion"
13854 )))
13855 })?;
13856 let (key_position, parent_kind) = match &parent.schema().partition_role {
13857 Some(PartitionRole::Parent {
13858 key_column_positions,
13859 kind,
13860 ..
13861 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13862 // v7.39 (round 645) — an INHERITANCE parent, which has no
13863 // role of its own: the relationship is recorded only in the
13864 // children. Three things differ from a partition parent and
13865 // all three are in this body.
13866 //
13867 // * The parent HOLDS ROWS, so it is a term of the union —
13868 // `FROM ONLY`, or expanding it would recurse.
13869 // * There is no partition key, so there is nothing to
13870 // prune: every child is a term.
13871 // * A child may declare columns of its own, so the terms
13872 // name the PARENT's columns rather than `*`. PG's
13873 // `SELECT * FROM parent` returns the parent's shape.
13874 //
13875 // Answered from this match rather than a branch before it —
13876 // round 644 measured what an extra early return beside an
13877 // existing test costs in this file.
13878 _ if crate::partition::has_inheritance_children(cat, parent_name) => {
13879 let cols = parent
13880 .schema()
13881 .columns
13882 .iter()
13883 .map(|c| quote_ident_for_sql(&c.name))
13884 .collect::<Vec<_>>()
13885 .join(", ");
13886 let carry_sys = references_ctid(outer);
13887 let sys = if carry_sys {
13888 let mut t = alloc::string::String::new();
13889 for s in SYSTEM_COLUMNS {
13890 t.push_str(", ");
13891 t.push_str(s);
13892 }
13893 t
13894 } else {
13895 alloc::string::String::new()
13896 };
13897 let mut body = alloc::format!(
13898 "SELECT {cols}{sys} FROM ONLY {}",
13899 quote_ident_for_sql(parent_name)
13900 );
13901 for child in crate::partition::children_of_parent(cat, parent_name) {
13902 body.push_str(&alloc::format!(
13903 " UNION ALL SELECT {cols}{sys} FROM {}",
13904 quote_ident_for_sql(&child)
13905 ));
13906 }
13907 return parse_select_or_corrupt(&body).map(Some);
13908 }
13909 _ => {
13910 return Err(EngineError::Unsupported(alloc::format!(
13911 "partition expansion: {parent_name:?} is not a parent"
13912 )));
13913 }
13914 };
13915 let key_col_name = parent.schema().columns[key_position].name.clone();
13916 // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
13917 // off the WHERE; for LIST / HASH we extract a single `=`
13918 // literal (and the rest of the planner falls back to "keep
13919 // every child" — same conservative path as 16.1/16.2).
13920 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13921 Some(expr) => extract_key_range(expr, &key_col_name),
13922 None => (None, None),
13923 };
13924 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13925 Some(expr) => extract_key_eq_value(expr, &key_col_name),
13926 None => None,
13927 };
13928 let children = crate::partition::children_of_parent(cat, parent_name);
13929 let mut kept: Vec<String> = Vec::new();
13930 let mut default_child: Option<String> = None;
13931 // First pass — apply per-strategy gates, defer DEFAULT until
13932 // we know whether some non-DEFAULT child matched.
13933 for child_name in &children {
13934 let Some(child) = cat.get(child_name) else {
13935 continue;
13936 };
13937 match &child.schema().partition_role {
13938 Some(PartitionRole::Range { lower, upper, .. }) => {
13939 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13940 kept.push(child_name.clone());
13941 }
13942 }
13943 // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
13944 // = <lit>`, only the child whose values contain that
13945 // literal survives. Otherwise (no equality predicate
13946 // or planner couldn't extract one) keep the child
13947 // conservatively.
13948 Some(PartitionRole::List { values, .. }) => match &eq_value {
13949 Some(v) => {
13950 if values.iter().any(|b| b.equals_value(v)) {
13951 kept.push(child_name.clone());
13952 }
13953 }
13954 None => kept.push(child_name.clone()),
13955 },
13956 // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
13957 // we know the residue class deterministically, so
13958 // only the matching REMAINDER child survives.
13959 Some(PartitionRole::Hash {
13960 modulus, remainder, ..
13961 }) => match &eq_value {
13962 Some(v) => {
13963 let h = crate::partition::pg_compatible_hash(v);
13964 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13965 kept.push(child_name.clone());
13966 }
13967 }
13968 None => kept.push(child_name.clone()),
13969 },
13970 Some(PartitionRole::Default { .. }) => {
13971 default_child = Some(child_name.clone());
13972 }
13973 _ => {}
13974 }
13975 }
13976 // PG-style DEFAULT semantics: the DEFAULT child must be
13977 // scanned iff some row could fall outside every concrete
13978 // child's bound predicate. We approximate that as "no
13979 // concrete child matched" (== full prune) — strictly
13980 // conservative for LIST / HASH (DEFAULT also catches rows
13981 // outside the union of value-sets / residues), and matches
13982 // PG for the equality case where we *do* know the routing
13983 // outcome.
13984 let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
13985 if let Some(d) = default_child {
13986 if kept.is_empty() {
13987 kept.push(d);
13988 } else if eq_value.is_none() {
13989 // Without an equality literal, the DEFAULT child may
13990 // still hold matching rows (e.g. LIKE on TEXT keys
13991 // for which a LIST partition exists). Keep it.
13992 kept.push(d);
13993 }
13994 }
13995 // Build the UNION ALL body text and re-parse — keeps the
13996 // rewrite expressible in surface SQL so the engine's existing
13997 // parser path handles the AST shape uniformly.
13998 if kept.is_empty() {
13999 // No children survive — caller falls back to scanning the
14000 // (empty) parent table. Returning None here is what
14001 // prevents the synthetic CTE from referring back to the
14002 // parent name and re-entering this rewrite pass.
14003 let _ = parent_name;
14004 return Ok(None);
14005 }
14006 // v7.39 (round 622, S05a) — the system columns of the CHILD the row
14007 // actually lives in.
14008 //
14009 // The parent is read through a synthetic CTE, so a `tableoid` on it
14010 // resolved against that CTE: every row of every child reported
14011 // `__spg_partition_pm`, an internal name no user ever typed, where
14012 // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
14013 // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
14014 // one asks "which partition is this row in", answering 0 rows where
14015 // PG answers 1. `ctid` had the same shape: it numbered the CTE's
14016 // output, so rows in different children got distinct ctids instead
14017 // of each child's own physical position.
14018 //
14019 // Naming them in the term is what carries them: the child scan
14020 // materialises its own six because the statement now references
14021 // them, and they land in SYSTEM_COLUMNS order right after the user
14022 // columns — the exact layout the positional `*` skip already
14023 // expects. Only done when the outer statement asks for one, so a
14024 // plain `SELECT * FROM parent` scans exactly what it scanned.
14025 let carry_sys = references_ctid(outer);
14026 let mut body = alloc::string::String::new();
14027 for (i, child_name) in kept.iter().enumerate() {
14028 if i > 0 {
14029 body.push_str(" UNION ALL ");
14030 }
14031 body.push_str("SELECT *");
14032 if carry_sys {
14033 for sys in SYSTEM_COLUMNS {
14034 body.push_str(", ");
14035 body.push_str(sys);
14036 }
14037 }
14038 body.push_str(" FROM ");
14039 body.push_str("e_ident_for_sql(child_name));
14040 }
14041 parse_select_or_corrupt(&body).map(Some)
14042 }
14043}
14044
14045/// Rewrite a `TableRef` pointing at a partition parent so it
14046/// references the synthetic CTE created by the expansion. If the
14047/// original ref had no alias, preserve the parent name as an alias
14048/// so column references like `events_partitioned.received_at`
14049/// keep resolving.
14050fn rewrite_partition_parent_table_ref(
14051 t: &mut spg_sql::ast::TableRef,
14052 parents: &[alloc::string::String],
14053 synth_name: &impl Fn(&str) -> alloc::string::String,
14054) {
14055 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
14056 return;
14057 }
14058 // v7.39 (round 644) — an ONLY reference stays pointed at the parent
14059 // itself. The rewrite is keyed on the NAME, so in
14060 // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
14061 // parent list and this then rewrote BOTH — including the one that
14062 // asked not to descend. PG answers 0 for that join; SPG answered 2.
14063 // Folded into the existing test — see the note in
14064 // `collect_partition_parent_refs` for what a separate one cost.
14065 if t.only || !parents.iter().any(|p| p == &t.name) {
14066 return;
14067 }
14068 if t.alias.is_none() {
14069 t.alias = Some(t.name.clone());
14070 }
14071 t.name = synth_name(&t.name);
14072}
14073
14074/// Walk a `TableRef` and push its `name` if it resolves to a partition
14075/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
14076/// `generate_series_args` references — those aren't catalog tables.
14077fn collect_partition_parent_refs(
14078 t: &spg_sql::ast::TableRef,
14079 cat: &spg_storage::Catalog,
14080 out: &mut Vec<alloc::string::String>,
14081) {
14082 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
14083 return;
14084 }
14085 // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
14086 // The keyword used to be absorbed at parse time, so this fanned out
14087 // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
14088 // answered 2 where PG answers 0.
14089 //
14090 // Folded into the existing test rather than given an early return of
14091 // its own: as two extra lines in this function's body it cost
14092 // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
14093 // outside the panel. Rounds 641 and 643 met the same wall from the
14094 // other two directions — adding to a hot function and taking away
14095 // from a cold one. What goes in a body near the row loop is a
14096 // codegen decision whatever its shape.
14097 if !t.only && crate::partition::has_children(cat, &t.name) {
14098 out.push(t.name.clone());
14099 }
14100}
14101
14102/// v7.37.6-B partition-key range derived from a WHERE expression.
14103/// `i64` microseconds since epoch with the same sign convention as
14104/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
14105/// / `=`),`false` ⇒ exclusive(`>` / `<`).
14106#[derive(Debug, Clone, Copy)]
14107pub(crate) struct PartitionFilterBound {
14108 pub micros: i64,
14109 pub inclusive: bool,
14110}
14111
14112/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
14113/// shapes; tighten the running lo / hi as we go. Anything outside that
14114/// (OR / nested calls / non-key columns)is ignored — caller treats
14115/// `None` as "no constraint on that side."
14116fn extract_key_range(
14117 expr: &spg_sql::ast::Expr,
14118 key_col: &str,
14119) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
14120 let mut lo: Option<PartitionFilterBound> = None;
14121 let mut hi: Option<PartitionFilterBound> = None;
14122 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14123 while let Some(e) = stack.pop() {
14124 match e {
14125 spg_sql::ast::Expr::Binary {
14126 lhs,
14127 op: spg_sql::ast::BinOp::And,
14128 rhs,
14129 } => {
14130 stack.push(lhs);
14131 stack.push(rhs);
14132 }
14133 // BETWEEN is desugared at parse time into `lhs >= low AND
14134 // lhs <= high`, so it lands here as two regular Binary
14135 // arms via the AND walker above.
14136 spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
14137 let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
14138 (Some(lhs.as_ref()), rhs.as_ref(), false)
14139 } else if is_column_ref(rhs, key_col) {
14140 (Some(rhs.as_ref()), lhs.as_ref(), true)
14141 } else {
14142 (None, lhs.as_ref(), false)
14143 };
14144 if col_ref.is_none() {
14145 continue;
14146 }
14147 let Some(lit) = literal_to_micros(lit_side) else {
14148 continue;
14149 };
14150 use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
14151 let effective_op = if swapped {
14152 match op {
14153 Lt => Gt,
14154 LtEq => GtEq,
14155 Gt => Lt,
14156 GtEq => LtEq,
14157 other => *other,
14158 }
14159 } else {
14160 *op
14161 };
14162 match effective_op {
14163 Eq => {
14164 tighten_lo(
14165 &mut lo,
14166 PartitionFilterBound {
14167 micros: lit,
14168 inclusive: true,
14169 },
14170 );
14171 tighten_hi(
14172 &mut hi,
14173 PartitionFilterBound {
14174 micros: lit,
14175 inclusive: true,
14176 },
14177 );
14178 }
14179 GtEq => {
14180 tighten_lo(
14181 &mut lo,
14182 PartitionFilterBound {
14183 micros: lit,
14184 inclusive: true,
14185 },
14186 );
14187 }
14188 Gt => {
14189 tighten_lo(
14190 &mut lo,
14191 PartitionFilterBound {
14192 micros: lit,
14193 inclusive: false,
14194 },
14195 );
14196 }
14197 LtEq => {
14198 tighten_hi(
14199 &mut hi,
14200 PartitionFilterBound {
14201 micros: lit,
14202 inclusive: true,
14203 },
14204 );
14205 }
14206 Lt => {
14207 tighten_hi(
14208 &mut hi,
14209 PartitionFilterBound {
14210 micros: lit,
14211 inclusive: false,
14212 },
14213 );
14214 }
14215 _ => {}
14216 }
14217 }
14218 _ => {}
14219 }
14220 }
14221 (lo, hi)
14222}
14223
14224fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14225 match slot {
14226 None => *slot = Some(new),
14227 Some(cur) => {
14228 if new.micros > cur.micros
14229 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14230 {
14231 *slot = Some(new);
14232 }
14233 }
14234 }
14235}
14236
14237fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14238 match slot {
14239 None => *slot = Some(new),
14240 Some(cur) => {
14241 if new.micros < cur.micros
14242 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14243 {
14244 *slot = Some(new);
14245 }
14246 }
14247 }
14248}
14249
14250fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
14251 if let spg_sql::ast::Expr::Column(c) = e {
14252 c.name.eq_ignore_ascii_case(key_col)
14253 } else {
14254 false
14255 }
14256}
14257
14258/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
14259/// `key_col = <literal>` predicate out for LIST/HASH partition
14260/// pruning. Returns `None` when no equality literal can be lifted
14261/// (planner then keeps every child — correctness preserved). The
14262/// returned `Value<'static>` is an owned coercion so the caller can
14263/// outlive any AST node it was extracted from.
14264pub(crate) fn extract_key_eq_value(
14265 expr: &spg_sql::ast::Expr,
14266 key_col: &str,
14267) -> Option<spg_storage::Value<'static>> {
14268 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14269 while let Some(e) = stack.pop() {
14270 match e {
14271 spg_sql::ast::Expr::Binary {
14272 lhs,
14273 op: spg_sql::ast::BinOp::And,
14274 rhs,
14275 } => {
14276 stack.push(lhs);
14277 stack.push(rhs);
14278 }
14279 spg_sql::ast::Expr::Binary {
14280 lhs,
14281 op: spg_sql::ast::BinOp::Eq,
14282 rhs,
14283 } => {
14284 let lit_side = if is_column_ref(lhs, key_col) {
14285 rhs.as_ref()
14286 } else if is_column_ref(rhs, key_col) {
14287 lhs.as_ref()
14288 } else {
14289 continue;
14290 };
14291 let cloned = lit_side.clone();
14292 let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
14293 continue;
14294 };
14295 // Coerce to an owned Value<'static> so the caller
14296 // can hold it past the WHERE expression's lifetime.
14297 let owned: spg_storage::Value<'static> = match v {
14298 spg_storage::Value::Text(s) => {
14299 spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
14300 }
14301 spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
14302 spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
14303 spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
14304 spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
14305 spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
14306 spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
14307 spg_storage::Value::Null => spg_storage::Value::Null,
14308 // Anything else (Vector / Json / Bytes / Numeric /
14309 // arrays / interval / …) isn't a current partition
14310 // key type; skip without pruning.
14311 _ => continue,
14312 };
14313 return Some(owned);
14314 }
14315 _ => {}
14316 }
14317 }
14318 None
14319}
14320
14321/// Coerce a literal Expr(after the parser folded sequence calls etc.)
14322/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
14323/// pruning and routing agree on the literal vocabulary. Returns
14324/// `None` when the literal isn't recognised(planner then skips
14325/// pruning on that branch — correctness preserved).
14326fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
14327 let cloned = e.clone();
14328 let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
14329 match value {
14330 spg_storage::Value::Timestamp(m) => Some(m),
14331 spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
14332 spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
14333 _ => None,
14334 }
14335}
14336
14337/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
14338/// satisfying the WHERE-derived filter range. PG-style half-open:
14339/// child upper exclusive. Filter inclusivity is honoured per-bound.
14340fn range_satisfies_filter(
14341 range_lo: &spg_storage::PartitionBound,
14342 range_hi: &spg_storage::PartitionBound,
14343 filter_lo: Option<&PartitionFilterBound>,
14344 filter_hi: Option<&PartitionFilterBound>,
14345) -> bool {
14346 use spg_storage::PartitionBound;
14347 // For each filter side, reject children that can't host any row
14348 // matching the predicate.
14349 if let Some(lo) = filter_lo {
14350 // child upper bound vs filter lower:
14351 // if filter is x >= L, child rejects iff child.hi <= L
14352 // if filter is x > L, child rejects iff child.hi <= L
14353 // (child.hi exclusive, so equality with L still rejects)
14354 match range_hi {
14355 PartitionBound::MinValue => return false,
14356 PartitionBound::MaxValue => {}
14357 PartitionBound::TimestampTz(hi) => {
14358 if *hi <= lo.micros {
14359 return false;
14360 }
14361 }
14362 // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
14363 // matched against TIMESTAMPTZ filters here; keep child
14364 // (conservative: don't prune).
14365 PartitionBound::BigInt(_)
14366 | PartitionBound::Int(_)
14367 | PartitionBound::SmallInt(_)
14368 | PartitionBound::Date(_)
14369 | PartitionBound::Text(_) => {}
14370 }
14371 }
14372 if let Some(hi) = filter_hi {
14373 // child lower bound vs filter upper:
14374 // if filter is x <= U, child rejects iff child.lo > U
14375 // if filter is x < U, child rejects iff child.lo >= U
14376 match range_lo {
14377 PartitionBound::MaxValue => return false,
14378 PartitionBound::MinValue => {}
14379 PartitionBound::TimestampTz(lo) => {
14380 let rejects = if hi.inclusive {
14381 *lo > hi.micros
14382 } else {
14383 *lo >= hi.micros
14384 };
14385 if rejects {
14386 return false;
14387 }
14388 }
14389 PartitionBound::BigInt(_)
14390 | PartitionBound::Int(_)
14391 | PartitionBound::SmallInt(_)
14392 | PartitionBound::Date(_)
14393 | PartitionBound::Text(_) => {}
14394 }
14395 }
14396 true
14397}
14398
14399fn quote_ident_for_sql(name: &str) -> alloc::string::String {
14400 // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
14401 // identifier, otherwise quoted). Conservative: always quote so
14402 // children with reserved names round-trip safely through the
14403 // CTE-body parse.
14404 let mut out = alloc::string::String::with_capacity(name.len() + 2);
14405 out.push('"');
14406 for c in name.chars() {
14407 if c == '"' {
14408 out.push('"');
14409 }
14410 out.push(c);
14411 }
14412 out.push('"');
14413 out
14414}
14415
14416fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
14417 let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
14418 EngineError::Unsupported(alloc::format!(
14419 "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
14420 ))
14421 })?;
14422 let Statement::Select(body) = parsed else {
14423 return Err(EngineError::Unsupported(alloc::format!(
14424 "partition expansion: generated SQL {sql:?} is not a SELECT"
14425 )));
14426 };
14427 Ok(body)
14428}
14429
14430/// v7.39 (read01 round 65/66) — the column shape a set-returning function
14431/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
14432/// yields ONE column named after the call's alias when there is one (`FROM
14433/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
14434/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
14435fn setof_column_shape_from(
14436 declared: &str,
14437 name: &str,
14438 alias: Option<&str>,
14439 got: &[ColumnSchema],
14440) -> alloc::vec::Vec<ColumnSchema> {
14441 let upper = declared.to_ascii_uppercase();
14442 if upper.starts_with("TABLE(") {
14443 let raw = &declared["TABLE(".len()..declared.len() - 1];
14444 return raw
14445 .split(',')
14446 .zip(got.iter())
14447 .map(|(decl, g)| {
14448 let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
14449 ColumnSchema::new(cname.to_string(), g.ty, true)
14450 })
14451 .collect();
14452 }
14453 let cname = alias.unwrap_or(name);
14454 got.first()
14455 .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
14456 .unwrap_or_default()
14457}
14458
14459/// The plpgsql twin: the interpreter hands back raw value rows, so the types
14460/// come off the first row.
14461fn setof_column_shape(
14462 declared: &str,
14463 name: &str,
14464 alias: Option<&str>,
14465 first_row: Option<&alloc::vec::Vec<Value<'static>>>,
14466) -> alloc::vec::Vec<ColumnSchema> {
14467 let got: alloc::vec::Vec<ColumnSchema> = first_row
14468 .map(|r| {
14469 r.iter()
14470 .enumerate()
14471 .map(|(i, v)| {
14472 ColumnSchema::new(
14473 alloc::format!("col{i}"),
14474 v.data_type().unwrap_or(DataType::Text),
14475 true,
14476 )
14477 })
14478 .collect()
14479 })
14480 .unwrap_or_default();
14481 setof_column_shape_from(declared, name, alias, &got)
14482}
14483
14484/// v7.39 (read01 round 67) — expand every set-returning call in a target list
14485/// for ONE input row, PG's ProjectSet semantics.
14486///
14487/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
14488/// output has as many rows as the LONGEST of them, and a shorter one is padded
14489/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
14490/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
14491/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
14492/// is zero rows, not one NULL row.
14493///
14494/// Non-SRF items repeat, evaluated once per output row from the same input row.
14495/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
14496/// used to reach the scalar function dispatcher, which reported the aggregate as
14497/// an *unknown function* — the same "symptom two layers above the cause" shape
14498/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
14499/// sees a call, not the clause it came from. The statement knows.
14500/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
14501/// clause may appear.
14502///
14503/// PG rejects `FOR UPDATE` on exactly the shapes that have no
14504/// identifiable base row to lock, each with its own wording. SPG
14505/// accepted all of them and locked nothing, so a query that PG refuses
14506/// outright came back looking like it had taken locks.
14507///
14508/// Every wording read off live PG 18.4.
14509impl crate::Engine {
14510 /// v7.39.2 — a column name in WHERE / ORDER BY / GROUP BY / HAVING
14511 /// that names nothing is refused before the scan, not when a row
14512 /// reaches it.
14513 ///
14514 /// The projection resolves its names eagerly; a predicate only meets
14515 /// them per row. So on an EMPTY table `SELECT a FROM t WHERE nosuch
14516 /// = 1` answered zero rows and no error, and the same statement over
14517 /// a table with one row raised. Measured on PostgreSQL 18.6 and
14518 /// MySQL 9.7.2: both refuse it whatever the row count. A typo in a
14519 /// predicate therefore passed a test written against an empty
14520 /// fixture and failed in production — or, worse, ran nightly over an
14521 /// empty window and reported nothing.
14522 ///
14523 /// Deliberately narrow: ONE plain base table, nothing else. A join,
14524 /// a CTE, a set operation, a lateral or function source, or a
14525 /// subquery in the clause all bring a second scope into which a name
14526 /// may legitimately resolve, and refusing one of those would be a
14527 /// worse defect than the one this closes. Those shapes keep the
14528 /// old behaviour; the walk below does not descend into a subquery
14529 /// for the same reason.
14530 /// v7.39.2 — refuse a call whose argument count no overload accepts,
14531 /// BEFORE the scan rather than per row.
14532 ///
14533 /// `SELECT lower(t, n) FROM t` answered zero rows and no error over
14534 /// an EMPTY table and raised the moment the table had one row in it,
14535 /// because the arity check lives inside the row-time dispatch. It is
14536 /// the same shape as the unknown-column-in-a-predicate defect closed
14537 /// earlier in this release, and it hides in the same place: a query
14538 /// written against an empty fixture passes its test.
14539 ///
14540 /// The accepted counts come from `eval::arity::REFUSED_ARITIES`,
14541 /// which is derived by asking the dispatch itself offline and can
14542 /// only ever UNDER-refuse — see that file for why the two other
14543 /// candidate oracles were refuted.
14544 /// v7.39.3 — MySQL's column names are case-insensitive; PostgreSQL's
14545 /// quoted ones are not. See `EvalContext::col_eq`.
14546 fn col_name_eq(&self, a: &str, b: &str) -> bool {
14547 if self.speaks_mysql {
14548 a.eq_ignore_ascii_case(b)
14549 } else {
14550 a == b
14551 }
14552 }
14553
14554 pub(crate) fn validate_function_arity(
14555 &self,
14556 stmt: &SelectStatement,
14557 ) -> Result<(), EngineError> {
14558 let mut calls: Vec<(alloc::string::String, Vec<Expr>)> = Vec::new();
14559 for it in &stmt.items {
14560 if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14561 collect_function_calls(expr, &mut calls);
14562 }
14563 }
14564 if let Some(w) = &stmt.where_ {
14565 collect_function_calls(w, &mut calls);
14566 }
14567 for o in &stmt.order_by {
14568 collect_function_calls(&o.expr, &mut calls);
14569 }
14570 // The columns a name in this statement could resolve to. Only
14571 // plain base tables; anything else and the types are not
14572 // statically knowable, so nothing is refused early.
14573 let cat = self.active_catalog();
14574 let mut cols: Vec<ColumnSchema> = Vec::new();
14575 if let Some(from) = &stmt.from {
14576 for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14577 if let Some(table) = cat.get(&t.name) {
14578 cols.extend(table.schema().columns.iter().cloned());
14579 }
14580 }
14581 }
14582 for (name, args) in calls {
14583 let Ok(i) = crate::eval::arity::REFUSED_ARITIES
14584 .binary_search_by(|(n, _)| (*n).cmp(name.as_str()))
14585 else {
14586 continue;
14587 };
14588 if !crate::eval::arity::REFUSED_ARITIES[i]
14589 .1
14590 .contains(&args.len())
14591 {
14592 continue;
14593 }
14594 // v7.39.2 — PostgreSQL names the SIGNATURE it could not
14595 // match, and before the scan there are no values to read a
14596 // type from. Where every argument's type is knowable
14597 // statically — a column of a source table, or a literal —
14598 // the sentence is PostgreSQL's exactly; where one is not,
14599 // this leaves the call to the row-time raise, which has the
14600 // values. Refusing early with a WORSE message would trade
14601 // one defect for another.
14602 let mut types: Vec<alloc::string::String> = Vec::new();
14603 for a in &args {
14604 let Some(t) = static_arg_type(a, &cols) else {
14605 types.clear();
14606 break;
14607 };
14608 types.push(t);
14609 }
14610 if types.len() != args.len() {
14611 continue;
14612 }
14613 return Err(EngineError::Eval(EvalError::WrongArity {
14614 name,
14615 types: types.join(", "),
14616 }));
14617 }
14618 Ok(())
14619 }
14620
14621 pub(crate) fn validate_clause_columns(
14622 &self,
14623 stmt: &SelectStatement,
14624 ) -> Result<(), EngineError> {
14625 let Some(from) = &stmt.from else {
14626 return Ok(());
14627 };
14628 if !stmt.ctes.is_empty() {
14629 return Ok(());
14630 }
14631 // v7.39.2 — every source, not just the first. A join is checkable
14632 // for the same reason one table is: with no CTE and no
14633 // subquery-shaped source, a bare name has to come from one of
14634 // them. Refusing the check for joins left `SELECT … FROM a JOIN b
14635 // … WHERE nosuch = 1` labelled `'field list'` where MySQL 9.7.2
14636 // says `'where clause'`.
14637 let plain = |t: &spg_sql::ast::TableRef| -> bool {
14638 t.unnest_expr.is_none()
14639 && t.generate_series_args.is_none()
14640 && t.lateral_subquery.is_none()
14641 && t.jsonb_each_text_arg.is_none()
14642 && t.table_fn_call.is_none()
14643 && t.rows_from.is_none()
14644 && t.json_table.is_none()
14645 && !t.scalar_fn_item
14646 };
14647 let cat = self.active_catalog();
14648 let mut sources: Vec<(String, &spg_storage::Table)> = Vec::new();
14649 for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14650 if !plain(t) {
14651 return Ok(());
14652 }
14653 let Some(table) = cat.get(&t.name) else {
14654 return Ok(());
14655 };
14656 sources.push((t.alias.clone().unwrap_or_else(|| t.name.clone()), table));
14657 }
14658 let known = |c: &spg_sql::ast::ColumnName| -> bool {
14659 // A system column is not in a table's list and is a perfectly
14660 // good predicate: `WHERE ctid = '(0,4)'::tid` and `WHERE
14661 // tableoid::regclass::text = 'pm_a'` are both real, and the
14662 // first draft of this check refused them. The e2e suite said
14663 // so immediately, which is what it is for.
14664 if is_system_column(&c.name) {
14665 return true;
14666 }
14667 if let Some(q) = &c.qualifier {
14668 // A qualifier must name one of this statement's sources,
14669 // and that source must carry the column. An alias
14670 // REPLACES the written name, which is PostgreSQL's rule
14671 // and MySQL's: `FROM pg_cast c WHERE pg_cast.oid <> 0`
14672 // is an error on both.
14673 return match sources.iter().find(|(a, _)| a == q) {
14674 Some((_, t)) => t
14675 .schema()
14676 .columns
14677 .iter()
14678 .any(|sc| self.col_name_eq(&sc.name, &c.name)),
14679 None => false,
14680 };
14681 }
14682 sources
14683 .iter()
14684 .any(|(_, t)| {
14685 t.schema()
14686 .columns
14687 .iter()
14688 .any(|sc| self.col_name_eq(&sc.name, &c.name))
14689 })
14690 // An output name the statement itself defines: ORDER BY,
14691 // GROUP BY and HAVING may all name one.
14692 || stmt.items.iter().any(|it| match it {
14693 SelectItem::Expr { expr, alias } => {
14694 alias.as_deref() == Some(c.name.as_str())
14695 || matches!(expr, Expr::Column(pc) if pc.name == c.name)
14696 }
14697 _ => false,
14698 })
14699 };
14700 // v7.39.2 — the CLAUSE travels with the reference, because MySQL
14701 // names it: `Unknown column 'x' in 'where clause'`, `'order
14702 // clause'`, `'group statement'`, `'having clause'`. Measured on
14703 // 9.7.2, and a driver's error handling reads the sentence as well
14704 // as the number. PostgreSQL says only `column "x" does not
14705 // exist`, with no clause, so its wording is unchanged.
14706 //
14707 // This walk is the only place the clause is still known: by the
14708 // time a row-time resolver meets the name, the expression has
14709 // been detached from the statement that held it.
14710 let mut refs: Vec<(spg_sql::ast::ColumnName, &'static str)> = Vec::new();
14711 let mut push = |e: &Expr, ctx: &'static str, out: &mut Vec<_>| {
14712 let mut here: Vec<spg_sql::ast::ColumnName> = Vec::new();
14713 collect_plain_column_refs(e, &mut here);
14714 out.extend(here.into_iter().map(|c| (c, ctx)));
14715 };
14716 if let Some(w) = &stmt.where_ {
14717 push(w, "where clause", &mut refs);
14718 }
14719 if let Some(g) = &stmt.group_by {
14720 for e in g {
14721 push(e, "group statement", &mut refs);
14722 }
14723 }
14724 if let Some(h) = &stmt.having {
14725 push(h, "having clause", &mut refs);
14726 }
14727 for o in &stmt.order_by {
14728 push(&o.expr, "order clause", &mut refs);
14729 }
14730 // v7.39.2 — and the join predicates, which MySQL calls the `on
14731 // clause`. Measured on 9.7.2: `Unknown column 'j1.nosuch' in 'on
14732 // clause'`, qualifier and all.
14733 for j in &from.joins {
14734 if let Some(on) = &j.on {
14735 push(on, "on clause", &mut refs);
14736 }
14737 }
14738 for (c, ctx) in &refs {
14739 if !known(c) {
14740 if self.speaks_mysql {
14741 // The QUALIFIER travels with it: MySQL 9.7.2 answers
14742 // `Unknown column 'j1.nosuch' in 'on clause'`, not the
14743 // bare name. Measured.
14744 let shown = match &c.qualifier {
14745 Some(q) => alloc::format!("{q}.{}", c.name),
14746 None => c.name.clone(),
14747 };
14748 return Err(EngineError::Eval(EvalError::TypeMismatch {
14749 detail: alloc::format!("Unknown column '{shown}' in '{ctx}'"),
14750 }));
14751 }
14752 // PostgreSQL 18.6 names the missing TABLE when the
14753 // qualifier is the part that resolves to nothing
14754 // (`missing FROM-clause entry for table "pg_cast"`) and
14755 // the COLUMN otherwise. Raising the column error for both
14756 // dropped the table name a caller matches on.
14757 if let Some(q) = &c.qualifier
14758 && !sources.iter().any(|(a, _)| a == q)
14759 {
14760 return Err(EngineError::Eval(EvalError::UnknownQualifier {
14761 qualifier: q.clone(),
14762 column: c.name.clone(),
14763 }));
14764 }
14765 // v7.39.2 — and a qualified reference whose qualifier
14766 // DOES resolve prints the whole thing, unquoted:
14767 // `column ea.no_such does not exist` (measured on PG
14768 // 18.6). The bare `column "no_such" does not exist` drops
14769 // the alias a caller matches on, which is what the
14770 // sqlx round-20 pin says.
14771 if let Some(q) = &c.qualifier {
14772 return Err(EngineError::Eval(EvalError::QualifiedColumnNotFound {
14773 qualifier: q.clone(),
14774 column: c.name.clone(),
14775 }));
14776 }
14777 return Err(EngineError::Eval(EvalError::ColumnNotFound {
14778 name: c.name.clone(),
14779 }));
14780 }
14781 }
14782 Ok(())
14783 }
14784}
14785
14786/// v7.39.2 — the column references of an expression, NOT descending into
14787/// a subquery.
14788///
14789/// A correlated subquery resolves its names against an outer scope this
14790/// walk cannot see, so descending would refuse valid queries. Missing a
14791/// typo inside one is the safe direction; refusing a good query is not.
14792/// v7.39.2 — the type PostgreSQL would name for an argument, when it
14793/// can be known without a row: a column of a source table, or a
14794/// literal. `None` for anything else, which is what keeps the pre-scan
14795/// refusal from printing a worse sentence than the row-time one.
14796pub(crate) fn static_arg_type(e: &Expr, cols: &[ColumnSchema]) -> Option<alloc::string::String> {
14797 use spg_sql::ast::Literal as L;
14798 match e {
14799 Expr::Column(c) => cols
14800 .iter()
14801 .find(|s| s.name.eq_ignore_ascii_case(&c.name))
14802 .map(|s| crate::conversions::pg_type_name_for_error(s.ty)),
14803 // A bare literal has no type yet on PostgreSQL — it names it
14804 // `unknown` in this very sentence — except where the lexeme
14805 // fixes one.
14806 Expr::Literal(L::String(_)) | Expr::Literal(L::Null) => {
14807 Some(alloc::string::String::from("unknown"))
14808 }
14809 Expr::Literal(L::Integer(_)) => Some(alloc::string::String::from("integer")),
14810 Expr::Literal(L::Bool(_)) => Some(alloc::string::String::from("boolean")),
14811 _ => None,
14812 }
14813}
14814
14815/// v7.39.2 — the function calls of an expression, name and argument
14816/// count, NOT descending into a subquery (its scope is its own).
14817fn collect_function_calls(e: &Expr, out: &mut Vec<(alloc::string::String, Vec<Expr>)>) {
14818 match e {
14819 Expr::FunctionCall { name, args } => {
14820 out.push((name.to_ascii_lowercase(), args.clone()));
14821 for a in args {
14822 collect_function_calls(a, out);
14823 }
14824 }
14825 Expr::Binary { lhs, rhs, .. } => {
14826 collect_function_calls(lhs, out);
14827 collect_function_calls(rhs, out);
14828 }
14829 Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14830 collect_function_calls(expr, out);
14831 }
14832 _ => {}
14833 }
14834}
14835
14836fn collect_plain_column_refs(e: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
14837 match e {
14838 Expr::Column(c) => out.push(c.clone()),
14839 Expr::Binary { lhs, rhs, .. } => {
14840 collect_plain_column_refs(lhs, out);
14841 collect_plain_column_refs(rhs, out);
14842 }
14843 Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14844 collect_plain_column_refs(expr, out);
14845 }
14846 Expr::FunctionCall { args, .. } => {
14847 for a in args {
14848 collect_plain_column_refs(a, out);
14849 }
14850 }
14851 _ => {}
14852 }
14853}
14854
14855fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
14856 let Some(lock) = &stmt.locking else {
14857 return Ok(());
14858 };
14859 let verb = lock_clause_verb(lock.strength);
14860 let refuse = |what: &str| {
14861 Err(EngineError::Unsupported(alloc::format!(
14862 "{verb} is not allowed with {what}"
14863 )))
14864 };
14865 if !stmt.unions.is_empty() {
14866 return refuse("UNION/INTERSECT/EXCEPT");
14867 }
14868 if stmt.distinct || !stmt.distinct_on.is_empty() {
14869 return refuse("DISTINCT clause");
14870 }
14871 if stmt.group_by.is_some() || stmt.group_by_all {
14872 return refuse("GROUP BY clause");
14873 }
14874 let has_agg = stmt.items.iter().any(|it| match it {
14875 spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
14876 _ => false,
14877 });
14878 if has_agg {
14879 return refuse("aggregate functions");
14880 }
14881 // `FOR UPDATE OF t` must name a relation that is actually in FROM.
14882 for want in &lock.of_tables {
14883 if !locking_from_names(stmt)
14884 .iter()
14885 .any(|n| n.eq_ignore_ascii_case(want))
14886 {
14887 return Err(EngineError::Unsupported(alloc::format!(
14888 "relation \"{want}\" in {verb} clause not found in FROM clause"
14889 )));
14890 }
14891 }
14892 Ok(())
14893}
14894
14895/// How PG names the clause in its diagnostics.
14896const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
14897 use spg_sql::ast::LockStrength as LS;
14898 match s {
14899 LS::Update => "FOR UPDATE",
14900 LS::NoKeyUpdate => "FOR NO KEY UPDATE",
14901 LS::Share => "FOR SHARE",
14902 LS::KeyShare => "FOR KEY SHARE",
14903 }
14904}
14905
14906/// Every relation name (or alias) the FROM clause exposes.
14907fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
14908 let mut out = alloc::vec::Vec::new();
14909 if let Some(f) = &stmt.from {
14910 let mut push = |t: &spg_sql::ast::TableRef| {
14911 if let Some(a) = &t.alias {
14912 out.push(a.clone());
14913 }
14914 out.push(t.name.clone());
14915 };
14916 push(&f.primary);
14917 for j in &f.joins {
14918 push(&j.table);
14919 }
14920 }
14921 out
14922}
14923
14924fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
14925 use spg_sql::ast::Expr;
14926 if let Some(w) = &stmt.where_
14927 && aggregate::contains_aggregate(w)
14928 {
14929 return Err(EngineError::Unsupported(
14930 "aggregate functions are not allowed in WHERE".into(),
14931 ));
14932 }
14933 let mut nested = false;
14934 let mut check = |e: &Expr| {
14935 let mut probe = e.clone();
14936 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14937 let args = match n {
14938 Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
14939 _ => return false,
14940 };
14941 if args.iter().any(aggregate::contains_aggregate) {
14942 nested = true;
14943 }
14944 false
14945 });
14946 };
14947 for it in &stmt.items {
14948 if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14949 check(expr);
14950 }
14951 }
14952 if let Some(h) = &stmt.having {
14953 check(h);
14954 }
14955 for o in &stmt.order_by {
14956 check(&o.expr);
14957 }
14958 if nested {
14959 return Err(EngineError::Unsupported(
14960 "aggregate function calls cannot be nested".into(),
14961 ));
14962 }
14963 Ok(())
14964}
14965
14966/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
14967/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
14968/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
14969/// to a set and then applies the enclosing expression once per element. SPG only
14970/// ever recognised an SRF that WAS the item, so everything above died on
14971/// "unknown function unnest" — the set-returning call, wrapped in anything at
14972/// all, fell through to the scalar function dispatcher which has no such name.
14973///
14974/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
14975/// rewritten to read that column, and the rewritten expression is evaluated once
14976/// per output row against the input row extended with the lifted values. The
14977/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
14978/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
14979/// executors (the single-table scan, the synthetic-table pipeline, and the
14980/// unnest FROM path) each evaluated the key as an ordinary expression, where the
14981/// literal `n` is just the constant n — the same sort key for every row. The
14982/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
14983/// back in input order, not in a wrong order. Statement prep resolves the common
14984/// case, but only when the SELECT item is an expression — a `*` is not one, and
14985/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
14986/// spelling landed on exactly the shape prep could not resolve.
14987///
14988/// A set-returning item is left alone: copying it into ORDER BY would make the
14989/// key "the whole set", evaluated once per INPUT row.
14990fn resolve_positional_order_by(
14991 order_by: &[spg_sql::ast::OrderBy],
14992 projection: &[ProjectedItem],
14993) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
14994 order_by
14995 .iter()
14996 .filter_map(|o| {
14997 let mut o = o.clone();
14998 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
14999 && *n >= 1
15000 && let Ok(idx) = usize::try_from(*n - 1)
15001 && let Some(item) = projection.get(idx)
15002 && !expr_contains_builtin_srf(&item.expr)
15003 {
15004 // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
15005 // item is itself an integer LITERAL must not be
15006 // substituted textually: the literal would read as an
15007 // ordinal again downstream, and `SELECT 10 … ORDER BY
15008 // 1` died with "position 10 is not in select list"
15009 // where PG happily returns the rows. Ordering by a
15010 // constant orders nothing, so the key drops.
15011 if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
15012 return None;
15013 }
15014 o.expr = item.expr.clone();
15015 }
15016 Some(o)
15017 })
15018 .collect()
15019}
15020
15021/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
15022/// this expression? Statement preparation (`resolve_order_by_position`) runs
15023/// before any catalog is in hand, and it only needs to know "is this item's value
15024/// a set", which the builtin SRFs answer syntactically.
15025pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
15026 let mut found = false;
15027 let mut probe = e.clone();
15028 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
15029 if is_top_level_unnest(n) {
15030 found = true;
15031 return true;
15032 }
15033 false
15034 });
15035 found
15036}
15037
15038/// v7.39 (round 599) — everything about a target-list SRF that does not
15039/// depend on the row.
15040///
15041/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
15042/// each SRF-bearing projection expression, walked and rewrote the tree,
15043/// formatted a `__srf_N` name per node, and copied the whole column schema.
15044/// A counting allocator put the path at 24 allocations per input row for a
15045/// single-element `unnest`, against 0 for the same scan without one — 211 MB
15046/// where the plain scan took 4.3 — and the shape held whatever the array
15047/// contained, which is what invariant work looks like.
15048struct SrfPlan {
15049 /// The lifted SRF calls, in slot order.
15050 nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
15051 /// Per projection position, the expression with its SRF calls replaced
15052 /// by `__srf_N` column references. `None` means the item has none.
15053 rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
15054 /// The input schema followed by one column per slot. Only the slots'
15055 /// TYPES vary per row, and they are patched in place.
15056 ext_cols: alloc::vec::Vec<ColumnSchema>,
15057 /// v7.39 (round 743) — the rewritten projection COMPILED against the
15058 /// extended schema, once per plan. The per-output-row evaluation ran
15059 /// the interpreter (~560 ns/row on the unnest panel cell); the Step
15060 /// VM reads the `__srf_N` slots as plain columns. `None` = that item
15061 /// is not fully compilable and keeps the interpreter.
15062 compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
15063 base_cols: usize,
15064}
15065
15066fn build_srf_plan(
15067 engine: &Engine,
15068 projection: &[ProjectedItem],
15069 srf_idxs: &[usize],
15070 ctx: &EvalContext<'_>,
15071) -> Result<SrfPlan, EngineError> {
15072 // Lift every SRF node out of every item that contains one.
15073 let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
15074 let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
15075 let mut reject: Option<EngineError> = None;
15076 for &i in srf_idxs {
15077 let mut e = projection[i].expr.clone();
15078 crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
15079 if reject.is_some() {
15080 return true;
15081 }
15082 // PG refuses a set-returning function inside a conditional: the set
15083 // would have to be produced before anyone knows whether the branch
15084 // is even taken.
15085 let conditional = match n {
15086 spg_sql::ast::Expr::Case { .. } => Some("CASE"),
15087 spg_sql::ast::Expr::FunctionCall { name, .. }
15088 if name.eq_ignore_ascii_case("coalesce") =>
15089 {
15090 Some("COALESCE")
15091 }
15092 _ => None,
15093 };
15094 if let Some(kind) = conditional
15095 && engine.expr_contains_srf(n)
15096 {
15097 reject = Some(EngineError::Unsupported(alloc::format!(
15098 "set-returning functions are not allowed in {kind}"
15099 )));
15100 return true;
15101 }
15102 if !engine.is_srf_node(n) {
15103 return false;
15104 }
15105 let slot = nodes.len();
15106 nodes.push(n.clone());
15107 *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
15108 qualifier: None,
15109 name: alloc::format!("__srf_{slot}"),
15110 });
15111 true
15112 });
15113 rewritten[i] = Some(e);
15114 }
15115 if let Some(err) = reject {
15116 return Err(err);
15117 }
15118 let base_cols = ctx.columns.len();
15119 let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
15120 for slot in 0..nodes.len() {
15121 ext_cols.push(ColumnSchema::new(
15122 alloc::format!("__srf_{slot}"),
15123 DataType::Text,
15124 true,
15125 ));
15126 }
15127 // v7.39 (round 743) — compile the rewritten items against the
15128 // EXTENDED schema. The slot columns' declared type is a per-row
15129 // patched detail the compiled column read does not consult.
15130 let compiled: Vec<Option<eval::CompiledExpr>> = {
15131 let mut ext_ctx = ctx.clone();
15132 ext_ctx.columns = &ext_cols;
15133 projection
15134 .iter()
15135 .enumerate()
15136 .map(|(i, p)| {
15137 let e = rewritten[i].as_ref().unwrap_or(&p.expr);
15138 if eval::fully_compilable(e) {
15139 Some(eval::compile_expr(e, &ext_ctx))
15140 } else {
15141 None
15142 }
15143 })
15144 .collect()
15145 };
15146 Ok(SrfPlan {
15147 nodes,
15148 rewritten,
15149 ext_cols,
15150 compiled,
15151 base_cols,
15152 })
15153}
15154
15155/// One input row expanded through a plan built once for the whole scan.
15156/// v7.39 (round 621) — expand a projection whose target list contains
15157/// set-returning items, remembering which INPUT row each output row came from.
15158///
15159/// The three materialised-source tails — `FROM unnest(…)`, `FROM
15160/// generate_series(…)`, and the one that serves VALUES / a derived table /
15161/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
15162/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
15163/// v(x)` answered `function unnest(integer[]) does not exist` on all the
15164/// others, for a query PG answers. Sharing the expansion is the point: a
15165/// fourth copy would have been the fourth place to forget.
15166fn expand_projection_srfs(
15167 engine: &Engine,
15168 projection: &[ProjectedItem],
15169 srf_idxs: &[usize],
15170 filtered: &[Row<'static>],
15171 ctx: &EvalContext<'_>,
15172) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
15173 let mut out = alloc::vec::Vec::with_capacity(filtered.len());
15174 let mut src = alloc::vec::Vec::with_capacity(filtered.len());
15175 // v7.39 (round 726) — ONE plan for the whole scan. The per-row
15176 // spelling rebuilt it for every input row: a full clone of the
15177 // rewritten projection trees and the extended schema, 50k times on
15178 // the panel's unnest cell.
15179 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15180 // v7.39 (round 733) — shard the expansion. Each shard clones the
15181 // plan (its ext_cols slot types are per-row mutable) and builds a
15182 // MINIMAL context — EvalContext is not Sync — which is sound only
15183 // when every expression involved is pure: the whole projection and
15184 // every SRF argument must be fully_compilable, or the row loop
15185 // stays serial with the full session context.
15186 // The projection is judged in its REWRITTEN form — the SRF call
15187 // itself is never compilable, but after the lift it is a plain
15188 // `__srf_N` column reference.
15189 let all_pure = projection
15190 .iter()
15191 .enumerate()
15192 .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
15193 && plan.nodes.iter().all(|n| match n {
15194 Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
15195 other => eval::fully_compilable(other),
15196 });
15197 if all_pure
15198 && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
15199 && let Some(r) = engine.parallel_runner.0.as_deref()
15200 {
15201 let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
15202 let chunk = filtered.len().div_ceil(n_shards);
15203 type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
15204 let schema_cols = ctx.columns;
15205 let alias = ctx.table_alias;
15206 let mysql = ctx.mysql_dialect;
15207 let style = ctx.render_style;
15208 let plan_ref = &plan;
15209 let results = r.run_shards(n_shards, &|si| {
15210 let lo = si * chunk;
15211 let hi = ((si + 1) * chunk).min(filtered.len());
15212 let mut sctx = eval::EvalContext::new(schema_cols, alias);
15213 sctx.mysql_dialect = mysql;
15214 sctx.render_style = style;
15215 // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
15216 // compiled programs); each shard rebuilds it, which also
15217 // recompiles against the shard's own context. Build errors
15218 // were already surfaced by the outer build above.
15219 let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
15220 Ok(p) => p,
15221 Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
15222 };
15223 let mut run = || -> ShardOut {
15224 let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
15225 let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
15226 for (i, row) in filtered[lo..hi].iter().enumerate() {
15227 let expanded =
15228 expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
15229 sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
15230 o.extend(expanded);
15231 }
15232 Ok((o, sidx))
15233 };
15234 alloc::boxed::Box::new(run())
15235 });
15236 for boxed in results {
15237 let shard = boxed
15238 .downcast::<ShardOut>()
15239 .expect("runner echoes the closure's box");
15240 let (o, sidx) = (*shard)?;
15241 out.extend(o);
15242 src.extend(sidx);
15243 }
15244 return Ok((out, src));
15245 }
15246 for (i, row) in filtered.iter().enumerate() {
15247 let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
15248 src.extend(core::iter::repeat_n(i, expanded.len()));
15249 out.extend(expanded);
15250 }
15251 Ok((out, src))
15252}
15253
15254/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
15255///
15256/// A key that names a select-list item reads it out of the EXPANDED row,
15257/// because PG sorts after the expansion. A key that names a source column the
15258/// query does not project is evaluated against the input row that output row
15259/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
15260fn srf_order_key(
15261 ob: &spg_sql::ast::OrderBy,
15262 out_col: Option<usize>,
15263 out: &Row<'static>,
15264 src: &Row<'static>,
15265 ctx: &EvalContext<'_>,
15266) -> Result<Value<'static>, EngineError> {
15267 match out_col {
15268 Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
15269 None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
15270 }
15271}
15272
15273fn expand_srf_row_with(
15274 engine: &Engine,
15275 plan: &mut SrfPlan,
15276 projection: &[ProjectedItem],
15277 row: &Row<'static>,
15278 ctx: &EvalContext<'_>,
15279) -> Result<Vec<Row<'static>>, EngineError> {
15280 let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
15281 for n in &plan.nodes {
15282 lists.push(engine.srf_values(n, row, ctx)?);
15283 }
15284 let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
15285 // Only the slots' element types depend on the row; the names and the
15286 // input schema around them do not.
15287 for (slot, list) in lists.iter().enumerate() {
15288 plan.ext_cols[plan.base_cols + slot].ty = list
15289 .iter()
15290 .find_map(|v| v.data_type())
15291 .unwrap_or(DataType::Text);
15292 }
15293 let mut ext_ctx = ctx.clone();
15294 ext_ctx.columns = &plan.ext_cols;
15295 let mut out = Vec::with_capacity(n_rows);
15296 // v7.39 (round 726) — the base columns are the SAME for every
15297 // expanded row; clone them once and rewrite only the SRF slots per
15298 // k. The old form cloned the whole input row per OUTPUT row — for
15299 // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
15300 // TEXT column the projection never reads.
15301 let base_len = row.values.len();
15302 let mut ext_vals = row.values.clone();
15303 ext_vals.resize(base_len + lists.len(), Value::Null);
15304 let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15305 for k in 0..n_rows {
15306 for (slot, list) in lists.iter().enumerate() {
15307 // Past the end of THIS srf's rows → NULL (PG pads).
15308 ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
15309 }
15310 let ext_row = Row::new(core::mem::take(&mut ext_vals));
15311 let mut vals = Vec::with_capacity(projection.len());
15312 for (i, p) in projection.iter().enumerate() {
15313 // v7.39 (round 743) — compiled when possible; the
15314 // interpreter for the rest, with its exact wording.
15315 vals.push(match &plan.compiled[i] {
15316 Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
15317 .map_err(EngineError::Eval)?,
15318 None => {
15319 let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
15320 eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
15321 }
15322 });
15323 }
15324 ext_vals = ext_row.values;
15325 out.push(Row::new(vals));
15326 }
15327 Ok(out)
15328}
15329
15330/// The one-shot spelling, for the callers that expand a single row.
15331/// v7.39 (round 600) — which output column each ORDER BY key names, for a
15332/// query whose target list contains a set-returning function.
15333///
15334/// The keys used to be built from the INPUT row, before the SRF expanded, so
15335/// anything that named the SRF's own output was evaluated as a scalar call:
15336/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
15337/// "function unnest(integer[]) does not exist", and so did the spellings that
15338/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
15339/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
15340/// back in input order. PG sorts AFTER the expansion, so a key that names a
15341/// select-list item reads that item's value out of the expanded row.
15342///
15343/// `None` keeps the key on the input row, which is where an ORDER BY naming
15344/// a column the query does not project has to be evaluated.
15345/// v7.38.19 — the output column an ORDER BY term reads, when reading it
15346/// is provably the same as building a key from the input row.
15347///
15348/// A sort key is a COPY of the sort column, made because the source row
15349/// is gone by the time the sort runs — only the projection survives. On
15350/// `SELECT s_long FROM t ORDER BY s_long` that copy is of data the
15351/// projected row already holds, and on 400,000 rows of 192-character
15352/// text it is 400,000 allocations, 400,000 frees and 77 MB of copying.
15353/// A profile of that cell put the allocator at 2,025 leaf samples of the
15354/// working set, second only to the comparison chain.
15355///
15356/// The condition is narrow on purpose. `srf_order_output_cols` resolves
15357/// an ORDER BY term the way SQL does — a positional ordinal, or a name
15358/// matching the select list — and SQL resolves against the select list
15359/// BEFORE the input columns. The key path resolves against the INPUT
15360/// columns. For `SELECT g AS id … ORDER BY id` on a table that also has
15361/// an `id`, those are different columns, and swapping one for the other
15362/// would change answers rather than timings.
15363///
15364/// So this takes only the case where the two cannot disagree: a bare
15365/// unqualified column name, matching exactly one output item, whose own
15366/// expression is that same column. The projected cell then IS the input
15367/// cell, and the key would have been its copy.
15368/// True when comparing two of this column's VALUES gives the same order
15369/// as comparing the sort KEYS built from them.
15370///
15371/// It does not hold widely. A user ENUM stores its label as text but
15372/// orders by DECLARATION position; an array orders element-wise; a
15373/// domain or composite carries its own rules. For those the two paths
15374/// answer differently, and a sort that skipped the key would silently
15375/// reorder the result. This is the short list where they agree.
15376fn value_order_is_key_order(col: &ColumnSchema) -> bool {
15377 use spg_storage::DataType as T;
15378 col.user_enum_type.is_none()
15379 && col.user_domain_type.is_none()
15380 && col.user_composite_type.is_none()
15381 && col.collation_name.is_none()
15382 && col.collation == spg_storage::Collation::Binary
15383 && matches!(
15384 col.ty,
15385 T::SmallInt | T::Int | T::BigInt | T::Text | T::Varchar(_) | T::Bool | T::Uuid
15386 )
15387}
15388
15389/// The full ORDER BY comparison between two rows, named by index.
15390///
15391/// v7.38.19 — what a permutation sort falls back to when its key ties.
15392fn row_cmp_by_index(
15393 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15394 terms: &[(usize, bool, Option<bool>)],
15395 colls: &[Option<crate::collate::Collated>],
15396 mysql: bool,
15397 ia: u32,
15398 ib: u32,
15399) -> core::cmp::Ordering {
15400 let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
15401 for (i, (col, desc, nf)) in terms.iter().enumerate() {
15402 let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col)) else {
15403 continue;
15404 };
15405 let ord = match (va, vb) {
15406 (Value::Text(x), Value::Text(y)) => match colls.get(i).and_then(Option::as_ref) {
15407 Some(c) => {
15408 let o = c.compare(x, y);
15409 if *desc { o.reverse() } else { o }
15410 }
15411 None if !mysql => {
15412 let o = crate::orderby::str_cmp_prefix_first(x, y);
15413 if *desc { o.reverse() } else { o }
15414 }
15415 None => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15416 },
15417 _ => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15418 };
15419 if ord != core::cmp::Ordering::Equal {
15420 return ord;
15421 }
15422 }
15423 core::cmp::Ordering::Equal
15424}
15425
15426/// Whether ordering these rows by BYTES is what the collation in force
15427/// would have answered anyway.
15428///
15429/// v7.38.19 — a collated sort used to be shut out of the keyed path
15430/// entirely, and the cost of that showed up the moment the byte path
15431/// got fast: on the same fixture, the same binary took 92 ms under `C`
15432/// and 371 ms under `en_US`, so declaring a collation had become a
15433/// four-fold tax on a query that sorts md5 hex.
15434///
15435/// It need not be. For several locales `[0-9a-z]` orders exactly as
15436/// bytes do -- `collate::ascii_byte_order` carries that fact, and the
15437/// test beside it re-derives the whole allowlist by sorting a corpus
15438/// twice rather than asserting it. So when the collation is one of
15439/// those AND every value in every sort column is drawn from that
15440/// alphabet, the byte answer IS the collated answer.
15441///
15442/// Both halves are required. A collation outside the list can put `z`
15443/// between `s` and `t`; a value outside the alphabet can be `Ápple`,
15444/// which no locale in the list orders by its bytes. Either one and this
15445/// returns false, and the sort takes the collator's own path.
15446fn byte_order_answers_the_collation(
15447 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15448 terms: &[(usize, bool, Option<bool>)],
15449 colls: &[Option<crate::collate::Collated>],
15450) -> bool {
15451 if colls.iter().all(Option::is_none) {
15452 return true;
15453 }
15454 if !colls
15455 .iter()
15456 .flatten()
15457 .all(crate::collate::Collated::ascii_byte_order)
15458 {
15459 return false;
15460 }
15461 tagged.iter().all(|(_, row)| {
15462 terms.iter().all(|(col, _, _)| match row.values.get(*col) {
15463 // Only TEXT is collation-sensitive; a number or a NULL
15464 // orders the same under every collation there is.
15465 Some(Value::Text(t)) => crate::collate::is_ascii_alnum_lower(t),
15466 _ => true,
15467 })
15468 })
15469}
15470
15471/// An eight-byte key for each row's sort column, paired with the row's
15472/// index — or `None` when the column cannot give one on every row.
15473///
15474/// v7.38.19 — the pair is what the sort array holds instead of the row.
15475/// Two kinds of column can supply it:
15476///
15477/// * an INTEGER, whose whole value fits. Flipping the sign bit maps
15478/// the signed order onto the unsigned one, so the key is EXACT and
15479/// a comparison never has to look at the row at all.
15480/// * TEXT, as the first eight bytes big-endian, zero-padded. That
15481/// orders the same as the string — two that differ inside those
15482/// bytes differ at the same index either way, and one shorter than
15483/// eight pads with zeros exactly where `[u8]`'s own comparison runs
15484/// out — but it is a PREFIX, so equal keys must still ask the full
15485/// comparator.
15486///
15487/// The `None` is the safety of it: a NULL or any other type has no
15488/// faithful eight-byte key, so such a column takes the ordinary path
15489/// rather than being given a made-up one.
15490/// The prefix keys for a sort, at the width the DATA asks for.
15491///
15492/// v7.40.1 — the width used to be eight bytes for every text column, and
15493/// the panel's two text cells priced both halves of that choice against
15494/// PostgreSQL 18.6, in memory on both legs, 400,000 rows:
15495///
15496/// ```text
15497/// short text (9 bytes, shared prefix) SPG 96.6 PG 71.0 1.36x behind
15498/// long text (192 bytes, byte 0 decides) SPG 70.9 PG 72.4 parity
15499/// ```
15500///
15501/// `'k' || lpad(n, 8, '0')` is nine bytes, so an eight-byte prefix drops
15502/// the last digit: ten rows share every key, forty thousand tie-runs
15503/// each fall back to the full comparator, and each of those reads at
15504/// random into a 400,000-element array. The md5 column decides on byte
15505/// zero and never ties, which is why only one of the two cells lost.
15506///
15507/// Widened to sixteen bytes and measured -- same window, two binaries
15508/// named by md5, order digests identical:
15509///
15510/// ```text
15511/// short text 104.9 -> 56.9 ms 1.84x faster (and 0.80x of PG)
15512/// long text 74.9 -> 90.7 ms 1.21x SLOWER
15513/// ```
15514///
15515/// So a fixed width is the wrong shape either way: `(u128, u32)` is 32
15516/// bytes against `(u64, u32)`'s 16, and a column that already decided on
15517/// byte zero pays double the sort's memory traffic for eight bytes it
15518/// never reads. That is the tax a shared hot path levies on the workload
15519/// it does not help.
15520///
15521/// The width comes from the longest value instead, which is exact and
15522/// free -- it is one pass the loop below already makes. Every value at
15523/// sixteen bytes or under makes the wide key the WHOLE key, so `exact`
15524/// is true and the tie fallback with its random reads disappears
15525/// altogether; anything longer keeps the narrow key and pays nothing.
15526enum PrefixKeys {
15527 Narrow(Vec<(u64, u32)>, bool),
15528 Wide(Vec<(u128, u32)>, bool),
15529}
15530
15531fn sort_keys_of(
15532 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15533 col: usize,
15534) -> Option<PrefixKeys> {
15535 let n = u32::try_from(tagged.len()).ok()?;
15536 let is_text = match tagged.first()?.1.values.get(col)? {
15537 Value::Text(_) => true,
15538 Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => false,
15539 _ => return None,
15540 };
15541 if !is_text {
15542 let mut out: Vec<(u64, u32)> = Vec::with_capacity(tagged.len());
15543 for (i, row) in (0..n).zip(tagged.iter()) {
15544 let key = match row.1.values.get(col) {
15545 Some(Value::SmallInt(v)) => (i64::from(*v) as u64) ^ (1 << 63),
15546 Some(Value::Int(v)) => (i64::from(*v) as u64) ^ (1 << 63),
15547 Some(Value::BigInt(v)) => (*v as u64) ^ (1 << 63),
15548 _ => return None,
15549 };
15550 out.push((key, i));
15551 }
15552 return Some(PrefixKeys::Narrow(out, true));
15553 }
15554 // One pass, and it answers both questions: the bytes of every key,
15555 // and whether the longest of them fits the wide one.
15556 let mut wide: Vec<(u128, u32)> = Vec::with_capacity(tagged.len());
15557 let mut longest = 0usize;
15558 for (i, row) in (0..n).zip(tagged.iter()) {
15559 let Some(Value::Text(t)) = row.1.values.get(col) else {
15560 return None;
15561 };
15562 let bytes = t.as_bytes();
15563 longest = longest.max(bytes.len());
15564 let mut k = [0u8; 16];
15565 let take = bytes.len().min(16);
15566 k[..take].copy_from_slice(&bytes[..take]);
15567 wide.push((u128::from_be_bytes(k), i));
15568 }
15569 if longest <= 16 {
15570 return Some(PrefixKeys::Wide(wide, true));
15571 }
15572 // Longer than the wide key: the narrow one costs half the memory
15573 // traffic and decides exactly as much, since neither is the whole
15574 // value. Built from the wide keys rather than reading the rows again.
15575 let narrow = wide
15576 .into_iter()
15577 .map(|(k, i)| ((k >> 64) as u64, i))
15578 .collect();
15579 Some(PrefixKeys::Narrow(narrow, false))
15580}
15581
15582/// Sort a prefix-key permutation, whatever the key's width.
15583///
15584/// v7.40.1 -- extracted so the two widths share one body. `low_card`
15585/// keeps the run-at-a-time shortcut and `exact` keeps the "a tie means
15586/// the values are equal" one; both are the caller's to decide.
15587struct PrefixSort {
15588 /// The first ORDER BY term is descending.
15589 first_desc: bool,
15590 /// The key does not discriminate, so sort it and settle each run of
15591 /// equal keys in one pass instead of n log n comparisons.
15592 low_card: bool,
15593 /// The key IS the value, so a tie means the values are equal.
15594 exact: bool,
15595 /// One ORDER BY term, so nothing else can speak after a tie.
15596 single_term: bool,
15597 /// v7.40.4 — what the two parallelism GUCs say. See `crate::parsort`.
15598 workers: crate::parsort::Workers,
15599}
15600
15601fn sort_prefix_permutation<K: Copy + Ord + Send + Sync>(
15602 mut order: Vec<(K, u32)>,
15603 how: &PrefixSort,
15604 row_cmp: &(dyn Fn(u32, u32) -> core::cmp::Ordering + Sync),
15605 same_value: &dyn Fn(u32, u32) -> bool,
15606) -> Vec<u32> {
15607 let PrefixSort {
15608 first_desc,
15609 low_card,
15610 exact,
15611 single_term,
15612 workers,
15613 } = *how;
15614 if low_card {
15615 // Integer sort first, then one pass per run.
15616 order = crate::parsort::sort_total(
15617 order,
15618 workers,
15619 &|&(pa, ia): &(K, u32), &(pb, ib): &(K, u32)| {
15620 let c = pa.cmp(&pb);
15621 let c = if first_desc { c.reverse() } else { c };
15622 c.then_with(|| ia.cmp(&ib))
15623 },
15624 );
15625 let mut lo = 0;
15626 while lo < order.len() {
15627 let mut hi = lo + 1;
15628 while hi < order.len() && order[hi].0 == order[lo].0 {
15629 hi += 1;
15630 }
15631 if hi - lo > 1 {
15632 let head = order[lo].1;
15633 let uniform = order[lo + 1..hi].iter().all(|&(_, i)| same_value(head, i));
15634 if !uniform {
15635 order[lo..hi]
15636 .sort_by(|&(_, ia), &(_, ib)| row_cmp(ia, ib).then_with(|| ia.cmp(&ib)));
15637 }
15638 // A uniform run is already in index order, which IS the
15639 // stable answer.
15640 }
15641 lo = hi;
15642 }
15643 } else {
15644 order = crate::parsort::sort_total(
15645 order,
15646 workers,
15647 &|&(pa, ia): &(K, u32), &(pb, ib): &(K, u32)| {
15648 let c = pa.cmp(&pb);
15649 let c = if first_desc { c.reverse() } else { c };
15650 if c != core::cmp::Ordering::Equal {
15651 return c;
15652 }
15653 // An EXACT key that ties means the values are equal, so only
15654 // the remaining terms can speak. A prefix that ties has
15655 // decided nothing yet and the first term must be asked again,
15656 // which `row_cmp` does by walking every term from the start.
15657 if exact && single_term {
15658 return ia.cmp(&ib);
15659 }
15660 row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
15661 },
15662 );
15663 }
15664 order.into_iter().map(|(_, i)| i).collect()
15665}
15666
15667/// Whether a PREFIX key is worth sorting a permutation on.
15668///
15669/// v7.38.19 — it is not always, and the panel says so in one cell. The
15670/// `text (26 values)` fixture is two hundred identical characters drawn
15671/// from twenty-six letters, so every eight-byte prefix inside a letter
15672/// is the same and 15,000 rows tie on it. Each tie then pays the prefix
15673/// compare, a two-hundred-byte comparison, AND a random read into a
15674/// 400,000-element array — while sorting the rows in place keeps the
15675/// partition contiguous. Measured: 160 ms sorting rows, 247 ms sorting
15676/// the permutation, on the very fixture built to be degenerate.
15677///
15678/// So the permutation is taken when the key DECIDES, and a sample says
15679/// whether it does. An exact key always decides; a prefix has to earn
15680/// it.
15681fn key_discriminates<K: Copy + Ord>(keys: &[(K, u32)]) -> bool {
15682 const SAMPLE: usize = 1024;
15683 let step = (keys.len() / SAMPLE).max(1);
15684 let mut seen: Vec<K> = keys
15685 .iter()
15686 .step_by(step)
15687 .take(SAMPLE)
15688 .map(|&(k, _)| k)
15689 .collect();
15690 let taken = seen.len();
15691 if taken < 8 {
15692 return true;
15693 }
15694 seen.sort_unstable();
15695 seen.dedup();
15696 seen.len() * 2 >= taken
15697}
15698
15699fn order_by_output_cols_if_identical(
15700 order_by: &[spg_sql::ast::OrderBy],
15701 projection: &[ProjectedItem],
15702 schema_cols: &[ColumnSchema],
15703) -> Option<Vec<usize>> {
15704 if order_by.is_empty() {
15705 return None;
15706 }
15707 let mut out = Vec::with_capacity(order_by.len());
15708 for ob in order_by {
15709 let Expr::Column(c) = &ob.expr else {
15710 return None;
15711 };
15712 if c.qualifier.is_some() {
15713 return None;
15714 }
15715 let mut hit = None;
15716 for (i, p) in projection.iter().enumerate() {
15717 if !p.output_name.eq_ignore_ascii_case(&c.name) {
15718 continue;
15719 }
15720 if hit.is_some() {
15721 return None; // ambiguous — SQL would reject it too
15722 }
15723 // The item must BE that column, not merely be named for it.
15724 let Expr::Column(pc) = &p.expr else {
15725 return None;
15726 };
15727 if !pc.name.eq_ignore_ascii_case(&c.name) {
15728 return None;
15729 }
15730 let sc = schema_cols
15731 .iter()
15732 .find(|s| s.name.eq_ignore_ascii_case(&c.name))?;
15733 if !value_order_is_key_order(sc) {
15734 return None;
15735 }
15736 hit = Some(i);
15737 }
15738 out.push(hit?);
15739 }
15740 Some(out)
15741}
15742
15743fn srf_order_output_cols(
15744 order_by: &[spg_sql::ast::OrderBy],
15745 projection: &[ProjectedItem],
15746) -> Vec<Option<usize>> {
15747 order_by
15748 .iter()
15749 .map(|ob| {
15750 // A positive ordinal is the Nth output column, directly.
15751 // `resolve_positional_order_by` deliberately leaves an ordinal
15752 // pointing at a set-returning item alone — copying the call into
15753 // ORDER BY would have made the key "the whole set" back when keys
15754 // came from the input row. Reading the expanded row's column is
15755 // what it should have meant, and is what this does.
15756 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
15757 && *n >= 1
15758 && let Ok(idx) = usize::try_from(*n - 1)
15759 && idx < projection.len()
15760 {
15761 return Some(idx);
15762 }
15763 // An unqualified name matching exactly one output name. SQL
15764 // resolves ORDER BY against the select list first, so this wins
15765 // over an input column of the same name — which is the whole
15766 // point of `SELECT g AS id … ORDER BY id`.
15767 if let Expr::Column(c) = &ob.expr
15768 && c.qualifier.is_none()
15769 {
15770 let mut hit = None;
15771 for (i, p) in projection.iter().enumerate() {
15772 if p.output_name.eq_ignore_ascii_case(&c.name) {
15773 if hit.is_some() {
15774 hit = None;
15775 break;
15776 }
15777 hit = Some(i);
15778 }
15779 }
15780 if hit.is_some() {
15781 return hit;
15782 }
15783 }
15784 // Or the same expression as a select-list item — which is what
15785 // `ORDER BY 1` becomes once `resolve_positional_order_by` has
15786 // run, and what a repeated `ORDER BY unnest(…)` is.
15787 projection.iter().position(|p| p.expr == ob.expr)
15788 })
15789 .collect()
15790}
15791
15792fn expand_srf_row(
15793 engine: &Engine,
15794 projection: &[ProjectedItem],
15795 srf_idxs: &[usize],
15796 row: &Row<'static>,
15797 ctx: &EvalContext<'_>,
15798) -> Result<Vec<Row<'static>>, EngineError> {
15799 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15800 expand_srf_row_with(engine, &mut plan, projection, row, ctx)
15801}
15802
15803impl Engine {
15804 /// The rows one target-list SRF yields for an input row. `None` from
15805 /// `srf_target_idxs` means the expression is not set-returning at all.
15806 fn srf_values(
15807 &self,
15808 expr: &spg_sql::ast::Expr,
15809 row: &Row<'static>,
15810 ctx: &EvalContext<'_>,
15811 ) -> Result<Vec<Value<'static>>, EngineError> {
15812 if top_level_srf_kind(expr).is_some() {
15813 return top_level_srf_output(expr, row, ctx);
15814 }
15815 // A user set-returning function. Its body runs through the real
15816 // executor, like every function body since round 63.
15817 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
15818 return Err(EngineError::Unsupported(
15819 "expected a SELECT-list SRF call".into(),
15820 ));
15821 };
15822 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15823 for a in args {
15824 vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
15825 }
15826 let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
15827 // v7.39 (read01 round 68) — in a target list a multi-column function is
15828 // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
15829 // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
15830 // what it is for. A single-column function contributes its bare value.
15831 Ok(rows
15832 .into_iter()
15833 .map(|r| {
15834 if r.values.len() == 1 {
15835 r.values.into_iter().next().unwrap_or(Value::Null)
15836 } else {
15837 Value::Composite(
15838 cols.iter()
15839 .map(|c| c.name.clone())
15840 .zip(r.values)
15841 .collect::<alloc::vec::Vec<_>>(),
15842 )
15843 }
15844 })
15845 .collect())
15846 }
15847
15848 /// Is THIS node a set-returning call: one of the builtin kinds, or a user
15849 /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
15850 fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
15851 if is_top_level_unnest(e) {
15852 return true;
15853 }
15854 let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
15855 return false;
15856 };
15857 self.active_catalog().functions_named(name).iter().any(|f| {
15858 let r = f.returns.trim().to_ascii_uppercase();
15859 r.starts_with("SETOF") || r.starts_with("TABLE(")
15860 })
15861 }
15862
15863 /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
15864 fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
15865 let mut found = false;
15866 let mut probe = e.clone();
15867 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
15868 if self.is_srf_node(n) {
15869 found = true;
15870 return true;
15871 }
15872 false
15873 });
15874 found
15875 }
15876
15877 /// Which projection items CONTAIN a set-returning call. Before round 78 this
15878 /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
15879 /// ordinary scalar call all the way down to the function dispatcher, which
15880 /// then reported `unnest` as an unknown function.
15881 fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
15882 projection
15883 .iter()
15884 .enumerate()
15885 .filter(|(_, p)| self.expr_contains_srf(&p.expr))
15886 .map(|(i, _)| i)
15887 .collect()
15888 }
15889}
15890
15891impl Engine {
15892 /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
15893 /// no `(f(args)).*` item.
15894 fn lower_record_expansion(
15895 &self,
15896 stmt: &SelectStatement,
15897 ) -> Result<Option<SelectStatement>, EngineError> {
15898 use spg_sql::ast::{Expr, SelectItem};
15899 let is_marker = |it: &SelectItem| {
15900 matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
15901 if name == "__record_expand")
15902 };
15903 if !stmt.items.iter().any(is_marker) {
15904 return Ok(None);
15905 }
15906 let mut out = stmt.clone();
15907 let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
15908 let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
15909 for (n, item) in stmt.items.iter().enumerate() {
15910 if !is_marker(item) {
15911 items.push(item.clone());
15912 continue;
15913 }
15914 let SelectItem::Expr {
15915 expr: Expr::FunctionCall { args, .. },
15916 ..
15917 } = item
15918 else {
15919 unreachable!("checked by is_marker");
15920 };
15921 let Some(Expr::FunctionCall {
15922 name: fname,
15923 args: fargs,
15924 }) = args.first()
15925 else {
15926 return Err(EngineError::Unsupported(
15927 "(<expr>).* expands a function's record — it needs a function call".into(),
15928 ));
15929 };
15930 let cols = self.setof_declared_columns(fname)?;
15931 let alias = alloc::format!("__rec{n}");
15932 let mut tref = bare_table_ref_named(&alias);
15933 tref.table_fn_call = Some(alloc::boxed::Box::new((
15934 fname.to_ascii_lowercase(),
15935 fargs.clone(),
15936 )));
15937 tref.alias = Some(alias.clone());
15938 lateral_refs.push(tref);
15939 for c in cols {
15940 items.push(SelectItem::Expr {
15941 expr: Expr::Column(spg_sql::ast::ColumnName {
15942 qualifier: Some(alias.clone()),
15943 name: c,
15944 }),
15945 alias: None,
15946 });
15947 }
15948 }
15949 out.items = items;
15950 // The function joins the FROM. With no FROM it BECOMES the FROM; with one
15951 // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
15952 // (the arguments may reference the outer row — the round-69 correlation).
15953 for tref in lateral_refs {
15954 match &mut out.from {
15955 None => {
15956 out.from = Some(spg_sql::ast::FromClause {
15957 primary: tref,
15958 joins: alloc::vec::Vec::new(),
15959 });
15960 }
15961 Some(from) => from.joins.push(spg_sql::ast::FromJoin {
15962 kind: spg_sql::ast::JoinKind::Cross,
15963 table: tref,
15964 on: None,
15965 using_cols: None,
15966 natural: false,
15967 }),
15968 }
15969 }
15970 Ok(Some(out))
15971 }
15972
15973 /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
15974 /// v text)` names them; a `SETOF <scalar>` is one column named after the
15975 /// function.
15976 fn setof_declared_columns(
15977 &self,
15978 name: &str,
15979 ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
15980 let cat = self.active_catalog();
15981 let overloads = cat.functions_named(name);
15982 let def = overloads.first().ok_or_else(|| {
15983 EngineError::Unsupported(alloc::format!("function {name} does not exist"))
15984 })?;
15985 let declared = def.returns.trim();
15986 let upper = declared.to_ascii_uppercase();
15987 if upper.starts_with("TABLE(") {
15988 let raw = &declared["TABLE(".len()..declared.len() - 1];
15989 return Ok(raw
15990 .split(',')
15991 .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
15992 .collect());
15993 }
15994 Ok(alloc::vec![name.to_string()])
15995 }
15996}
15997
15998/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
15999/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
16000/// COLUMNS list (data-independent), NESTED children inlined in
16001/// declaration order (PG's flattened output shape).
16002/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
16003/// correlated JSON_TABLE's static schema without evaluating its doc.
16004pub(crate) fn json_table_schema_pub(
16005 cols: &[spg_sql::ast::JsonTableColumn],
16006) -> alloc::vec::Vec<ColumnSchema> {
16007 json_table_schema(cols)
16008}
16009
16010fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
16011 use spg_sql::ast::JsonTableColumn as C;
16012 let mut out = alloc::vec::Vec::new();
16013 for c in cols {
16014 match c {
16015 C::Ordinality { name } => {
16016 out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
16017 }
16018 C::Regular {
16019 name, ty, exists, ..
16020 } => {
16021 let dt = if *exists {
16022 DataType::Bool
16023 } else {
16024 crate::conversions::column_type_to_data_type(*ty)
16025 };
16026 out.push(ColumnSchema::new(name.clone(), dt, true));
16027 }
16028 C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
16029 }
16030 }
16031 out
16032}
16033
16034/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
16035/// JSON_TABLE column's declared type (the DEFAULT expr may be a
16036/// string literal like `'none'` that must land as the column type).
16037fn coerce_json_table_default(
16038 v: Value<'static>,
16039 ty: spg_sql::ast::ColumnTypeName,
16040 name: &str,
16041) -> Result<Value<'static>, EngineError> {
16042 if v.is_null() {
16043 return Ok(Value::Null);
16044 }
16045 let dt = crate::conversions::column_type_to_data_type(ty);
16046 crate::conversions::coerce_value(v, dt, name, 0)
16047}
16048
16049/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
16050fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
16051 use crate::json::JsonValue as J;
16052 match v {
16053 Value::Null => J::Null,
16054 Value::Bool(b) => J::Bool(*b),
16055 Value::SmallInt(n) => J::Number(f64::from(*n)),
16056 Value::Int(n) => J::Number(f64::from(*n)),
16057 Value::BigInt(n) => J::Number(*n as f64),
16058 Value::Float(x) => J::Number(*x),
16059 Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
16060 other => J::String(crate::eval::value_to_text(other)),
16061 }
16062}
16063
16064fn bare_table_ref_named(name: &str) -> TableRef {
16065 TableRef {
16066 name: name.to_string(),
16067 alias: None,
16068 only: false,
16069 as_of_segment: None,
16070 unnest_expr: None,
16071 unnest_column_aliases: alloc::vec::Vec::new(),
16072 with_ordinality: false,
16073 generate_series_args: None,
16074 lateral_subquery: None,
16075 jsonb_each_text_arg: None,
16076 table_fn_call: None,
16077 rows_from: None,
16078 json_table: None,
16079 scalar_fn_item: false,
16080 }
16081}
16082
16083impl Engine {
16084 /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
16085 /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
16086 /// entries are the array-able SRFs, already lowered by the parser into their
16087 /// scalar array form.
16088 fn rows_from_rows(
16089 &self,
16090 primary: &TableRef,
16091 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
16092 let entries = primary
16093 .rows_from
16094 .as_ref()
16095 .expect("caller guards rows_from.is_some()");
16096 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
16097 let ctx = self.ev_ctx(&empty, None);
16098 let dummy = Row::new(alloc::vec::Vec::new());
16099 let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
16100 let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
16101 for (name, args) in entries {
16102 let (vals, colname) = if name == "__array" {
16103 // The parser lowered this one to `<array expr>`; its rows are the
16104 // array's elements.
16105 let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
16106 (
16107 array_value_to_elements(&arr)?,
16108 alloc::string::String::from("unnest"),
16109 )
16110 } else {
16111 let call = spg_sql::ast::Expr::FunctionCall {
16112 name: name.clone(),
16113 args: args.clone(),
16114 };
16115 (self.srf_values(&call, &dummy, &ctx)?, name.clone())
16116 };
16117 let ty = vals
16118 .first()
16119 .and_then(spg_storage::Value::data_type)
16120 .unwrap_or(DataType::Text);
16121 cols.push(ColumnSchema::new(colname, ty, true));
16122 lists.push(vals);
16123 }
16124 let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
16125 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
16126 for k in 0..n {
16127 let mut vals: alloc::vec::Vec<Value<'static>> =
16128 alloc::vec::Vec::with_capacity(lists.len() + 1);
16129 for l in &lists {
16130 vals.push(l.get(k).cloned().unwrap_or(Value::Null));
16131 }
16132 rows.push(Row::new(vals));
16133 }
16134 if primary.with_ordinality {
16135 cols.push(ColumnSchema::new(
16136 "ordinality".to_string(),
16137 DataType::BigInt,
16138 false,
16139 ));
16140 rows = rows
16141 .into_iter()
16142 .enumerate()
16143 .map(|(i, r)| {
16144 let mut v = r.values;
16145 v.push(Value::BigInt(i as i64 + 1));
16146 Row::new(v)
16147 })
16148 .collect();
16149 }
16150 Ok((rows, cols))
16151 }
16152}
16153
16154/// v7.39 (round 232) — PG names the offending set operation in its
16155/// arity / type-mismatch messages ("each UNION query must have the same
16156/// number of columns"). `UNION ALL` is still spelled UNION there.
16157fn set_op_name(kind: UnionKind) -> &'static str {
16158 match kind {
16159 UnionKind::All | UnionKind::Distinct => "UNION",
16160 UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
16161 UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
16162 }
16163}
16164
16165/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
16166/// type: a bare string or NULL literal that no context has typed yet. SPG
16167/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
16168/// be the syntax. A wildcard or a non-literal expression is never unknown.
16169/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
16170/// LABELS as text (the wire render) but the value is an oid-carrying
16171/// dual, so a UNION with a numeric column must not be refused on the
16172/// label (pg_dump: `SELECT classid … UNION ALL SELECT
16173/// 'pg_opfamily'::regclass …`).
16174fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
16175 fn is_regcast(e: &Expr) -> bool {
16176 matches!(
16177 e,
16178 Expr::Cast {
16179 target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
16180 ..
16181 }
16182 )
16183 }
16184 stmt.items
16185 .iter()
16186 .map(|item| match item {
16187 SelectItem::Expr { expr, .. } => is_regcast(expr),
16188 _ => false,
16189 })
16190 .collect()
16191}
16192
16193fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
16194 stmt.items
16195 .iter()
16196 .map(|item| match item {
16197 SelectItem::Expr { expr, .. } => matches!(
16198 expr,
16199 Expr::Literal(spg_sql::ast::Literal::String(_))
16200 | Expr::Literal(spg_sql::ast::Literal::Null)
16201 ),
16202 _ => false,
16203 })
16204 .collect()
16205}
16206
16207/// v7.39 (round 233) — retype one branch column's cells, reporting the
16208/// conversion failure the way PG does rather than leaving the column
16209/// half-converted. Used when the other branch typed an untyped literal.
16210fn coerce_branch_column(
16211 rows: &mut [Row<'static>],
16212 col_idx: usize,
16213 target: DataType,
16214 col_name: &str,
16215) -> Result<(), EngineError> {
16216 for row in rows.iter_mut() {
16217 let Some(slot) = row.values.get_mut(col_idx) else {
16218 continue;
16219 };
16220 if matches!(slot, Value::Null) {
16221 continue;
16222 }
16223 *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
16224 }
16225 Ok(())
16226}
16227
16228/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
16229/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
16230/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
16231/// reference to q's output columns substituted by the underlying column.
16232///
16233/// Admission is deliberately narrow — anything that changes cardinality,
16234/// order, or scope stays on the materialising path:
16235/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
16236/// FROM with no ordinality or positional column aliases, and no
16237/// subquery anywhere its expressions (an inner scope could reference
16238/// q too — descending is a later knife);
16239/// * inner: one stored table, bare-column projection only, no
16240/// CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
16241/// * every outer column reference must resolve inside q's output list —
16242/// a name that does not is an ERROR today, and flattening would
16243/// silently legalise it against the base table.
16244fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16245 use spg_sql::ast::SelectItem;
16246 let inner = primary.lateral_subquery.as_deref()?;
16247 // Outer shape.
16248 if !stmt.ctes.is_empty()
16249 || !stmt.unions.is_empty()
16250 || stmt.distinct
16251 || !stmt.distinct_on.is_empty()
16252 || !stmt.window_check_exprs.is_empty()
16253 || stmt.locking.is_some()
16254 || primary.with_ordinality
16255 || !primary.unnest_column_aliases.is_empty()
16256 {
16257 return None;
16258 }
16259 // Inner shape.
16260 if !inner.ctes.is_empty()
16261 || !inner.unions.is_empty()
16262 || inner.distinct
16263 || !inner.distinct_on.is_empty()
16264 || inner.group_by.is_some()
16265 || inner.group_by_all
16266 || inner.having.is_some()
16267 || !inner.order_by.is_empty()
16268 || inner.limit.is_some()
16269 || inner.offset.is_some()
16270 || !inner.window_check_exprs.is_empty()
16271 || inner.locking.is_some()
16272 {
16273 return None;
16274 }
16275 let ifrom = inner.from.as_ref()?;
16276 let it = &ifrom.primary;
16277 if !ifrom.joins.is_empty()
16278 || it.name.is_empty()
16279 || it.lateral_subquery.is_some()
16280 || it.unnest_expr.is_some()
16281 || it.generate_series_args.is_some()
16282 || it.as_of_segment.is_some()
16283 || it.jsonb_each_text_arg.is_some()
16284 || it.table_fn_call.is_some()
16285 || it.rows_from.is_some()
16286 || it.json_table.is_some()
16287 || it.with_ordinality
16288 || !it.unnest_column_aliases.is_empty()
16289 {
16290 return None;
16291 }
16292 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16293 return None;
16294 }
16295 // The output map: q's visible name -> the underlying column.
16296 let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
16297 let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
16298 alloc::collections::BTreeMap::new();
16299 for item in &inner.items {
16300 let SelectItem::Expr { expr, alias } = item else {
16301 return None;
16302 };
16303 let Expr::Column(c) = expr else {
16304 return None;
16305 };
16306 if let Some(q) = c.qualifier.as_deref()
16307 && !q.eq_ignore_ascii_case(&inner_alias)
16308 {
16309 return None;
16310 }
16311 let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
16312 // A duplicated output name would make substitution ambiguous.
16313 if map
16314 .insert(out_name.to_ascii_lowercase(), c.clone())
16315 .is_some()
16316 {
16317 return None;
16318 }
16319 }
16320 if map.is_empty() {
16321 return None;
16322 }
16323 let derived_alias = primary
16324 .alias
16325 .clone()
16326 .unwrap_or_else(|| primary.name.clone())
16327 .to_ascii_lowercase();
16328 // Substitute in a clone; bail (None) on the first reference the map
16329 // cannot answer.
16330 let mut out = stmt.clone();
16331 let ok = core::cell::Cell::new(true);
16332 let mut subst = |e: &mut Expr| -> bool {
16333 match e {
16334 Expr::Column(c) => {
16335 match c.qualifier.as_deref() {
16336 Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
16337 None => {}
16338 Some(_) => {
16339 ok.set(false);
16340 return true;
16341 }
16342 }
16343 match map.get(&c.name.to_ascii_lowercase()) {
16344 Some(target) => *c = target.clone(),
16345 None => ok.set(false),
16346 }
16347 true
16348 }
16349 // Any subquery could reference q from its own scope;
16350 // descending is a later knife — bail for now.
16351 Expr::ScalarSubquery(_)
16352 | Expr::Exists { .. }
16353 | Expr::InSubquery { .. }
16354 | Expr::RowInSubquery { .. }
16355 | Expr::RowCmpSubquery { .. } => {
16356 ok.set(false);
16357 true
16358 }
16359 _ => false,
16360 }
16361 };
16362 for item in &mut out.items {
16363 match item {
16364 SelectItem::Expr { expr, .. } => {
16365 crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
16366 }
16367 // `SELECT * FROM (…) q` means q's columns, in q's order.
16368 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
16369 }
16370 }
16371 if let Some(w) = &mut out.where_ {
16372 crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
16373 }
16374 if let Some(gs) = &mut out.group_by {
16375 for g in gs {
16376 crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
16377 }
16378 }
16379 if let Some(h) = &mut out.having {
16380 crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
16381 }
16382 for o in &mut out.order_by {
16383 crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
16384 }
16385 for d in &mut out.distinct_on {
16386 crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
16387 }
16388 if !ok.get() {
16389 return None;
16390 }
16391 // FROM becomes the stored table; the filters conjoin.
16392 out.from = Some(spg_sql::ast::FromClause {
16393 primary: it.clone(),
16394 joins: Vec::new(),
16395 });
16396 out.where_ = match (inner.where_.clone(), out.where_.take()) {
16397 (Some(a), Some(b)) => Some(Expr::Binary {
16398 lhs: alloc::boxed::Box::new(a),
16399 op: spg_sql::ast::BinOp::And,
16400 rhs: alloc::boxed::Box::new(b),
16401 }),
16402 (Some(a), None) => Some(a),
16403 (None, b) => b,
16404 };
16405 Some(out)
16406}
16407
16408/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
16409/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
16410/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
16411/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
16412/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
16413/// DISTINCT, an SRF, or an unprovable inner shape stays put.
16414fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16415 use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
16416 let inner = primary.lateral_subquery.as_deref()?;
16417 // Outer: exactly `SELECT count(*)`, nothing else.
16418 if !stmt.ctes.is_empty()
16419 || !stmt.unions.is_empty()
16420 || stmt.distinct
16421 || !stmt.distinct_on.is_empty()
16422 || stmt.where_.is_some()
16423 || stmt.group_by.is_some()
16424 || stmt.having.is_some()
16425 || !stmt.order_by.is_empty()
16426 || stmt.limit.is_some()
16427 || stmt.offset.is_some()
16428 || stmt.items.len() != 1
16429 {
16430 return None;
16431 }
16432 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16433 return None;
16434 };
16435 let E::FunctionCall { name, args } = expr else {
16436 return None;
16437 };
16438 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16439 return None;
16440 }
16441 // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
16442 let Some(LimitExpr::Literal(k)) = &inner.offset else {
16443 return None;
16444 };
16445 let k = i64::from(*k);
16446 if inner.limit.is_some() || inner.order_by.is_empty() {
16447 return None;
16448 }
16449 let mut counted = inner.clone();
16450 counted.order_by = Vec::new();
16451 counted.offset = None;
16452 // The stripped inner must now be a provable simple shape (its
16453 // items become irrelevant — count(*) reads none of them — but an
16454 // SRF item would change the row count, so the flatten predicate's
16455 // scrutiny still applies).
16456 let base = matview_flatten_probe(&counted)?;
16457 let mut out = stmt.clone();
16458 out.items = alloc::vec![SelectItem::Expr {
16459 expr: E::FunctionCall {
16460 name: String::from("greatest"),
16461 args: alloc::vec![
16462 E::Binary {
16463 lhs: alloc::boxed::Box::new(E::FunctionCall {
16464 name: String::from("count_star"),
16465 args: alloc::vec![],
16466 }),
16467 op: spg_sql::ast::BinOp::Sub,
16468 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16469 },
16470 E::Literal(spg_sql::ast::Literal::Integer(0)),
16471 ],
16472 },
16473 alias: Some(String::from("count")),
16474 }];
16475 out.from = Some(spg_sql::ast::FromClause {
16476 primary: base,
16477 joins: Vec::new(),
16478 });
16479 out.where_ = counted.where_.clone();
16480 Some(out)
16481}
16482
16483/// The inner-shape probe `try_count_over_offset` shares with the
16484/// flatten: single stored table, no modifiers, no subqueries, no SRF
16485/// items. Returns the base TableRef.
16486fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
16487 use spg_sql::ast::SelectItem;
16488 if !inner.ctes.is_empty()
16489 || !inner.unions.is_empty()
16490 || inner.distinct
16491 || !inner.distinct_on.is_empty()
16492 || inner.group_by.is_some()
16493 || inner.group_by_all
16494 || inner.having.is_some()
16495 || !inner.order_by.is_empty()
16496 || inner.limit.is_some()
16497 || inner.offset.is_some()
16498 || !inner.window_check_exprs.is_empty()
16499 || inner.locking.is_some()
16500 {
16501 return None;
16502 }
16503 let ifrom = inner.from.as_ref()?;
16504 let it = &ifrom.primary;
16505 if !ifrom.joins.is_empty()
16506 || it.name.is_empty()
16507 || it.lateral_subquery.is_some()
16508 || it.unnest_expr.is_some()
16509 || it.generate_series_args.is_some()
16510 || it.as_of_segment.is_some()
16511 || it.jsonb_each_text_arg.is_some()
16512 || it.table_fn_call.is_some()
16513 || it.rows_from.is_some()
16514 || it.json_table.is_some()
16515 || it.with_ordinality
16516 {
16517 return None;
16518 }
16519 for item in &inner.items {
16520 match item {
16521 SelectItem::Expr { expr, .. } => {
16522 if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
16523 return None;
16524 }
16525 }
16526 SelectItem::Wildcard => {}
16527 SelectItem::QualifiedWildcard(_) => return None,
16528 }
16529 }
16530 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16531 return None;
16532 }
16533 Some(it.clone())
16534}
16535
16536/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
16537/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
16538/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
16539/// constant-LENGTH array literal unnests to exactly k rows per input
16540/// row (NULL elements are rows too). One SRF item only, elements
16541/// subquery-free, and the stripped inner must pass the same probe the
16542/// count-over-offset rewrite uses.
16543fn try_count_over_const_unnest(
16544 stmt: &SelectStatement,
16545 primary: &TableRef,
16546) -> Option<SelectStatement> {
16547 use spg_sql::ast::{Expr as E, SelectItem};
16548 let inner = primary.lateral_subquery.as_deref()?;
16549 if !stmt.ctes.is_empty()
16550 || !stmt.unions.is_empty()
16551 || stmt.distinct
16552 || !stmt.distinct_on.is_empty()
16553 || stmt.where_.is_some()
16554 || stmt.group_by.is_some()
16555 || stmt.having.is_some()
16556 || !stmt.order_by.is_empty()
16557 || stmt.limit.is_some()
16558 || stmt.offset.is_some()
16559 || stmt.items.len() != 1
16560 {
16561 return None;
16562 }
16563 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16564 return None;
16565 };
16566 let E::FunctionCall { name, args } = expr else {
16567 return None;
16568 };
16569 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16570 return None;
16571 }
16572 // Inner: exactly one item, and it is unnest(ARRAY[...]).
16573 if inner.items.len() != 1
16574 || !inner.order_by.is_empty()
16575 || inner.limit.is_some()
16576 || inner.offset.is_some()
16577 {
16578 return None;
16579 }
16580 let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
16581 return None;
16582 };
16583 let E::FunctionCall {
16584 name: fname,
16585 args: fargs,
16586 } = item
16587 else {
16588 return None;
16589 };
16590 if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
16591 return None;
16592 }
16593 let E::Array(elems) = &fargs[0] else {
16594 return None;
16595 };
16596 if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
16597 return None;
16598 }
16599 let k = elems.len() as i64;
16600 // The stripped inner (the SRF item replaced by a plain constant)
16601 // must be the provable simple shape.
16602 let mut counted = inner.clone();
16603 counted.items = alloc::vec![SelectItem::Expr {
16604 expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
16605 alias: None,
16606 }];
16607 let base = matview_flatten_probe(&counted)?;
16608 let mut out = stmt.clone();
16609 out.items = alloc::vec![SelectItem::Expr {
16610 expr: E::Binary {
16611 lhs: alloc::boxed::Box::new(E::FunctionCall {
16612 name: String::from("count_star"),
16613 args: alloc::vec![],
16614 }),
16615 op: spg_sql::ast::BinOp::Mul,
16616 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16617 },
16618 alias: Some(String::from("count")),
16619 }];
16620 out.from = Some(spg_sql::ast::FromClause {
16621 primary: base,
16622 joins: Vec::new(),
16623 });
16624 out.where_ = counted.where_.clone();
16625 Some(out)
16626}