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(&mut tagged, &descs, &colls);
824 }
825 let mut out_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
826 // v7.37 D.41 — `SELECT DISTINCT` over a window projection: the window
827 // pipeline builds one output row per input row, so DISTINCT must dedup the
828 // projected rows (PG evaluates window functions before DISTINCT). Applied
829 // after ORDER BY (duplicate rows share sort keys, so order is preserved)
830 // and before LIMIT.
831 if stmt.distinct {
832 // v7.38.14 — see the synthetic-source sites below: the mask was
833 // always available here, from the same projection this function
834 // already built.
835 out_rows = dedup_rows(
836 out_rows,
837 FoldSpec::of_masks(
838 self.speaks_mysql,
839 &fold_mask(&projection),
840 &pad_mask(&projection),
841 ),
842 );
843 }
844 apply_offset_and_limit(&mut out_rows, stmt.offset_literal(), stmt.limit_literal());
845 let final_cols: Vec<ColumnSchema> = projection
846 .into_iter()
847 .map(|p| p.to_column_schema())
848 .collect();
849 Ok(QueryResult::Rows {
850 columns: final_cols,
851 rows: out_rows,
852 })
853 }
854
855 /// v4.11: materialise each CTE into a temp table inside a
856 /// cloned catalog, then run the body SELECT against a fresh
857 /// engine instance that owns the enriched catalog. The clone
858 /// is moderately expensive — only paid by CTE-bearing queries.
859 /// Subqueries inside CTE bodies / the main body resolve as
860 /// usual; `clock_fn` is propagated so `NOW()` lines up.
861 /// v7.16.2 — mailrs round-10 A.3. Materialise the
862 /// `information_schema.*` / `pg_catalog.*` virtual views
863 /// the SELECT references, then re-execute the SELECT
864 /// against an enriched catalog where those views are real
865 /// tables. Same pattern as `exec_with_ctes`. The temp
866 /// engine carries `meta_views_materialised = true` so its
867 /// own meta-dispatch short-circuits — without that we'd
868 /// infinite-recurse since the temp catalog's view name
869 /// still starts with `__spg_info_` and re-triggers the
870 /// check.
871 pub(crate) fn exec_select_with_meta_views(
872 &self,
873 stmt: &SelectStatement,
874 cancel: CancelToken<'_>,
875 ) -> Result<QueryResult, EngineError> {
876 let catalog = self.meta_view_catalog(stmt)?;
877 let mut temp = Engine::restore(catalog);
878 if let Some(c) = self.clock {
879 temp = temp.with_clock(c);
880 }
881 if let Some(f) = self.salt_fn {
882 temp = temp.with_salt_fn(f);
883 }
884 // v7.39 (round 522) — the temp engine holds the materialised
885 // catalog and, until now, nothing of the SESSION. So every
886 // session-scoped answer changed the moment a system view
887 // appeared in the FROM clause: `SELECT current_user` said
888 // `unmei` and `SELECT current_user FROM pg_class` said `admin`;
889 // `current_setting('work_mem')` fell back to the boot default
890 // after a SET; `application_name` read empty. A privilege check
891 // written against a catalog join was reading a different
892 // identity than the same check written without one.
893 //
894 // Carry what a session can be observed through — its parameters
895 // (which is also where the session user lives), the role store
896 // the privilege builtins read, the dialect, and the rendering
897 // settings a timestamp is spelled with.
898 temp.session_params.clone_from(&self.session_params);
899 temp.users.clone_from(&self.users);
900 temp.backslash_escapes = self.backslash_escapes;
901 temp.speaks_mysql = self.speaks_mysql;
902 temp.mysql_strict = self.mysql_strict;
903 temp.render_style = self.render_style;
904 temp.tz_offset_fn = self.tz_offset_fn;
905 temp.tz_localize_fn = self.tz_localize_fn;
906 temp.tz_abbrev_fn = self.tz_abbrev_fn;
907 temp.meta_views_materialised = true;
908 temp.exec_select_cancel(stmt, cancel)
909 }
910
911 /// v7.39 (round 462) — the catalog a meta-view SELECT resolves
912 /// against: this engine's catalog with every `__spg_*` view the
913 /// statement references materialised into it.
914 ///
915 /// Split out of `exec_select_with_meta_views` so Describe can reach
916 /// the same shapes execution reaches. Describe used to look the FROM
917 /// relation up in the plain catalog, where a system view does not
918 /// exist, and reported "no columns" for every one of them — so an
919 /// extended-protocol client reading `pg_stat_user_tables` got rows
920 /// with no column metadata. Sharing the materialisation means a
921 /// view added here is described correctly the day it is added.
922 pub(crate) fn meta_view_catalog(&self, stmt: &SelectStatement) -> Result<Catalog, EngineError> {
923 let mut needed: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
924 collect_meta_view_names(stmt, &mut needed);
925 let mut catalog = self.active_catalog().clone();
926 for view in &needed {
927 if catalog.get(view).is_some() {
928 continue;
929 }
930 match view.as_str() {
931 "__spg_info_columns" => {
932 let (schema, rows) = synth_information_schema_columns(
933 self.active_catalog(),
934 self.speaks_mysql,
935 &self.mysql_schema_name(),
936 );
937 materialise_meta_view(&mut catalog, view, schema, rows)?;
938 }
939 "__spg_info_tables" => {
940 let (schema, rows) = synth_information_schema_tables(
941 self.active_catalog(),
942 self.speaks_mysql,
943 &self.mysql_schema_name(),
944 );
945 materialise_meta_view(&mut catalog, view, schema, rows)?;
946 }
947 "__spg_pg_class" => {
948 let (schema, rows) = synth_pg_class(
949 self.active_catalog(),
950 i64::try_from(self.vacuum_oldest_active()).unwrap_or(i64::MAX),
951 );
952 materialise_meta_view(&mut catalog, view, schema, rows)?;
953 }
954 "__spg_pg_attribute" => {
955 let (schema, rows) = synth_pg_attribute(self.active_catalog());
956 materialise_meta_view(&mut catalog, view, schema, rows)?;
957 }
958 // v7.17.0 Phase 3.P0-50 — pg_catalog.pg_type for
959 // sqlx / SQLAlchemy / Diesel / pgAdmin lookups.
960 "__spg_pg_type" => {
961 let (schema, rows) = synth_pg_type(self.active_catalog());
962 materialise_meta_view(&mut catalog, view, schema, rows)?;
963 }
964 // v7.39 (round 621) — pg_catalog.pg_operator, which did not
965 // exist at all.
966 "__spg_pg_operator" => {
967 let (schema, rows) = synth_pg_operator(self.active_catalog());
968 materialise_meta_view(&mut catalog, view, schema, rows)?;
969 }
970 // v7.17.0 Phase 3.P0-51 — pg_catalog.pg_proc for
971 // function-name introspection (ORM / pgAdmin).
972 "__spg_pg_proc" => {
973 let (schema, rows) = synth_pg_proc(self.active_catalog());
974 materialise_meta_view(&mut catalog, view, schema, rows)?;
975 }
976 // v7.24 (round-16 D) — pg_catalog.pg_trigger. The
977 // round-16 "why doesn't prod fire the trigger"
978 // question was unanswerable because triggers had NO
979 // introspection surface; tgname/tgenabled plus the
980 // pragmatic relname/timing/events/function columns
981 // make "is it registered and enabled" a one-liner.
982 "__spg_pg_trigger" => {
983 let (schema, rows) = synth_pg_trigger(self.active_catalog());
984 materialise_meta_view(&mut catalog, view, schema, rows)?;
985 }
986 // v7.17.0 Phase 3.P0-52 — pg_catalog.pg_namespace
987 // (schema list for admin tools' tree views).
988 "__spg_pg_namespace" => {
989 let (schema, rows) = synth_pg_namespace(self.active_catalog());
990 materialise_meta_view(&mut catalog, view, schema, rows)?;
991 }
992 // v7.39 — pg_tables convenience view (was a pgwire
993 // canned response that ignored projections).
994 "__spg_pg_tables" => {
995 let (schema, rows) =
996 crate::system_catalog::synth_pg_tables(self.active_catalog());
997 materialise_meta_view(&mut catalog, view, schema, rows)?;
998 }
999 // v7.37.24 (24.1) — pg_catalog.pg_enum (label list
1000 // for ENUM types; sqlx / ORM enum codecs read this).
1001 "__spg_pg_enum" => {
1002 let (schema, rows) =
1003 crate::system_catalog::synth_pg_enum(self.active_catalog());
1004 materialise_meta_view(&mut catalog, view, schema, rows)?;
1005 }
1006 // v7.37.21 (21.13) — pg_catalog.pg_replication_slots
1007 // (shape-stable empty until 21.12 persists slot state).
1008 // v7.39 (round 277) — session-scoped prepared statements.
1009 "__spg_pg_prepared_statements" => {
1010 let (schema, rows) = crate::system_catalog::synth_pg_prepared_statements(
1011 &self.prepared_statements,
1012 );
1013 materialise_meta_view(&mut catalog, view, schema, rows)?;
1014 }
1015 "__spg_pg_replication_slots" => {
1016 let (schema, rows) =
1017 crate::system_catalog::synth_pg_replication_slots(self.active_catalog());
1018 materialise_meta_view(&mut catalog, view, schema, rows)?;
1019 }
1020 // v7.37.21 (21.13-b) — pg_catalog.pg_publication
1021 // (one row per CREATE PUBLICATION).
1022 "__spg_pg_publication" => {
1023 let (schema, rows) = crate::system_catalog::synth_pg_publication(self);
1024 materialise_meta_view(&mut catalog, view, schema, rows)?;
1025 }
1026 // v7.37.21 (21.13-c) — pg_catalog.pg_subscription
1027 // (one row per CREATE SUBSCRIPTION; subconninfo
1028 // redacted so dashboards can't leak credentials).
1029 "__spg_pg_subscription" => {
1030 let (schema, rows) = crate::system_catalog::synth_pg_subscription(self);
1031 materialise_meta_view(&mut catalog, view, schema, rows)?;
1032 }
1033 // v7.37.22 (22.x-stat-db) — pg_catalog.pg_stat_database
1034 // (one row for SPG's single database; counters are
1035 // shape-stable 0 until wiring lands).
1036 "__spg_pg_stat_database" => {
1037 let (schema, rows) = crate::system_catalog::synth_pg_stat_database(
1038 self,
1039 self.stat_tup_inserted,
1040 self.stat_tup_updated,
1041 self.stat_tup_deleted,
1042 );
1043 materialise_meta_view(&mut catalog, view, schema, rows)?;
1044 }
1045 // v7.37.22 (22.14) — pg_catalog.pg_stat_user_tables
1046 // (per-table churn counters; live_tup = row count).
1047 "__spg_pg_stat_user_tables" => {
1048 // r192 — DML counters come from the engine-side
1049 // non-transactional map, not the (tx-shadowed)
1050 // catalog tables.
1051 let (schema, rows) = crate::system_catalog::synth_pg_stat_user_tables(
1052 self.active_catalog(),
1053 &self.table_write_stats,
1054 );
1055 materialise_meta_view(&mut catalog, view, schema, rows)?;
1056 }
1057 // v7.37.22 (22.15) — pg_catalog.pg_stat_user_indexes
1058 // (per-index usage counters; flag unused indexes).
1059 "__spg_pg_stat_user_indexes" => {
1060 let (schema, rows) =
1061 crate::system_catalog::synth_pg_stat_user_indexes(self.active_catalog());
1062 materialise_meta_view(&mut catalog, view, schema, rows)?;
1063 }
1064 // v7.37.22 (22.16) — pg_catalog.pg_stat_bgwriter.
1065 "__spg_pg_stat_bgwriter" => {
1066 let (schema, rows) =
1067 crate::system_catalog::synth_pg_stat_bgwriter(self.active_catalog());
1068 materialise_meta_view(&mut catalog, view, schema, rows)?;
1069 }
1070 // v7.38 (read01 P3.14) — pg_catalog.pg_stat_checkpointer /
1071 // pg_stat_wal shell views (shape-stable, counters pending).
1072 "__spg_pg_stat_checkpointer" => {
1073 let (schema, rows) =
1074 crate::system_catalog::synth_pg_stat_checkpointer(self.active_catalog());
1075 materialise_meta_view(&mut catalog, view, schema, rows)?;
1076 }
1077 "__spg_pg_stat_wal" => {
1078 let (schema, rows) =
1079 crate::system_catalog::synth_pg_stat_wal(self.active_catalog());
1080 materialise_meta_view(&mut catalog, view, schema, rows)?;
1081 }
1082 // v7.38 (read01 P3.15) — pg_catalog.pg_stat_slru /
1083 // pg_stat_subscription_stats shell views.
1084 "__spg_pg_stat_slru" => {
1085 let (schema, rows) =
1086 crate::system_catalog::synth_pg_stat_slru(self.active_catalog());
1087 materialise_meta_view(&mut catalog, view, schema, rows)?;
1088 }
1089 "__spg_pg_stat_subscription_stats" => {
1090 let (schema, rows) = crate::system_catalog::synth_pg_stat_subscription_stats(
1091 self.active_catalog(),
1092 );
1093 materialise_meta_view(&mut catalog, view, schema, rows)?;
1094 }
1095 // v7.37.22 (22.17) — pg_catalog.pg_stat_archiver.
1096 "__spg_pg_stat_archiver" => {
1097 let (schema, rows) =
1098 crate::system_catalog::synth_pg_stat_archiver(self.active_catalog());
1099 materialise_meta_view(&mut catalog, view, schema, rows)?;
1100 }
1101 // v7.37.21 (21.13-d) — pg_catalog.pg_stat_replication.
1102 "__spg_pg_stat_replication" => {
1103 let (schema, rows) =
1104 crate::system_catalog::synth_pg_stat_replication(self.active_catalog());
1105 materialise_meta_view(&mut catalog, view, schema, rows)?;
1106 }
1107 // v7.37.24 (24.13) — pg_catalog.pg_am.
1108 "__spg_pg_am" => {
1109 let (schema, rows) = crate::system_catalog::synth_pg_am(self.active_catalog());
1110 materialise_meta_view(&mut catalog, view, schema, rows)?;
1111 }
1112 // v7.37.22 (22.18) — pg_catalog.pg_stat_io (PG 16+).
1113 "__spg_pg_stat_io" => {
1114 let (schema, rows) =
1115 crate::system_catalog::synth_pg_stat_io(self.active_catalog());
1116 materialise_meta_view(&mut catalog, view, schema, rows)?;
1117 }
1118 // v7.37.22 (22.19) — pg_catalog.pg_stat_user_functions.
1119 "__spg_pg_stat_user_functions" => {
1120 let (schema, rows) =
1121 crate::system_catalog::synth_pg_stat_user_functions(self.active_catalog());
1122 materialise_meta_view(&mut catalog, view, schema, rows)?;
1123 }
1124 // v7.39 (round 287) — pg_catalog.pg_largeobject{,_metadata}.
1125 "__spg_pg_largeobject" => {
1126 let (schema, rows) =
1127 crate::system_catalog::synth_pg_largeobject(self.active_catalog());
1128 materialise_meta_view(&mut catalog, view, schema, rows)?;
1129 }
1130 "__spg_pg_largeobject_metadata" => {
1131 let (schema, rows) =
1132 crate::system_catalog::synth_pg_largeobject_metadata(self.active_catalog());
1133 materialise_meta_view(&mut catalog, view, schema, rows)?;
1134 }
1135 // v7.37.23 (23.7-a) — pg_catalog.pg_statistic_ext.
1136 "__spg_pg_statistic_ext" => {
1137 let (schema, rows) =
1138 crate::system_catalog::synth_pg_statistic_ext(self.active_catalog());
1139 materialise_meta_view(&mut catalog, view, schema, rows)?;
1140 }
1141 // v7.38.18 — pg_catalog.pg_stats, the readable view.
1142 "__spg_pg_stats" => {
1143 let (schema, rows) = crate::system_catalog::synth_pg_stats(
1144 self.active_catalog(),
1145 &self.statistics,
1146 );
1147 materialise_meta_view(&mut catalog, view, schema, rows)?;
1148 }
1149 // v7.37.24 (24.15) — pg_catalog.pg_statistic.
1150 "__spg_pg_statistic" => {
1151 let (schema, rows) = crate::system_catalog::synth_pg_statistic(
1152 self.active_catalog(),
1153 &self.statistics,
1154 );
1155 materialise_meta_view(&mut catalog, view, schema, rows)?;
1156 }
1157 // v7.37.22 (22.20) — pg_catalog.pg_stat_progress_vacuum.
1158 "__spg_pg_stat_progress_vacuum" => {
1159 let (schema, rows) =
1160 crate::system_catalog::synth_pg_stat_progress_vacuum(self.active_catalog());
1161 materialise_meta_view(&mut catalog, view, schema, rows)?;
1162 }
1163 // v7.37.22 (22.21) — pg_catalog.pg_stat_progress_create_index.
1164 "__spg_pg_stat_progress_create_index" => {
1165 let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_create_index(
1166 self.active_catalog(),
1167 );
1168 materialise_meta_view(&mut catalog, view, schema, rows)?;
1169 }
1170 // v7.37.22 (22.22) — pg_catalog.pg_stat_progress_analyze.
1171 "__spg_pg_stat_progress_analyze" => {
1172 let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_analyze(
1173 self.active_catalog(),
1174 );
1175 materialise_meta_view(&mut catalog, view, schema, rows)?;
1176 }
1177 // v7.37.24 (24.16) — pg_catalog.pg_inherits
1178 // (partition parent → child OID mapping).
1179 "__spg_pg_inherits" => {
1180 let (schema, rows) =
1181 crate::system_catalog::synth_pg_inherits(self.active_catalog());
1182 materialise_meta_view(&mut catalog, view, schema, rows)?;
1183 }
1184 // v7.39 (round 650) — the text-search catalogs, filled
1185 // with what SPG actually has rather than PG's thirty.
1186 "__spg_pg_ts_config_map" => {
1187 let (schema, rows) =
1188 crate::system_catalog::synth_pg_ts_config_map(self.active_catalog());
1189 materialise_meta_view(&mut catalog, view, schema, rows)?;
1190 }
1191 "__spg_pg_ts_config" => {
1192 let (schema, rows) =
1193 crate::system_catalog::synth_pg_ts_config(self.active_catalog());
1194 materialise_meta_view(&mut catalog, view, schema, rows)?;
1195 }
1196 "__spg_pg_ts_dict" => {
1197 let (schema, rows) =
1198 crate::system_catalog::synth_pg_ts_dict(self.active_catalog());
1199 materialise_meta_view(&mut catalog, view, schema, rows)?;
1200 }
1201 "__spg_pg_ts_parser" => {
1202 let (schema, rows) =
1203 crate::system_catalog::synth_pg_ts_parser(self.active_catalog());
1204 materialise_meta_view(&mut catalog, view, schema, rows)?;
1205 }
1206 "__spg_pg_ts_template" => {
1207 let (schema, rows) =
1208 crate::system_catalog::synth_pg_ts_template(self.active_catalog());
1209 materialise_meta_view(&mut catalog, view, schema, rows)?;
1210 }
1211 // v7.37.24 (24.17) — pg_catalog.pg_depend
1212 // (dependency graph; shape-stable empty since
1213 // SPG's drop enforcement is per-kind, not per-object).
1214 "__spg_pg_depend" => {
1215 let (schema, rows) =
1216 crate::system_catalog::synth_pg_depend(self.active_catalog());
1217 materialise_meta_view(&mut catalog, view, schema, rows)?;
1218 }
1219 // 7.38.1 S5.1 — pg_catalog.pg_opclass (pg_dump wall #1).
1220 "__spg_pg_opclass" => {
1221 let (schema, rows) =
1222 crate::system_catalog::synth_pg_opclass(self.active_catalog());
1223 materialise_meta_view(&mut catalog, view, schema, rows)?;
1224 }
1225 "__spg_pg_opfamily" => {
1226 let (schema, rows) =
1227 crate::system_catalog::synth_pg_opfamily(self.active_catalog());
1228 materialise_meta_view(&mut catalog, view, schema, rows)?;
1229 }
1230 "__spg_pg_amop" => {
1231 let (schema, rows) =
1232 crate::system_catalog::synth_pg_amop(self.active_catalog());
1233 materialise_meta_view(&mut catalog, view, schema, rows)?;
1234 }
1235 "__spg_pg_amproc" => {
1236 let (schema, rows) =
1237 crate::system_catalog::synth_pg_amproc(self.active_catalog());
1238 materialise_meta_view(&mut catalog, view, schema, rows)?;
1239 }
1240 // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1241 // ORM reflection + pg_dump read the deparsed default text).
1242 "__spg_pg_attrdef" => {
1243 let (schema, rows) =
1244 crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1245 materialise_meta_view(&mut catalog, view, schema, rows)?;
1246 }
1247 // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1248 "__spg_pg_policy" => {
1249 let (schema, rows) =
1250 crate::system_catalog::synth_pg_policy(self.active_catalog());
1251 materialise_meta_view(&mut catalog, view, schema, rows)?;
1252 }
1253 "__spg_pg_policies" => {
1254 let (schema, rows) =
1255 crate::system_catalog::synth_pg_policies(self.active_catalog());
1256 materialise_meta_view(&mut catalog, view, schema, rows)?;
1257 }
1258 // v7.37.24 (24.14) — pg_catalog.pg_collation.
1259 "__spg_pg_collation" => {
1260 let (schema, rows) =
1261 crate::system_catalog::synth_pg_collation(self.active_catalog());
1262 materialise_meta_view(&mut catalog, view, schema, rows)?;
1263 }
1264 // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1265 "__spg_pg_tablespace" => {
1266 let (schema, rows) =
1267 crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1268 materialise_meta_view(&mut catalog, view, schema, rows)?;
1269 }
1270 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1271 // for pgAdmin / DataGrip "indexes per table" listings.
1272 "__spg_pg_indexes" => {
1273 let (schema, rows) = synth_pg_indexes(self.active_catalog());
1274 materialise_meta_view(&mut catalog, view, schema, rows)?;
1275 }
1276 // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1277 // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1278 "__spg_pg_description" => {
1279 let (schema, rows) =
1280 crate::system_catalog::synth_pg_description(self.active_catalog());
1281 materialise_meta_view(&mut catalog, view, schema, rows)?;
1282 }
1283 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1284 // for index introspection by ORM compilers.
1285 "__spg_pg_index" => {
1286 let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1287 materialise_meta_view(&mut catalog, view, schema, rows)?;
1288 }
1289 // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1290 // for FK / UNIQUE / PK / CHECK introspection.
1291 "__spg_pg_constraint" => {
1292 let (schema, rows) = synth_pg_constraint(self.active_catalog());
1293 materialise_meta_view(&mut catalog, view, schema, rows)?;
1294 }
1295 // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1296 // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1297 "__spg_pg_sequence" => {
1298 let (schema, rows) = synth_pg_sequence(self.active_catalog());
1299 materialise_meta_view(&mut catalog, view, schema, rows)?;
1300 }
1301 // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1302 // pg_roles / pg_user. SPG is single-database so
1303 // pg_database surfaces just `postgres`; pg_roles
1304 // / pg_user walk the engine's UserStore.
1305 "__spg_pg_database" => {
1306 let (schema, rows) = synth_pg_database(self);
1307 materialise_meta_view(&mut catalog, view, schema, rows)?;
1308 }
1309 "__spg_pg_roles" => {
1310 let (schema, rows) = synth_pg_roles(self);
1311 materialise_meta_view(&mut catalog, view, schema, rows)?;
1312 }
1313 // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1314 // same roles, with PG's own `use*` column names. It used to
1315 // publish pg_roles' columns under this name.
1316 "__spg_pg_user" => {
1317 let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1318 materialise_meta_view(&mut catalog, view, schema, rows)?;
1319 }
1320 // v7.39 (read01 round 58) — role membership.
1321 "__spg_pg_auth_members" => {
1322 let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1323 materialise_meta_view(&mut catalog, view, schema, rows)?;
1324 }
1325 // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1326 // pg_views surfaces every CREATE VIEW result; SPG
1327 // ships one row per declared view from the catalog.
1328 "__spg_pg_views" => {
1329 let (schema, rows) = synth_pg_views(self.active_catalog());
1330 materialise_meta_view(&mut catalog, view, schema, rows)?;
1331 }
1332 // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1333 // catalogued query-rewrite RULE.
1334 "__spg_pg_rules" => {
1335 let (schema, rows) =
1336 crate::system_catalog::synth_pg_rules(self.active_catalog());
1337 materialise_meta_view(&mut catalog, view, schema, rows)?;
1338 }
1339 // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1340 // catalogue `pg_get_ruledef(oid)` resolves against.
1341 "__spg_pg_rewrite" => {
1342 let (schema, rows) =
1343 crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1344 materialise_meta_view(&mut catalog, view, schema, rows)?;
1345 }
1346 // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1347 // and PG's own column names.
1348 "__spg_pg_matviews" => {
1349 let (schema, rows) =
1350 crate::system_catalog::synth_pg_matviews(self.active_catalog());
1351 materialise_meta_view(&mut catalog, view, schema, rows)?;
1352 }
1353 // pg_catalog.pg_extension — native capability list
1354 // (mailrs embed round-12).
1355 // v7.39 (round 546) — the catalogs SPG has real content
1356 // for, from the facts it already holds.
1357 "__spg_pg_db_role_setting" => {
1358 let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1359 materialise_meta_view(&mut catalog, view, schema, rows)?;
1360 }
1361 "__spg_pg_language" => {
1362 let (schema, rows) = crate::system_catalog::synth_pg_language();
1363 materialise_meta_view(&mut catalog, view, schema, rows)?;
1364 }
1365 "__spg_pg_sequences" => {
1366 let (schema, rows) =
1367 crate::system_catalog::synth_pg_sequences(self.active_catalog());
1368 materialise_meta_view(&mut catalog, view, schema, rows)?;
1369 }
1370 "__spg_pg_range" => {
1371 let (schema, rows) = crate::system_catalog::synth_pg_range();
1372 materialise_meta_view(&mut catalog, view, schema, rows)?;
1373 }
1374 "__spg_pg_partitioned_table" => {
1375 let (schema, rows) =
1376 crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1377 materialise_meta_view(&mut catalog, view, schema, rows)?;
1378 }
1379 "__spg_pg_authid" => {
1380 let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1381 materialise_meta_view(&mut catalog, view, schema, rows)?;
1382 }
1383 "__spg_pg_group" => {
1384 let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1385 materialise_meta_view(&mut catalog, view, schema, rows)?;
1386 }
1387 "__spg_pg_shadow" => {
1388 let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1389 materialise_meta_view(&mut catalog, view, schema, rows)?;
1390 }
1391 // v7.39 (round 544) — pg_cast, probed from the real
1392 // cast implementation.
1393 "__spg_pg_cast" => {
1394 let (schema, rows) = crate::system_catalog::synth_pg_cast();
1395 materialise_meta_view(&mut catalog, view, schema, rows)?;
1396 }
1397 // v7.39 (round 541) — an empty catalog that exists.
1398 "__spg_pg_foreign_table" => {
1399 let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1400 materialise_meta_view(&mut catalog, view, schema, rows)?;
1401 }
1402 "__spg_pg_extension" => {
1403 let (schema, rows) = synth_pg_extension();
1404 materialise_meta_view(&mut catalog, view, schema, rows)?;
1405 }
1406 // v7.39 (round 502) — the timezone catalogues.
1407 "__spg_pg_timezone_names" => {
1408 let (schema, rows) = synth_pg_timezone_names(self);
1409 materialise_meta_view(&mut catalog, view, schema, rows)?;
1410 }
1411 "__spg_pg_timezone_abbrevs" => {
1412 let (schema, rows) = synth_pg_timezone_abbrevs(self);
1413 materialise_meta_view(&mut catalog, view, schema, rows)?;
1414 }
1415 // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1416 "__spg_pg_settings" => {
1417 let (schema, rows) = synth_pg_settings(self);
1418 materialise_meta_view(&mut catalog, view, schema, rows)?;
1419 }
1420 // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1421 // v7.39 (read01 round 51) — information_schema.role_table_grants
1422 // and .table_privileges. Both report the owner's seven implicit
1423 // table privileges; SPG's single role owns everything.
1424 // v7.39 (read01 round 59) — information_schema.column_privileges.
1425 "__spg_info_column_privileges" => {
1426 let (schema, rows) =
1427 crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1428 materialise_meta_view(&mut catalog, view, schema, rows)?;
1429 }
1430 "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1431 let grantee = self.current_role().to_string();
1432 let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1433 self.active_catalog(),
1434 &grantee,
1435 );
1436 materialise_meta_view(&mut catalog, view, schema, rows)?;
1437 }
1438 "__spg_info_key_column_usage" => {
1439 // v7.39.11 — the session's dialect decides the
1440 // column list; see the synthesiser.
1441 let mysql = self.in_mysql_dialect();
1442 let (schema, rows) = synth_info_key_column_usage(self.active_catalog(), mysql);
1443 materialise_meta_view(&mut catalog, view, schema, rows)?;
1444 }
1445 // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1446 "__spg_info_referential_constraints" => {
1447 let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1448 materialise_meta_view(&mut catalog, view, schema, rows)?;
1449 }
1450 // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1451 "__spg_info_statistics" => {
1452 let (schema, rows) = synth_info_statistics(self.active_catalog());
1453 materialise_meta_view(&mut catalog, view, schema, rows)?;
1454 }
1455 // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1456 "__spg_info_routines" => {
1457 let (schema, rows) = synth_info_routines();
1458 materialise_meta_view(&mut catalog, view, schema, rows)?;
1459 }
1460 // v7.37.24 (24.3) — information_schema.attributes.
1461 "__spg_info_attributes" => {
1462 let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1463 self.active_catalog(),
1464 );
1465 materialise_meta_view(&mut catalog, view, schema, rows)?;
1466 }
1467 // v7.37.24 (24.2) — information_schema.domains.
1468 "__spg_info_domains" => {
1469 let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1470 self.active_catalog(),
1471 );
1472 materialise_meta_view(&mut catalog, view, schema, rows)?;
1473 }
1474 // v7.37.24 (24.9) — information_schema.schemata.
1475 "__spg_info_schemata" => {
1476 let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1477 self.active_catalog(),
1478 self.speaks_mysql,
1479 &self.listed_database_names(),
1480 );
1481 materialise_meta_view(&mut catalog, view, schema, rows)?;
1482 }
1483 // v7.37.24 (24.9) — information_schema.views.
1484 "__spg_info_views" => {
1485 let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1486 self.active_catalog(),
1487 );
1488 materialise_meta_view(&mut catalog, view, schema, rows)?;
1489 }
1490 // v7.37.24 (24.9) — information_schema.table_constraints.
1491 "__spg_info_table_constraints" => {
1492 let (schema, rows) =
1493 crate::system_catalog::synth_information_schema_table_constraints(
1494 self.active_catalog(),
1495 self.speaks_mysql,
1496 );
1497 materialise_meta_view(&mut catalog, view, schema, rows)?;
1498 }
1499 // v7.37.17 — information_schema.constraint_column_usage.
1500 "__spg_info_constraint_column_usage" => {
1501 let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1502 self.active_catalog(),
1503 );
1504 materialise_meta_view(&mut catalog, view, schema, rows)?;
1505 }
1506 // v7.37.17 — information_schema.triggers.
1507 "__spg_info_triggers" => {
1508 let (schema, rows) =
1509 crate::system_catalog::synth_info_triggers(self.active_catalog());
1510 materialise_meta_view(&mut catalog, view, schema, rows)?;
1511 }
1512 // v7.37.17 — information_schema.check_constraints.
1513 "__spg_info_check_constraints" => {
1514 let (schema, rows) =
1515 crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1516 materialise_meta_view(&mut catalog, view, schema, rows)?;
1517 }
1518 // v7.37.17 — information_schema.sequences.
1519 "__spg_info_sequences" => {
1520 let (schema, rows) =
1521 crate::system_catalog::synth_info_sequences(self.active_catalog());
1522 materialise_meta_view(&mut catalog, view, schema, rows)?;
1523 }
1524 // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1525 "__spg_mysql_user" => {
1526 let (schema, rows) = synth_mysql_user(self);
1527 materialise_meta_view(&mut catalog, view, schema, rows)?;
1528 }
1529 "__spg_mysql_db" => {
1530 let (schema, rows) = synth_mysql_db();
1531 materialise_meta_view(&mut catalog, view, schema, rows)?;
1532 }
1533 // v7.39 (round 541) — the catalogs PG has that SPG is
1534 // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1535 other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1536 let (schema, rows) =
1537 crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1538 materialise_meta_view(&mut catalog, view, schema, rows)?;
1539 }
1540 _ => {
1541 return Err(EngineError::Unsupported(alloc::format!(
1542 "meta view {view:?} is not yet materialisable; \
1543 v7.16.2 covers information_schema.columns / .tables \
1544 and pg_catalog.pg_class / pg_attribute; \
1545 v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1546 pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1547 pg_user / pg_views / pg_matviews / pg_settings"
1548 )));
1549 }
1550 }
1551 }
1552 Ok(catalog)
1553 }
1554
1555 pub(crate) fn exec_with_ctes(
1556 &self,
1557 stmt: &SelectStatement,
1558 cancel: CancelToken<'_>,
1559 ) -> Result<QueryResult, EngineError> {
1560 cancel.check()?;
1561 // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1562 // bodies are supported here. Writable CTEs on a SELECT
1563 // outer require `&mut self` and route through the
1564 // top-level `exec_select_cancel_mut` entry; sentori
1565 // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1566 // INSERT, not a SELECT, so this restriction is harmless
1567 // in practice.
1568 if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1569 // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1570 // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1571 // of a statement, not nested inside a subquery; this path is
1572 // reached exactly when one is nested. The old text described SPG's
1573 // own executor plumbing ("the top-level mutable entry"), which
1574 // means nothing to a client.
1575 return Err(EngineError::Unsupported(
1576 "WITH clause containing a data-modifying statement must be at the top level".into(),
1577 ));
1578 }
1579 let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1580 // Strip CTEs from the body before running on the temp engine
1581 // so we don't recurse forever.
1582 let mut body = stmt.clone();
1583 body.ctes = Vec::new();
1584 let mut temp = Engine::restore(catalog);
1585 if let Some(c) = self.clock {
1586 temp = temp.with_clock(c);
1587 }
1588 if let Some(f) = self.salt_fn {
1589 temp = temp.with_salt_fn(f);
1590 }
1591 temp.exec_select_cancel(&body, cancel)
1592 }
1593
1594 /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1595 /// `&self` SELECT path. Caller guarantees no modifying CTE
1596 /// bodies are present.
1597 pub(crate) fn materialise_ctes_readonly(
1598 &self,
1599 ctes: &[spg_sql::ast::Cte],
1600 cancel: CancelToken<'_>,
1601 ) -> Result<crate::Catalog, EngineError> {
1602 cancel.check()?;
1603 let mut catalog = self.active_catalog().clone();
1604 for cte in ctes {
1605 let body_select = cte.body.as_select().ok_or_else(|| {
1606 EngineError::Unsupported(alloc::format!(
1607 "data-modifying CTE not supported on this SELECT entry"
1608 ))
1609 })?;
1610 // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1611 // (PG scoping: the WITH name wins for the outer query and later
1612 // CTEs, while THIS body still sees the real table — a
1613 // non-recursive body's self-name is the table, probe P2). This
1614 // materialiser works on a CLONE, so the shadow is simply: run
1615 // the body against the untouched clone, then drop the real
1616 // table from the clone before installing the CTE's temp. A
1617 // RECURSIVE self-reference is the CTE itself (P6), so there the
1618 // drop happens before the iterating materialiser runs.
1619 let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1620 let synthetic = spg_sql::ast::Cte {
1621 name: cte.name.clone(),
1622 body: spg_sql::ast::CteBody::Select(body_select.clone()),
1623 recursive: true,
1624 column_overrides: cte.column_overrides.clone(),
1625 search: None,
1626 cycle: None,
1627 };
1628 if catalog.get(&cte.name).is_some() {
1629 let _ = catalog.drop_table(&cte.name);
1630 }
1631 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1632 } else {
1633 let mut cte_engine = Engine::restore(catalog.clone());
1634 if let Some(c) = self.clock {
1635 cte_engine = cte_engine.with_clock(c);
1636 }
1637 if let Some(f) = self.salt_fn {
1638 cte_engine = cte_engine.with_salt_fn(f);
1639 }
1640 let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1641 let QueryResult::Rows { columns, rows } = body_result else {
1642 return Err(EngineError::Unsupported(alloc::format!(
1643 "CTE {:?} body did not return rows",
1644 cte.name
1645 )));
1646 };
1647 (columns, rows)
1648 };
1649 let inferred = infer_column_types(&columns, &rows);
1650 let mut columns = inferred;
1651 if !cte.column_overrides.is_empty() {
1652 if cte.column_overrides.len() != columns.len() {
1653 return Err(EngineError::Unsupported(alloc::format!(
1654 "CTE {:?} column list has {} names but body returns {} columns",
1655 cte.name,
1656 cte.column_overrides.len(),
1657 columns.len()
1658 )));
1659 }
1660 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1661 col.name.clone_from(name);
1662 }
1663 }
1664 let schema = TableSchema::new(cte.name.clone(), columns);
1665 // v7.39 (round 156) — the body ran against the untouched clone;
1666 // from here on the CTE name resolves to the temp (PG scoping).
1667 if catalog.get(&cte.name).is_some() {
1668 let _ = catalog.drop_table(&cte.name);
1669 }
1670 catalog.create_table(schema).map_err(EngineError::Storage)?;
1671 let table = catalog
1672 .get_mut(&cte.name)
1673 .expect("just-created CTE table must exist");
1674 for row in rows {
1675 table.insert(row).map_err(EngineError::Storage)?;
1676 }
1677 }
1678 Ok(catalog)
1679 }
1680
1681 /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1682 /// Retained for non-DML callers; the DML path (writable CTE on
1683 /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1684 /// `dml.rs` which installs the CTE temps directly on the
1685 /// active catalog so the outer statement's writes hit real
1686 /// tables.
1687 #[allow(dead_code)]
1688 pub(crate) fn materialise_ctes(
1689 &mut self,
1690 ctes: &[spg_sql::ast::Cte],
1691 cancel: CancelToken<'_>,
1692 ) -> Result<crate::Catalog, EngineError> {
1693 cancel.check()?;
1694 // v7.37.43-T4.4 — modifying CTEs need to write through the
1695 // SAME catalog as the outer statement, not a clone (PG's
1696 // writable CTE puts all modifications in one transaction).
1697 // For the read-only case the original logic cloned, but
1698 // since the outer statement also goes through the cloned
1699 // engine and ALL writes must converge, we now drive the
1700 // accumulator off `self.active_catalog().clone()` and
1701 // commit the modifying writes directly to `self`'s active
1702 // catalog so the surface is consistent.
1703 let mut catalog = self.active_catalog().clone();
1704 // v7.39 (round 149) — a modifying CTE body's target must be a
1705 // real relation, never a sibling CTE (PG: relation does not
1706 // exist); checked before any alias lands in the accumulator.
1707 for cte in ctes {
1708 let body_target = match &cte.body {
1709 spg_sql::ast::CteBody::Select(_) => None,
1710 spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1711 spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1712 spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1713 spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1714 };
1715 if let Some(t) = body_target
1716 && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1717 && catalog.get(t).is_none()
1718 {
1719 return Err(EngineError::Storage(
1720 spg_storage::StorageError::TableNotFound { name: t.into() },
1721 ));
1722 }
1723 }
1724 for cte in ctes {
1725 if catalog.get(&cte.name).is_some() {
1726 return Err(EngineError::Unsupported(alloc::format!(
1727 "CTE name {:?} shadows an existing table; rename the CTE",
1728 cte.name
1729 )));
1730 }
1731 let (columns, rows) = match &cte.body {
1732 // v7.39 (round 145) — see the sibling site: only a body that
1733 // truly self-references takes the iterating materialiser.
1734 spg_sql::ast::CteBody::Select(body)
1735 if cte.recursive && select_refers_to(body, &cte.name) =>
1736 {
1737 // Recursive CTE — the existing helper takes a
1738 // SELECT body and the snapshot catalog.
1739 let synthetic = spg_sql::ast::Cte {
1740 name: cte.name.clone(),
1741 body: spg_sql::ast::CteBody::Select(body.clone()),
1742 recursive: true,
1743 column_overrides: cte.column_overrides.clone(),
1744 search: None,
1745 cycle: None,
1746 };
1747 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1748 }
1749 spg_sql::ast::CteBody::Select(body) => {
1750 // v7.25 (round-17) — run against the accumulated
1751 // catalog so later CTEs can reference earlier
1752 // ones in the same WITH clause.
1753 let mut cte_engine = Engine::restore(catalog.clone());
1754 if let Some(c) = self.clock {
1755 cte_engine = cte_engine.with_clock(c);
1756 }
1757 if let Some(f) = self.salt_fn {
1758 cte_engine = cte_engine.with_salt_fn(f);
1759 }
1760 let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1761 let QueryResult::Rows { columns, rows } = body_result else {
1762 return Err(EngineError::Unsupported(alloc::format!(
1763 "CTE {:?} body did not return rows",
1764 cte.name
1765 )));
1766 };
1767 (columns, rows)
1768 }
1769 spg_sql::ast::CteBody::Insert(body) => {
1770 self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1771 }
1772 spg_sql::ast::CteBody::Update(body) => {
1773 self.exec_modifying_cte_update(&cte.name, body, cancel)?
1774 }
1775 spg_sql::ast::CteBody::Delete(body) => {
1776 self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1777 }
1778 spg_sql::ast::CteBody::Merge(body) => {
1779 self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1780 }
1781 };
1782 // v4.22: the projection builder labels any non-column
1783 // expression as Text — including literal SELECT 1.
1784 // Promote each column's type to whatever the rows
1785 // actually carry so the CTE storage table accepts them.
1786 let inferred = infer_column_types(&columns, &rows);
1787 let mut columns = inferred;
1788 if !cte.column_overrides.is_empty() {
1789 if cte.column_overrides.len() != columns.len() {
1790 return Err(EngineError::Unsupported(alloc::format!(
1791 "CTE {:?} column list has {} names but body returns {} columns",
1792 cte.name,
1793 cte.column_overrides.len(),
1794 columns.len()
1795 )));
1796 }
1797 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1798 col.name.clone_from(name);
1799 }
1800 }
1801 let schema = TableSchema::new(cte.name.clone(), columns);
1802 catalog.create_table(schema).map_err(EngineError::Storage)?;
1803 let table = catalog
1804 .get_mut(&cte.name)
1805 .expect("just-created CTE table must exist");
1806 for row in rows {
1807 table.insert(row).map_err(EngineError::Storage)?;
1808 }
1809 }
1810 Ok(catalog)
1811 }
1812
1813 /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1814 /// against `self` (so the mutation lands in the active catalog
1815 /// inside the current transaction) and captures the RETURNING
1816 /// projection — column schema + rows — to materialise as the
1817 /// CTE alias's table. An INSERT without RETURNING produces a
1818 /// 0-row table with a synthetic single-column placeholder
1819 /// (matches PG: the CTE alias is still defined, but referencing
1820 /// it from the outer query without RETURNING raises a
1821 /// column-resolution error at scan time).
1822 fn exec_modifying_cte_insert(
1823 &mut self,
1824 cte_name: &str,
1825 body: &spg_sql::ast::InsertStatement,
1826 _cancel: CancelToken<'_>,
1827 ) -> Result<
1828 (
1829 Vec<spg_storage::ColumnSchema>,
1830 Vec<spg_storage::Row<'static>>,
1831 ),
1832 EngineError,
1833 > {
1834 // round 151 — a WITH-headed body keeps its own ctes; the body
1835 // statement routes through its writable-CTE entry (outer CTEs
1836 // are never copied into bodies, so no recursion risk).
1837 let body = body.clone();
1838 let result = self.exec_insert(body)?;
1839 match result {
1840 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1841 QueryResult::CommandOk { .. } => {
1842 // No RETURNING — emit a sentinel single-column
1843 // schema with zero rows so the alias is defined.
1844 let placeholder = spg_storage::ColumnSchema::new(
1845 alloc::format!("{cte_name}_returning_absent"),
1846 spg_storage::DataType::Text,
1847 true,
1848 );
1849 Ok((alloc::vec![placeholder], Vec::new()))
1850 }
1851 }
1852 }
1853
1854 /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1855 /// as INSERT above.
1856 fn exec_modifying_cte_update(
1857 &mut self,
1858 cte_name: &str,
1859 body: &spg_sql::ast::UpdateStatement,
1860 cancel: CancelToken<'_>,
1861 ) -> Result<
1862 (
1863 Vec<spg_storage::ColumnSchema>,
1864 Vec<spg_storage::Row<'static>>,
1865 ),
1866 EngineError,
1867 > {
1868 let body = body.clone();
1869 let result = self.exec_update_cancel(&body, cancel)?;
1870 match result {
1871 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1872 QueryResult::CommandOk { .. } => {
1873 let placeholder = spg_storage::ColumnSchema::new(
1874 alloc::format!("{cte_name}_returning_absent"),
1875 spg_storage::DataType::Text,
1876 true,
1877 );
1878 Ok((alloc::vec![placeholder], Vec::new()))
1879 }
1880 }
1881 }
1882
1883 /// v7.37.43-T4.4 — execute a DELETE CTE body.
1884 fn exec_modifying_cte_delete(
1885 &mut self,
1886 cte_name: &str,
1887 body: &spg_sql::ast::DeleteStatement,
1888 cancel: CancelToken<'_>,
1889 ) -> Result<
1890 (
1891 Vec<spg_storage::ColumnSchema>,
1892 Vec<spg_storage::Row<'static>>,
1893 ),
1894 EngineError,
1895 > {
1896 let body = body.clone();
1897 let result = self.exec_delete_cancel(&body, cancel)?;
1898 match result {
1899 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1900 QueryResult::CommandOk { .. } => {
1901 let placeholder = spg_storage::ColumnSchema::new(
1902 alloc::format!("{cte_name}_returning_absent"),
1903 spg_storage::DataType::Text,
1904 true,
1905 );
1906 Ok((alloc::vec![placeholder], Vec::new()))
1907 }
1908 }
1909 }
1910
1911 /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1912 fn exec_modifying_cte_merge(
1913 &mut self,
1914 cte_name: &str,
1915 body: &spg_sql::ast::MergeStatement,
1916 cancel: CancelToken<'_>,
1917 ) -> Result<
1918 (
1919 Vec<spg_storage::ColumnSchema>,
1920 Vec<spg_storage::Row<'static>>,
1921 ),
1922 EngineError,
1923 > {
1924 let body = body.clone();
1925 let result = self.exec_merge_cancel(&body, cancel)?;
1926 match result {
1927 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1928 QueryResult::CommandOk { .. } => {
1929 let placeholder = spg_storage::ColumnSchema::new(
1930 alloc::format!("{cte_name}_returning_absent"),
1931 spg_storage::DataType::Text,
1932 true,
1933 );
1934 Ok((alloc::vec![placeholder], Vec::new()))
1935 }
1936 }
1937 }
1938
1939 /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1940 /// UNION (or UNION ALL) of an anchor that does not reference
1941 /// the CTE name, and one or more recursive terms that do. The
1942 /// anchor runs first; each subsequent iteration runs the
1943 /// recursive term against a temp catalog where the CTE name is
1944 /// bound to the *previous* iteration's output. Iteration stops
1945 /// when the recursive term yields no rows; UNION (DISTINCT)
1946 /// deduplicates against the accumulated result, UNION ALL does
1947 /// not. A hard cap on total rows prevents runaway queries.
1948 #[allow(clippy::too_many_lines)]
1949 pub(crate) fn materialise_recursive_cte(
1950 &self,
1951 cte: &spg_sql::ast::Cte,
1952 base_catalog: &Catalog,
1953 cancel: CancelToken<'_>,
1954 ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1955 const MAX_TOTAL_ROWS: usize = 1_000_000;
1956 const MAX_ITERATIONS: usize = 100_000;
1957 cancel.check()?;
1958 // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1959 // a modifying recursive CTE is parser-rejectable but we
1960 // guard here defensively.
1961 let body_select = cte.body.as_select().ok_or_else(|| {
1962 EngineError::Unsupported(alloc::format!(
1963 "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1964 cte.name
1965 ))
1966 })?;
1967 if body_select.unions.is_empty() {
1968 return Err(EngineError::Unsupported(alloc::format!(
1969 "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1970 cte.name
1971 )));
1972 }
1973 // Anchor: the body's leading SELECT, with unions stripped.
1974 let mut anchor = body_select.clone();
1975 let all_union_terms = core::mem::take(&mut anchor.unions);
1976 anchor.ctes = Vec::new();
1977 // v7.37 D.42 — split the UNION members: those that do NOT reference the
1978 // CTE are additional ANCHOR terms, only the ones that do recurse. A
1979 // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1980 // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1981 // treating the non-recursive `SELECT r2` as a recursive term made it
1982 // re-emit its constant row every iteration → runaway loop.
1983 let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1984 .into_iter()
1985 .partition(|(_, t)| !select_refers_to(t, &cte.name));
1986 let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1987 let QueryResult::Rows {
1988 columns: anchor_cols,
1989 rows: mut anchor_rows,
1990 } = anchor_result
1991 else {
1992 return Err(EngineError::Unsupported(alloc::format!(
1993 "WITH RECURSIVE {:?}: anchor did not return rows",
1994 cte.name
1995 )));
1996 };
1997 // Append every non-recursive UNION member's rows to the anchor set.
1998 for (_, term) in &anchor_terms {
1999 let mut term = term.clone();
2000 term.ctes = Vec::new();
2001 if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
2002 anchor_rows.extend(rows);
2003 }
2004 }
2005 // The projection builder labels non-column expressions Text;
2006 // refine column types from the anchor's actual values so the
2007 // intermediate iter-catalog tables accept them.
2008 let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
2009 if !cte.column_overrides.is_empty() {
2010 if cte.column_overrides.len() != columns.len() {
2011 return Err(EngineError::Unsupported(alloc::format!(
2012 "CTE {:?} column list has {} names but anchor returns {} columns",
2013 cte.name,
2014 cte.column_overrides.len(),
2015 columns.len()
2016 )));
2017 }
2018 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
2019 col.name.clone_from(name);
2020 }
2021 }
2022 let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
2023 let mut working_set: Vec<Row<'static>> = anchor_rows;
2024 let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
2025 // Track at least one "all UNION ALL" flag — if every union
2026 // kind is ALL we skip the dedup step (faster + matches PG).
2027 let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
2028 if !all_union_all {
2029 for r in &all_rows {
2030 seen.insert(encode_row_key(r));
2031 }
2032 }
2033 // v7.39 (round 598) — the engine and its catalog are built ONCE.
2034 // Each iteration used to clone the catalog, create the CTE table,
2035 // and construct a whole `Engine` — which initialises 82 fields — to
2036 // hold that round's working set. A counting allocator put the loop
2037 // at 63 allocations and 104 kB per iteration, or 1 GB for a
2038 // 10,000-row recursive CTE, and none of it varied with how much
2039 // else was in the catalog: the per-round rebuild WAS the cost. The
2040 // table is emptied and refilled instead.
2041 let mut iter_catalog = base_catalog.clone();
2042 let schema = TableSchema::new(cte.name.clone(), columns.clone());
2043 iter_catalog
2044 .create_table(schema)
2045 .map_err(EngineError::Storage)?;
2046 let mut iter_engine = Engine::restore(iter_catalog);
2047 if let Some(c) = self.clock {
2048 iter_engine = iter_engine.with_clock(c);
2049 }
2050 if let Some(f) = self.salt_fn {
2051 iter_engine = iter_engine.with_salt_fn(f);
2052 }
2053 // The recursive terms are cloned once too — the clone stripped the
2054 // CTE list off each of them, per term per iteration.
2055 let recursive_terms: Vec<SelectStatement> = union_terms
2056 .iter()
2057 .map(|(_, t)| {
2058 let mut t = t.clone();
2059 t.ctes = Vec::new();
2060 t
2061 })
2062 .collect();
2063 // v7.39 (round 618) — plan every recursive term once. Taken only if
2064 // ALL of them plan, so a query never runs half on each path.
2065 let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2066 .iter()
2067 .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2068 .collect();
2069 let fast_ctx = term_plans.as_ref().map(|plans| {
2070 let alias = plans[0].alias.clone();
2071 (alias, ())
2072 });
2073 for iter in 0..MAX_ITERATIONS {
2074 cancel.check()?;
2075 if working_set.is_empty() {
2076 break;
2077 }
2078 if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2079 // The worktable IS the working set: no table to empty and
2080 // refill, and no query execution per round.
2081 let mut next_set: Vec<Row<'static>> = Vec::new();
2082 for plan in plans {
2083 let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2084 for row in &working_set {
2085 cancel.check()?;
2086 if let Some(w) = plan.where_ {
2087 let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2088 if !matches!(v, Value::Bool(true)) {
2089 continue;
2090 }
2091 }
2092 let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2093 for it in &plan.items {
2094 vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2095 }
2096 let out = Row::new(vals);
2097 if !all_union_all {
2098 let key = encode_row_key(&out);
2099 if !seen.insert(key) {
2100 continue;
2101 }
2102 }
2103 next_set.push(out);
2104 }
2105 }
2106 if next_set.is_empty() {
2107 break;
2108 }
2109 all_rows.extend(next_set.iter().cloned());
2110 working_set = next_set;
2111 if all_rows.len() > MAX_TOTAL_ROWS {
2112 return Err(EngineError::Unsupported(alloc::format!(
2113 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2114 cte.name
2115 )));
2116 }
2117 if iter + 1 == MAX_ITERATIONS {
2118 return Err(EngineError::Unsupported(alloc::format!(
2119 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2120 cte.name
2121 )));
2122 }
2123 continue;
2124 }
2125 {
2126 // Truncated rather than dropped and recreated: the table's
2127 // own structure is what dropping it throws away, and it is
2128 // identical every round.
2129 let cat = iter_engine.base_catalog_mut();
2130 let table = cat.get_mut(&cte.name).expect("created above");
2131 table.truncate();
2132 for row in &working_set {
2133 table.insert(row.clone()).map_err(EngineError::Storage)?;
2134 }
2135 }
2136 // Run each recursive term in sequence and collect new rows.
2137 let mut next_set: Vec<Row<'static>> = Vec::new();
2138 for term in &recursive_terms {
2139 let r = iter_engine.exec_select_cancel(term, cancel)?;
2140 let QueryResult::Rows {
2141 columns: rc,
2142 rows: rs,
2143 } = r
2144 else {
2145 return Err(EngineError::Unsupported(alloc::format!(
2146 "WITH RECURSIVE {:?}: recursive term did not return rows",
2147 cte.name
2148 )));
2149 };
2150 if rc.len() != columns.len() {
2151 return Err(EngineError::Unsupported(alloc::format!(
2152 "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2153 cte.name,
2154 rc.len(),
2155 columns.len()
2156 )));
2157 }
2158 for row in rs {
2159 if !all_union_all {
2160 let key = encode_row_key(&row);
2161 if !seen.insert(key) {
2162 continue;
2163 }
2164 }
2165 next_set.push(row);
2166 }
2167 }
2168 if next_set.is_empty() {
2169 break;
2170 }
2171 all_rows.extend(next_set.iter().cloned());
2172 working_set = next_set;
2173 if all_rows.len() > MAX_TOTAL_ROWS {
2174 return Err(EngineError::Unsupported(alloc::format!(
2175 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2176 cte.name
2177 )));
2178 }
2179 if iter + 1 == MAX_ITERATIONS {
2180 return Err(EngineError::Unsupported(alloc::format!(
2181 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2182 cte.name
2183 )));
2184 }
2185 }
2186 Ok((columns, all_rows))
2187 }
2188
2189 pub(crate) fn resolve_select_subqueries(
2190 &self,
2191 stmt: &mut SelectStatement,
2192 cancel: CancelToken<'_>,
2193 ) -> Result<(), EngineError> {
2194 for item in &mut stmt.items {
2195 if let SelectItem::Expr { expr, alias } = item {
2196 // An UNCORRELATED subquery is replaced by its value right
2197 // here, and the shape the column was named for goes with
2198 // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2199 // boolean literal, so SPG answered `?column?` where PG18
2200 // answers `exists`. Only a subquery at the TOP of the item
2201 // loses its name this way — one nested inside a call still
2202 // reports the call.
2203 if alias.is_none()
2204 && matches!(
2205 expr,
2206 Expr::ScalarSubquery(_)
2207 | Expr::Exists { .. }
2208 | Expr::InSubquery { .. }
2209 | Expr::RowInSubquery { .. }
2210 | Expr::RowCmpSubquery { .. }
2211 )
2212 {
2213 *alias = Some(default_output_name(expr, self.speaks_mysql));
2214 }
2215 self.resolve_expr_subqueries(expr, cancel)?;
2216 }
2217 }
2218 if let Some(w) = &mut stmt.where_ {
2219 self.resolve_expr_subqueries(w, cancel)?;
2220 }
2221 // v7.24.1 — JOIN ON conditions can carry subqueries too;
2222 // they were never walked, so even an UNCORRELATED subquery
2223 // in ON hit "subquery reached row eval".
2224 if let Some(from) = &mut stmt.from {
2225 for j in &mut from.joins {
2226 if let Some(on) = &mut j.on {
2227 self.resolve_expr_subqueries(on, cancel)?;
2228 }
2229 }
2230 }
2231 if let Some(gs) = &mut stmt.group_by {
2232 for g in gs {
2233 self.resolve_expr_subqueries(g, cancel)?;
2234 }
2235 }
2236 if let Some(h) = &mut stmt.having {
2237 self.resolve_expr_subqueries(h, cancel)?;
2238 }
2239 for o in &mut stmt.order_by {
2240 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2241 }
2242 for (_, peer) in &mut stmt.unions {
2243 self.resolve_select_subqueries(peer, cancel)?;
2244 }
2245 Ok(())
2246 }
2247
2248 #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2249 pub(crate) fn resolve_expr_subqueries(
2250 &self,
2251 e: &mut Expr,
2252 cancel: CancelToken<'_>,
2253 ) -> Result<(), EngineError> {
2254 // Replace-on-this-node cases first.
2255 if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2256 *e = replacement;
2257 return Ok(());
2258 }
2259 match e {
2260 Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } => {
2261 self.resolve_expr_subqueries(expr, cancel)?
2262 }
2263 Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2264 Expr::AggregateOrdered { call, order_by, .. } => {
2265 self.resolve_expr_subqueries(call, cancel)?;
2266 for o in order_by.iter_mut() {
2267 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2268 }
2269 }
2270 Expr::Binary { lhs, rhs, .. } => {
2271 self.resolve_expr_subqueries(lhs, cancel)?;
2272 self.resolve_expr_subqueries(rhs, cancel)?;
2273 }
2274 Expr::Unary { expr, .. }
2275 | Expr::Cast { expr, .. }
2276 | Expr::IsNull { expr, .. }
2277 | Expr::BoolTest { expr, .. }
2278 | Expr::FieldAccess { base: expr, .. } => {
2279 self.resolve_expr_subqueries(expr, cancel)?;
2280 }
2281 Expr::FunctionCall { args, .. } => {
2282 for a in args {
2283 self.resolve_expr_subqueries(a, cancel)?;
2284 }
2285 }
2286 Expr::Like { expr, pattern, .. } => {
2287 self.resolve_expr_subqueries(expr, cancel)?;
2288 self.resolve_expr_subqueries(pattern, cancel)?;
2289 }
2290 Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2291 // v4.12 window functions — recurse into args + ORDER BY
2292 // + PARTITION BY in case they carry inner subqueries.
2293 Expr::WindowFunction {
2294 args,
2295 partition_by,
2296 order_by,
2297 ..
2298 } => {
2299 for a in args {
2300 self.resolve_expr_subqueries(a, cancel)?;
2301 }
2302 for p in partition_by {
2303 self.resolve_expr_subqueries(p, cancel)?;
2304 }
2305 for (e, _, _) in order_by {
2306 self.resolve_expr_subqueries(e, cancel)?;
2307 }
2308 }
2309 // Subquery nodes are handled in subquery_replacement
2310 // (which returned None — defensive no-op); Literal /
2311 // Column are leaves.
2312 Expr::ScalarSubquery(_)
2313 | Expr::Exists { .. }
2314 | Expr::InSubquery { .. }
2315 | Expr::RowInSubquery { .. }
2316 | Expr::RowCmpSubquery { .. }
2317 | Expr::Literal(_)
2318 | Expr::Placeholder(_)
2319 | Expr::Column(_) => {}
2320 // v7.30.2 — list elements can carry scalar subqueries
2321 // (`x IN (1, (SELECT …))`).
2322 Expr::InList { expr, list, .. } => {
2323 self.resolve_expr_subqueries(expr, cancel)?;
2324 for item in list {
2325 self.resolve_expr_subqueries(item, cancel)?;
2326 }
2327 }
2328 // v7.10.10 — recurse children.
2329 Expr::Array(items) => {
2330 for elem in items {
2331 self.resolve_expr_subqueries(elem, cancel)?;
2332 }
2333 }
2334 Expr::ArraySubscript { target, index } => {
2335 self.resolve_expr_subqueries(target, cancel)?;
2336 self.resolve_expr_subqueries(index, cancel)?;
2337 }
2338 Expr::ArraySlice { target, lo, hi } => {
2339 self.resolve_expr_subqueries(target, cancel)?;
2340 if let Some(l) = lo {
2341 self.resolve_expr_subqueries(l, cancel)?;
2342 }
2343 if let Some(h) = hi {
2344 self.resolve_expr_subqueries(h, cancel)?;
2345 }
2346 }
2347 Expr::AnyAll { expr, array, .. } => {
2348 self.resolve_expr_subqueries(expr, cancel)?;
2349 // Quantified subquery — an uncorrelated one
2350 // materialises up front; a correlated one stays for
2351 // the per-row resolver.
2352 if let Expr::ScalarSubquery(inner) = array.as_mut() {
2353 if !crate::subquery::select_is_correlated(inner) {
2354 let s = (**inner).clone();
2355 **array = self.materialize_quantified_rows(&s, cancel)?;
2356 }
2357 } else {
2358 self.resolve_expr_subqueries(array, cancel)?;
2359 }
2360 }
2361 Expr::Case {
2362 operand,
2363 branches,
2364 else_branch,
2365 } => {
2366 if let Some(o) = operand {
2367 self.resolve_expr_subqueries(o, cancel)?;
2368 }
2369 for (w, t) in branches {
2370 self.resolve_expr_subqueries(w, cancel)?;
2371 self.resolve_expr_subqueries(t, cancel)?;
2372 }
2373 if let Some(e) = else_branch {
2374 self.resolve_expr_subqueries(e, cancel)?;
2375 }
2376 }
2377 }
2378 Ok(())
2379 }
2380}
2381
2382impl Engine {
2383 /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2384 /// `SelectItem::Wildcard` to all schema columns and
2385 /// `SelectItem::Expr` via the regular eval path.
2386 pub(crate) fn project_row_simple(
2387 &self,
2388 row: &Row<'static>,
2389 items: &[SelectItem],
2390 schema_cols: &[ColumnSchema],
2391 alias: &str,
2392 ) -> Result<Row<'static>, EngineError> {
2393 let ctx = self.ev_ctx(schema_cols, Some(alias));
2394 let cancel = CancelToken::none();
2395 let mut out_vals = Vec::new();
2396 for item in items {
2397 match item {
2398 // In a single-table projection (AS OF SEGMENT / RETURNING) a
2399 // qualified `t.*` covers exactly the same columns as a bare `*`.
2400 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2401 out_vals.extend(row.values.iter().cloned());
2402 }
2403 SelectItem::Expr { expr, .. } => {
2404 let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2405 out_vals.push(v);
2406 }
2407 }
2408 }
2409 Ok(Row::new(out_vals))
2410 }
2411
2412 /// v6.10.2 — derive the output `ColumnSchema` list for an
2413 /// AS OF SEGMENT projection. Wildcards take the full schema;
2414 /// expressions take the alias if present or a synthetic
2415 /// `?column?` (PG convention) otherwise.
2416 pub(crate) fn derive_output_columns(
2417 &self,
2418 items: &[SelectItem],
2419 schema_cols: &[ColumnSchema],
2420 table_alias: &str,
2421 ) -> Vec<ColumnSchema> {
2422 let mut out = Vec::new();
2423 for item in items {
2424 match item {
2425 // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2426 // a single-table projection.
2427 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2428 out.extend(schema_cols.iter().cloned());
2429 }
2430 SelectItem::Expr { expr, alias } => {
2431 // Bare column references inherit the schema
2432 // column's name + type — PG names `RETURNING id`
2433 // "id" and types it BIGINT, and the sqlx embed
2434 // path type-checks RowDescription against the
2435 // Rust target (mailrs embed round-12).
2436 if let Expr::Column(col) = expr
2437 && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2438 {
2439 let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2440 let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2441 // v7.39 (read01 round 54) — carry the enum identity:
2442 // it lives outside the DataType lattice, so a derived
2443 // table built from this schema otherwise forgets it and
2444 // the OUTER `ORDER BY <enum col>` silently sorts by the
2445 // label's TEXT instead of member order.
2446 c.user_enum_type = sc.user_enum_type.clone();
2447 out.push(c);
2448 continue;
2449 }
2450 let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2451 // v7.30.4 (mailrs round-27, P0) — type the
2452 // expression with the same inference the SELECT
2453 // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2454 // The old Text default broke every typed decode
2455 // of `RETURNING uidnext - 1 AS uid`: four days
2456 // of inbound mail indexed nowhere. Inference
2457 // failure keeps the old Text fallback rather
2458 // than inventing new error paths here.
2459 // v7.39 (round 258) — take the enum identity from the
2460 // same projection build, not just the type: a constant
2461 // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2462 // VALUES row lowers to) is an EXPRESSION, so it landed
2463 // here and the derived table forgot the enum.
2464 let (ty, nullable) = build_projection(
2465 core::slice::from_ref(item),
2466 schema_cols,
2467 table_alias,
2468 self.speaks_mysql,
2469 Some(self.active_catalog()),
2470 )
2471 .ok()
2472 .and_then(|p| p.into_iter().next())
2473 .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2474 out.push(ColumnSchema::new(name, ty, nullable));
2475 }
2476 }
2477 }
2478 out
2479 }
2480
2481 /// v4.5: SELECT with cooperative cancellation. The token is
2482 /// honoured between UNION peers and inside the bare-SELECT row
2483 /// loop; HNSW kNN graph walks and the aggregate executor don't
2484 /// honour it yet (deferred — those paths bound their work
2485 /// internally by `LIMIT k` and `GROUP BY` cardinality).
2486 /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2487 /// its (lowercased) name, or None if the name isn't a virtual view.
2488 /// Callers decide whether to return it directly (`SELECT *`) or stage
2489 /// it as a temp table for the full query pipeline.
2490 fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2491 Some(match name {
2492 "spg_statistic" => self.exec_spg_statistic(),
2493 "spg_stat_replication" => self.exec_spg_stat_replication(),
2494 "spg_stat_segment" => self.exec_spg_stat_segment(),
2495 "spg_memory_stats" => self.exec_spg_memory_stats(),
2496 "spg_stat_query" => self.exec_spg_stat_query(),
2497 "pg_stat_statements" => self.exec_pg_stat_statements(),
2498 "spg_stat_activity" => self.exec_spg_stat_activity(),
2499 "pg_stat_activity" => self.exec_pg_stat_activity(),
2500 "pg_locks" => self.exec_pg_locks(),
2501 "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2502 "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2503 "spg_partition_health" => self.exec_spg_partition_health(),
2504 "spg_audit_chain" => self.exec_spg_audit_chain(),
2505 "spg_audit_verify" => self.exec_spg_audit_verify(),
2506 "spg_table_ddl" => self.exec_spg_table_ddl(),
2507 "spg_role_ddl" => self.exec_spg_role_ddl(),
2508 "spg_database_ddl" => self.exec_spg_database_ddl(),
2509 _ => return None,
2510 })
2511 }
2512
2513 /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2514 /// describes against: this engine's catalog with the view staged as a
2515 /// table, exactly as `exec_select_cancel_as` stages it for a
2516 /// non-bare query.
2517 ///
2518 /// These views never reach the catalog — each is a fixed row set built
2519 /// inside its own `exec_*` — so Describe reported no columns for all
2520 /// seventeen of them. Rows are deliberately not inserted: Describe
2521 /// only needs the shape, and `infer_column_types` reads the rows we
2522 /// already have in hand.
2523 pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2524 let from = stmt.from.as_ref()?;
2525 if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2526 return None;
2527 }
2528 let lower = from.primary.name.to_ascii_lowercase();
2529 let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2530 return None;
2531 };
2532 let mut catalog = self.active_catalog().clone();
2533 let cols = infer_column_types(&columns, &rows);
2534 catalog
2535 .create_table(TableSchema::new(from.primary.name.clone(), cols))
2536 .ok()?;
2537 Some(catalog)
2538 }
2539
2540 pub(crate) fn exec_select_cancel(
2541 &self,
2542 stmt: &SelectStatement,
2543 cancel: CancelToken<'_>,
2544 ) -> Result<QueryResult, EngineError> {
2545 self.exec_select_cancel_as(stmt, cancel, None)
2546 }
2547
2548 /// v7.39 (round 334, V55) — the same read core, authorised as
2549 /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2550 /// function's OWNER: that is the entire point of the form, and without
2551 /// it every definer function failed with "permission denied" on the
2552 /// very table it exists to expose.
2553 /// v7.39 (round 559) — see the call site. `None` for anything but
2554 /// the bare shape, so every other query keeps its old path.
2555 fn try_bare_count_star(
2556 &self,
2557 stmt: &SelectStatement,
2558 as_role: Option<&str>,
2559 ) -> Result<Option<QueryResult>, EngineError> {
2560 use spg_sql::ast::SelectItem;
2561 if as_role.is_some()
2562 || !stmt.ctes.is_empty()
2563 || !stmt.unions.is_empty()
2564 || stmt.where_.is_some()
2565 || stmt.group_by.is_some()
2566 || stmt.having.is_some()
2567 || stmt.distinct
2568 || !stmt.order_by.is_empty()
2569 || stmt.limit.is_some()
2570 || stmt.offset.is_some()
2571 || stmt.items.len() != 1
2572 {
2573 return Ok(None);
2574 }
2575 let Some(from) = &stmt.from else {
2576 return Ok(None);
2577 };
2578 if !from.joins.is_empty()
2579 || stmt.locking.is_some()
2580 || from.primary.lateral_subquery.is_some()
2581 || from.primary.unnest_expr.is_some()
2582 || from.primary.generate_series_args.is_some()
2583 || from.primary.name.is_empty()
2584 || from.primary.name.starts_with("__spg_")
2585 {
2586 return Ok(None);
2587 }
2588 // A partition PARENT holds no rows of its own — they live in the
2589 // children — so its header count is 0 and the ordinary path has
2590 // to fan out. Caught by the partition conformance cases.
2591 //
2592 // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2593 // of them, which is worse: its header count is a real number,
2594 // just not the answer. `SELECT count(*) FROM par` returned 1
2595 // where PG returns 2, because this shortcut fired before the
2596 // fan-out could. The question is "does anything descend from
2597 // this", not "was it declared a partition parent".
2598 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2599 return Ok(None);
2600 }
2601 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2602 return Ok(None);
2603 };
2604 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2605 return Ok(None);
2606 };
2607 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2608 return Ok(None);
2609 }
2610 // A row-security policy filters rows, so the header count is not
2611 // the answer; the ordinary path applies the policy.
2612 let Some(table) = self.active_catalog().get(&from.primary.name) else {
2613 return Ok(None);
2614 };
2615 if table.schema().row_security {
2616 return Ok(None);
2617 }
2618 // Rows frozen to the cold tier are not in `headers`, so the
2619 // header count would miss them. Caught by the cold-tier e2e.
2620 if table.has_cold_rows_fast() {
2621 return Ok(None);
2622 }
2623 let n = table.count_visible(&self.current_snapshot());
2624 let col = alias.clone().unwrap_or_else(|| String::from("count"));
2625 Ok(Some(QueryResult::Rows {
2626 columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2627 rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2628 i64::try_from(n).unwrap_or(i64::MAX)
2629 )])],
2630 }))
2631 }
2632
2633 /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2634 /// that col>` served from the index, never reading a row.
2635 ///
2636 /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2637 /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2638 /// count (2x at 1k). PG needs its visibility map for this — a heap
2639 /// tuple carries its own visibility, so an index entry alone cannot
2640 /// say whether the row is live, and PG reads the heap for any page
2641 /// the map does not mark all-visible. SPG keeps a header array
2642 /// beside the rows, so the locator answers it directly and there is
2643 /// no map to be stale.
2644 /// v7.39 (round 564) — the shape test, once, for both the
2645 /// materialising scan and the streaming one.
2646 ///
2647 /// Two callers asking the same question in two places is how a fact
2648 /// starts drifting; the answer here is the single copy. Returns the
2649 /// table, the alias the predicate is written against, the projected
2650 /// column's position, and the name the single output column takes.
2651 pub(crate) fn index_only_shape<'s>(
2652 &'s self,
2653 stmt: &'s SelectStatement,
2654 ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2655 use spg_sql::ast::SelectItem;
2656 if !stmt.ctes.is_empty()
2657 || !stmt.unions.is_empty()
2658 || stmt.group_by.is_some()
2659 || stmt.having.is_some()
2660 || stmt.distinct
2661 || stmt.locking.is_some()
2662 || !stmt.order_by.is_empty()
2663 || stmt.limit.is_some()
2664 || stmt.offset.is_some()
2665 || stmt.items.len() != 1
2666 {
2667 return None;
2668 }
2669 let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2670 return None;
2671 };
2672 if !from.joins.is_empty()
2673 || from.primary.lateral_subquery.is_some()
2674 || from.primary.unnest_expr.is_some()
2675 || from.primary.generate_series_args.is_some()
2676 || from.primary.name.is_empty()
2677 || from.primary.name.starts_with("__spg_")
2678 {
2679 return None;
2680 }
2681 // v7.39 (round 645) — see the note on the sibling shortcut above:
2682 // an inheritance parent's own header count is not the answer.
2683 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2684 return None;
2685 }
2686 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2687 return None;
2688 };
2689 let spg_sql::ast::Expr::Column(c) = expr else {
2690 return None;
2691 };
2692 let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2693 if let Some(q) = c.qualifier.as_deref()
2694 && !q.eq_ignore_ascii_case(alias_name)
2695 {
2696 return None;
2697 }
2698 let table = self.active_catalog().get(&from.primary.name)?;
2699 if table.schema().row_security {
2700 return None;
2701 }
2702 let cols = &table.schema().columns;
2703 let pos = cols
2704 .iter()
2705 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2706 let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2707 Some((table, alias_name, pos, out))
2708 }
2709
2710 /// v7.39 (round 565) — would this statement be answered out of the
2711 /// index alone?
2712 ///
2713 /// EXPLAIN has to name the node the executor will actually run, and
2714 /// the only honest way to know is to ask the same two questions the
2715 /// executor asks: the statement's shape, and everything decidable
2716 /// about the scan before it walks. Neither is re-stated here.
2717 pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2718 let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2719 return false;
2720 };
2721 let Some(where_) = stmt.where_.as_ref() else {
2722 return false;
2723 };
2724 crate::index_access::index_only_precheck(
2725 where_,
2726 &table.schema().columns,
2727 table,
2728 alias_name,
2729 pos,
2730 self.speaks_mysql,
2731 )
2732 .is_some()
2733 }
2734
2735 fn try_index_only_scan(
2736 &self,
2737 stmt: &SelectStatement,
2738 ) -> Result<Option<QueryResult>, EngineError> {
2739 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2740 return Ok(None);
2741 };
2742 // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2743 // are not materialised here, and a partition parent's own
2744 // heap/indexes are empty (its rows live in the children).
2745 if !stmt.ctes.is_empty() {
2746 return Ok(None);
2747 }
2748 if let Some(from) = &stmt.from
2749 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2750 {
2751 return Ok(None);
2752 }
2753 let where_ = stmt.where_.as_ref().expect("shape checked it");
2754 let cols = &table.schema().columns;
2755 let Some(values) = crate::index_access::try_index_only_range(
2756 where_,
2757 cols,
2758 table,
2759 alias_name,
2760 &self.current_snapshot(),
2761 pos,
2762 self.speaks_mysql,
2763 ) else {
2764 return Ok(None);
2765 };
2766 let schema = alloc::vec![ColumnSchema::new(
2767 out_name,
2768 cols[pos].ty,
2769 cols[pos].nullable
2770 )];
2771 Ok(Some(QueryResult::Rows {
2772 columns: schema,
2773 rows: values
2774 .into_iter()
2775 .map(|v| Row::new(alloc::vec![v]))
2776 .collect(),
2777 }))
2778 }
2779
2780 /// v7.39 (round 564) — the same scan, emitting each value instead of
2781 /// building a `Vec<Row>` for the encoder to walk once and drop.
2782 ///
2783 /// A profile of the server serving a 50k-row range put 10.2% of the
2784 /// connection thread's CPU on BUILDING that vector and another 9.7%
2785 /// on dropping it — a fifth of the query, spent allocating and
2786 /// freeing one single-element `Vec` per output row so that the wire
2787 /// encoder could borrow each value for a few nanoseconds. The
2788 /// streaming interface it then hands them to takes `&[Value]`
2789 /// already.
2790 ///
2791 /// Returns `None` when the shape does not apply, so the caller falls
2792 /// back before anything has been emitted.
2793 pub(crate) fn try_index_only_stream<F>(
2794 &self,
2795 stmt: &SelectStatement,
2796 emit: &mut F,
2797 ) -> Result<Option<usize>, EngineError>
2798 where
2799 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2800 {
2801 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2802 return Ok(None);
2803 };
2804 // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2805 // are not materialised here, and a partition parent's own
2806 // heap/indexes are empty (its rows live in the children).
2807 if !stmt.ctes.is_empty() {
2808 return Ok(None);
2809 }
2810 if let Some(from) = &stmt.from
2811 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2812 {
2813 return Ok(None);
2814 }
2815 let where_ = stmt.where_.as_ref().expect("shape checked it");
2816 let cols = &table.schema().columns;
2817 let schema = alloc::vec![ColumnSchema::new(
2818 out_name,
2819 cols[pos].ty,
2820 cols[pos].nullable
2821 )];
2822 let snapshot = self.current_snapshot();
2823 // The header goes out only once the walk has agreed to run — a
2824 // shape rejection after it would leave the client with a
2825 // RowDescription for a result that never comes.
2826 let mut wrote_header = false;
2827 let counted = crate::index_access::index_only_range_each(
2828 where_,
2829 cols,
2830 table,
2831 alias_name,
2832 &snapshot,
2833 pos,
2834 self.speaks_mysql,
2835 &mut |v: spg_storage::Value<'_>| {
2836 if !wrote_header {
2837 emit(crate::StreamItem::Header(&schema))?;
2838 wrote_header = true;
2839 }
2840 emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2841 },
2842 );
2843 match counted {
2844 None => Ok(None),
2845 Some(Err(e)) => Err(e),
2846 Some(Ok(n)) => {
2847 if !wrote_header {
2848 emit(crate::StreamItem::Header(&schema))?;
2849 }
2850 Ok(Some(n))
2851 }
2852 }
2853 }
2854
2855 /// `DISTINCT ON`'s de-duplication, which runs after the inner
2856 /// SELECT has produced its rows.
2857 ///
2858 /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2859 /// reason round 848 established: a debug build gives every branch's
2860 /// locals a slot in the frame whichever branch runs, and this one is
2861 /// eighty lines of hashing, key slicing and survivor sorting that a
2862 /// statement without `DISTINCT ON` never touches. Round 867
2863 /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2864 /// reaches none of it — the segment that had been blamed on
2865 /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2866 #[inline(never)]
2867 fn apply_distinct_on(
2868 &self,
2869 result: QueryResult,
2870 don_hidden: usize,
2871 don_limit: &(
2872 Option<spg_sql::ast::LimitExpr>,
2873 Option<spg_sql::ast::LimitExpr>,
2874 ),
2875 don_top1: usize,
2876 orig_order_by: &[spg_sql::ast::OrderBy],
2877 ) -> Result<QueryResult, EngineError> {
2878 let QueryResult::Rows { columns, rows } = result else {
2879 return Ok(result);
2880 };
2881 // The keys are the hidden trailing columns appended above.
2882 // v7.39 (round 729) — top-1 mode: the trailing columns are the
2883 // DON keys plus the ORDER tail; keep each group's best in one
2884 // hash pass, then sort the SURVIVORS with the original spec.
2885 let mut kept: alloc::vec::Vec<Row<'static>>;
2886 let key_start;
2887 if don_top1 > 0 {
2888 let tail = don_top1 - 1;
2889 key_start = columns.len().saturating_sub(don_hidden + tail);
2890 let ord_start = key_start + don_hidden;
2891 let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2892 .iter()
2893 .map(|o| (o.desc, o.nulls_first))
2894 .collect();
2895 let mysql = self.speaks_mysql;
2896 let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2897 for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2898 let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2899 let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2900 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2901 core::cmp::Ordering::Less => return true,
2902 core::cmp::Ordering::Greater => return false,
2903 core::cmp::Ordering::Equal => {}
2904 }
2905 }
2906 false
2907 };
2908 let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2909 let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2910 let mut keybuf = String::new();
2911 for row in rows {
2912 keybuf.clear();
2913 for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2914 aggregate::push_canonical_key(&mut keybuf, v);
2915 }
2916 match slot.get(keybuf.as_str()) {
2917 Some(&i) => {
2918 if better(&row, &best[i]) {
2919 best[i] = row;
2920 }
2921 }
2922 None => {
2923 slot.insert(keybuf.clone(), best.len());
2924 best.push(row);
2925 }
2926 }
2927 }
2928 // Survivors sort with the FULL original spec (keys are still
2929 // aboard as hidden columns).
2930 let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2931 .iter()
2932 .map(|o| (o.desc, o.nulls_first))
2933 .collect();
2934 best.sort_by(|a, b| {
2935 for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2936 let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2937 let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2938 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2939 core::cmp::Ordering::Equal => {}
2940 o => return o,
2941 }
2942 }
2943 core::cmp::Ordering::Equal
2944 });
2945 for r in &mut best {
2946 r.values.truncate(key_start);
2947 }
2948 kept = best;
2949 } else {
2950 key_start = columns.len().saturating_sub(don_hidden);
2951 let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2952 kept = alloc::vec::Vec::new();
2953 for mut row in rows {
2954 let key: alloc::vec::Vec<Value<'static>> =
2955 row.values.get(key_start..).unwrap_or(&[]).to_vec();
2956 if seen.iter().any(|k| k == &key) {
2957 continue;
2958 }
2959 seen.push(key);
2960 row.values.truncate(key_start);
2961 kept.push(row);
2962 }
2963 }
2964 let mut columns = columns;
2965 columns.truncate(key_start);
2966 // PG limits what DISTINCT ON left, not what fed it.
2967 let kept = apply_deferred_limit(kept, don_limit);
2968 Ok(QueryResult::Rows {
2969 columns,
2970 rows: kept,
2971 })
2972 }
2973
2974 pub(crate) fn exec_select_cancel_as(
2975 &self,
2976 stmt: &SelectStatement,
2977 cancel: CancelToken<'_>,
2978 as_role: Option<&str>,
2979 ) -> Result<QueryResult, EngineError> {
2980 // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2981 // <all columns>` is legal PG (the wildcard expands to grouped
2982 // columns); SPG refused the whole shape. Expand the wildcard
2983 // into explicit column refs up front — the aggregate layer's
2984 // existing "must appear in the GROUP BY clause" validation
2985 // then answers PG's sentence for any non-grouped column.
2986 if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2987 return self.exec_select_cancel_as(&expanded, cancel, as_role);
2988 }
2989 // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2990 // a row.
2991 //
2992 // The aggregate layer already short-circuits this to
2993 // `rows.len()`, so the O(1) part was never the problem — the
2994 // cost is UPSTREAM, materialising every visible row so that
2995 // layer can take its length. Measured over pgwire on 500k rows:
2996 // PG18 8.2 ms with two parallel workers, 10.3 ms with
2997 // parallelism off, SPG 16.5 ms — 1.6x slower than a
2998 // single-threaded PG on the commonest aggregate there is, and no
2999 // ledger entry recorded it.
3000 //
3001 // Counting visible HEADERS needs no row at all. PG cannot do
3002 // this: its visibility lives in the heap tuples themselves, so
3003 // it has to read them (that is why its own count(*) is a full
3004 // scan, parallel or not).
3005 // v7.39 (read01 round 57) — the table-privilege gate on the common
3006 // read core. A superuser session returns from it immediately.
3007 // v7.39 (round 529) — resolve an ORDER BY that names an output
3008 // ALIAS. The statement-level pass never reached a SELECT nested in
3009 // a FROM clause, a CTE or a scalar subquery, so the same query
3010 // worked on its own and failed the moment anything wrapped it —
3011 // which is what generated SQL does constantly.
3012 let aliased;
3013 let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
3014 let mut s = stmt.clone();
3015 crate::orderby::resolve_order_by_position(&mut s);
3016 aliased = s;
3017 &aliased
3018 } else {
3019 stmt
3020 };
3021 // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
3022 //
3023 // Its keys were evaluated against the PROJECTED row, so a key that
3024 // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
3025 // ORDER BY g, v DESC`, the canonical "latest row per group" — could
3026 // not be read at all and the query failed. PG evaluates them on the
3027 // input. They are projected as hidden columns here and stripped
3028 // again below, the same way the grouping-set ordering columns
3029 // already travel.
3030 //
3031 // And the dedup ran AFTER the inner statement's LIMIT, so
3032 // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
3033 // PG answers two: the limit had already taken two rows of the same
3034 // group before anything deduplicated them. A paginated DISTINCT ON
3035 // returned short pages, with no error. The limit is deferred to
3036 // after the dedup, which is PG's order.
3037 let don_stmt;
3038 // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
3039 // order spec (the rewritten stmt's is emptied).
3040 let orig_order_by = stmt.order_by.clone();
3041 let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
3042 (stmt, 0, (None, None), 0usize)
3043 } else {
3044 let mut s = stmt.clone();
3045 let hidden = s.distinct_on.len();
3046 for (i, e) in stmt.distinct_on.iter().enumerate() {
3047 s.items.push(SelectItem::Expr {
3048 expr: e.clone(),
3049 alias: Some(alloc::format!("__distinct_on_{i}")),
3050 });
3051 }
3052 // v7.39 (round 729) — group-top-1 short circuit. When the
3053 // DISTINCT ON keys are exactly the ORDER BY's leading keys,
3054 // the answer is "per group, the row that wins the remaining
3055 // order" — a single O(n) hash pass. The old path sorted the
3056 // ENTIRE input first (500k rows, ~180 ms on the panel cell)
3057 // to keep 100. The inner query runs UNSORTED with every
3058 // order key appended as a hidden column; the dedup below
3059 // keeps each group's best, then sorts the SURVIVORS.
3060 // Declared-collation order keys stay on the sorting path
3061 // (the value comparator here is collation-blind).
3062 let prefix_matches = s.order_by.len() >= hidden
3063 && stmt
3064 .distinct_on
3065 .iter()
3066 .zip(s.order_by.iter())
3067 .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3068 let colls_plain =
3069 crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3070 .map(|cs| cs.iter().all(Option::is_none))
3071 .unwrap_or(false);
3072 let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3073 let tail = s.order_by.len() - hidden;
3074 for (j, o) in s.order_by[hidden..].iter().enumerate() {
3075 s.items.push(SelectItem::Expr {
3076 expr: o.expr.clone(),
3077 alias: Some(alloc::format!("__don_ord_{j}")),
3078 });
3079 }
3080 // Carry the tail's direction flags through the aliases'
3081 // ORDER; the survivors re-sort below with the full spec.
3082 s.order_by = Vec::new();
3083 tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3084 } else {
3085 0
3086 };
3087 // Only a folded literal is deferred; a placeholder or an
3088 // expression keeps the path it has today rather than being
3089 // resolved a second way here.
3090 let deferrable = matches!(
3091 (&s.limit, &s.offset),
3092 (
3093 None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3094 None | Some(spg_sql::ast::LimitExpr::Literal(_))
3095 )
3096 );
3097 let deferred = if deferrable {
3098 (s.limit.take(), s.offset.take())
3099 } else {
3100 (None, None)
3101 };
3102 don_stmt = s;
3103 (&don_stmt, hidden, deferred, top1_tail)
3104 };
3105 self.acl_check_select_as(stmt, as_role)?;
3106 validate_aggregate_placement(stmt)?;
3107 // BEFORE the fast paths below, not after: a name that resolves to
3108 // nothing is not a question the count fast path or the index-only
3109 // scan should get to answer first. Placed after them at first,
3110 // and the two of them swallowed `WHERE` and `ORDER BY` while
3111 // `GROUP BY` and `HAVING`, which cannot take those routes, raised
3112 // — the same statement answering two ways depending on the plan.
3113 self.validate_clause_columns(stmt)?;
3114 self.validate_function_arity(stmt)?;
3115 // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3116 // privilege gate above. Placed before it at first, and the
3117 // security-definer e2e caught it immediately: a SECURITY INVOKER
3118 // function whose body is `SELECT count(*) FROM t` answered
3119 // instead of being refused, because the fast path never reached
3120 // the check.
3121 if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3122 return Ok(r);
3123 }
3124 // v7.39 (round 560) — an index-only range scan. Same placement
3125 // reasoning as the count above: after the privilege gate.
3126 if let Some(r) = self.try_index_only_scan(stmt)? {
3127 return Ok(r);
3128 }
3129 validate_locking_clause(stmt)?;
3130 let result = self.exec_select_cancel_inner(stmt, cancel)?;
3131 // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3132 // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3133 // They carry the per-branch mask through the UNION-ALL sort and must not
3134 // appear in the output. Stripped per SELECT level (grouping-set queries
3135 // are often wrapped in a derived subquery), before DISTINCT ON.
3136 let result = strip_synthetic_order_cols(result);
3137 // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3138 // rows arrive here already ORDER BY'd; keep the FIRST row of
3139 // each group the expressions define (PG semantics). The
3140 // expressions evaluate against the projected schema — an
3141 // expression that isn't in the select list errors honestly.
3142 if stmt.distinct_on.is_empty() {
3143 return Ok(result);
3144 }
3145 self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3146 }
3147
3148 /// The UNION chain: execute the head as a bare block, then fold each
3149 /// peer in with left-associative dedup.
3150 ///
3151 /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3152 /// reason round 848 established. A statement with no unions returns
3153 /// one line above the call — and every nested subquery on a deep
3154 /// path is such a statement, so each level of the recursion carried
3155 /// 170 lines of locals it could not reach. Round 867 measured that
3156 /// frame at 34,800 bytes, the largest single one on the descent,
3157 /// after two earlier attributions had blamed its caller and then its
3158 /// callee: the gap between two marks is the frame of everything
3159 /// BETWEEN them, and this function had no mark of its own.
3160 #[inline(never)]
3161 fn exec_union_chain(
3162 &self,
3163 stmt_ref: &SelectStatement,
3164 stmt: &SelectStatement,
3165 cancel: CancelToken<'_>,
3166 ) -> Result<QueryResult, EngineError> {
3167 // UNION path: clone-strip the head into a bare block (its own
3168 // DISTINCT and any inner ORDER BY are dropped by parser rule —
3169 // the wrapper SelectStatement carries them), execute, then chain
3170 // peers with left-associative dedup semantics.
3171 // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3172 // output columns; a position past their count is PG's 42P10.
3173 crate::orderby::check_order_by_positions(stmt_ref)?;
3174 let mut head_unknown = branch_unknown_mask(stmt_ref);
3175 let head_regcast = branch_regcast_mask(stmt_ref);
3176 let mut head = stmt_ref.clone();
3177 head.unions = Vec::new();
3178 head.order_by = Vec::new();
3179 head.limit = None;
3180 let QueryResult::Rows {
3181 mut columns,
3182 mut rows,
3183 } = self.exec_bare_select_cancel(&head, cancel)?
3184 else {
3185 unreachable!("bare SELECT cannot return CommandOk")
3186 };
3187 for (kind, peer) in &stmt_ref.unions {
3188 // v7.37.17 (17.6 siblings) — a peer carrying its own
3189 // unions is a nested INTERSECT group (the parser's
3190 // precedence regrouping); recurse through the
3191 // union-aware wrapper for it.
3192 let peer_result = if peer.unions.is_empty() {
3193 self.exec_bare_select_cancel(peer, cancel)?
3194 } else {
3195 self.exec_select_cancel(peer, cancel)?
3196 };
3197 let QueryResult::Rows {
3198 columns: peer_cols,
3199 rows: mut peer_rows,
3200 } = peer_result
3201 else {
3202 unreachable!("bare SELECT cannot return CommandOk")
3203 };
3204 if peer_cols.len() != columns.len() {
3205 // v7.39 (round 232) — PG's wording, which clients match on.
3206 return Err(EngineError::Unsupported(alloc::format!(
3207 "each {} query must have the same number of columns",
3208 set_op_name(*kind)
3209 )));
3210 }
3211 // v7.39 (round 232+233) — PG resolves each result column to one
3212 // type before it merges anything, and refuses the query when the
3213 // two branches have no common type. SPG's unifier
3214 // (`unify_union_columns`) is value-driven and deliberately
3215 // conservative — "a column where any cell fails to coerce is left
3216 // exactly as it was" — so a mismatch produced a column holding
3217 // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3218 // back with integers and text interleaved) instead of an error.
3219 //
3220 // The check has to read the branch ASTs, not just their schemas:
3221 // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3222 // as TEXT and is indistinguishable from a real text column by
3223 // schema alone — yet PG treats the two completely differently
3224 // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3225 // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3226 let peer_unknown = branch_unknown_mask(peer);
3227 let peer_regcast = branch_regcast_mask(peer);
3228 for i in 0..columns.len() {
3229 let hu = head_unknown.get(i).copied().unwrap_or(false);
3230 let pu = peer_unknown.get(i).copied().unwrap_or(false);
3231 let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3232 let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3233 || head_regcast.get(i).copied().unwrap_or(false);
3234 match (hu, pu) {
3235 // Both sides carry a real type: they must share a category.
3236 (false, false) => {
3237 if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3238 return Err(EngineError::Unsupported(alloc::format!(
3239 "{} types {} and {} cannot be matched",
3240 set_op_name(*kind),
3241 crate::conversions::pg_type_name_for_error(ht),
3242 crate::conversions::pg_type_name_for_error(pt),
3243 )));
3244 }
3245 }
3246 // One side is an untyped literal: it takes the other's
3247 // type, and failing to convert is the error PG reports.
3248 (true, false) => {
3249 coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3250 columns[i].ty = pt;
3251 head_unknown[i] = false;
3252 }
3253 (false, true) => {
3254 coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3255 }
3256 // Both untyped — nothing to resolve against yet.
3257 (true, true) => {}
3258 }
3259 }
3260 // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3261 // nullable (PG semantics). Previously the result kept only the head's
3262 // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3263 // non-null `1`) wrongly reported the column NOT NULL, which let
3264 // `count(col)`'s NOT-NULL fast-path count the NULL row.
3265 for (i, pc) in peer_cols.iter().enumerate() {
3266 if pc.nullable {
3267 columns[i].nullable = true;
3268 }
3269 }
3270 // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3271 // text by the session collation (CI + accent + PAD SPACE), like
3272 // GROUP BY. PG stays byte-exact.
3273 let mysql = self.speaks_mysql;
3274 // v7.38.14 — the mask, which 7.38.13 recorded as impossible here
3275 // and was wrong about. `columns` and `peer_cols` are both in
3276 // scope; what was actually missing is that the branches' output
3277 // schemas did not CARRY the collation, so a mask built from them
3278 // would have marked every column byte-wise. Unifying the
3279 // projection-to-schema conversion fixed the supply side, and the
3280 // mask is now buildable from what was always there.
3281 //
3282 // Either side byte-wise keeps the position byte-wise, mirroring
3283 // `eval::resolve::mysql_text_fold_applies`: a set operation
3284 // between a folding column and a declared-binary one must not
3285 // quietly fold the binary one's values away.
3286 let set_mask: alloc::vec::Vec<bool> = columns
3287 .iter()
3288 .zip(peer_cols.iter())
3289 .map(|(l, r)| {
3290 matches!(l.collation, spg_storage::Collation::Binary)
3291 || matches!(r.collation, spg_storage::Collation::Binary)
3292 })
3293 .collect();
3294 let fold = FoldSpec::of(mysql, &set_mask);
3295 match kind {
3296 UnionKind::All => rows.extend(peer_rows),
3297 UnionKind::Distinct => {
3298 rows.extend(peer_rows);
3299 rows = dedup_rows(rows, fold);
3300 }
3301 // v7.37.17 (17.6 siblings) — PG set semantics.
3302 // v7.39 (round 591) — all four ask the same question of the
3303 // right side, and all four used to answer it by scanning it
3304 // once per left row. `PeerIndex` buckets it by the hash
3305 // DISTINCT already uses, so the answer is a lookup.
3306 // INTERSECT: distinct rows present on both sides.
3307 UnionKind::Intersect => {
3308 let idx = PeerIndex::build(&peer_rows, fold);
3309 rows = dedup_rows(rows, fold)
3310 .into_iter()
3311 .filter(|r| idx.contains(r))
3312 .collect();
3313 }
3314 // INTERSECT ALL: multiset intersection — each row
3315 // keeps min(left count, right count) occurrences.
3316 UnionKind::IntersectAll => {
3317 let mut idx = PeerIndex::build(&peer_rows, fold);
3318 let mut kept: Vec<Row<'static>> = Vec::new();
3319 for r in rows {
3320 if idx.take_one(&r) {
3321 kept.push(r);
3322 }
3323 }
3324 rows = kept;
3325 }
3326 // EXCEPT: distinct left rows absent from the right.
3327 UnionKind::Except => {
3328 let idx = PeerIndex::build(&peer_rows, fold);
3329 rows = dedup_rows(rows, fold)
3330 .into_iter()
3331 .filter(|r| !idx.contains(r))
3332 .collect();
3333 }
3334 // EXCEPT ALL: multiset subtraction — each right
3335 // occurrence cancels one left occurrence.
3336 UnionKind::ExceptAll => {
3337 let mut idx = PeerIndex::build(&peer_rows, fold);
3338 let mut kept: Vec<Row<'static>> = Vec::new();
3339 for r in rows {
3340 if !idx.take_one(&r) {
3341 kept.push(r);
3342 }
3343 }
3344 rows = kept;
3345 }
3346 }
3347 }
3348 // PG resolves a UNION / VALUES result column to one common type
3349 // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3350 // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3351 // built each branch independently, leaving mixed-type columns
3352 // that broke ORDER BY, comparisons, and value-based window
3353 // frames. Unify + coerce before the combined ORDER BY sees them.
3354 unify_union_columns(&mut columns, &mut rows);
3355 // ORDER BY at the top of a UNION applies to the combined result.
3356 // Eval against the projected schema (NOT the source table).
3357 if !stmt.order_by.is_empty() {
3358 // v7.39 (read01 round 54) — the combined-result ctx must carry the
3359 // catalog, and the projected columns must keep their enum identity
3360 // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3361 // by TEXT instead of member order — silently wrong rows, not an
3362 // error. (Same shape as the enum-order knife's GROUP BY fix.)
3363 let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3364 // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3365 // survive to here when the head projects a Wildcard (the
3366 // group-tail wrapper shape): map them onto the Nth
3367 // projected column so the combined sort works.
3368 let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3369 .order_by
3370 .iter()
3371 .map(|o| {
3372 let mut o = o.clone();
3373 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3374 && *n >= 1
3375 && let Ok(idx) = usize::try_from(*n - 1)
3376 && idx < columns.len()
3377 {
3378 o.expr = Expr::Column(spg_sql::ast::ColumnName {
3379 qualifier: None,
3380 name: columns[idx].name.clone(),
3381 });
3382 }
3383 o
3384 })
3385 .collect();
3386 let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3387 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3388 for r in rows {
3389 // v7.39.12 — a correlated subquery in ORDER BY is resolved
3390 // for this row before the key is built; see
3391 // `Engine::order_by_resolved_for_row`.
3392 let per_row =
3393 self.order_by_resolved_for_row(&resolved_order, &r, &synth_ctx, cancel)?;
3394 let keys = build_order_keys(
3395 per_row.as_deref().unwrap_or(&resolved_order),
3396 &r,
3397 &synth_ctx,
3398 )?;
3399 tagged.push((keys, r));
3400 }
3401 sort_by_keys(&mut tagged, &descs);
3402 rows = tagged.into_iter().map(|(_, r)| r).collect();
3403 }
3404 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3405 Ok(QueryResult::Rows { columns, rows })
3406 }
3407
3408 fn exec_select_cancel_inner(
3409 &self,
3410 stmt: &SelectStatement,
3411 cancel: CancelToken<'_>,
3412 ) -> Result<QueryResult, EngineError> {
3413 cancel.check()?;
3414 // v7.38 P0 元机制 A — first observable point inside the
3415 // planner / executor. Tests use this to inject a delay or
3416 // a cancellation race before any row is produced. Release
3417 // build expands to `let _ = (...);` — zero cost.
3418 crate::injection_point!("planner_first_row_fetch", &stmt.from);
3419 // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3420 // PG analyses every definition, referenced or not, so `SELECT i FROM
3421 // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3422 // succeeded here (the parser used to drop the unreferenced defs
3423 // whole). The check is the CREATE VIEW check's shape (round 700): a
3424 // LIMIT-0 run of the same FROM with the definitions' key
3425 // expressions as the projection — it cannot disagree with what a
3426 // referencing window would have done, because it resolves the same
3427 // names the same way. Zero cost for the ordinary statement: the
3428 // list is empty unless a WINDOW clause left unreferenced defs.
3429 if !stmt.window_check_exprs.is_empty() {
3430 let mut probe = stmt.clone();
3431 probe.items = stmt
3432 .window_check_exprs
3433 .iter()
3434 .map(|e| spg_sql::ast::SelectItem::Expr {
3435 expr: e.clone(),
3436 alias: None,
3437 })
3438 .collect();
3439 probe.window_check_exprs = Vec::new();
3440 probe.distinct = false;
3441 probe.distinct_on = Vec::new();
3442 probe.group_by = None;
3443 probe.group_by_all = false;
3444 probe.having = None;
3445 probe.unions = Vec::new();
3446 probe.order_by = Vec::new();
3447 probe.locking = None;
3448 probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3449 probe.offset = None;
3450 probe.limit_with_ties = false;
3451 self.exec_select_cancel_inner(&probe, cancel)?;
3452 }
3453 // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3454 // takes the catalog, so the parser leaves a marker and the rewrite lands
3455 // here: the call moves into a LATERAL FROM item and the item becomes one
3456 // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3457 // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3458 // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3459 // second one.
3460 if let Some(lowered) = self.lower_record_expansion(stmt)? {
3461 return self.exec_select_cancel_inner(&lowered, cancel);
3462 }
3463 // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3464 // FROM / JOIN graph references any catalogued view name,
3465 // re-parse the view body and prepend it as a synthetic
3466 // CTE. Recurses on views-in-views via the regular CTE
3467 // dispatch below. Fast-path: skip the walker entirely when
3468 // the catalog has no views (the typical OLTP load).
3469 if !self.active_catalog().views_all().is_empty() {
3470 if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3471 return self.exec_select_cancel(&rewritten, cancel);
3472 }
3473 }
3474 // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3475 // gets rewritten to a UNION-ALL over the children that overlap
3476 // the WHERE-derived key range. Uses the same CTE-injection
3477 // trick as VIEW expansion above so downstream resolution
3478 // doesn't need a partition-aware code path.
3479 if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3480 return self.exec_select_cancel(&rewritten, cancel);
3481 }
3482 // v7.16.2 — information_schema / pg_catalog virtual
3483 // views (mailrs round-10 A.3). If the SELECT touches a
3484 // synthetic meta-table name (`__spg_info_*` /
3485 // `__spg_pg_*` — produced by the parser for
3486 // `information_schema.X` / `pg_catalog.X`), clone the
3487 // catalog, materialise the requested view as a real
3488 // temporary table, and re-execute against an enriched
3489 // engine. Same pattern as `exec_with_ctes` for CTEs.
3490 if !self.meta_views_materialised && select_references_meta_view(stmt) {
3491 return self.exec_select_with_meta_views(stmt, cancel);
3492 }
3493 // v6.10.2 — cold-tier time-travel short-circuit. When the
3494 // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3495 // dedicated cold-segment scan instead of the regular
3496 // hot+index path. The scope is intentionally narrow for
3497 // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3498 // optionally with a single-column-equality WHERE. JOINs /
3499 // aggregates / ORDER BY / subqueries on top of a time-
3500 // travelled scan are STABILITY § "Out of v6.10".
3501 if let Some(from) = &stmt.from
3502 && let Some(seg_id) = from.primary.as_of_segment
3503 {
3504 return self.exec_select_as_of_segment(stmt, from, seg_id);
3505 }
3506 // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3507 // pre-CTE because they don't read from the catalog and
3508 // shouldn't participate in regular FROM resolution.
3509 // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3510 // short-circuits. A meta-view FROM materialises to a fixed row
3511 // set. For a bare `SELECT *` we return it directly; otherwise we
3512 // stage it as a temp table and run the normal pipeline, so
3513 // projection / WHERE / ORDER BY / aggregates work over these views
3514 // (they were `SELECT *`-only before). A real table shadowing the
3515 // name wins (checked first), which also stops the staged re-run
3516 // from recursing back into meta-view detection.
3517 if let Some(from) = &stmt.from
3518 && from.joins.is_empty()
3519 && self.active_catalog().get(&from.primary.name).is_none()
3520 {
3521 let lower = from.primary.name.to_ascii_lowercase();
3522 if let Some(result) = self.meta_view_result(&lower) {
3523 let bare = stmt.where_.is_none()
3524 && stmt.group_by.is_none()
3525 && stmt.having.is_none()
3526 && stmt.unions.is_empty()
3527 && stmt.order_by.is_empty()
3528 && stmt.limit.is_none()
3529 && stmt.offset.is_none()
3530 && !stmt.distinct
3531 && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3532 if bare {
3533 return Ok(result);
3534 }
3535 if let QueryResult::Rows { columns, rows } = result {
3536 let mut catalog = self.active_catalog().clone();
3537 let cols = infer_column_types(&columns, &rows);
3538 let schema = TableSchema::new(from.primary.name.clone(), cols);
3539 catalog.create_table(schema).map_err(EngineError::Storage)?;
3540 let t = catalog
3541 .get_mut(&from.primary.name)
3542 .expect("just-created meta-view table must exist");
3543 for row in rows {
3544 t.insert(row).map_err(EngineError::Storage)?;
3545 }
3546 let mut eng = Engine::restore(catalog);
3547 if let Some(c) = self.clock {
3548 eng = eng.with_clock(c);
3549 }
3550 if let Some(f) = self.salt_fn {
3551 eng = eng.with_salt_fn(f);
3552 }
3553 // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3554 // connection identity so `WHERE pid = pg_backend_pid()`
3555 // matches inside the staged meta-view run.
3556 if let Some(f) = self.backend_pid_fn {
3557 eng.set_backend_pid_fn(f);
3558 }
3559 return eng.exec_select_cancel(stmt, cancel);
3560 }
3561 return Ok(result);
3562 }
3563 }
3564 // v4.11: CTEs materialise into a temporary enriched catalog
3565 // *before* anything else — the body SELECT can then refer
3566 // to CTE names via the regular FROM-clause resolution.
3567 // Uncorrelated only: each CTE body runs once against the
3568 // current catalog, not against later CTEs' results (left-
3569 // to-right materialisation would relax this, but we keep
3570 // it simple for v4.11 MVP).
3571 if !stmt.ctes.is_empty() {
3572 return self.exec_with_ctes(stmt, cancel);
3573 }
3574 // v4.10: subqueries (uncorrelated) are resolved here, before
3575 // the executor sees the row loop. We clone the statement so
3576 // we can mutate without disturbing the caller's AST — most
3577 // queries pass through with no subquery nodes and the clone
3578 // is cheap; with subqueries the materialisation cost
3579 // dominates anyway.
3580 let mut stmt_owned;
3581 let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3582 stmt_owned = stmt.clone();
3583 // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3584 // aggregate-wrapped correlated scalar subquery whose
3585 // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3586 // executor streams one join instead of splicing a per-row
3587 // subplan. Runs before the per-row/batch resolver, which then
3588 // only sees the subqueries the pull-up left behind.
3589 self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3590 // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3591 // the "per-key latest" scalar subquery shape (inbox / feed
3592 // / timeline applications) becomes a CTE + LEFT JOIN
3593 // against a GROUP BY pre-aggregation that reuses the v7.33
3594 // first_ordered argmax executor. Runs AFTER unique-key
3595 // pull-up (so the unique-key fast path still wins for
3596 // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3597 // Phase 1 (this commit) is skeleton only — no-op pass.
3598 self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3599 // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3600 // sublink pull-up to semi/anti-join, before the resolver gets
3601 // a chance to walk per-row.
3602 self.pull_up_exists_sublinks(&mut stmt_owned);
3603 // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3604 // exec_with_ctes so they materialise once before the body
3605 // SELECT runs. exec_with_ctes strips ctes from the body
3606 // clone, then re-enters select.
3607 if !stmt_owned.ctes.is_empty() {
3608 return self.exec_with_ctes(&stmt_owned, cancel);
3609 }
3610 // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3611 // SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3612 // BEFORE `resolve_select_subqueries` materialises the inner
3613 // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3614 // INSUBQ benchmark). Run the inner once, collect the result
3615 // values into a `HashSet<i64>` directly, then probe A.pk per
3616 // value and tally. Returns `Some` when the shape matches.
3617 if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3618 return Ok(out);
3619 }
3620 self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3621 &stmt_owned
3622 } else {
3623 stmt
3624 };
3625 if stmt_ref.unions.is_empty() {
3626 return self.exec_bare_select_cancel(stmt_ref, cancel);
3627 }
3628 self.exec_union_chain(stmt_ref, stmt, cancel)
3629 }
3630
3631 #[allow(clippy::too_many_lines)]
3632 #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3633 /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3634 /// Synthesises a single-column virtual table whose column type
3635 /// is TEXT and whose rows are the array elements. Routes
3636 /// through the regular projection / WHERE / ORDER BY / LIMIT
3637 /// machinery so set-returning UNNEST composes naturally with
3638 /// the rest of the SELECT surface.
3639 fn exec_select_unnest(
3640 &self,
3641 stmt: &SelectStatement,
3642 primary: &TableRef,
3643 cancel: CancelToken<'_>,
3644 ) -> Result<QueryResult, EngineError> {
3645 let expr = primary
3646 .unnest_expr
3647 .as_deref()
3648 .expect("caller guards unnest_expr.is_some()");
3649 // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3650 // N value columns instead of one; the shared builder does
3651 // the work and the tail below (WHERE / agg / projection)
3652 // runs against the wider schema.
3653 let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3654 match unnest_zip_args(expr) {
3655 Some(args) => Some(unnest_zip_rows(args)?),
3656 None => None,
3657 };
3658 // Evaluate the array expression once. Empty schema / empty
3659 // row — uncorrelated UNNEST cannot reference outer columns.
3660 // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3661 // introspection family (enum_range / enum_first / enum_last) resolves
3662 // its labels from the argument's STATIC enum type against the
3663 // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3664 // fell through to the generic arm, got NULL, and expanded to zero rows
3665 // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3666 // carry the catalog) worked.
3667 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3668 let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3669 let dummy_row = Row::new(alloc::vec::Vec::new());
3670 // v7.11.13 — unnest dispatches per array element type so
3671 // INT[] / BIGINT[] surface their PG types in projection.
3672 // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3673 // columns (PG: lexeme | positions | weights); everything else
3674 // keeps the alias / "unnest" defaults below.
3675 let mut composite_names: Option<&[&str]> = None;
3676 let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3677 if let Some(m) = multi {
3678 m
3679 } else {
3680 // v7.39 (round 236) — flatten a multidimensional array into
3681 // its row-major elements (PG) before the 1-D-only match.
3682 let unnest_src = {
3683 let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3684 crate::eval::values::flatten_2d(&v).unwrap_or(v)
3685 };
3686 let mut return_multi: Option<(
3687 alloc::vec::Vec<DataType>,
3688 alloc::vec::Vec<Row<'static>>,
3689 )> = None;
3690 let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3691 {
3692 Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3693 Value::TextArray(items) => {
3694 let rows = items
3695 .into_iter()
3696 .map(|item| {
3697 Row::new(alloc::vec![match item {
3698 Some(s) => Value::text(s),
3699 None => Value::Null,
3700 }])
3701 })
3702 .collect();
3703 (DataType::Text, rows)
3704 }
3705 Value::IntArray(items) => {
3706 let rows = items
3707 .into_iter()
3708 .map(|item| {
3709 Row::new(alloc::vec![match item {
3710 Some(n) => Value::Int(n),
3711 None => Value::Null,
3712 }])
3713 })
3714 .collect();
3715 (DataType::Int, rows)
3716 }
3717 Value::BigIntArray(items) => {
3718 let rows = items
3719 .into_iter()
3720 .map(|item| {
3721 Row::new(alloc::vec![match item {
3722 Some(n) => Value::BigInt(n),
3723 None => Value::Null,
3724 }])
3725 })
3726 .collect();
3727 (DataType::BigInt, rows)
3728 }
3729 Value::Multirange { kind, ranges } => {
3730 let rows = ranges
3731 .iter()
3732 .map(|sp| {
3733 Row::new(alloc::vec![Value::Range {
3734 kind,
3735 lower: sp.lower.clone(),
3736 upper: sp.upper.clone(),
3737 lower_inc: sp.lower_inc,
3738 upper_inc: sp.upper_inc,
3739 empty: false,
3740 }])
3741 })
3742 .collect();
3743 (DataType::Range(kind), rows)
3744 }
3745 // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3746 // one row per lexeme, PG18-measured columns
3747 // lexeme | positions | weights (`a | {1,3} |
3748 // {D,D}`); a position-less lexeme (a stripped
3749 // vector) reads NULL in both array columns.
3750 Value::TsVector(lexemes) => {
3751 composite_names = Some(&["lexeme", "positions", "weights"]);
3752 let rows = lexemes
3753 .iter()
3754 .map(|l| {
3755 let (pos, wts) = if l.positions.is_empty() {
3756 (Value::Null, Value::Null)
3757 } else {
3758 let letter = match l.weight {
3759 3 => "A",
3760 2 => "B",
3761 1 => "C",
3762 _ => "D",
3763 };
3764 (
3765 Value::SmallIntArray(
3766 l.positions
3767 .iter()
3768 .map(|p| {
3769 Some(i16::try_from(*p).unwrap_or(i16::MAX))
3770 })
3771 .collect(),
3772 ),
3773 Value::TextArray(
3774 l.positions
3775 .iter()
3776 .map(|_| Some(letter.into()))
3777 .collect(),
3778 ),
3779 )
3780 };
3781 Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3782 })
3783 .collect();
3784 return_multi = Some((
3785 alloc::vec![
3786 DataType::Text,
3787 DataType::SmallIntArray,
3788 DataType::TextArray
3789 ],
3790 rows,
3791 ));
3792 (DataType::Text, alloc::vec::Vec::new())
3793 }
3794 // v7.39.11 — every remaining array-family value,
3795 // through the one element menu, so a type does not
3796 // have to be written out here a second time to be
3797 // unnestable. `unnest(ARRAY[1,2]::smallint[])`
3798 // raised "expects an array argument, got
3799 // smallint[]" until this arm — the arms above name
3800 // int / bigint / text / json and stop — and so did
3801 // every catalog vector. Found while closing
3802 // sentori's §4 against 7.39.10.
3803 ref v if crate::eval::values::array_len(v).is_some() => {
3804 let elems = crate::eval::values::array_elements(v).unwrap_or_default();
3805 let dt = elems
3806 .iter()
3807 .find_map(spg_storage::Value::data_type)
3808 .unwrap_or(DataType::Text);
3809 let rows = elems
3810 .into_iter()
3811 .map(|e| Row::new(alloc::vec![e]))
3812 .collect();
3813 (dt, rows)
3814 }
3815 other => {
3816 // v7.39 (round 622, S05a) — see table_access.rs:
3817 // the same sentence, and it is a type mismatch.
3818 return Err(EngineError::Eval(EvalError::TypeMismatch {
3819 detail: alloc::format!(
3820 "unnest() expects an array argument, got {}",
3821 crate::conversions::pg_type_name_for_error_opt(other.data_type())
3822 ),
3823 }));
3824 }
3825 };
3826 if let Some(m) = return_multi {
3827 m
3828 } else {
3829 (alloc::vec![elem_dtype], rows)
3830 }
3831 };
3832 let alias = primary
3833 .alias
3834 .clone()
3835 .unwrap_or_else(|| "unnest".to_string());
3836 // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3837 // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3838 // entries map positionally over the value columns. Without
3839 // the column list, a single column falls back to the table
3840 // alias (pre-v7.13.2 behaviour); multi-arg columns default
3841 // to PG's `unnest`.
3842 let n_vals = dtypes.len();
3843 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3844 .iter()
3845 .enumerate()
3846 .map(|(i, dt)| {
3847 let name = primary
3848 .unnest_column_aliases
3849 .get(i)
3850 .cloned()
3851 .unwrap_or_else(|| {
3852 if let Some(names) = composite_names {
3853 names
3854 .get(i)
3855 .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3856 } else if n_vals == 1 {
3857 alias.clone()
3858 } else {
3859 "unnest".to_string()
3860 }
3861 });
3862 ColumnSchema::new(name, *dt, true)
3863 })
3864 .collect();
3865 // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3866 // parser desugared a base-type-returning function here (see
3867 // TableRef::scalar_fn_item); the marker rides the column so it survives
3868 // every EvalContext an inner stage rebuilds.
3869 if primary.scalar_fn_item && schema_cols.len() == 1 {
3870 schema_cols[0].scalar_row_source = true;
3871 }
3872 // WITH ORDINALITY — trailing BIGINT counting rows from 1
3873 // in element order. The alias entry after the value
3874 // columns renames it (PG default: `ordinality`).
3875 let rows = if primary.with_ordinality {
3876 let ord_name = primary
3877 .unnest_column_aliases
3878 .get(n_vals)
3879 .cloned()
3880 .unwrap_or_else(|| "ordinality".to_string());
3881 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3882 rows.into_iter()
3883 .enumerate()
3884 .map(|(i, row)| {
3885 let mut vals = row.values.clone();
3886 vals.push(Value::BigInt(i as i64 + 1));
3887 Row::new(vals)
3888 })
3889 .collect()
3890 } else {
3891 rows
3892 };
3893 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3894 // `EvalContext::new` drops it and every catalog-dependent cast
3895 // (regclass / enum / composite / domain) silently degrades.
3896 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3897 // Apply WHERE.
3898 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3899 let mut out = alloc::vec::Vec::with_capacity(rows.len());
3900 for row in rows {
3901 cancel.check()?;
3902 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3903 if matches!(v, Value::Bool(true)) {
3904 out.push(row);
3905 }
3906 }
3907 out
3908 } else {
3909 rows
3910 };
3911 // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3912 // unnest source. Same routing the relational scan path
3913 // already takes — without it `SELECT COUNT(*) FROM
3914 // unnest(ARRAY[…])` either errored at projection time or
3915 // returned the wrong shape.
3916 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
3917 // v7.29 — a per-query memo so correlated scalar
3918 // subqueries batch-evaluate once (group map) instead of
3919 // executing per group.
3920 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3921 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3922 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3923 .map_err(|err| match err {
3924 EngineError::Eval(ev) => ev,
3925 other => eval::EvalError::TypeMismatch {
3926 detail: alloc::format!("{other}"),
3927 },
3928 })
3929 };
3930 // v7.39 (round 656) — hand the rows over as they are rather than
3931 // collecting a second vector of `RowRef` wrappers. Note this is
3932 // a set-returning-function path, NOT the relational scan: the
3933 // measured O(rows) cost lived in `run_single_table_aggregate`,
3934 // and converting these four first was a miss that cost a full
3935 // round — every test stayed green and the number did not move.
3936 let agg = aggregate::run(
3937 stmt,
3938 crate::join::AggRows::Owned(&filtered),
3939 &schema_cols,
3940 Some(&alias),
3941 Some(&agg_correlated),
3942 self.parallel_runner.0.as_deref(),
3943 Some(self.active_catalog()),
3944 Some(self),
3945 )?;
3946 return self.finish_agg_result(agg, stmt, cancel);
3947 }
3948 // Projection.
3949 let projection = build_projection(
3950 &stmt.items,
3951 &schema_cols,
3952 &alias,
3953 self.speaks_mysql,
3954 Some(self.active_catalog()),
3955 )?;
3956 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3957 alloc::vec::Vec::with_capacity(filtered.len());
3958 // v7.19 P5 — Set-Returning-Function in projection
3959 // position (PG `SELECT unnest(arr) FROM t` shape). When a
3960 // SELECT item evaluates to a top-level unnest(arr) call,
3961 // expand it: for each input row, evaluate the array, emit
3962 // one output row per element, broadcasting non-SRF
3963 // projections from the same input row. Multi-SRF + LCM
3964 // padding stays a documented carve-out; mailrs uses
3965 // single-SRF for redirect_uris.
3966 // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3967 // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3968 let srf_idxs = self.srf_target_idxs(&projection);
3969 // v7.39 (round 621) — which input row each output row came from. An
3970 // SRF turns one input row into many, and the ORDER BY below used to
3971 // index the EXPANDED rows by the INPUT row's position: the result was
3972 // silently truncated to the input row count and left unsorted, so
3973 // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3974 // answered three of its six rows, in no order. Without the ORDER BY
3975 // the same query was already right.
3976 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3977 if !srf_idxs.is_empty() {
3978 let (rows, src) =
3979 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3980 projected_rows = rows;
3981 src_of_row = src;
3982 } else {
3983 // v7.24 (round-16 B) — select-list subqueries resolve
3984 // per row (correlated-aware; plain exprs take the fast
3985 // path inside).
3986 let mut proj_memo = memoize::MemoizeCache::default();
3987 for row in &filtered {
3988 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3989 for p in &projection {
3990 vals.push(self.eval_expr_with_correlated(
3991 &p.expr,
3992 row,
3993 &scan_ctx,
3994 cancel,
3995 Some(&mut proj_memo),
3996 )?);
3997 }
3998 projected_rows.push(Row::new(vals));
3999 }
4000 }
4001 // ORDER BY / LIMIT — apply on the projected rows (cheap;
4002 // unnest result sets are small by design).
4003 let columns: alloc::vec::Vec<ColumnSchema> = projection
4004 .iter()
4005 // v7.39 (read01 round 54) — keep the column's enum identity through
4006 // the projection (it lives outside the DataType lattice), or a
4007 // derived table / UNION / windowed result forgets it and any outer
4008 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4009 .map(|p| p.to_column_schema())
4010 .collect();
4011 // Re-evaluate ORDER BY against the source schema (pre-projection
4012 // so col refs by name still resolve through `scan_ctx`).
4013 // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
4014 // column. Evaluated as an expression it is just the constant N: the same
4015 // key for every row, so the sort ran and changed nothing.
4016 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4017 if !order_by.is_empty() {
4018 // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
4019 // A key that names a select-list item reads it out of the expanded
4020 // row (PG sorts AFTER the expansion); one that names a source
4021 // column the query does not project is evaluated on the input row
4022 // it came from, which is what `srf_order_output_cols` decides.
4023 let out_cols = if srf_idxs.is_empty() {
4024 alloc::vec![None; order_by.len()]
4025 } else {
4026 srf_order_output_cols(&order_by, &projection)
4027 };
4028 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4029 .iter()
4030 .enumerate()
4031 .map(|(k, out)| -> Result<_, EngineError> {
4032 let src = src_of_row.get(k).copied().unwrap_or(k);
4033 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4034 .iter()
4035 .zip(out_cols.iter())
4036 .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
4037 .collect();
4038 Ok((k, keys?))
4039 })
4040 .collect::<Result<_, _>>()?;
4041 indexed.sort_by(|a, b| {
4042 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4043 let o = &order_by[idx];
4044 let cmp = order_by_value_cmp_in(
4045 o.desc,
4046 o.nulls_first,
4047 ka,
4048 kb,
4049 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4050 );
4051 if cmp != core::cmp::Ordering::Equal {
4052 return cmp;
4053 }
4054 }
4055 core::cmp::Ordering::Equal
4056 });
4057 projected_rows = indexed
4058 .into_iter()
4059 .map(|(i, _)| projected_rows[i].clone())
4060 .collect();
4061 }
4062 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4063 if stmt.distinct {
4064 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4065 // spec folds EVERY text position, so a column declared
4066 // `COLLATE utf8mb4_bin` had its values merged here exactly the
4067 // way 3b494b6e fixed on the main scan path. The projection is
4068 // already in scope at each of these sites, so the mask needs no
4069 // new plumbing -- it was simply never asked for.
4070 projected_rows = dedup_rows(
4071 projected_rows,
4072 FoldSpec::of_masks(
4073 scan_ctx.mysql_dialect,
4074 &fold_mask(&projection),
4075 &pad_mask(&projection),
4076 ),
4077 );
4078 }
4079 // LIMIT / OFFSET — apply at the tail.
4080 if let Some(offset) = stmt.offset_literal() {
4081 let off = (offset as usize).min(projected_rows.len());
4082 projected_rows.drain(..off);
4083 }
4084 if let Some(limit) = stmt.limit_literal() {
4085 projected_rows.truncate(limit as usize);
4086 }
4087 Ok(QueryResult::Rows {
4088 columns,
4089 rows: projected_rows,
4090 })
4091 }
4092
4093 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
4094 /// step])` set-returning source. Mirrors `exec_select_unnest`'s
4095 /// shape: evaluate the arg list once against an empty row,
4096 /// materialise the row stream by stepping start → stop, then
4097 /// route through the standard WHERE / projection / ORDER BY /
4098 /// LIMIT pipeline. Two arg-type combos in v7.17:
4099 /// * integer / integer [/ integer] — SmallInt, Int, BigInt
4100 /// (widened to BigInt internally; step defaults to 1)
4101 /// * timestamp / timestamp / interval — date-range
4102 /// iteration (mailrs's daily-report pattern)
4103 fn exec_select_generate_series(
4104 &self,
4105 stmt: &SelectStatement,
4106 primary: &TableRef,
4107 cancel: CancelToken<'_>,
4108 ) -> Result<QueryResult, EngineError> {
4109 let args = primary
4110 .generate_series_args
4111 .as_ref()
4112 .expect("caller guards generate_series_args.is_some()");
4113 let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
4114 let alias = primary
4115 .alias
4116 .clone()
4117 .unwrap_or_else(|| "generate_series".to_string());
4118 // `AS t(n)` — the first column-alias entry renames the
4119 // series column (PG semantics); bare alias keeps the
4120 // pre-existing behaviour of naming the column after it.
4121 let col_name = primary
4122 .unnest_column_aliases
4123 .first()
4124 .cloned()
4125 .unwrap_or_else(|| alias.clone());
4126 let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
4127 let mut schema_cols = alloc::vec![col_schema.clone()];
4128 // WITH ORDINALITY — trailing BIGINT counting rows from 1;
4129 // the second column-alias entry renames it.
4130 let rows = if primary.with_ordinality {
4131 let ord_name = primary
4132 .unnest_column_aliases
4133 .get(1)
4134 .cloned()
4135 .unwrap_or_else(|| "ordinality".to_string());
4136 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4137 rows.into_iter()
4138 .enumerate()
4139 .map(|(i, row)| {
4140 let mut vals = row.values.clone();
4141 vals.push(Value::BigInt(i as i64 + 1));
4142 Row::new(vals)
4143 })
4144 .collect()
4145 } else {
4146 rows
4147 };
4148 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4149 // `EvalContext::new` drops it and every catalog-dependent cast
4150 // (regclass / enum / composite / domain) silently degrades.
4151 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4152 // WHERE.
4153 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4154 let mut out = alloc::vec::Vec::with_capacity(rows.len());
4155 for row in rows {
4156 cancel.check()?;
4157 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4158 if matches!(v, Value::Bool(true)) {
4159 out.push(row);
4160 }
4161 }
4162 out
4163 } else {
4164 rows
4165 };
4166 // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4167 // returning sources. When the SELECT projection contains
4168 // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4169 // …) we route the filtered row stream through the same
4170 // aggregate executor the relational scan path uses, so
4171 // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4172 // a single 100 row instead of erroring at projection
4173 // time. GROUP BY / HAVING / ORDER BY over the aggregate
4174 // output all ride through `aggregate::run`.
4175 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
4176 // v7.29 — a per-query memo so correlated scalar
4177 // subqueries batch-evaluate once (group map) instead of
4178 // executing per group.
4179 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4180 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4181 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4182 .map_err(|err| match err {
4183 EngineError::Eval(ev) => ev,
4184 other => eval::EvalError::TypeMismatch {
4185 detail: alloc::format!("{other}"),
4186 },
4187 })
4188 };
4189 // v7.39 (round 656) — hand the rows over as they are rather than
4190 // collecting a second vector of `RowRef` wrappers. Note this is
4191 // a set-returning-function path, NOT the relational scan: the
4192 // measured O(rows) cost lived in `run_single_table_aggregate`,
4193 // and converting these four first was a miss that cost a full
4194 // round — every test stayed green and the number did not move.
4195 let agg = aggregate::run(
4196 stmt,
4197 crate::join::AggRows::Owned(&filtered),
4198 &schema_cols,
4199 Some(&alias),
4200 Some(&agg_correlated),
4201 self.parallel_runner.0.as_deref(),
4202 Some(self.active_catalog()),
4203 Some(self),
4204 )?;
4205 return self.finish_agg_result(agg, stmt, cancel);
4206 }
4207 // Projection.
4208 let projection = build_projection(
4209 &stmt.items,
4210 &schema_cols,
4211 &alias,
4212 self.speaks_mysql,
4213 Some(self.active_catalog()),
4214 )?;
4215 // v7.39 (round 621) — and here, for the same reason.
4216 let srf_idxs = self.srf_target_idxs(&projection);
4217 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4218 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4219 alloc::vec::Vec::with_capacity(filtered.len());
4220 let mut proj_memo = memoize::MemoizeCache::default();
4221 if !srf_idxs.is_empty() {
4222 let (rows, src) =
4223 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4224 projected_rows = rows;
4225 src_of_row = src;
4226 } else {
4227 for row in &filtered {
4228 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4229 for p in &projection {
4230 // v7.24 (round-16 B) — correlated-aware.
4231 vals.push(self.eval_expr_with_correlated(
4232 &p.expr,
4233 row,
4234 &scan_ctx,
4235 cancel,
4236 Some(&mut proj_memo),
4237 )?);
4238 }
4239 projected_rows.push(Row::new(vals));
4240 }
4241 }
4242 let columns: alloc::vec::Vec<ColumnSchema> = projection
4243 .iter()
4244 // v7.39 (read01 round 54) — keep the column's enum identity through
4245 // the projection (it lives outside the DataType lattice), or a
4246 // derived table / UNION / windowed result forgets it and any outer
4247 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4248 .map(|p| p.to_column_schema())
4249 .collect();
4250 // ORDER BY against the source schema.
4251 // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4252 // more of them than there were inputs), and a positional key means the
4253 // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4254 // and what the other two synthetic-source tails already did.
4255 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4256 if !order_by.is_empty() {
4257 let out_cols = if srf_idxs.is_empty() {
4258 alloc::vec![None; order_by.len()]
4259 } else {
4260 srf_order_output_cols(&order_by, &projection)
4261 };
4262 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4263 .iter()
4264 .enumerate()
4265 .map(|(k, out)| -> Result<_, EngineError> {
4266 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4267 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4268 .iter()
4269 .zip(out_cols.iter())
4270 .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4271 .collect();
4272 Ok((k, keys?))
4273 })
4274 .collect::<Result<_, _>>()?;
4275 indexed.sort_by(|a, b| {
4276 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4277 let o = &stmt.order_by[idx];
4278 let cmp = order_by_value_cmp_in(
4279 o.desc,
4280 o.nulls_first,
4281 ka,
4282 kb,
4283 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4284 );
4285 if cmp != core::cmp::Ordering::Equal {
4286 return cmp;
4287 }
4288 }
4289 core::cmp::Ordering::Equal
4290 });
4291 projected_rows = indexed
4292 .into_iter()
4293 .map(|(i, _)| projected_rows[i].clone())
4294 .collect();
4295 }
4296 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4297 if stmt.distinct {
4298 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4299 // spec folds EVERY text position, so a column declared
4300 // `COLLATE utf8mb4_bin` had its values merged here exactly the
4301 // way 3b494b6e fixed on the main scan path. The projection is
4302 // already in scope at each of these sites, so the mask needs no
4303 // new plumbing -- it was simply never asked for.
4304 projected_rows = dedup_rows(
4305 projected_rows,
4306 FoldSpec::of_masks(
4307 scan_ctx.mysql_dialect,
4308 &fold_mask(&projection),
4309 &pad_mask(&projection),
4310 ),
4311 );
4312 }
4313 if let Some(offset) = stmt.offset_literal() {
4314 let off = (offset as usize).min(projected_rows.len());
4315 projected_rows.drain(..off);
4316 }
4317 if let Some(limit) = stmt.limit_literal() {
4318 projected_rows.truncate(limit as usize);
4319 }
4320 Ok(QueryResult::Rows {
4321 columns,
4322 rows: projected_rows,
4323 })
4324 }
4325
4326 /// The FROM shapes that are not an ordinary table scan — joins, the
4327 /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4328 ///
4329 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4330 /// reason round 848 established in the parser: a debug build gives
4331 /// EVERY branch's locals a slot in the frame, whichever branch runs.
4332 /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4333 /// stacks several of them; a plain scan reaches none of these
4334 /// branches. Moving them out took the frame to 52,336.
4335 ///
4336 /// `Ok(None)` means "not one of these shapes, carry on".
4337 #[inline(never)]
4338 fn try_from_shape_paths(
4339 &self,
4340 stmt: &SelectStatement,
4341 from: &spg_sql::ast::FromClause,
4342 cancel: CancelToken<'_>,
4343 ) -> Result<Option<QueryResult>, EngineError> {
4344 if !from.joins.is_empty() {
4345 // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4346 // elimination: when a LEFT JOIN's right side is referenced
4347 // ONLY in the ON equality and the right-side join key is
4348 // UNIQUE/PK, the join preserves outer cardinality exactly
4349 // and contributes no values used downstream. Drop the
4350 // entire join. PG does this on the
4351 // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4352 // — A's row count is what survives, B never has to be
4353 // touched.
4354 if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4355 return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4356 }
4357 // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4358 // the v7.32 joinfold rewrite that turns inner JOINs into a
4359 // single-table scan when the catalogue can prove key-only
4360 // dependency. Tests use this to assert "without joinfold,
4361 // the join still executes correctly" (joinfold is a
4362 // semantically-equivalent rewrite, not a correctness fix).
4363 if !self.env_cfg().disable_joinfold {
4364 if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4365 return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4366 }
4367 }
4368 return self.exec_joined_select(stmt, from, cancel).map(Some);
4369 }
4370 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4371 // single-column table at SELECT entry by evaluating the
4372 // expression once against the empty row (UNNEST is
4373 // uncorrelated in v7.11; correlated / LATERAL unnest is a
4374 // v7.12 carve-out). Build a virtual `Table` in a heap-only
4375 // catalog, then route to the regular scan path.
4376 if from.primary.unnest_expr.is_some() {
4377 return self
4378 .exec_select_unnest(stmt, &from.primary, cancel)
4379 .map(Some);
4380 }
4381 // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4382 // returning function. Same dispatch shape as unnest but
4383 // emits a two-column (key TEXT, value TEXT) row stream.
4384 if from.primary.jsonb_each_text_arg.is_some() {
4385 return self
4386 .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4387 .map(Some);
4388 }
4389 // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4390 // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4391 // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4392 // array form. Each function runs; the results zip in LOCKSTEP with the
4393 // shorter padded to NULL — the SAME rule the target-list SRFs follow
4394 // (round 67), which is why `srf_values` is what evaluates each entry.
4395 if from.primary.rows_from.is_some() {
4396 let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4397 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4398 if let Some(col) = schema_cols.get_mut(i) {
4399 col.name = new_name.clone();
4400 }
4401 }
4402 let alias = from
4403 .primary
4404 .alias
4405 .clone()
4406 .unwrap_or_else(|| from.primary.name.clone());
4407 return self
4408 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4409 .map(Some);
4410 }
4411 // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4412 // COLUMNS (...))`. Materialise the row stream + schema by
4413 // walking the row path, then run the regular pipeline over it.
4414 if let Some(jt) = &from.primary.json_table {
4415 let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4416 let alias = from
4417 .primary
4418 .alias
4419 .clone()
4420 .unwrap_or_else(|| from.primary.name.clone());
4421 return self
4422 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4423 .map(Some);
4424 }
4425 if from.primary.table_fn_call.is_some() {
4426 let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4427 // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4428 // (from 1, in output order) AFTER the function's own columns. The
4429 // alias list names it like any other, which is why it is appended
4430 // BEFORE the renaming pass below.
4431 let rows = if from.primary.with_ordinality {
4432 schema_cols.push(ColumnSchema::new(
4433 "ordinality".to_string(),
4434 DataType::BigInt,
4435 false,
4436 ));
4437 rows.into_iter()
4438 .enumerate()
4439 .map(|(i, r)| {
4440 let mut vals = r.values;
4441 vals.push(Value::BigInt(i as i64 + 1));
4442 Row::new(vals)
4443 })
4444 .collect()
4445 } else {
4446 rows
4447 };
4448 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4449 if let Some(col) = schema_cols.get_mut(i) {
4450 col.name = new_name.clone();
4451 }
4452 }
4453 let alias = from
4454 .primary
4455 .alias
4456 .clone()
4457 .unwrap_or_else(|| from.primary.name.clone());
4458 return self
4459 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4460 .map(Some);
4461 }
4462 // v7.37.17 (17.6 siblings) — plain derived table in primary
4463 // position: `FROM ( SELECT … ) alias` (no joins). The inner
4464 // SELECT materialises once (it is uncorrelated by
4465 // construction), then the outer projection / WHERE /
4466 // aggregate / ORDER BY pipeline runs over the synthetic
4467 // table. Joined derived tables keep riding the LATERAL
4468 // machinery in join.rs.
4469 if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4470 // v7.39 (round 727) — flatten first. A simple derived table
4471 // (bare-column projection over one stored table, nothing that
4472 // changes cardinality or order) used to force the inner
4473 // SELECT through the SERIAL row-at-a-time projection pipeline
4474 // just to materialise a synthetic table the outer query then
4475 // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4476 // measured 18.6 ms against PG's 5 — and bare count over the
4477 // same filter WITHOUT the wrapper is 2 ms here, because it
4478 // rides the fused parallel lane. Rewriting to the unwrapped
4479 // form is PG's subquery pull-up; the whole tree gets the
4480 // fast lanes back.
4481 if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4482 return self.exec_select_cancel(&flat, cancel).map(Some);
4483 }
4484 // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4485 // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4486 // ORDER BY never changes the row count, and OFFSET drops
4487 // exactly k. The materialising path sorted 500k rows to
4488 // count 10k (57 ms); PG runs its parallel sort anyway
4489 // (28 ms). The rewrite skips the sort entirely on both
4490 // counts — a plan PG itself does not have.
4491 if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4492 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4493 }
4494 // v7.39 (round 743) — `count(*) OVER a derived whose only
4495 // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4496 // a constant-length array unnests to exactly k rows per
4497 // input row, NULL elements included. PG expands the set to
4498 // count it (6.6 ms on the panel cell); the identity doesn't.
4499 if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4500 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4501 }
4502 return self
4503 .exec_select_derived(stmt, &from.primary, cancel)
4504 .map(Some);
4505 }
4506 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4507 // [, step])` set-returning source. Dispatch mirrors UNNEST:
4508 // materialise the row stream from a single eval pass, then
4509 // run the regular projection / WHERE / ORDER BY / LIMIT
4510 // pipeline over the synthetic single-column table.
4511 if from.primary.generate_series_args.is_some() {
4512 return self
4513 .exec_select_generate_series(stmt, &from.primary, cancel)
4514 .map(Some);
4515 }
4516 Ok(None)
4517 }
4518
4519 /// Pick an index seek for this WHERE, if any of the four apply:
4520 /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4521 ///
4522 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4523 /// frame reason on `try_from_shape_paths`: in a debug build a
4524 /// closure's locals belong to the enclosing frame, and this one is
4525 /// four seek attempts wide on a function that nests.
4526 #[inline(never)]
4527 fn pick_indexed_rows<'r>(
4528 &'r self,
4529 stmt: &SelectStatement,
4530 table: &'r spg_storage::Table,
4531 schema_cols: &[spg_storage::ColumnSchema],
4532 alias: &str,
4533 ctx: &crate::eval::EvalContext<'_>,
4534 seek_snapshot: &crate::Snapshot,
4535 ) -> Option<crate::index_access::Seeked<'r>> {
4536 stmt.where_.as_ref().and_then(|w| {
4537 // BTree / col=literal seek first — covers the v7.11.3 multi-
4538 // column AND case and the leading-column equality lookup.
4539 try_index_seek(
4540 w,
4541 schema_cols,
4542 self.active_catalog(),
4543 table,
4544 alias,
4545 seek_snapshot,
4546 ctx.mysql_dialect,
4547 )
4548 .or_else(|| {
4549 // v7.12.3 — GIN-accelerated `WHERE col @@
4550 // tsquery` when the column has a `USING gin`
4551 // index. Returns an over-approximate candidate
4552 // set; the WHERE re-eval loop below verifies
4553 // the full `@@` predicate per row.
4554 try_gin_seek(
4555 w,
4556 schema_cols,
4557 self.active_catalog(),
4558 table,
4559 alias,
4560 ctx,
4561 seek_snapshot,
4562 )
4563 .map(crate::index_access::Seeked::over_approximate)
4564 })
4565 .or_else(|| {
4566 // v7.15.0 — trigram-GIN-accelerated
4567 // `WHERE col LIKE / ILIKE '<pat>'` when the
4568 // column has a `gin_trgm_ops` GIN index.
4569 // Over-approximate candidate set; the WHERE
4570 // re-eval verifies the LIKE per row.
4571 try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4572 .map(crate::index_access::Seeked::over_approximate)
4573 })
4574 .or_else(|| {
4575 // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4576 // accelerated `WHERE col @> <jsonb_literal>`
4577 // when the column has a `USING gin` index. The
4578 // posting-list intersection returns an over-
4579 // approximate candidate set; the WHERE re-eval
4580 // verifies the full `@>` predicate per row.
4581 try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4582 .map(crate::index_access::Seeked::over_approximate)
4583 })
4584 })
4585 }
4586
4587 /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4588 /// the two `count(*)` short-circuits. Out-of-line for the frame
4589 /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4590 /// of them, and in a debug build their locals sit in the frame
4591 /// regardless.
4592 #[inline(never)]
4593 fn try_seek_fast_paths(
4594 &self,
4595 stmt: &SelectStatement,
4596 table: &spg_storage::Table,
4597 schema_cols: &[spg_storage::ColumnSchema],
4598 alias: &str,
4599 seek_snapshot: &crate::Snapshot,
4600 cancel: CancelToken<'_>,
4601 ) -> Result<Option<QueryResult>, EngineError> {
4602 if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4603 // NSW kNN dispatches against the hot-tier vector index only
4604 // (vector cells aren't promoted to cold segments), so wrap
4605 // the returned row indices as `Cow::Borrowed` for the
4606 // unified `materialise_in_order` shape.
4607 let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4608 .into_iter()
4609 .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4610 .collect();
4611 return materialise_in_order(stmt, schema_cols, alias, &ordered, self.speaks_mysql)
4612 .map(Some);
4613 }
4614
4615 // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4616 // the scan via the BTree iterator in the requested direction
4617 // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4618 // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4619 // the load-bearing consumer; this skips the materialise-every-
4620 // row + partial-sort tail entirely. Walker output is already
4621 // in ORDER BY order so `materialise_in_order` (no extra sort)
4622 // is the natural sink.
4623 if let Some(walked) = try_pk_walk_top_n(
4624 stmt,
4625 self.active_catalog(),
4626 table,
4627 schema_cols,
4628 alias,
4629 self,
4630 cancel,
4631 self.speaks_mysql,
4632 ) {
4633 return materialise_in_order(stmt, schema_cols, alias, &walked, self.speaks_mysql)
4634 .map(Some);
4635 }
4636
4637 // Index seek: if WHERE is `col = literal` (or commuted) and the
4638 // referenced column has an index, dispatch each locator through
4639 // the catalog (hot tier → borrow, cold tier → page-read +
4640 // decode) and iterate just those rows. Otherwise fall back to a
4641 // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4642 // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4643 // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4644 // we don't pay the row materialisation cost twice. Returns
4645 // a bare `Rows{count}` if the shape matches.
4646 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
4647 && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4648 {
4649 return Ok(Some(out));
4650 }
4651 // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4652 // locators directly, skipping row materialisation + WHERE re-eval.
4653 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
4654 && let Some(out) = self.try_count_star_indexed_range_fast(
4655 stmt,
4656 table,
4657 schema_cols,
4658 alias,
4659 seek_snapshot,
4660 )
4661 {
4662 return Ok(Some(out));
4663 }
4664 Ok(None)
4665 }
4666
4667 /// The two rewrites that must happen before the FROM clause is even
4668 /// looked at: a meta-view reference needs the catalog views
4669 /// materialised, and a windowed projection belongs to the window
4670 /// executor. Out-of-line for the frame reason on
4671 /// `try_from_shape_paths`.
4672 #[inline(never)]
4673 fn try_pre_from_paths(
4674 &self,
4675 stmt: &SelectStatement,
4676 cancel: CancelToken<'_>,
4677 ) -> Result<Option<QueryResult>, EngineError> {
4678 if !self.meta_views_materialised && select_references_meta_view(stmt) {
4679 return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4680 }
4681 // v4.12: window-function path. When the projection contains
4682 // any `name(args) OVER (...)` we route to the dedicated
4683 // executor — partition + sort + per-row window value before
4684 // the regular projection.
4685 if select_has_window(stmt) {
4686 // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4687 // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4688 // needs the aggregation done first, then windows over the grouped
4689 // rows. Rewrite to an aggregate derived subquery + outer window query
4690 // (which the window-over-derived path, D.13, executes). Only fires on
4691 // the currently-erroring agg+window+GROUP BY shape, so it can't
4692 // regress working window-only or aggregate-only queries.
4693 if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4694 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4695 }
4696 return self.exec_select_with_window(stmt, cancel).map(Some);
4697 }
4698 Ok(None)
4699 }
4700
4701 /// A projection naming `ctid` or another system column: the schema
4702 /// has to be widened with them before the scan. Out-of-line for the
4703 /// frame reason on `try_from_shape_paths`.
4704 #[inline(never)]
4705 fn try_ctid_projection(
4706 &self,
4707 stmt: &SelectStatement,
4708 primary: &spg_sql::ast::TableRef,
4709 table: &spg_storage::Table,
4710 schema_cols: &[spg_storage::ColumnSchema],
4711 alias: &str,
4712 cancel: CancelToken<'_>,
4713 ) -> Result<Option<QueryResult>, EngineError> {
4714 if references_ctid(stmt) {
4715 let snapshot = self.current_snapshot();
4716 let mut ext_cols = schema_cols.to_vec();
4717 for name in SYSTEM_COLUMNS {
4718 ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4719 }
4720 let table_oid =
4721 crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4722 .unwrap_or(0);
4723 let headers = table.headers();
4724 let rows: Vec<Row<'static>> = table
4725 .scan_visible(&snapshot)
4726 .map(|(i, r)| {
4727 let mut vals = r.values.clone();
4728 // One block, offsets from 1, as PG numbers them.
4729 vals.push(Value::Tid(0, i as u32 + 1));
4730 let h = headers.get(i);
4731 vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4732 vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4733 // SPG keeps no per-statement command ids; PG shows 0 for
4734 // every row a reader can see, which is every row here.
4735 vals.push(Value::Cid(0));
4736 vals.push(Value::Cid(0));
4737 vals.push(Value::BigInt(table_oid));
4738 Row::new(vals)
4739 })
4740 .collect();
4741 return self
4742 .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4743 .map(Some);
4744 }
4745 Ok(None)
4746 }
4747
4748 /// A sequence read as a one-row relation (`SELECT last_value FROM
4749 /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4750 /// the frame reason on `try_from_shape_paths`.
4751 #[inline(never)]
4752 fn try_sequence_relation(
4753 &self,
4754 stmt: &SelectStatement,
4755 primary: &spg_sql::ast::TableRef,
4756 cancel: CancelToken<'_>,
4757 ) -> Result<Option<QueryResult>, EngineError> {
4758 if self.active_catalog().get(&primary.name).is_none()
4759 && let Some(seq) = self.active_catalog().sequence(&primary.name)
4760 {
4761 let rows = alloc::vec![Row::new(alloc::vec![
4762 Value::BigInt(seq.last_value),
4763 Value::BigInt(0),
4764 Value::Bool(seq.is_called),
4765 ])];
4766 let schema_cols = alloc::vec![
4767 ColumnSchema::new("last_value", DataType::BigInt, false),
4768 ColumnSchema::new("log_cnt", DataType::BigInt, false),
4769 ColumnSchema::new("is_called", DataType::Bool, false),
4770 ];
4771 let alias = primary
4772 .alias
4773 .clone()
4774 .unwrap_or_else(|| primary.name.clone());
4775 return self
4776 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4777 .map(Some);
4778 }
4779 Ok(None)
4780 }
4781
4782 pub(crate) fn exec_bare_select_cancel(
4783 &self,
4784 stmt: &SelectStatement,
4785 cancel: CancelToken<'_>,
4786 ) -> Result<QueryResult, EngineError> {
4787 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4788 // is meaningless without an ORDER BY; PG raises a hard
4789 // error and SPG mirrors the surface so the same DDL/app
4790 // path behaves identically on cutover.
4791 check_with_ties_requires_order_by(stmt)?;
4792 // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4793 // PG rejects window calls there outright. Checked here rather than
4794 // on the window path: `HAVING row_number() OVER () = 1` has no
4795 // window in its projection at all.
4796 crate::window::reject_window_in_row_clauses(stmt)?;
4797 // v7.39 (round 232) — the ORDER BY legality rules (positional
4798 // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4799 // check: before anything scans.
4800 crate::orderby::check_order_by_legality(stmt)?;
4801 // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4802 // equivalent statement the regular executor handles (merged join
4803 // columns collapse to a single unqualified output column; NATURAL
4804 // gets its common-column ON synthesised). The rewrite clears the
4805 // flags, so this re-entrant call is a no-op on the second pass.
4806 if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4807 return self.exec_bare_select_cancel(&rewritten, cancel);
4808 }
4809 // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4810 // exactly the group keys, IS a DISTINCT and was paying for the
4811 // aggregate executor to find that out. Same placement and shape
4812 // as the desugar above; the rewrite clears `group_by`, so the
4813 // re-entry is a no-op on the second pass. See `baregroup` for
4814 // what the gate rules out.
4815 if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4816 return self.exec_bare_select_cancel(&rewritten, cancel);
4817 }
4818 // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4819 // operand in a security-barrier subquery, then re-enter (the wrapped
4820 // operands are no longer bare RLS tables, so this is a no-op on the
4821 // second pass).
4822 if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4823 return self.exec_bare_select_cancel(&rewritten, cancel);
4824 }
4825 // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4826 // AND the RLS USING predicate into a single-table SELECT's WHERE.
4827 // Superuser sessions and non-RLS tables get `None` (no clone, no
4828 // change). Applied inline (shadowing `stmt`) rather than via re-entry
4829 // so it can't re-inject on a recursive pass.
4830 let rls_stmt;
4831 let stmt = match self.rls_select_predicate(stmt)? {
4832 Some(pred) => {
4833 let mut s = stmt.clone();
4834 s.where_ = Some(match s.where_.take() {
4835 Some(existing) => spg_sql::ast::Expr::Binary {
4836 lhs: alloc::boxed::Box::new(existing),
4837 op: spg_sql::ast::BinOp::And,
4838 rhs: alloc::boxed::Box::new(pred),
4839 },
4840 None => pred,
4841 });
4842 rls_stmt = s;
4843 &rls_stmt
4844 }
4845 None => stmt,
4846 };
4847 // v7.16.2 — same meta-view dispatch as
4848 // `exec_select_cancel`, applied here too because
4849 // `subquery_replacement` enters this function directly
4850 // for Exists / ScalarSubquery / InSubquery resolution
4851 // (bypassing the top-level entry to avoid double
4852 // subquery walking). Without this dispatch the subquery
4853 // hits `__spg_info_columns` and reports TableNotFound.
4854 if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4855 return Ok(done);
4856 }
4857 // Constant SELECT (no FROM) — evaluate each item once against an
4858 // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4859 // `SELECT '7'::INT`. Column references will surface as
4860 // ColumnNotFound on eval since the schema is empty.
4861 let Some(from) = &stmt.from else {
4862 return self.exec_constant_select(stmt);
4863 };
4864 // Multi-table FROM (one or more joined peers) goes through the
4865 // nested-loop join executor. Single-table FROM stays on the
4866 // existing scan + index-seek path.
4867 if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4868 return Ok(done);
4869 }
4870 // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4871 // tested — eight ORDER BY shapes byte-identical spilled against
4872 // in-memory, with 103 runs opened to prove the spill ran — and it
4873 // loses on wall clock, which is a hard stop whatever the memory
4874 // buys. Measured round 865, same psql client both sides, same
4875 // machine, row counts verified, and both sides confirmed to be
4876 // doing an external merge rather than an indexed walk:
4877 //
4878 // PG18 178.7 - 187.0 ms Sort Method: external merge, 85 MB
4879 // SPG spilled 269.7 - 299.6 ms 33 spill files at peak
4880 //
4881 // Non-overlapping, about 1.55x. Re-enable by restoring the call
4882 // below once that closes; nothing else has to change, which is
4883 // the point of it being a separate path.
4884 //
4885 // if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4886 // return Ok(done);
4887 // }
4888 //
4889 // v7.37 (round 882) — this walk stays unhooked, but its streaming
4890 // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4891 // bail in `try_exec_joined_streaming`. Collecting the answer was
4892 // most of what this one cost: handing rows over as the merge
4893 // produces them holds peak to the budget plus one row, and the
4894 // wall clock lands inside PG18's range rather than 1.55x outside
4895 // it. Numbers in `extsort.rs`'s header.
4896 let primary = &from.primary;
4897 // v7.39 (round 244) — a sequence is selectable as a one-row relation
4898 // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4899 // read it). Synthesize PG's three columns.
4900 if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4901 return Ok(done);
4902 }
4903 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4904 StorageError::TableNotFound {
4905 name: primary.name.clone(),
4906 }
4907 })?;
4908 let schema_cols = &table.schema().columns;
4909 // The qualifier accepted on column refs is the alias (if any) else the
4910 // bare table name.
4911 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4912 // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4913 // system columns at all: `SELECT ctid FROM t` answered "column
4914 // \"ctid\" does not exist", which takes out the dedup idiom every
4915 // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4916 // GROUP BY key)`.
4917 //
4918 // The value comes from the row's position, which the scan already
4919 // yields; the column is appended to the schema and the rows only
4920 // when the statement asks for it, so nothing else pays for it. That
4921 // also routes the query down the general path, past the index fast
4922 // paths below — they hand back rows without positions, and a ctid
4923 // that was sometimes right would be worse than none.
4924 if let Some(done) =
4925 self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4926 {
4927 return Ok(done);
4928 }
4929 let ctx = self.ev_ctx(schema_cols, Some(alias));
4930
4931 // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4932 // WHERE and an NSW index on `col` skips the full scan. The
4933 // walk returns rows already in ascending-distance order, so
4934 // ORDER BY / LIMIT are honoured implicitly.
4935 // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4936 // and thread it into every index-seek fast path below. No-op
4937 // today (every hot header is committed-alive).
4938 let seek_snapshot = self.current_snapshot();
4939 if let Some(done) =
4940 self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4941 {
4942 return Ok(done);
4943 }
4944 // full scan over the hot tier (cold-tier rows are only reached
4945 // via index seek in v5.1 — full table scans against cold-tier
4946 // data ship in v5.2 with the freezer's per-segment scan API).
4947 let indexed_rows =
4948 self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4949
4950 // Aggregate path: filter rows first, then hand off to the
4951 // aggregate executor which does its own projection + ORDER BY.
4952 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
4953 return self.run_single_table_aggregate(
4954 stmt,
4955 table,
4956 schema_cols,
4957 alias,
4958 indexed_rows,
4959 cancel,
4960 );
4961 }
4962 self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4963 }
4964
4965 /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4966 /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4967 /// uncorrelated FROM-primary case is the simpler shape, used by
4968 /// e2e pins. Materialises the (key, value) pair stream into a
4969 /// synthetic two-column TEXT table, then routes through the
4970 /// regular projection / WHERE / ORDER BY pipeline.
4971 /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4972 /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4973 /// item into (rows, schema). `outer_doc` is `Some` only when this
4974 /// is a NESTED level being expanded against a parent row item's
4975 /// already-parsed sub-document; the top-level call parses the doc
4976 /// expr itself. Row/column paths reuse the existing jsonpath
4977 /// evaluator (`json::json_table_path`); coercion reuses
4978 /// `coerce_value` on the JSON scalar text, so a json string
4979 /// coerces to DATE by its content, matching PG.
4980 #[allow(clippy::type_complexity)]
4981 pub(crate) fn json_table_rows(
4982 &self,
4983 jt: &spg_sql::ast::JsonTable,
4984 outer_doc: Option<&crate::json::JsonValue>,
4985 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4986 // Column schema is static (independent of data): flatten the
4987 // COLUMNS tree in declaration order (NESTED contributes its
4988 // children inline, the PG output shape).
4989 let schema = json_table_schema(&jt.columns);
4990
4991 // PASSING variables → a single JsonValue object the jsonpath
4992 // engine reads `$name` from.
4993 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4994 let ctx = EvalContext::new(&empty_schema, None);
4995 let dummy = Row::new(alloc::vec::Vec::new());
4996 let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4997 None
4998 } else {
4999 let mut entries = alloc::vec::Vec::new();
5000 for (name, e) in &jt.passing {
5001 let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
5002 entries.push((name.clone(), value_to_json_value(&v)));
5003 }
5004 Some(crate::json::JsonValue::Object(entries))
5005 };
5006
5007 // The document root: a NESTED level gets it from the parent;
5008 // the top level parses its doc expr.
5009 let root_owned;
5010 let root: &crate::json::JsonValue = match outer_doc {
5011 Some(d) => d,
5012 None => {
5013 let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
5014 let src = match &doc_val {
5015 Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
5016 Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
5017 other => {
5018 return Err(EngineError::Unsupported(alloc::format!(
5019 "JSON_TABLE document must be json/text, got {}",
5020 crate::conversions::pg_type_name_for_error_opt(other.data_type())
5021 )));
5022 }
5023 };
5024 root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
5025 &root_owned
5026 }
5027 };
5028
5029 let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
5030 .map_err(EngineError::Eval)?;
5031 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5032 for (idx, item) in items.iter().enumerate() {
5033 self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
5034 }
5035 Ok((rows, schema))
5036 }
5037
5038 /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
5039 /// Regular columns produce one value each; a NESTED column expands
5040 /// as an outer join (each nested match → one row sharing the
5041 /// parent cells; no nested match → one row with the nested cells
5042 /// NULL). Sibling NESTED at one level cross by concatenation of
5043 /// their independent expansions (PG's UNION-of-outer shape).
5044 fn json_table_emit_item(
5045 &self,
5046 jt: &spg_sql::ast::JsonTable,
5047 item: &crate::json::JsonValue,
5048 ordinality: usize,
5049 vars: Option<&crate::json::JsonValue>,
5050 out: &mut alloc::vec::Vec<Row<'static>>,
5051 ) -> Result<(), EngineError> {
5052 use spg_sql::ast::JsonTableColumn as C;
5053 // Parent cells (regular + ordinality), left-to-right; NESTED
5054 // columns contribute a run of child cells appended after.
5055 let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5056 let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
5057 alloc::vec::Vec::new();
5058 let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5059 for col in &jt.columns {
5060 match col {
5061 C::Ordinality { .. } => {
5062 parent_cells.push(Value::BigInt(ordinality as i64 + 1));
5063 }
5064 C::Regular { .. } => {
5065 parent_cells.push(self.json_table_column_value(col, item, vars)?);
5066 }
5067 C::Nested { path, columns } => {
5068 // Recurse: a nested JSON_TABLE over `item` filtered
5069 // by `path`, with the same PASSING vars.
5070 let sub = spg_sql::ast::JsonTable {
5071 doc: jt.doc.clone(), // unused (outer_doc provided)
5072 row_path: path.clone(),
5073 columns: columns.clone(),
5074 passing: alloc::vec::Vec::new(),
5075 };
5076 let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
5077 nested_widths.push(nschema.len());
5078 nested_runs.push(nrows);
5079 }
5080 }
5081 }
5082 if nested_runs.is_empty() {
5083 out.push(Row::new(parent_cells));
5084 return Ok(());
5085 }
5086 // PG sibling-NESTED semantics: each sibling expands
5087 // INDEPENDENTLY and the results CONCATENATE — a row from
5088 // sibling s fills only s's cells, every other sibling's cells
5089 // NULL. An empty sibling contributes ZERO rows (not a NULL
5090 // row). Only when EVERY sibling is empty does the parent still
5091 // emit one all-NULL row (the outer-join guarantee that a parent
5092 // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
5093 // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
5094 let before = out.len();
5095 for (s_idx, run) in nested_runs.iter().enumerate() {
5096 for nrow in run {
5097 let mut cells = parent_cells.clone();
5098 for (o_idx, w) in nested_widths.iter().enumerate() {
5099 if o_idx == s_idx {
5100 cells.extend(nrow.values.iter().cloned());
5101 } else {
5102 for _ in 0..*w {
5103 cells.push(Value::Null);
5104 }
5105 }
5106 }
5107 out.push(Row::new(cells));
5108 }
5109 }
5110 if out.len() == before {
5111 // Every sibling empty → one all-NULL nested row.
5112 let mut cells = parent_cells.clone();
5113 for w in &nested_widths {
5114 for _ in 0..*w {
5115 cells.push(Value::Null);
5116 }
5117 }
5118 out.push(Row::new(cells));
5119 }
5120 Ok(())
5121 }
5122
5123 /// v7.39 (round 205) — evaluate one Regular column against a row
5124 /// item: EXISTS → bool; else path → at most one value, coerced to
5125 /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
5126 fn json_table_column_value(
5127 &self,
5128 col: &spg_sql::ast::JsonTableColumn,
5129 item: &crate::json::JsonValue,
5130 vars: Option<&crate::json::JsonValue>,
5131 ) -> Result<Value<'static>, EngineError> {
5132 use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
5133 let C::Regular {
5134 name,
5135 ty,
5136 path,
5137 exists,
5138 format_json,
5139 wrapper,
5140 on_empty,
5141 on_error,
5142 } = col
5143 else {
5144 unreachable!("caller guards Regular");
5145 };
5146 let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5147 if *exists {
5148 return Ok(Value::Bool(!matches.is_empty()));
5149 }
5150 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5151 let ctx = EvalContext::new(&empty_schema, None);
5152 let dummy = Row::new(alloc::vec::Vec::new());
5153 let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5154 match b {
5155 B::Null => Ok(Some(Value::Null)),
5156 B::Error => Ok(None),
5157 B::Default(e) => Ok(Some(
5158 eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5159 )),
5160 }
5161 };
5162 // Empty match set → ON EMPTY.
5163 if matches.is_empty() {
5164 return match default_of(on_empty)? {
5165 Some(v) => coerce_json_table_default(v, *ty, name),
5166 None => Err(EngineError::Unsupported(alloc::format!(
5167 "no SQL/JSON item found for JSON_TABLE column {name:?}"
5168 ))),
5169 };
5170 }
5171 let first = &matches[0];
5172 // FORMAT JSON: return the PG-canonical json representation.
5173 // WITH WRAPPER wraps the whole match SET in an array (even a
5174 // single scalar → `[5]`); without it, the single match's json.
5175 if *format_json {
5176 let text = if *wrapper {
5177 crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5178 } else {
5179 first.canonical_json_text()
5180 };
5181 return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5182 }
5183 if first.is_json_null() {
5184 return Ok(Value::Null);
5185 }
5186 // Coerce the scalar text to the declared type; on failure → ON
5187 // ERROR (default NULL, DEFAULT expr, or raise).
5188 let dt = crate::conversions::column_type_to_data_type(*ty);
5189 let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5190 match crate::conversions::coerce_value(scalar, dt, name, 0) {
5191 Ok(v) => Ok(v),
5192 Err(e) => match default_of(on_error)? {
5193 Some(v) => coerce_json_table_default(v, *ty, name),
5194 None => Err(e),
5195 },
5196 }
5197 }
5198
5199 /// table function into (rows, default schema). Dispatch by name.
5200 pub(crate) fn table_fn_rows(
5201 &self,
5202 primary: &TableRef,
5203 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5204 let (fn_name, args) = primary
5205 .table_fn_call
5206 .as_deref()
5207 .expect("caller guards table_fn_call.is_some()");
5208 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5209 let ctx = EvalContext::new(&empty_schema, None);
5210 let dummy_row = Row::new(alloc::vec::Vec::new());
5211 let arg0: Option<Value<'static>> = match args.first() {
5212 Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5213 None => None,
5214 };
5215 match fn_name.as_str() {
5216 // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5217 // `…_recordset` (+ json_ variants). The row shape is the BASE
5218 // argument's declared type — a table's or a composite type's
5219 // column list — which only the catalog knows, so the parser hands
5220 // the raw arguments here rather than desugaring blind.
5221 "jsonb_populate_record"
5222 | "json_populate_record"
5223 | "jsonb_populate_recordset"
5224 | "json_populate_recordset" => {
5225 let type_name = match args.first() {
5226 Some(Expr::Cast {
5227 target: spg_sql::ast::CastTarget::Named(n),
5228 ..
5229 }) => n.clone(),
5230 _ => {
5231 return Err(EngineError::Unsupported(alloc::format!(
5232 "{fn_name}(): first argument must name a row type, \
5233 e.g. NULL::mytable"
5234 )));
5235 }
5236 };
5237 let cat = self.active_catalog();
5238 let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5239 t.schema().columns.clone()
5240 } else if let Some(c) = cat.composite_types().get(&type_name) {
5241 c.fields
5242 .iter()
5243 .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5244 .collect()
5245 } else {
5246 return Err(EngineError::Unsupported(alloc::format!(
5247 "type \"{type_name}\" does not exist"
5248 )));
5249 };
5250 let json_arg = match args.get(1) {
5251 Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5252 None => Value::Null,
5253 };
5254 // The set form iterates the JSON array; the scalar form is
5255 // the one-element case of the same walk.
5256 let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5257 crate::json::array_element_rows(&json_arg, false, fn_name)
5258 .map_err(EngineError::Eval)?
5259 .into_iter()
5260 .map(|s| s.map_or(Value::Null, Value::json))
5261 .collect()
5262 } else if matches!(json_arg, Value::Null) {
5263 alloc::vec::Vec::new()
5264 } else {
5265 alloc::vec![json_arg]
5266 };
5267 let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5268 for doc in &docs {
5269 let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5270 for c in &cols {
5271 // `->>` semantics: a missing key is NULL, present keys
5272 // arrive as text and cast to the declared column type.
5273 let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5274 .map_err(EngineError::Eval)?;
5275 let v = if matches!(raw, Value::Null) {
5276 Value::Null
5277 } else {
5278 crate::conversions::coerce_value(raw, c.ty, "", 0)
5279 .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5280 };
5281 vals.push(v);
5282 }
5283 rows.push(Row::new(vals));
5284 }
5285 Ok((rows, cols))
5286 }
5287 // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5288 // a text[] of 'name=value' reloptions/fdw options → one
5289 // (option_name, option_value) row per element. NULL or an
5290 // empty array yields zero rows (PG); an element without
5291 // '=' carries a NULL option_value, matching PG's split.
5292 "pg_options_to_table" => {
5293 let schema = alloc::vec![
5294 ColumnSchema::new("option_name", DataType::Text, true),
5295 ColumnSchema::new("option_value", DataType::Text, true),
5296 ];
5297 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5298 if let Some(Value::TextArray(items)) = arg0 {
5299 for item in items.into_iter().flatten() {
5300 let (name, value) = match item.split_once('=') {
5301 Some((n, v)) => (Value::text(n), Value::text(v)),
5302 None => (Value::text(item.as_str()), Value::Null),
5303 };
5304 rows.push(Row::new(alloc::vec![name, value]));
5305 }
5306 }
5307 Ok((rows, schema))
5308 }
5309 // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5310 // PG18's per-sequence state SRF, (last_value, is_called).
5311 // pg_dump reads it joined to pg_sequence for every dumped
5312 // sequence's setval line. The oid resolves through the
5313 // same relation_oid mapping seqrelid publishes.
5314 "pg_get_sequence_data" => {
5315 let schema = alloc::vec![
5316 ColumnSchema::new("last_value", DataType::BigInt, false),
5317 ColumnSchema::new("is_called", DataType::Bool, false),
5318 ];
5319 let want = match arg0 {
5320 Some(Value::Int(n)) => i64::from(n),
5321 Some(Value::BigInt(n)) => n,
5322 _ => {
5323 return Err(EngineError::Unsupported(
5324 "pg_get_sequence_data(): argument must be a sequence oid".into(),
5325 ));
5326 }
5327 };
5328 let cat = self.active_catalog();
5329 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5330 for (name, def) in cat.sequences_all() {
5331 if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5332 rows.push(Row::new(alloc::vec![
5333 Value::BigInt(def.last_value),
5334 Value::Bool(def.is_called),
5335 ]));
5336 break;
5337 }
5338 }
5339 Ok((rows, schema))
5340 }
5341 "pg_partition_tree" => {
5342 let cols = alloc::vec![
5343 ColumnSchema::new("relid".to_string(), DataType::Text, true),
5344 ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5345 ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5346 ColumnSchema::new("level".to_string(), DataType::Int, true),
5347 ];
5348 let Some(Value::Text(name)) = &arg0 else {
5349 // NULL (or missing) argument → zero rows (PG).
5350 return Ok((alloc::vec::Vec::new(), cols));
5351 };
5352 let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5353 if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5354 return Err(EngineError::Unsupported(alloc::format!(
5355 "relation \"{name}\" does not exist"
5356 )));
5357 }
5358 let rows = entries
5359 .into_iter()
5360 .map(|(relid, parent, isleaf, level)| {
5361 Row::new(alloc::vec![
5362 Value::text(relid),
5363 parent.map_or(Value::Null, Value::text),
5364 Value::Bool(isleaf),
5365 #[allow(clippy::cast_possible_truncation)]
5366 Value::Int(level as i32),
5367 ])
5368 })
5369 .collect();
5370 Ok((rows, cols))
5371 }
5372 "pg_partition_ancestors" => {
5373 let cols =
5374 alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5375 let Some(Value::Text(name)) = &arg0 else {
5376 return Ok((alloc::vec::Vec::new(), cols));
5377 };
5378 let cat = self.active_catalog();
5379 if cat.get(name.as_ref()).is_none() {
5380 return Err(EngineError::Unsupported(alloc::format!(
5381 "relation \"{name}\" does not exist"
5382 )));
5383 }
5384 // A relation outside any partition tree yields no rows (PG).
5385 let in_tree = cat
5386 .get(name.as_ref())
5387 .is_some_and(|t| t.schema().partition_role.is_some());
5388 let rows = if in_tree {
5389 crate::partition_walks::ancestors_of(cat, name.as_ref())
5390 .into_iter()
5391 .map(|n| Row::new(alloc::vec![Value::text(n)]))
5392 .collect()
5393 } else {
5394 alloc::vec::Vec::new()
5395 };
5396 Ok((rows, cols))
5397 }
5398 // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5399 // saw, what each token was called, which dictionary took it
5400 // and what came out. It is a projection of the same tokenizer
5401 // and the same map the indexer uses, so it cannot describe a
5402 // pipeline other than the one that runs.
5403 "ts_debug" => {
5404 use crate::fts::{TokenType, TsDict};
5405 let cols = alloc::vec![
5406 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5407 ColumnSchema::new("description".to_string(), DataType::Text, false),
5408 ColumnSchema::new("token".to_string(), DataType::Text, false),
5409 ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5410 ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5411 ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5412 ];
5413 // PG's one-arg form uses the session configuration; the
5414 // two-arg form names one.
5415 let (cfg_name, text) = match (&arg0, args.get(1)) {
5416 (Some(Value::Text(c)), Some(t)) => {
5417 let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5418 (c.to_string(), crate::eval::value_to_text(&v))
5419 }
5420 (Some(v), None) => (
5421 alloc::string::String::from("english"),
5422 crate::eval::value_to_text(v),
5423 ),
5424 _ => return Ok((alloc::vec::Vec::new(), cols)),
5425 };
5426 let english = match cfg_name
5427 .trim()
5428 .trim_start_matches("pg_catalog.")
5429 .to_ascii_lowercase()
5430 .as_str()
5431 {
5432 "english" => true,
5433 "simple" => false,
5434 other => {
5435 return Err(EngineError::Unsupported(alloc::format!(
5436 "text search configuration \"{other}\" does not exist"
5437 )));
5438 }
5439 };
5440 let rows = crate::fts::tokenize_typed(&text)
5441 .into_iter()
5442 .map(|tok| {
5443 let dict = tok.ty.dictionary(english);
5444 let dname = dict.map(|d| match d {
5445 TsDict::Simple => "simple",
5446 TsDict::EnglishStem => "english_stem",
5447 });
5448 let folded = tok.text.to_lowercase();
5449 let lexemes = dict.map(|d| match d {
5450 TsDict::Simple => alloc::vec![Some(folded.clone())],
5451 TsDict::EnglishStem => {
5452 if crate::fts::is_english_stopword(&folded) {
5453 alloc::vec::Vec::new()
5454 } else {
5455 alloc::vec![Some(crate::fts::porter_stem(&folded))]
5456 }
5457 }
5458 });
5459 Row::new(alloc::vec![
5460 Value::text(tok.ty.alias()),
5461 Value::text(tok.ty.description()),
5462 Value::text(tok.text),
5463 Value::TextArray(
5464 dname
5465 .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5466 .unwrap_or_default(),
5467 ),
5468 dname.map_or(Value::Null, Value::text),
5469 lexemes.map_or(Value::Null, Value::TextArray),
5470 ])
5471 })
5472 .collect();
5473 let _ = TokenType::AsciiWord;
5474 Ok((rows, cols))
5475 }
5476 // v7.39 (round 651) — `ts_token_type('default')`, the list the
5477 // parser actually produces. It is a projection of the
5478 // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5479 // read, so the three cannot disagree about what a token is.
5480 "ts_token_type" => {
5481 use crate::fts::TokenType as T;
5482 let cols = alloc::vec![
5483 ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5484 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5485 ColumnSchema::new("description".to_string(), DataType::Text, false),
5486 ];
5487 // PG takes the parser by name or oid; SPG has the one.
5488 if let Some(Value::Text(p)) = &arg0
5489 && !p.eq_ignore_ascii_case("default")
5490 && !p.eq_ignore_ascii_case("pg_catalog.default")
5491 {
5492 return Err(EngineError::Unsupported(alloc::format!(
5493 "text search parser \"{p}\" does not exist"
5494 )));
5495 }
5496 const TYPES: &[T] = &[
5497 T::AsciiWord,
5498 T::Word,
5499 T::NumWord,
5500 T::Email,
5501 T::Url,
5502 T::Host,
5503 T::SFloat,
5504 T::Version,
5505 T::HwordNumPart,
5506 T::HwordPart,
5507 T::HwordAsciiPart,
5508 T::Blank,
5509 T::Tag,
5510 T::Protocol,
5511 T::NumHword,
5512 T::AsciiHword,
5513 T::Hword,
5514 T::UrlPath,
5515 T::File,
5516 T::Float,
5517 T::Int,
5518 T::Uint,
5519 T::Entity,
5520 ];
5521 let rows = TYPES
5522 .iter()
5523 .map(|t| {
5524 Row::new(alloc::vec![
5525 Value::Int(*t as i32),
5526 Value::text(t.alias()),
5527 Value::text(t.description()),
5528 ])
5529 })
5530 .collect();
5531 Ok((rows, cols))
5532 }
5533 // v7.39 (read01 round 65) — a set-returning USER function in FROM
5534 // (`FROM rows_of(2)`). Its body runs through the real executor, like
5535 // every other function body since round 63.
5536 other => {
5537 if !self.active_catalog().functions_named(other).is_empty() {
5538 return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5539 }
5540 Err(EngineError::Unsupported(alloc::format!(
5541 "table function {other}() is not supported in FROM"
5542 )))
5543 }
5544 }
5545 }
5546
5547 /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5548 /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5549 /// are bound into it as literals and it goes through the read path, so the
5550 /// rows it yields are exactly the rows a hand-written query would see.
5551 ///
5552 /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5553 /// text)` names them, and a `SETOF <scalar>` yields a single column named
5554 /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5555 /// shows.
5556 fn exec_setof_user_function(
5557 &self,
5558 name: &str,
5559 args: &[spg_sql::ast::Expr],
5560 // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5561 // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5562 alias: Option<&str>,
5563 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5564 // The call's arguments belong to the ENCLOSING query, so they are
5565 // evaluated here and the body sees values.
5566 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5567 let arg_ctx = self.ev_ctx(&empty, None);
5568 let dummy = Row::new(alloc::vec::Vec::new());
5569 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5570 for a in args {
5571 vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5572 }
5573 self.setof_rows_of(name, &vals, alias)
5574 }
5575
5576 /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5577 /// arguments. Shared by the FROM position and the target-list expansion, so
5578 /// a function cannot behave differently depending on where it is called.
5579 pub(crate) fn setof_rows_of(
5580 &self,
5581 name: &str,
5582 arg_values: &[Value<'static>],
5583 alias: Option<&str>,
5584 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5585 let cat = self.active_catalog();
5586 let overloads = cat.functions_named(name);
5587 let def = overloads
5588 .iter()
5589 .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5590 .ok_or_else(|| {
5591 EngineError::Unsupported(alloc::format!(
5592 "function {name} does not exist with {} argument(s)",
5593 arg_values.len()
5594 ))
5595 })?;
5596 let declared = def.returns.trim().to_string();
5597 let upper = declared.to_ascii_uppercase();
5598 if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5599 return Err(EngineError::Unsupported(alloc::format!(
5600 "function {name}() does not return a set — it cannot be used in FROM"
5601 )));
5602 }
5603
5604 let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5605 // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5606 // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5607 if def.language.eq_ignore_ascii_case("plpgsql") {
5608 let out_rows = self
5609 .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5610 .map_err(EngineError::Eval)?;
5611 let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5612 let rows = out_rows.into_iter().map(Row::new).collect();
5613 return Ok((rows, cols));
5614 }
5615 let body = def.body.trim().trim_end_matches(';');
5616 let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5617 EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5618 })?;
5619 let spg_sql::ast::Statement::Select(body_select) = stmt else {
5620 return Err(EngineError::Unsupported(alloc::format!(
5621 "function {name}(): a set-returning body must be a SELECT"
5622 )));
5623 };
5624 let arg_names = spg_storage::function_arg_names(&def.args_repr);
5625 let bound = crate::eval::bind_user_fn_args(
5626 self.active_catalog(),
5627 &body_select,
5628 &arg_names,
5629 arg_values,
5630 )
5631 .map_err(EngineError::Eval)?;
5632 let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5633 let QueryResult::Rows { columns, rows } = out else {
5634 return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5635 };
5636 // Name the columns from the DECLARED shape — the same rule the plpgsql
5637 // path above uses, so a body's language cannot change the row shape.
5638 let cols = setof_column_shape_from(&declared, name, alias, &columns);
5639 Ok((rows, cols))
5640 }
5641
5642 fn exec_select_jsonb_each_text(
5643 &self,
5644 stmt: &SelectStatement,
5645 primary: &TableRef,
5646 cancel: CancelToken<'_>,
5647 ) -> Result<QueryResult, EngineError> {
5648 let (each_fn, arg_expr) = primary
5649 .jsonb_each_text_arg
5650 .as_ref()
5651 .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5652 .expect("caller guards jsonb_each_text_arg.is_some()");
5653 // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5654 // forms keep JSON rendering in the value column (JSON null
5655 // stays jsonb 'null', strings keep their quotes).
5656 let as_text = each_fn.ends_with("_text");
5657 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5658 let ctx = EvalContext::new(&empty_schema, None);
5659 let dummy_row = Row::new(alloc::vec::Vec::new());
5660 let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5661 let pairs =
5662 crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5663 let rows: alloc::vec::Vec<Row<'static>> = pairs
5664 .into_iter()
5665 .map(|(k, v)| {
5666 let key_val = Value::text(k);
5667 let value_val = match v {
5668 Some(s) if as_text => Value::text(s),
5669 Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5670 None => Value::Null,
5671 };
5672 Row::new(alloc::vec![key_val, value_val])
5673 })
5674 .collect();
5675 let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5676 let value_dtype = if as_text {
5677 spg_storage::DataType::Text
5678 } else {
5679 spg_storage::DataType::Json
5680 };
5681 let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5682 let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5683 let mut schema_cols = alloc::vec![key_col, value_col];
5684 // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5685 // LATERAL-position form of the same call already honours it.
5686 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5687 if let Some(col) = schema_cols.get_mut(i) {
5688 col.name = new_name.clone();
5689 }
5690 }
5691 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5692 // `EvalContext::new` drops it and every catalog-dependent cast
5693 // (regclass / enum / composite / domain) silently degrades.
5694 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5695 // WHERE.
5696 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5697 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5698 for row in rows {
5699 cancel.check()?;
5700 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5701 if matches!(v, Value::Bool(true)) {
5702 out.push(row);
5703 }
5704 }
5705 out
5706 } else {
5707 rows
5708 };
5709 // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5710 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
5711 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5712 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5713 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5714 .map_err(|err| match err {
5715 EngineError::Eval(ev) => ev,
5716 other => eval::EvalError::TypeMismatch {
5717 detail: alloc::format!("{other}"),
5718 },
5719 })
5720 };
5721 // v7.39 (round 656) — hand the rows over as they are rather than
5722 // collecting a second vector of `RowRef` wrappers. Note this is
5723 // a set-returning-function path, NOT the relational scan: the
5724 // measured O(rows) cost lived in `run_single_table_aggregate`,
5725 // and converting these four first was a miss that cost a full
5726 // round — every test stayed green and the number did not move.
5727 let agg = aggregate::run(
5728 stmt,
5729 crate::join::AggRows::Owned(&filtered),
5730 &schema_cols,
5731 Some(&alias),
5732 Some(&agg_correlated),
5733 self.parallel_runner.0.as_deref(),
5734 Some(self.active_catalog()),
5735 Some(self),
5736 )?;
5737 return self.finish_agg_result(agg, stmt, cancel);
5738 }
5739 // Projection.
5740 let projection = build_projection(
5741 &stmt.items,
5742 &schema_cols,
5743 &alias,
5744 self.speaks_mysql,
5745 Some(self.active_catalog()),
5746 )?;
5747 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5748 alloc::vec::Vec::with_capacity(filtered.len());
5749 for row in &filtered {
5750 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5751 for p in &projection {
5752 let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5753 vals.push(v);
5754 }
5755 projected_rows.push(Row::new(vals));
5756 }
5757 let columns: alloc::vec::Vec<ColumnSchema> = projection
5758 .iter()
5759 // v7.39 (read01 round 54) — keep the column's enum identity through
5760 // the projection (it lives outside the DataType lattice), or a
5761 // derived table / UNION / windowed result forgets it and any outer
5762 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5763 .map(|p| p.to_column_schema())
5764 .collect();
5765 // ORDER BY.
5766 if !stmt.order_by.is_empty() {
5767 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5768 .iter()
5769 .enumerate()
5770 .map(|(i, r)| -> Result<_, EngineError> {
5771 let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5772 .order_by
5773 .iter()
5774 .map(|ob| {
5775 eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5776 })
5777 .collect();
5778 Ok((i, keys?))
5779 })
5780 .collect::<Result<_, _>>()?;
5781 indexed.sort_by(|a, b| {
5782 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5783 let o = &stmt.order_by[idx];
5784 let cmp = order_by_value_cmp_in(
5785 o.desc,
5786 o.nulls_first,
5787 ka,
5788 kb,
5789 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5790 );
5791 if cmp != core::cmp::Ordering::Equal {
5792 return cmp;
5793 }
5794 }
5795 core::cmp::Ordering::Equal
5796 });
5797 projected_rows = indexed
5798 .into_iter()
5799 .map(|(i, _)| projected_rows[i].clone())
5800 .collect();
5801 }
5802 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5803 if stmt.distinct {
5804 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
5805 // spec folds EVERY text position, so a column declared
5806 // `COLLATE utf8mb4_bin` had its values merged here exactly the
5807 // way 3b494b6e fixed on the main scan path. The projection is
5808 // already in scope at each of these sites, so the mask needs no
5809 // new plumbing -- it was simply never asked for.
5810 projected_rows = dedup_rows(
5811 projected_rows,
5812 FoldSpec::of_masks(
5813 scan_ctx.mysql_dialect,
5814 &fold_mask(&projection),
5815 &pad_mask(&projection),
5816 ),
5817 );
5818 }
5819 if let Some(offset) = stmt.offset_literal() {
5820 let off = (offset as usize).min(projected_rows.len());
5821 projected_rows.drain(..off);
5822 }
5823 if let Some(limit) = stmt.limit_literal() {
5824 projected_rows.truncate(limit as usize);
5825 }
5826 Ok(QueryResult::Rows {
5827 columns,
5828 rows: projected_rows,
5829 })
5830 }
5831
5832 /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5833 /// ( SELECT … ) alias` in primary position. The inner SELECT
5834 /// materialises once through the regular bare-select executor
5835 /// (UNION tails included), then the outer WHERE / aggregate /
5836 /// projection / ORDER BY / LIMIT pipeline runs over the
5837 /// synthetic table — the same post-materialisation shape as
5838 /// exec_select_jsonb_each_text, generalised to N columns.
5839 fn exec_select_derived(
5840 &self,
5841 stmt: &SelectStatement,
5842 primary: &TableRef,
5843 cancel: CancelToken<'_>,
5844 ) -> Result<QueryResult, EngineError> {
5845 let inner = primary
5846 .lateral_subquery
5847 .as_deref()
5848 .expect("caller guards lateral_subquery.is_some()");
5849 // exec_select_cancel is the union-aware wrapper — the inner
5850 // SELECT may carry UNION tails on stmt.unions.
5851 let QueryResult::Rows {
5852 columns: inner_cols,
5853 rows,
5854 } = self.exec_select_cancel(inner, cancel)?
5855 else {
5856 return Err(EngineError::Unsupported(
5857 "derived table subquery must return rows".into(),
5858 ));
5859 };
5860 let alias = primary
5861 .alias
5862 .clone()
5863 .unwrap_or_else(|| primary.name.clone());
5864 // `AS t(a, b)` renames the materialised columns positionally
5865 // (extra inner columns keep their own names, PG behaviour).
5866 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5867 // v7.39 (read01 round 78) — a column-alias list longer than the item is
5868 // the error PG reports; SPG used to let the extra names through and then
5869 // fail two layers downstream with "column not found: <the extra name>".
5870 let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5871 if primary.unnest_column_aliases.len() > n_out {
5872 return Err(EngineError::Unsupported(alloc::format!(
5873 "table \"{alias}\" has {n_out} columns available but {} columns specified",
5874 primary.unnest_column_aliases.len()
5875 )));
5876 }
5877 if primary.scalar_fn_item && schema_cols.len() == 1 {
5878 schema_cols[0].scalar_row_source = true;
5879 }
5880 // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5881 // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5882 // The column-alias list, if given, names it like any other column.
5883 let mut rows = rows;
5884 if primary.with_ordinality {
5885 schema_cols.push(ColumnSchema::new(
5886 "ordinality".to_string(),
5887 DataType::BigInt,
5888 false,
5889 ));
5890 rows = rows
5891 .into_iter()
5892 .enumerate()
5893 .map(|(i, r)| {
5894 let mut v = r.values;
5895 #[allow(clippy::cast_possible_wrap)]
5896 v.push(Value::BigInt(i as i64 + 1));
5897 Row::new(v)
5898 })
5899 .collect();
5900 }
5901 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5902 if let Some(col) = schema_cols.get_mut(i) {
5903 col.name = new_name.clone();
5904 }
5905 }
5906 self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5907 }
5908
5909 /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5910 /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5911 /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5912 /// derived-table executor and the FROM-position table functions.
5913 fn exec_select_over_rows(
5914 &self,
5915 stmt: &SelectStatement,
5916 rows: alloc::vec::Vec<Row<'static>>,
5917 schema_cols: alloc::vec::Vec<ColumnSchema>,
5918 alias: &str,
5919 cancel: CancelToken<'_>,
5920 ) -> Result<QueryResult, EngineError> {
5921 let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5922 // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5923 // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5924 // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5925 // (the same path the aggregate branch uses); the old plain eval_expr let
5926 // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5927 let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5928 // WHERE.
5929 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5930 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5931 for row in rows {
5932 cancel.check()?;
5933 let v = self.eval_expr_with_correlated(
5934 w,
5935 &row,
5936 &scan_ctx,
5937 cancel,
5938 Some(&mut corr_memo.borrow_mut()),
5939 )?;
5940 if matches!(v, Value::Bool(true)) {
5941 out.push(row);
5942 }
5943 }
5944 out
5945 } else {
5946 rows
5947 };
5948 // Aggregate dispatch.
5949 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
5950 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5951 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5952 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5953 .map_err(|err| match err {
5954 EngineError::Eval(ev) => ev,
5955 other => eval::EvalError::TypeMismatch {
5956 detail: alloc::format!("{other}"),
5957 },
5958 })
5959 };
5960 // v7.39 (round 656) — hand the rows over as they are rather than
5961 // collecting a second vector of `RowRef` wrappers. Note this is
5962 // a set-returning-function path, NOT the relational scan: the
5963 // measured O(rows) cost lived in `run_single_table_aggregate`,
5964 // and converting these four first was a miss that cost a full
5965 // round — every test stayed green and the number did not move.
5966 let agg = aggregate::run(
5967 stmt,
5968 crate::join::AggRows::Owned(&filtered),
5969 &schema_cols,
5970 Some(alias),
5971 Some(&agg_correlated),
5972 self.parallel_runner.0.as_deref(),
5973 Some(self.active_catalog()),
5974 Some(self),
5975 )?;
5976 return self.finish_agg_result(agg, stmt, cancel);
5977 }
5978 // Projection.
5979 let projection = build_projection(
5980 &stmt.items,
5981 &schema_cols,
5982 alias,
5983 self.speaks_mysql,
5984 Some(self.active_catalog()),
5985 )?;
5986 // v7.39 (round 621) — a target-list SRF expands here too. This tail
5987 // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5988 // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5989 // answered `function unnest(integer[]) does not exist` for a query PG
5990 // answers.
5991 let srf_idxs = self.srf_target_idxs(&projection);
5992 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5993 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5994 alloc::vec::Vec::with_capacity(filtered.len());
5995 if !srf_idxs.is_empty() {
5996 let (rows, src) =
5997 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5998 projected_rows = rows;
5999 src_of_row = src;
6000 } else {
6001 for row in &filtered {
6002 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
6003 for p in &projection {
6004 let v = self.eval_expr_with_correlated(
6005 &p.expr,
6006 row,
6007 &scan_ctx,
6008 cancel,
6009 Some(&mut corr_memo.borrow_mut()),
6010 )?;
6011 vals.push(v);
6012 }
6013 projected_rows.push(Row::new(vals));
6014 }
6015 }
6016 let columns: alloc::vec::Vec<ColumnSchema> = projection
6017 .iter()
6018 // v7.39 (read01 round 54) — keep the column's enum identity through
6019 // the projection (it lives outside the DataType lattice), or a
6020 // derived table / UNION / windowed result forgets it and any outer
6021 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
6022 .map(|p| p.to_column_schema())
6023 .collect();
6024 // ORDER BY over the source rows (same shape as the other
6025 // synthetic-table executors).
6026 // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
6027 // OUTPUT column. Evaluated as an expression, as it was here, the literal
6028 // `1` is just the constant 1: the same sort key for every row, so the
6029 // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
6030 // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
6031 // landing on this executor) came back in input order.
6032 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6033 if !order_by.is_empty() {
6034 // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
6035 // SRF makes more of them than there were inputs.
6036 let out_cols = if srf_idxs.is_empty() {
6037 alloc::vec![None; order_by.len()]
6038 } else {
6039 srf_order_output_cols(&order_by, &projection)
6040 };
6041 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
6042 .iter()
6043 .enumerate()
6044 .map(|(k, out)| -> Result<_, EngineError> {
6045 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
6046 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
6047 .iter()
6048 .zip(out_cols.iter())
6049 .map(|(ob, oc)| {
6050 // v7.39 (read01 round 54) — this path builds its
6051 // sort keys itself instead of going through
6052 // `build_order_keys`, so it skipped the enum-ordinal
6053 // substitution: an OUTER `ORDER BY <enum col>` over
6054 // a DERIVED TABLE sorted by the label TEXT, not by
6055 // member order. Silently wrong rows, not an error.
6056 let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
6057 Ok(
6058 match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
6059 Some(ord) => Value::Float(ord),
6060 None => v,
6061 },
6062 )
6063 })
6064 .collect();
6065 Ok((k, keys?))
6066 })
6067 .collect::<Result<_, _>>()?;
6068 indexed.sort_by(|a, b| {
6069 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
6070 let o = &stmt.order_by[idx];
6071 let cmp = order_by_value_cmp_in(
6072 o.desc,
6073 o.nulls_first,
6074 ka,
6075 kb,
6076 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
6077 );
6078 if cmp != core::cmp::Ordering::Equal {
6079 return cmp;
6080 }
6081 }
6082 core::cmp::Ordering::Equal
6083 });
6084 projected_rows = indexed
6085 .into_iter()
6086 .map(|(i, _)| projected_rows[i].clone())
6087 .collect();
6088 }
6089 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
6090 if stmt.distinct {
6091 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
6092 // spec folds EVERY text position, so a column declared
6093 // `COLLATE utf8mb4_bin` had its values merged here exactly the
6094 // way 3b494b6e fixed on the main scan path. The projection is
6095 // already in scope at each of these sites, so the mask needs no
6096 // new plumbing -- it was simply never asked for.
6097 projected_rows = dedup_rows(
6098 projected_rows,
6099 FoldSpec::of_masks(
6100 scan_ctx.mysql_dialect,
6101 &fold_mask(&projection),
6102 &pad_mask(&projection),
6103 ),
6104 );
6105 }
6106 if let Some(offset) = stmt.offset_literal() {
6107 let off = (offset as usize).min(projected_rows.len());
6108 projected_rows.drain(..off);
6109 }
6110 if let Some(limit) = stmt.limit_literal() {
6111 projected_rows.truncate(limit as usize);
6112 }
6113 Ok(QueryResult::Rows {
6114 columns,
6115 rows: projected_rows,
6116 })
6117 }
6118
6119 /// Constant `SELECT` with no FROM: evaluate each projection item
6120 /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
6121 fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
6122 let empty_schema: Vec<ColumnSchema> = Vec::new();
6123 let ctx = self.ev_ctx(&empty_schema, None);
6124 // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
6125 // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
6126 // `SELECT string_agg('x',',')` → x). Before this it fell through to the
6127 // scalar projection, where the aggregate name looked like an unknown
6128 // function. The WHERE filters that one row, so `… WHERE false` leaves
6129 // the aggregate zero input rows (`count(*)` → 0).
6130 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
6131 let dummy = Row::new(Vec::new());
6132 let passes = match &stmt.where_ {
6133 Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
6134 None => true,
6135 };
6136 let rows: Vec<RowRef<'_>> = if passes {
6137 alloc::vec![RowRef::Owned(&dummy)]
6138 } else {
6139 Vec::new()
6140 };
6141 let agg = aggregate::run(
6142 stmt,
6143 crate::join::AggRows::Refs(&rows),
6144 &empty_schema,
6145 None,
6146 None,
6147 self.parallel_runner.0.as_deref(),
6148 Some(self.active_catalog()),
6149 Some(self),
6150 )?;
6151 return self.finish_agg_result(agg, stmt, CancelToken::none());
6152 }
6153 let projection = build_projection(
6154 &stmt.items,
6155 &empty_schema,
6156 "",
6157 self.speaks_mysql,
6158 Some(self.active_catalog()),
6159 )?;
6160 // `SELECT … WHERE cond` with no FROM — the one conceptual
6161 // row survives only when the condition is true (previously
6162 // the WHERE was silently ignored: `SELECT 1 WHERE false`
6163 // returned a row).
6164 let dummy_row = Row::new(Vec::new());
6165 if let Some(w) = &stmt.where_ {
6166 let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
6167 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6168 let columns: Vec<ColumnSchema> = projection
6169 .into_iter()
6170 .map(|p| p.to_column_schema())
6171 .collect();
6172 return Ok(QueryResult::Rows {
6173 columns,
6174 rows: Vec::new(),
6175 });
6176 }
6177 }
6178 // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6179 // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6180 // desugar to unnest) expands here: one output row per SRF row, sibling
6181 // scalar columns repeated. unnest / array_elements / path_query reach a
6182 // real FROM via the parser rewrite and never land here.
6183 // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6184 let srf_idxs = self.srf_target_idxs(&projection);
6185 if !srf_idxs.is_empty() {
6186 let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6187 let columns: Vec<ColumnSchema> = projection
6188 .into_iter()
6189 .map(|p| p.to_column_schema())
6190 .collect();
6191 // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6192 // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6193 // to. This returned straight out of the expansion, so
6194 // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6195 // input order — the sort was not wrong, it never ran. (There is
6196 // exactly one conceptual input row here, which is why the ordinary
6197 // scan pipeline is not on this path at all.)
6198 if !stmt.order_by.is_empty() {
6199 let synth_ctx =
6200 EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6201 let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6202 .order_by
6203 .iter()
6204 .map(|o| {
6205 let mut o = o.clone();
6206 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6207 && *n >= 1
6208 && let Ok(idx) = usize::try_from(*n - 1)
6209 && idx < columns.len()
6210 {
6211 o.expr = Expr::Column(spg_sql::ast::ColumnName {
6212 qualifier: None,
6213 name: columns[idx].name.clone(),
6214 });
6215 }
6216 o
6217 })
6218 .collect();
6219 let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6220 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6221 for r in rows {
6222 // v7.39.12 — a correlated subquery in ORDER BY is resolved
6223 // for this row before the key is built; see
6224 // `Engine::order_by_resolved_for_row`.
6225 let per_row = self.order_by_resolved_for_row(
6226 &resolved,
6227 &r,
6228 &synth_ctx,
6229 CancelToken::none(),
6230 )?;
6231 let keys =
6232 build_order_keys(per_row.as_deref().unwrap_or(&resolved), &r, &synth_ctx)?;
6233 tagged.push((keys, r));
6234 }
6235 sort_by_keys(&mut tagged, &descs);
6236 rows = tagged.into_iter().map(|(_, r)| r).collect();
6237 }
6238 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6239 return Ok(QueryResult::Rows { columns, rows });
6240 }
6241 let mut values = Vec::with_capacity(projection.len());
6242 for p in &projection {
6243 values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6244 }
6245 let columns: Vec<ColumnSchema> = projection
6246 .into_iter()
6247 .map(|p| p.to_column_schema())
6248 .collect();
6249 // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6250 // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6251 // returns none. (The SRF and aggregate arms above already applied
6252 // them; this tail was the one that didn't.)
6253 let mut rows = alloc::vec![Row::new(values)];
6254 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6255 Ok(QueryResult::Rows { columns, rows })
6256 }
6257
6258 /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6259 /// circuit. Catches
6260 /// SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6261 /// BEFORE `resolve_select_subqueries` materialises the inner result
6262 /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6263 /// values into a `HashSet<i64>` directly, then probes A.pk per
6264 /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6265 /// (~150 µs / query at INSUBQ benchmark scale).
6266 pub(crate) fn try_count_star_pk_in_subquery_fast(
6267 &self,
6268 stmt: &SelectStatement,
6269 cancel: CancelToken<'_>,
6270 ) -> Result<Option<QueryResult>, EngineError> {
6271 use spg_sql::ast::SelectItem;
6272 if stmt.distinct
6273 || stmt.limit_with_ties
6274 || stmt.group_by.is_some()
6275 || stmt.having.is_some()
6276 || !stmt.unions.is_empty()
6277 || !stmt.order_by.is_empty()
6278 || stmt.limit.is_some()
6279 || stmt.offset.is_some()
6280 || stmt.items.len() != 1
6281 {
6282 return Ok(None);
6283 }
6284 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6285 return Ok(None);
6286 };
6287 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6288 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6289 if !is_count_star {
6290 return Ok(None);
6291 }
6292 let Some(from) = stmt.from.as_ref() else {
6293 return Ok(None);
6294 };
6295 if !from.joins.is_empty()
6296 || from.primary.lateral_subquery.is_some()
6297 || from.primary.unnest_expr.is_some()
6298 || from.primary.generate_series_args.is_some()
6299 || from.primary.table_fn_call.is_some()
6300 || from.primary.as_of_segment.is_some()
6301 {
6302 return Ok(None);
6303 }
6304 let Some(where_expr) = stmt.where_.as_ref() else {
6305 return Ok(None);
6306 };
6307 // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6308 // negated=false; no other predicates.
6309 let Expr::InSubquery {
6310 expr: col_expr,
6311 subquery,
6312 negated: false,
6313 } = where_expr
6314 else {
6315 return Ok(None);
6316 };
6317 let Expr::Column(c) = col_expr.as_ref() else {
6318 return Ok(None);
6319 };
6320 let outer_alias = from
6321 .primary
6322 .alias
6323 .as_deref()
6324 .unwrap_or(from.primary.name.as_str());
6325 if let Some(q) = c.qualifier.as_deref()
6326 && !q.eq_ignore_ascii_case(outer_alias)
6327 {
6328 return Ok(None);
6329 }
6330 // Outer column must be a single-column PK on integer family.
6331 let catalog = self.active_catalog();
6332 let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6333 return Ok(None);
6334 };
6335 let outer_schema = outer_table.schema();
6336 let Some(outer_pos) = outer_schema
6337 .columns
6338 .iter()
6339 .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6340 else {
6341 return Ok(None);
6342 };
6343 if !matches!(
6344 outer_schema.columns[outer_pos].ty,
6345 spg_storage::DataType::BigInt
6346 | spg_storage::DataType::Int
6347 | spg_storage::DataType::SmallInt
6348 ) {
6349 return Ok(None);
6350 }
6351 if !outer_schema
6352 .uniqueness_constraints
6353 .iter()
6354 .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6355 {
6356 return Ok(None);
6357 }
6358 let Some(idx) = outer_table.index_on(outer_pos) else {
6359 return Ok(None);
6360 };
6361 // Inner must be uncorrelated. The cheap-correlation pre-check
6362 // exists upstream; here we just attempt the bare exec.
6363 if crate::subquery::select_is_correlated(subquery) {
6364 return Ok(None);
6365 }
6366 let mut inner = (**subquery).clone();
6367 self.resolve_select_subqueries(&mut inner, cancel)?;
6368 let r = match self.exec_bare_select_cancel(&inner, cancel) {
6369 Ok(r) => r,
6370 Err(_) => return Ok(None),
6371 };
6372 let QueryResult::Rows { columns, rows, .. } = r else {
6373 return Ok(None);
6374 };
6375 if columns.len() != 1 {
6376 return Ok(None);
6377 }
6378 // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6379 // subquery projects a column known to be UNIQUE/PK on its table
6380 // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6381 // in `tbl.uniqueness_constraints`), survivor values are
6382 // guaranteed distinct and the per-survivor `HashSet::insert`
6383 // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6384 //
6385 // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6386 // projection that is a bare Column ref, table-column lookup in
6387 // catalog confirms the column appears as a unique constraint's
6388 // sole member. UNIQUE NOT NULL is required — a nullable unique
6389 // column may have multiple NULLs, but NULLs are already skipped
6390 // above (`Value::Null => continue`), so a UNIQUE-only column is
6391 // still safe to dedup-skip.
6392 let inner_unique = (|| -> bool {
6393 if inner.distinct
6394 || inner.group_by.is_some()
6395 || !inner.unions.is_empty()
6396 || inner.having.is_some()
6397 || inner.items.len() != 1
6398 {
6399 return false;
6400 }
6401 let Some(inner_from) = inner.from.as_ref() else {
6402 return false;
6403 };
6404 if !inner_from.joins.is_empty()
6405 || inner_from.primary.lateral_subquery.is_some()
6406 || inner_from.primary.unnest_expr.is_some()
6407 || inner_from.primary.generate_series_args.is_some()
6408 || inner_from.primary.table_fn_call.is_some()
6409 {
6410 return false;
6411 }
6412 let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6413 return false;
6414 };
6415 let Expr::Column(pc) = proj else {
6416 return false;
6417 };
6418 let inner_alias = inner_from
6419 .primary
6420 .alias
6421 .as_deref()
6422 .unwrap_or(inner_from.primary.name.as_str());
6423 if let Some(q) = pc.qualifier.as_deref()
6424 && !q.eq_ignore_ascii_case(inner_alias)
6425 {
6426 return false;
6427 }
6428 let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6429 return false;
6430 };
6431 let isch = inner_table.schema();
6432 let Some(ipos) = isch
6433 .columns
6434 .iter()
6435 .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6436 else {
6437 return false;
6438 };
6439 isch.uniqueness_constraints
6440 .iter()
6441 .any(|u| u.columns.as_slice() == [ipos])
6442 })();
6443 // Collect inner i64 values directly into a HashSet, then probe.
6444 let mut count: i64 = 0;
6445 let mut probed = if inner_unique {
6446 hashbrown::HashSet::<i64>::new()
6447 } else {
6448 hashbrown::HashSet::<i64>::with_capacity(rows.len())
6449 };
6450 for row in &rows {
6451 let v = row.values.first().cloned().unwrap_or(Value::Null);
6452 let n = match v {
6453 Value::BigInt(n) => n,
6454 Value::Int(n) => i64::from(n),
6455 Value::SmallInt(n) => i64::from(n),
6456 Value::Null => continue,
6457 _ => return Ok(None),
6458 };
6459 // De-duplicate inner key set so a duplicate inner value
6460 // doesn't double-count the same outer row. Skipped when
6461 // the inner projection is statically unique.
6462 if !inner_unique && !probed.insert(n) {
6463 continue;
6464 }
6465 // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6466 // the `IndexKey::from_value` enum-dispatch and the per-call
6467 // `IndexKey` wrapper construction. The outer column is
6468 // already gated to integer-family above, so an i64 key
6469 // always corresponds to a valid PK lookup.
6470 if !idx.lookup_eq_i64(n).is_empty() {
6471 count += 1;
6472 }
6473 }
6474 let columns_out = alloc::vec![ColumnSchema::new(
6475 "count".to_string(),
6476 spg_storage::DataType::BigInt,
6477 false,
6478 )];
6479 let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6480 Ok(Some(QueryResult::Rows {
6481 columns: columns_out,
6482 rows: rows_out,
6483 }))
6484 }
6485
6486 /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6487 /// SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6488 /// (the post-subquery-replacement shape of the INSUBQ probe
6489 /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6490 /// The general aggregate path materialises every seeked row into
6491 /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6492 /// For COUNT(*) we only care how many keys hit; iterate the list
6493 /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6494 /// row materialisation, the aggregate state machine, and the per-
6495 /// row WHERE re-eval (the seek already filtered by the same list).
6496 /// Returns `None` when the shape doesn't match.
6497 fn try_count_star_pk_in_list_fast(
6498 &self,
6499 stmt: &SelectStatement,
6500 table: &spg_storage::Table,
6501 schema_cols: &[ColumnSchema],
6502 alias: &str,
6503 ) -> Option<QueryResult> {
6504 use spg_sql::ast::{ColumnName, SelectItem};
6505 // Gates on the SELECT shape.
6506 if stmt.distinct
6507 || stmt.limit_with_ties
6508 || stmt.group_by.is_some()
6509 || stmt.having.is_some()
6510 || !stmt.unions.is_empty()
6511 || !stmt.order_by.is_empty()
6512 || stmt.limit.is_some()
6513 || stmt.offset.is_some()
6514 || stmt.items.len() != 1
6515 {
6516 return None;
6517 }
6518 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6519 return None;
6520 };
6521 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6522 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6523 if !is_count_star {
6524 return None;
6525 }
6526 // WHERE must be `<col> IN (literal list)` with no other
6527 // conjuncts (the seek result is a true subset of the row
6528 // population for this predicate).
6529 let where_expr = stmt.where_.as_ref()?;
6530 let Expr::InList {
6531 expr: col_expr,
6532 list,
6533 negated: false,
6534 } = where_expr
6535 else {
6536 return None;
6537 };
6538 let Expr::Column(c) = col_expr.as_ref() else {
6539 return None;
6540 };
6541 if let Some(q) = c.qualifier.as_deref()
6542 && !q.eq_ignore_ascii_case(alias)
6543 {
6544 return None;
6545 }
6546 let col_pos = schema_cols
6547 .iter()
6548 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6549 // The column must be a single-column PK on an integer family
6550 // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6551 // so the antiset stays collision-free under `HashSet<i64>`.
6552 let schema = table.schema();
6553 if !matches!(
6554 schema.columns[col_pos].ty,
6555 spg_storage::DataType::BigInt
6556 | spg_storage::DataType::Int
6557 | spg_storage::DataType::SmallInt
6558 ) {
6559 return None;
6560 }
6561 if !schema
6562 .uniqueness_constraints
6563 .iter()
6564 .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6565 {
6566 return None;
6567 }
6568 let idx = table.index_on(col_pos)?;
6569 // Tally non-empty seek results across all literal values.
6570 let mut count: i64 = 0;
6571 for lit in list {
6572 let Expr::Literal(l) = lit else {
6573 return None;
6574 };
6575 // r1039 — through the shared resolver, so a literal spelled
6576 // in another type ('5' against an integer PK) is read as the
6577 // column's before it becomes a key. This tally answers from
6578 // the index alone, so a key in the wrong space would return a
6579 // COUNT of zero rather than fall back to a scan.
6580 let col = schema.columns.get(col_pos)?;
6581 let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6582 let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6583 if !idx.lookup_eq(&key).is_empty() {
6584 count += 1;
6585 }
6586 }
6587 let columns = alloc::vec![ColumnSchema::new(
6588 "count".to_string(),
6589 spg_storage::DataType::BigInt,
6590 false,
6591 )];
6592 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6593 let _ = ColumnName {
6594 qualifier: None,
6595 name: String::new(),
6596 };
6597 Some(QueryResult::Rows { columns, rows })
6598 }
6599
6600 /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6601 /// BETWEEN a AND b` on an indexed column. The index range walk yields
6602 /// exactly the matching (visible) rows, so we count locators directly —
6603 /// skipping the row materialisation, the aggregate state machine, and the
6604 /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6605 /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6606 /// when the shape doesn't match.
6607 fn try_count_star_indexed_range_fast(
6608 &self,
6609 stmt: &SelectStatement,
6610 table: &spg_storage::Table,
6611 schema_cols: &[ColumnSchema],
6612 alias: &str,
6613 snapshot: &spg_storage::snapshot::Snapshot,
6614 ) -> Option<QueryResult> {
6615 use spg_sql::ast::SelectItem;
6616 if stmt.distinct
6617 || stmt.limit_with_ties
6618 || stmt.group_by.is_some()
6619 || stmt.having.is_some()
6620 || !stmt.unions.is_empty()
6621 || !stmt.order_by.is_empty()
6622 || stmt.limit.is_some()
6623 || stmt.offset.is_some()
6624 || stmt.items.len() != 1
6625 {
6626 return None;
6627 }
6628 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6629 return None;
6630 };
6631 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6632 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6633 if !is_count_star {
6634 return None;
6635 }
6636 let where_expr = stmt.where_.as_ref()?;
6637 let count = crate::index_access::try_range_count(
6638 where_expr,
6639 schema_cols,
6640 table,
6641 alias,
6642 snapshot,
6643 self.speaks_mysql,
6644 )?;
6645 let columns = alloc::vec![ColumnSchema::new(
6646 "count".to_string(),
6647 spg_storage::DataType::BigInt,
6648 false,
6649 )];
6650 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6651 Some(QueryResult::Rows { columns, rows })
6652 }
6653
6654 /// Single-table aggregate path: filter the (optionally index-seeked)
6655 /// rows, then hand off to the aggregate executor which does its own
6656 /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6657 fn run_single_table_aggregate<'a>(
6658 &self,
6659 stmt: &SelectStatement,
6660 table: &'a spg_storage::Table,
6661 schema_cols: &'a [ColumnSchema],
6662 alias: &str,
6663 indexed_rows: Option<crate::index_access::Seeked<'a>>,
6664 cancel: CancelToken<'_>,
6665 ) -> Result<QueryResult, EngineError> {
6666 // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6667 // REPEATABLE (see run_single_table_scan). Aggregates
6668 // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6669 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6670 let ctx = self
6671 .ev_ctx(schema_cols, Some(alias))
6672 .with_sample_rng(&sample_cell);
6673 // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6674 // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6675 // and every abandoned buffer on the way stays resident: RSS is a
6676 // high-water mark, so the intermediates are paid for even though
6677 // they are freed. Round 656 measured the scan at 17 bytes/row
6678 // where the survivor list itself only needs 8.
6679 let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6680 Vec::with_capacity(table.rows().len())
6681 } else {
6682 // With a WHERE, the row count is an UPPER bound and reserving it
6683 // is the worse trade: `… WHERE id = 5` over 50M rows would take
6684 // 400 MB of pointers to hold one survivor. Let it grow.
6685 Vec::new()
6686 };
6687 // v6.2.6 — Memoize: per-query LRU cache for correlated
6688 // scalar subqueries. Fresh per row-loop entry so each
6689 // SELECT execution gets an isolated cache.
6690 let mut memo = memoize::MemoizeCache::new();
6691 // v7.37 (perf) — single-table aggregate's WHERE filter
6692 // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6693 // correlated`) per row, even for subquery-free WHEREs that
6694 // the single-table SCAN path has compiled since v7.32
6695 // (perf knife D). The asymmetry meant a fold-to-filter
6696 // rewrite (joinfold) that swapped a JOIN for a single-table
6697 // aggregate over a compiled WHERE saw the tree-walker
6698 // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6699 // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6700 // step. Compile once if eligible; fall back to the walker
6701 // for subquery-bearing or non-compilable WHEREs.
6702 let compiled_where: Option<eval::CompiledExpr> = stmt
6703 .where_
6704 .as_ref()
6705 .filter(|w| eval::fully_compilable(w))
6706 .map(|w| {
6707 // v7.38.8 — the scan filter runs the cheap half of its
6708 // conjunction first. Called from HERE and not from
6709 // `eval::compiled`, deliberately: the row loop lives in
6710 // that file, and adding a function to it cost this
6711 // query 11 % through layout alone while doing no work
6712 // for it. See `crate::qualorder`.
6713 match crate::qualorder::reordered(w) {
6714 Some(r) => eval::compile_expr(&r, &ctx),
6715 None => eval::compile_expr(w, &ctx),
6716 }
6717 });
6718 let mut eval_stack: Vec<Value<'static>> = Vec::new();
6719 let mut row_passes_where = |row: &Row<'static>,
6720 eval_stack: &mut Vec<Value<'static>>,
6721 memo: &mut memoize::MemoizeCache|
6722 -> Result<bool, EngineError> {
6723 match (&compiled_where, &stmt.where_) {
6724 (Some(cw), _) => {
6725 // v7.39 (round 479) — the predicate wants a bool, not a
6726 // Value. The owned entry ended in `Value::into_owned`
6727 // and the caller then dropped it, once per row; round
6728 // 478's profile put that pair above the comparison
6729 // itself.
6730 Ok(eval::compiled::eval_compiled_pred(
6731 cw,
6732 row,
6733 &ctx,
6734 eval_stack,
6735 ctx.mysql_dialect,
6736 )
6737 .map_err(EngineError::Eval)?)
6738 }
6739 (None, Some(w)) => {
6740 let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6741 Ok(crate::eval::predicate_is_true(
6742 &cond,
6743 "WHERE",
6744 ctx.mysql_dialect,
6745 )?)
6746 }
6747 (None, None) => Ok(true),
6748 }
6749 };
6750 if let Some(seeked) = &indexed_rows {
6751 // v7.38.19 — an EXACT seek has already applied the whole
6752 // predicate, so asking again is asking the index's question
6753 // a second time, once per row.
6754 //
6755 // Profiled on `count(*) FROM events WHERE project_id = 3`
6756 // over 200,000 rows: `try_index_seek` 1,814 leaf samples and
6757 // `binop::compare` 1,633 — and `compare`'s first arm is
6758 // `(Int, Int) => a.cmp(b)`, so it was never that a
6759 // comparison is expensive. It was that 25,000 of them were
6760 // re-deciding what the walk had decided. The same query with
6761 // `GROUP BY project_id` bolted on ran in half the time,
6762 // doing strictly more work, because that path reached the
6763 // rows differently.
6764 //
6765 // `exact` is false for every arm that has not proven it —
6766 // the GIN, trigram and jsonb walks, an `AND` whose other
6767 // conjuncts went unapplied, a collated key, a type whose key
6768 // cannot name it. See `index_access::Seeked`.
6769 if seeked.exact {
6770 filtered.extend(seeked.rows.iter().map(Cow::as_ref));
6771 } else {
6772 for cow in &seeked.rows {
6773 let row = cow.as_ref();
6774 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6775 continue;
6776 }
6777 filtered.push(row);
6778 }
6779 }
6780 }
6781 // v7.36 (cold-tier coverage) — single-table aggregate's
6782 // non-indexed full scan was hot-only and silently lost cold
6783 // rows on COUNT/SUM/etc. Materialise cold rows once into
6784 // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6785 // shape stays unchanged; the cold rows live until the end of
6786 // the aggregate run.
6787 let cold_rows_storage = if indexed_rows.is_none() {
6788 self.iter_cold_rows_of_table(table)
6789 } else {
6790 Vec::new()
6791 };
6792 if indexed_rows.is_none() {
6793 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6794 // single-table aggregate full-scan path. Mirrors the gate on
6795 // `run_single_table_scan`: this is a user-query result path,
6796 // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6797 // reader's snapshot cannot see (e.g. tombstoned versions),
6798 // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6799 // under the default gate-off: every hot row is frozen or
6800 // committed-and-alive, so `is_row_visible` returns true.
6801 // Cold-tier rows are frozen (visible) by definition — left
6802 // ungated, matching the plain-scan path.
6803 let scan_snapshot = self.current_snapshot();
6804 // v7.39 (pg_stat knife B) — this full-scan branch walks
6805 // headers directly (serial and sharded alike); count the
6806 // sequential scan here.
6807 table.note_seq_scan();
6808 // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6809 // filter dominate the pre-aggregate wall time on big
6810 // scans (P1's ground truth: accumulation is only ~17%).
6811 // Shard THAT work when the host injected an executor and
6812 // the WHERE is compiled (the compiled evaluator is pure
6813 // over &row; the tree-walker fallback can hit correlated
6814 // subqueries and stays serial). Shards return surviving
6815 // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6816 // 'static bound — and the main thread only dereferences.
6817 let n = table.row_count();
6818 let par = self.parallel_runner.0.as_deref().filter(|_| {
6819 n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6820 });
6821 // v7.38.11 — ask the BRIN summary first. When it prunes,
6822 // the work left is a few thousand rows and sharding it
6823 // costs more than it saves, so the serial pruned loop below
6824 // takes it; the shard machinery is left exactly as it was
6825 // rather than taught about slots.
6826 let brin_slots = stmt
6827 .where_
6828 .as_ref()
6829 .and_then(|w| crate::brin::candidate_slots(w, table));
6830 let brin_prunes = brin_slots
6831 .as_ref()
6832 .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6833 if let Some(r) = par
6834 && !brin_prunes
6835 {
6836 let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6837 let chunk = n.div_ceil(n_shards);
6838 type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6839 let cw = &compiled_where;
6840 let snap_ref = &scan_snapshot;
6841 let results = r.run_shards(n_shards, &|s| {
6842 let lo = s * chunk;
6843 let hi = ((s + 1) * chunk).min(n);
6844 let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6845 // EvalContext carries Cells (sampler / row counters)
6846 // and is !Sync — each shard builds its own from the
6847 // same Sync inputs. The compiled WHERE is gated to
6848 // the pure-scalar whitelist, which reads none of the
6849 // session state the engine-built ctx would add
6850 // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6851 // sampled scans never take this branch).
6852 let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6853 let mut stack: Vec<Value<'static>> = Vec::new();
6854 let out: ShardOut = (|| {
6855 for i in lo..hi {
6856 if !table.is_row_visible(i, snap_ref) {
6857 continue;
6858 }
6859 let row = &table.rows()[i];
6860 // v7.39 (round 480) — the parallel full-scan
6861 // shard is the path the aggregate benchmark
6862 // actually takes, and it was still on the OWNED
6863 // entry: round 480's profile attributed 68.7 %
6864 // of `drop_glue<Value>` to this closure, which
6865 // is why round 479's fix to the indexed path
6866 // barely moved the total.
6867 //
6868 // The `matches!(…, Value::Bool(true))` form was
6869 // also a narrower reading than the rest of the
6870 // engine uses — `predicate_is_true` is what
6871 // handles NULL and MySQL truthiness — so the
6872 // bool entry fixes the shape as well as the cost.
6873 let pass = match cw {
6874 Some(c) => eval::compiled::eval_compiled_pred(
6875 c,
6876 row,
6877 &shard_ctx,
6878 &mut stack,
6879 shard_ctx.mysql_dialect,
6880 )
6881 .map_err(EngineError::Eval)?,
6882 None => true,
6883 };
6884 if pass {
6885 keep.push(i);
6886 }
6887 }
6888 Ok(keep)
6889 })();
6890 alloc::boxed::Box::new(out)
6891 });
6892 // v7.39 (round 567) — `rows()` is a 32-way trie, so
6893 // indexing it is four dependent loads and a scan that
6894 // reads every row paid them every row. A profile of
6895 // `SELECT sum(id)` over 500k rows put 37.8% of the
6896 // connection thread's CPU on THIS ONE LINE. The cursor
6897 // holds the leaf, making that one descent per 32.
6898 let mut rows_cur = table.rows().run_cursor();
6899 for boxed in results {
6900 let shard = boxed
6901 .downcast::<ShardOut>()
6902 .expect("runner echoes the closure's box");
6903 for i in (*shard)? {
6904 if let Some(row) = rows_cur.get(i) {
6905 filtered.push(row);
6906 }
6907 }
6908 }
6909 } else {
6910 let mut rows_cur = table.rows().run_cursor();
6911 // v7.38.11 — the slots the BRIN summary could not rule
6912 // out. The predicate still runs on every row that
6913 // survives: the summary decides what to SKIP, never
6914 // what to return.
6915 let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6916 for range in ranges {
6917 for i in range {
6918 if !table.is_row_visible(i, &scan_snapshot) {
6919 continue;
6920 }
6921 let Some(row) = rows_cur.get(i) else { continue };
6922 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6923 continue;
6924 }
6925 filtered.push(row);
6926 }
6927 }
6928 }
6929 for row in &cold_rows_storage {
6930 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6931 continue;
6932 }
6933 filtered.push(row);
6934 }
6935 }
6936 // v7.29 — a per-query memo so correlated scalar
6937 // subqueries batch-evaluate once (group map) instead of
6938 // executing per group.
6939 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6940 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6941 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6942 .map_err(|err| match err {
6943 EngineError::Eval(ev) => ev,
6944 other => eval::EvalError::TypeMismatch {
6945 detail: alloc::format!("{other}"),
6946 },
6947 })
6948 };
6949 // v7.39 (round 656) — the plain relational scan. This collect() was
6950 // the measured defect: one 64-byte `RowRef` per surviving row to
6951 // wrap an 8-byte pointer `filtered` already holds. Scalar
6952 // aggregates measured ~81 bytes/row of working memory because of
6953 // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6954 // one number. `AggRows::Ptrs` reads the pointers directly.
6955 let agg = aggregate::run(
6956 stmt,
6957 crate::join::AggRows::Ptrs(&filtered),
6958 schema_cols,
6959 Some(alias),
6960 Some(&agg_correlated),
6961 self.parallel_runner.0.as_deref(),
6962 Some(self.active_catalog()),
6963 Some(self),
6964 )?;
6965 self.finish_agg_result(agg, stmt, cancel)
6966 }
6967
6968 /// Single-table scan + projection path: WHERE filter (compiled when
6969 /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6970 /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6971 fn run_single_table_scan<'a>(
6972 &self,
6973 stmt: &SelectStatement,
6974 table: &'a spg_storage::Table,
6975 schema_cols: &'a [ColumnSchema],
6976 alias: &str,
6977 indexed_rows: Option<crate::index_access::Seeked<'a>>,
6978 cancel: CancelToken<'_>,
6979 ) -> Result<QueryResult, EngineError> {
6980 // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6981 // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6982 // deterministic `__tsm_fract(seed)` draws share one scan-local
6983 // state (isolated from the global random() PRNG); a fresh cell per
6984 // scan makes a repeat / rescan reproduce the same sample. Unused
6985 // and cheap when the query carries no sample.
6986 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6987 let ctx = self
6988 .ev_ctx(schema_cols, Some(alias))
6989 .with_sample_rng(&sample_cell);
6990 let projection = build_projection(
6991 &stmt.items,
6992 schema_cols,
6993 alias,
6994 self.speaks_mysql,
6995 Some(self.active_catalog()),
6996 )?;
6997 // v7.19 P5 — single-table SELECT path for SRF
6998 // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6999 // unnest in the projection list. When present, the
7000 // per-row processor emits one output row per array
7001 // element (broadcasting non-SRF projections from the
7002 // same input row). Empty / NULL arrays emit zero rows
7003 // for that input — PG semantics.
7004 // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
7005 let srf_idxs = self.srf_target_idxs(&projection);
7006 let srf_position = srf_idxs.first().copied();
7007 // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
7008 let mut srf_plan = if srf_position.is_some() {
7009 Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
7010 } else {
7011 None
7012 };
7013
7014 // Materialise the filter pass into `(order_key, projected_row)`
7015 // tuples. The order key is `None` when there's no ORDER BY clause.
7016 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
7017 // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
7018 // output row to the per-query byte budget as it is built, so a
7019 // fat single-table scan / sort REJECTS with QueryBytesExceeded
7020 // at ~the ceiling instead of materialising the whole table and
7021 // only noticing at the final enforce_row_limit check. Without
7022 // this, N concurrent fat scans peak at N×table and OOM the host.
7023 // `max_query_bytes = None` (the embedded default) = no ceiling,
7024 // so existing unbudgeted behaviour is byte-identical.
7025 let mut budget = ByteBudget::new(self.max_query_bytes);
7026 // v6.2.6 — Memoize per-row WHERE eval shares one cache.
7027 let mut memo = memoize::MemoizeCache::new();
7028 // v7.32 (perf knife D) — subquery-free WHERE compiles once;
7029 // the row loop then runs a flat step program instead of a
7030 // tree interpretation per row.
7031 let compiled_where: Option<eval::CompiledExpr> = stmt
7032 .where_
7033 .as_ref()
7034 .filter(|w| eval::fully_compilable(w))
7035 .map(|w| {
7036 // v7.38.8 — the scan filter runs the cheap half of its
7037 // conjunction first. Called from HERE and not from
7038 // `eval::compiled`, deliberately: the row loop lives in
7039 // that file, and adding a function to it cost this
7040 // query 11 % through layout alone while doing no work
7041 // for it. See `crate::qualorder`.
7042 match crate::qualorder::reordered(w) {
7043 Some(r) => eval::compile_expr(&r, &ctx),
7044 None => eval::compile_expr(w, &ctx),
7045 }
7046 });
7047 let mut eval_stack: Vec<Value<'static>> = Vec::new();
7048 // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
7049 // SELECT-item scalar subquery for the PK-probe fast path. The
7050 // analysis (gate checks + catalog lookups) takes ~500 ns; doing
7051 // it once per query instead of once per row × 100 rows saves
7052 // ~50 µs and lets the per-row evaluation reduce to a single
7053 // index probe + outer-column read.
7054 let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
7055 .iter()
7056 .map(|p| {
7057 if let Expr::ScalarSubquery(inner) = &p.expr {
7058 self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
7059 } else {
7060 None
7061 }
7062 })
7063 .collect();
7064 let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
7065 // v7.39 (round 487) — a projection item that is a bare column
7066 // reference binds its position ONCE per query.
7067 //
7068 // Per row it used to walk `eval_expr_with_correlated` (a memo
7069 // lookup for "does this have a subquery", then an un-memoised
7070 // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
7071 // then `resolve_column`, which finds the column by scanning the
7072 // schema and comparing NAMES. On `SELECT g FROM h` that chain was
7073 // 19 % of self time for what is ultimately one cell read.
7074 //
7075 // `compile_column_pos` is the Step VM's resolver, already
7076 // `pub(crate)` and already reused by the aggregate's bind-once
7077 // path: it mirrors `resolve_column`'s happy layers and returns
7078 // None for anything that would reach an error, an ambiguity, or a
7079 // miss, so those still go the interpreter's way and keep its
7080 // exact message. A composite column is excluded for the same
7081 // reason `compile_into` excludes it — it must be rehydrated from
7082 // stored JSON, which is not a cell read.
7083 let proj_direct = bind_direct_columns(&projection, &ctx);
7084 let any_proj_direct = proj_direct.iter().any(Option::is_some);
7085 // v7.39 (round 605) — a projection item that cannot depend on the row
7086 // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
7087 // allocations a row against one for a plain column, `'abc' || 'def'`
7088 // six and `upper('abc')` five, all of them producing the same value
7089 // 50,000 times. An item that fails to evaluate is left alone, so its
7090 // error still comes from the row loop in the interpreter's wording.
7091 let proj_const: Vec<Option<Value<'static>>> = projection
7092 .iter()
7093 .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
7094 .collect();
7095 let any_proj_const = proj_const.iter().any(Option::is_some);
7096 crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
7097 // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
7098 // projection. Statement prep (`resolve_order_by_position`) can only map
7099 // `ORDER BY 1` onto the first SELECT item when that item is an
7100 // expression; a `*` is not one, so the literal survived to here and was
7101 // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
7102 // at all. The parser rewrites `SELECT unnest(a) x` into
7103 // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
7104 // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
7105 // back in input order. The projection is built by now, so the Nth output
7106 // column is known — resolve against it.
7107 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
7108 // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
7109 // EXPANDED rows, so a key naming a select-list item reads that item.
7110 let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
7111 srf_order_output_cols(&order_by, &projection)
7112 } else {
7113 Vec::new()
7114 };
7115 let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
7116 // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
7117 // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
7118 // Hoisted above the closure so the projection-eval path can
7119 // gate `memo` passing on it: the SELECT-item correlated-scalar
7120 // batch path scans the FULL inner table once (~5 ms for 12.5 k
7121 // rows) and is only a win when N outer rows is large; for small
7122 // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
7123 let early_cap: Option<usize> = if order_by.is_empty()
7124 && !stmt.distinct
7125 && !stmt.limit_with_ties
7126 && srf_position.is_none()
7127 && stmt.where_.is_none()
7128 {
7129 stmt.limit_literal()
7130 .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
7131 } else {
7132 None
7133 };
7134 // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
7135 // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
7136 // full-sort by the test gate) keep only the running top-`keep`
7137 // rows in memory instead of materialising every projected row,
7138 // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
7139 // space, not O(rows). `None` = accumulate everything (the prior
7140 // behaviour). The final `partial_sort_tagged(keep)` below still
7141 // runs and produces the identical rows.
7142 // v7.39 (round 683) — the declared collation for each ORDER BY
7143 // position, resolved once and carried beside `descs` for the same
7144 // reason `descs` is carried: it is per key position, not per row.
7145 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
7146 let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
7147 && !stmt.distinct
7148 && !stmt.limit_with_ties
7149 && srf_position.is_none()
7150 && !self.env_cfg().disable_topk
7151 {
7152 stmt.limit_literal().and_then(|l| {
7153 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
7154 (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
7155 })
7156 } else {
7157 None
7158 };
7159 // v7.38.19 — when the sort column is one the projection already
7160 // carries, build no key at all and sort by reading it.
7161 //
7162 // Restricted to the FULL sort: a top-N compares against a stored
7163 // boundary key and `WITH TIES` extends past the limit through the
7164 // keys, both of which need one to exist. DISTINCT keys on them
7165 // too, and an SRF's keys come from the EXPANDED row.
7166 // A COLLATION does not rule it out, but it has to be one that
7167 // orders these values the way bytes do -- decided on the values
7168 // themselves, further down, once they exist.
7169 let sort_by_output: Option<Vec<usize>> = if stmt.distinct
7170 || stmt.limit_with_ties
7171 || srf_position.is_some()
7172 || topk_stream.is_some()
7173 {
7174 None
7175 } else {
7176 order_by_output_cols_if_identical(&order_by, &projection, schema_cols)
7177 };
7178 // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
7179 // kept rows in `tagged`. Probing on the PROJECTED row as soon as
7180 // it is built means a duplicate costs neither a build_order_keys
7181 // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
7182 // a tagged slot, and the sort below runs over u survivors, not
7183 // n input rows — PG's hash-distinct-then-sort plan shape.
7184 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
7185 hashbrown::HashMap::new();
7186 let distinct_hb = hashbrown::DefaultHashBuilder::default();
7187 // v7.38.13 — which output positions must NOT fold. Built once per
7188 // scan from the projection, which carries the source column's
7189 // byte-wise-ness; see `FoldSpec`.
7190 let distinct_mask = fold_mask(&projection);
7191 // v7.39 (round 485) — one projection buffer for the whole scan
7192 // rather than a fresh `Vec` per input row. A row that survives
7193 // the DISTINCT probe takes the buffer with it (`mem::take`) and
7194 // the next row allocates a new one; a row that duplicates an
7195 // earlier one leaves the buffer — and its capacity — in place.
7196 // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
7197 // projected rows are duplicates, so that is 49 900 allocate /
7198 // free pairs the scan no longer performs. Shapes where every row
7199 // survives (plain projection, `DISTINCT` over a unique column)
7200 // allocate exactly as often as before.
7201 let mut proj_buf: Vec<Value<'static>> = Vec::new();
7202 // v7.39 (round 571) — buffers handed back by the top-N trim.
7203 // Round 485 made the scan share ONE projection buffer, but a
7204 // surviving row takes it (`mem::take`) and without DISTINCT
7205 // almost every row survives, so the next one starts from zero
7206 // capacity and allocates. The trim drops `keep` rows at a time
7207 // and their buffers come back here instead of being freed.
7208 let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
7209 let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
7210 // v7.39 (round 581) — the worst row the accumulator is currently
7211 // keeping. Anything that loses to it cannot reach the answer, so
7212 // it is dropped before its projection is ever built.
7213 let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
7214 // v7.38.20 — the boundary's own leading eight bytes, so a losing
7215 // row can be turned away before a key is built for it. Kept
7216 // beside the boundary and refreshed with it; `None` whenever the
7217 // boundary's first key is not one this can read, which sends
7218 // every row down the ordinary path.
7219 // v7.38.21 — and whether those bytes may be trusted under the
7220 // collation in force, which is the boundary's own text to answer.
7221 let mut topk_boundary_prefix: Option<(crate::orderby::PrefixKind, u64, bool)> = None;
7222 // v7.39 (round 582) — resolve each ORDER BY column once, not
7223 // once per row. See `order_by_bound_positions`.
7224 let order_bound =
7225 crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
7226 // v7.39.12 — a correlated scalar subquery in ORDER BY is
7227 // resolved for the row before its key is built.
7228 //
7229 // Uncorrelated subqueries are replaced by a literal before
7230 // execution; a correlated one cannot be, so it reached the
7231 // per-row evaluator — the one place that cannot run a subquery
7232 // — and the statement raised "subquery reached row eval".
7233 // Reported by sentori against 7.39.11; see
7234 // `Engine::order_by_resolved_for_row`.
7235 //
7236 // The `any` runs once, here, so an ordinary ORDER BY pays one
7237 // bool per row and nothing else.
7238 let order_has_subquery = order_by
7239 .iter()
7240 .any(|o| crate::subquery::expr_has_subquery(&o.expr));
7241 let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
7242 // v7.39 (round 581) — and it stops asking when the answer is
7243 // always "keep".
7244 //
7245 // The check earns its place only on rows it rejects. Over
7246 // ascending ids, `ORDER BY id DESC` never rejects one — every
7247 // row beats the current worst — so the comparison is pure
7248 // overhead there, measured at +5.5% in three batches out of
7249 // three. After a window of rows it looks at what it has
7250 // actually rejected and switches itself off if the shape is not
7251 // paying. The answers do not depend on it either way.
7252 // v7.38.21 — resolved once per query, not per row.
7253 //
7254 // No collation at all is the case v7.38.20 shipped. A DECLARED
7255 // one may still be answered by bytes, and which collations those
7256 // are is `Collated::ascii_byte_order`'s to say — the same
7257 // allowlist `byte_order_answers_the_collation` consults, so the
7258 // two cannot come to disagree about a collation. What that
7259 // allowlist requires of the TEXT is checked per row and on the
7260 // boundary, because a streaming top-N has no batch to check.
7261 let boundary_no_collation = order_colls.iter().all(Option::is_none);
7262 let boundary_collations_permit = boundary_no_collation
7263 || order_colls
7264 .iter()
7265 .flatten()
7266 .all(crate::collate::Collated::ascii_byte_order);
7267 const BOUNDARY_WINDOW: u32 = 8192;
7268 let mut boundary_checks: u32 = 0;
7269 let mut boundary_rejects: u32 = 0;
7270 let mut boundary_check_on = true;
7271 // Inline the per-row work in a closure so the indexed and full-
7272 // scan branches share the body.
7273 // v7.38.19 — `check_where` is per CALL SITE, not per closure: the
7274 // full-scan loops below must apply the predicate, and the
7275 // indexed loop must not when the seek already did. A captured
7276 // flag would have to be right for both.
7277 let mut process_row = |row: &Row<'static>,
7278 loop_idx: usize,
7279 check_where: bool|
7280 -> Result<(), EngineError> {
7281 if loop_idx.is_multiple_of(256) {
7282 cancel.check()?;
7283 }
7284 if !check_where {
7285 // The seek answered the whole predicate. See
7286 // `index_access::Seeked`.
7287 } else if let Some(cw) = &compiled_where {
7288 let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7289 .map_err(EngineError::Eval)?;
7290 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7291 return Ok(());
7292 }
7293 } else if let Some(where_expr) = &stmt.where_ {
7294 let cond =
7295 self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7296 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7297 return Ok(());
7298 }
7299 }
7300 // Under DISTINCT the keys are built AFTER the dup probe
7301 // (survivors only); the non-distinct order is unchanged.
7302 // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7303 // row further down, and building them here would evaluate the
7304 // ORDER BY against the INPUT row: a key naming the SRF's own
7305 // output became a scalar call to it, which is where
7306 // "function unnest(integer[]) does not exist" came from.
7307 let order_keys = if order_by.is_empty()
7308 || stmt.distinct
7309 || srf_position.is_some()
7310 // v7.38.19 — the branch below builds whatever key it
7311 // needs from the projected values, collation included,
7312 // so nothing has to be built here for it.
7313 //
7314 // A draft that skipped them here but still let the
7315 // COLLATED case fall through to the key-based sort put a
7316 // mixed column back in INSERT order: every key empty,
7317 // every row equal, a stable sort faithfully preserving
7318 // nothing. The rule is one decision, not two.
7319 || sort_by_output.is_some()
7320 {
7321 Vec::new()
7322 } else {
7323 // v7.38.20 — turn a decisively losing row away before
7324 // its key is built. Only the FIRST key is read, and only
7325 // its leading eight bytes; a tie there decides nothing
7326 // and falls through to the full path below.
7327 //
7328 // ASC only: under DESC the boundary is the largest kept
7329 // key and the comparison flips, which this deliberately
7330 // does not try to express — a second direction in a
7331 // fast-path predicate is how one of them ends up wrong.
7332 if boundary_check_on
7333 && let Some((_, descs)) = &topk_stream
7334 && !descs.first().copied().unwrap_or(false)
7335 && order_by.len() == 1
7336 && boundary_collations_permit
7337 && let Some((bkind, bp, boundary_is_ascii)) = topk_boundary_prefix
7338 && let Some((rkind, rp, row_is_ascii)) =
7339 crate::orderby::first_key_prefix(&order_bound, row)
7340 && bkind == rkind
7341 && (boundary_no_collation || (boundary_is_ascii && row_is_ascii))
7342 && rp > bp
7343 {
7344 boundary_checks += 1;
7345 boundary_rejects += 1;
7346 if boundary_checks == BOUNDARY_WINDOW {
7347 boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7348 }
7349 return Ok(());
7350 }
7351 let mut buf = key_pool.pop().unwrap_or_default();
7352 if order_has_subquery {
7353 // A substituted literal is no longer a bound column.
7354 let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7355 crate::orderby::build_order_keys_bound(
7356 per_row.as_deref().unwrap_or(&order_by),
7357 &unbound,
7358 &order_colls,
7359 row,
7360 &ctx,
7361 &mut buf,
7362 )?;
7363 } else {
7364 crate::orderby::build_order_keys_bound(
7365 &order_by,
7366 &order_bound,
7367 &order_colls,
7368 row,
7369 &ctx,
7370 &mut buf,
7371 )?;
7372 }
7373 // v7.39 (round 581) — reject before projecting.
7374 //
7375 // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7376 // 50 distinct `g` decides nearly every row on the FIRST
7377 // key, and PG answers it FASTER than the single-key form
7378 // (7.4 ms against 10.4) because a rejected row costs it
7379 // one comparison. SPG built both keys AND the projected
7380 // row for all 500k before throwing them away. The keys
7381 // are needed to compare; the projection is not.
7382 if boundary_check_on
7383 && let Some((_, descs)) = &topk_stream
7384 && let Some(b) = &topk_boundary
7385 {
7386 boundary_checks += 1;
7387 let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7388 == core::cmp::Ordering::Greater;
7389 if loses {
7390 boundary_rejects += 1;
7391 }
7392 if boundary_checks == BOUNDARY_WINDOW {
7393 // Keep asking only if it has been rejecting at
7394 // least a quarter of what it saw.
7395 boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7396 }
7397 if loses {
7398 buf.clear();
7399 key_pool.push(buf);
7400 return Ok(());
7401 }
7402 }
7403 buf
7404 };
7405 if srf_position.is_some() {
7406 let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7407 for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7408 if stmt.distinct {
7409 let bucket = seen_distinct
7410 .entry(norm_hash_row(
7411 &out,
7412 &distinct_hb,
7413 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7414 ))
7415 .or_default();
7416 if bucket.iter().any(|i| {
7417 row_eq_norm(
7418 &tagged[i].1,
7419 &out,
7420 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7421 )
7422 }) {
7423 continue;
7424 }
7425 bucket.push(tagged.len());
7426 }
7427 budget.charge(approx_row_bytes(&out))?;
7428 // The keys come from THIS expanded row: a key naming a
7429 // select-list item reads its value, anything else is
7430 // still evaluated against the input row.
7431 let keys = if order_by.is_empty() {
7432 Vec::new()
7433 } else {
7434 let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7435 for (k, ob) in order_by.iter().enumerate() {
7436 kv.push(match srf_order_cols.get(k).copied().flatten() {
7437 Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7438 None => eval::eval_expr(&ob.expr, row, &ctx)
7439 .map_err(EngineError::Eval)?,
7440 });
7441 }
7442 // Packed by the same code every other ORDER BY uses,
7443 // so DESC / NULLS FIRST / the MySQL rule are not
7444 // restated here.
7445 let key_row = Row::new(kv);
7446 let mut buf = Vec::new();
7447 crate::orderby::build_order_keys_bound(
7448 &order_by,
7449 &srf_key_bound,
7450 &order_colls,
7451 &key_row,
7452 &ctx,
7453 &mut buf,
7454 )?;
7455 buf
7456 };
7457 tagged.push((keys, out));
7458 }
7459 } else {
7460 let values = &mut proj_buf;
7461 values.clear();
7462 values.reserve(projection.len());
7463 for (i, p) in projection.iter().enumerate() {
7464 // v7.37.x (docker-fair SCALARSQ attack) — pre-
7465 // analysed PK-probe fast path. The per-row work is
7466 // a read of outer.col from the row plus an index
7467 // probe — no Expr clone, no walker, no
7468 // `eval_expr_with_correlated` framework.
7469 if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7470 values.push(self.probe_with_pk_fast_path(fp, row));
7471 continue;
7472 }
7473 // v7.39 (round 605) — the same value every row.
7474 if any_proj_const && let Some(v) = &proj_const[i] {
7475 values.push(v.clone());
7476 continue;
7477 }
7478 // v7.39 (round 487) — bound column: read the cell.
7479 // This is `rehydrate_cell`'s body for a non-composite
7480 // column, which is what the whole chain below reduces
7481 // to once the name has been resolved.
7482 if any_proj_direct && let Some(pos) = proj_direct[i] {
7483 crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7484 values.push(row.values[pos].clone().into_owned());
7485 continue;
7486 }
7487 // v7.24 (round-16 B) — correlated-aware.
7488 // v7.37.x (docker-fair SCALARSQ attack) — share the
7489 // per-row memo with projection. Required for the
7490 // batch-evaluated correlated-scalar path to fire on
7491 // SELECT-item scalar subqueries; otherwise each row
7492 // re-executes the inner.
7493 //
7494 // Skip the memo when the outer row count is small
7495 // (early-limited): the batch path scans the FULL
7496 // inner table to build a GroupMap (~5 ms for a
7497 // 12.5 k-row inner), while per-row execution with a
7498 // PK index seek is ~5 µs per call — much cheaper for
7499 // N ≤ ~1000 outer rows.
7500 let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7501 let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7502 values.push(
7503 self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7504 );
7505 }
7506 crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7507 if stmt.distinct {
7508 let bucket = seen_distinct
7509 .entry(norm_hash_values(
7510 &proj_buf,
7511 &distinct_hb,
7512 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7513 ))
7514 .or_default();
7515 if bucket.iter().any(|i| {
7516 values_eq_norm(
7517 &tagged[i].1.values,
7518 &proj_buf,
7519 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7520 )
7521 }) {
7522 crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7523 return Ok(());
7524 }
7525 bucket.push(tagged.len());
7526 }
7527 let out = Row::new(core::mem::replace(
7528 &mut proj_buf,
7529 proj_pool.pop().unwrap_or_default(),
7530 ));
7531 let order_keys = if stmt.distinct && !order_by.is_empty() {
7532 // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7533 // the bound-cell path precisely so an ORDER BY key that
7534 // names a column is READ instead of evaluated, and the
7535 // non-DISTINCT branch above has passed it ever since;
7536 // this branch never did, so `SELECT DISTINCT k .. ORDER
7537 // BY k` resolved "k" by string for every surviving row.
7538 let mut buf = key_pool.pop().unwrap_or_default();
7539 if order_has_subquery {
7540 // A substituted literal is no longer a bound column.
7541 let per_row =
7542 self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7543 crate::orderby::build_order_keys_bound(
7544 per_row.as_deref().unwrap_or(&order_by),
7545 &unbound,
7546 &order_colls,
7547 row,
7548 &ctx,
7549 &mut buf,
7550 )?;
7551 } else {
7552 crate::orderby::build_order_keys_bound(
7553 &order_by,
7554 &order_bound,
7555 &order_colls,
7556 row,
7557 &ctx,
7558 &mut buf,
7559 )?;
7560 }
7561 buf
7562 } else {
7563 order_keys
7564 };
7565 budget.charge(approx_row_bytes(&out))?;
7566 tagged.push((order_keys, out));
7567 }
7568 // Streaming top-N: bound the accumulator to O(keep) rows.
7569 if let Some((k, descs)) = &topk_stream {
7570 crate::orderby::topk_trim_recycling(
7571 &mut tagged,
7572 *k,
7573 descs,
7574 &mut proj_pool,
7575 &mut key_pool,
7576 &mut topk_boundary,
7577 );
7578 // The prefix follows the boundary it summarises.
7579 topk_boundary_prefix = topk_boundary
7580 .as_ref()
7581 .and_then(|b| b.first())
7582 .and_then(crate::orderby::order_key_prefix);
7583 }
7584 Ok(())
7585 };
7586 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7587 // load-bearing full-scan path. This is the primary single-table
7588 // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7589 // in-place writers retain dead/old versions, an ungated scan
7590 // here would return them, so the gate must land BEFORE the
7591 // writers flip (see the plan's activation-order rule). A no-op
7592 // today: every hot row is frozen or committed-and-alive under
7593 // the reader's snapshot, so `is_row_visible` returns true for
7594 // all of them (verified by the full e2e suite staying green).
7595 let scan_snapshot = self.current_snapshot();
7596 let mut emitted: usize = 0;
7597 if let Some(seeked) = &indexed_rows {
7598 let recheck = !seeked.exact;
7599 for (loop_idx, cow) in seeked.rows.iter().enumerate() {
7600 if let Some(cap) = early_cap
7601 && emitted >= cap
7602 {
7603 break;
7604 }
7605 process_row(cow.as_ref(), loop_idx, recheck)?;
7606 emitted = emitted.saturating_add(1);
7607 }
7608 } else {
7609 // v7.39 (round 570) — the row store is a 32-way trie, so
7610 // indexing it is four dependent loads. Round 567 measured
7611 // -18% on the aggregate scan from holding the leaf between
7612 // rows; this is the same loop for the projecting scan.
7613 let mut rows_cur = table.rows().run_cursor();
7614 // v7.38.11 — see the aggregate scan above: a BRIN index on a
7615 // column this WHERE bounds says which slots cannot match.
7616 let brin_slots = stmt
7617 .where_
7618 .as_ref()
7619 .and_then(|w| crate::brin::candidate_slots(w, table))
7620 .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7621 for i in brin_slots.into_iter().flatten() {
7622 if let Some(cap) = early_cap
7623 && emitted >= cap
7624 {
7625 break;
7626 }
7627 // Skip rows this snapshot cannot see (invisible rows do
7628 // not count toward the LIMIT).
7629 if !table.is_row_visible(i, &scan_snapshot) {
7630 continue;
7631 }
7632 let Some(row) = rows_cur.get(i) else { continue };
7633 process_row(row, i, true)?;
7634 emitted = emitted.saturating_add(1);
7635 }
7636 // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7637 // rows into the same loop. The full-scan path here is the
7638 // load-bearing single-table SELECT executor, and pre-
7639 // 7.35.1 it only walked `table.rows()` (hot), so any
7640 // `SELECT … FROM t` against a table with cold segments
7641 // silently returned a subset.
7642 let cold_rows = self.iter_cold_rows_of_table(table);
7643 for (offset, row) in cold_rows.iter().enumerate() {
7644 if let Some(cap) = early_cap
7645 && emitted >= cap
7646 {
7647 break;
7648 }
7649 process_row(row, table.row_count() + offset, true)?;
7650 emitted = emitted.saturating_add(1);
7651 }
7652 }
7653
7654 // (DISTINCT already de-duped STREAMING inside process_row, so the
7655 // sort below only sees the u survivors and the partial-sort
7656 // budget applies to DISTINCT too.)
7657 if !order_by.is_empty() {
7658 // Partial-sort fast path: when LIMIT is small relative to
7659 // the row count, select_nth_unstable + sort just the
7660 // prefix is O(n + k log k) instead of O(n log n).
7661 // WITH TIES needs the full sort so the tie extension can
7662 // scan past `limit` to find rows that share the last-kept
7663 // row's key.
7664 let keep = if stmt.limit_with_ties
7665 // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7666 // forces the full-sort fallback by suppressing the
7667 // partial-sort `keep` budget. See
7668 // `xtests/sigil/test-mode-gucs.md`.
7669 || self.env_cfg().disable_topk
7670 {
7671 None
7672 } else {
7673 stmt.limit_literal()
7674 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7675 };
7676 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7677 if let Some(cols) = &sort_by_output {
7678 // No keys were built; the sort reads the projected row.
7679 // The comparator is the value-level one the window
7680 // functions and the key path both defer to, so DESC,
7681 // NULLS placement, the MySQL fold and the collation are
7682 // not restated here.
7683 let terms: Vec<(usize, bool, Option<bool>)> = cols
7684 .iter()
7685 .zip(order_by.iter())
7686 .map(|(c, o)| (*c, o.desc, o.nulls_first))
7687 .collect();
7688 let mysql = ctx.mysql_dialect;
7689 // v7.38.19 — sort a PERMUTATION carrying the first eight
7690 // bytes, not the rows.
7691 //
7692 // The elements above are `(Vec<OrderKey>, Row)`, 48 bytes,
7693 // and driftsort moves them ~n log n times: 7.4 M moves at
7694 // 400,000 rows. Worse, every comparison chases three
7695 // dependent loads PER SIDE to reach the byte it wants --
7696 // the row's `Vec`, the `Value`, then the string's own
7697 // buffer -- and a profile of this sort put 35% of its
7698 // working samples in the sort machinery around that.
7699 //
7700 // A `(u64, u32)` is 16 bytes and the comparison reads it
7701 // straight out of the array. The u64 is the first eight
7702 // bytes big-endian, zero-padded, which ORDERS THE SAME as
7703 // the string: if two differ inside those bytes they differ
7704 // at the same index either way, and a string shorter than
7705 // eight pads with zeros exactly where `[u8]`'s own
7706 // comparison runs out. Equal prefixes fall through to the
7707 // full comparator, so nothing rests on the padding being
7708 // clever.
7709 //
7710 // The tail-break on the index is what keeps the sort
7711 // STABLE, which `sort_by` was giving for free and an
7712 // unstable sort over a permutation would not.
7713 // v7.38.19 — three ways to sort these rows, and which
7714 // one is right turns on the values, which is why it is
7715 // decided here rather than at plan time.
7716 //
7717 // * the collation orders these values the way bytes do
7718 // -- take the eight-byte key below
7719 // * it does not, but there IS a collation -- build its
7720 // sort key once per row and order the permutation on
7721 // those, which is what the key path did, done from
7722 // the projected value instead of during the scan
7723 // * no collation at all -- the eight-byte key again
7724 //
7725 // The middle case is the one a draft got wrong by
7726 // leaving the rows to a key path whose keys it had just
7727 // skipped building.
7728 let mut keep_sorted = false;
7729 let bytes_answer = byte_order_answers_the_collation(&tagged, &terms, &order_colls);
7730 if !bytes_answer && let Some(coll) = order_colls.first().and_then(Option::as_ref) {
7731 let (first_col, first_desc, _) = terms[0];
7732 let mut order: Vec<(Vec<u8>, u32)> = Vec::with_capacity(tagged.len());
7733 for (i, row) in tagged.iter().enumerate() {
7734 let k = match row.1.values.get(first_col) {
7735 Some(Value::Text(t)) => coll.sort_key_of(t).unwrap_or_else(|| {
7736 let mut v = Vec::with_capacity(t.len() + 1);
7737 v.push(0);
7738 v.extend_from_slice(t.as_bytes());
7739 v
7740 }),
7741 _ => Vec::new(),
7742 };
7743 order.push((k, u32::try_from(i).unwrap_or(u32::MAX)));
7744 }
7745 order.sort_by(|(ka, ia), (kb, ib)| {
7746 let c = ka.cmp(kb);
7747 let c = if first_desc { c.reverse() } else { c };
7748 if c != core::cmp::Ordering::Equal {
7749 return c;
7750 }
7751 row_cmp_by_index(&tagged, &terms, &order_colls, mysql, *ia, *ib)
7752 .then_with(|| ia.cmp(ib))
7753 });
7754 let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7755 core::mem::take(&mut tagged).into_iter().map(Some).collect();
7756 tagged = order
7757 .iter()
7758 .map(|&(_, i)| {
7759 slots[i as usize]
7760 .take()
7761 .expect("the permutation names each row once")
7762 })
7763 .collect();
7764 keep_sorted = true;
7765 }
7766 // v7.38.20 — a key that does NOT discriminate is still
7767 // worth sorting on, as long as the runs it leaves are
7768 // handled once instead of n log n times.
7769 //
7770 // `text (26 values)` is two hundred identical characters
7771 // drawn from twenty-six letters, so every eight-byte
7772 // prefix inside a letter is the same and 15,384 rows tie
7773 // on it. A comparison sort then asks ~7.4 M questions of
7774 // which nearly all are a two-hundred-byte `memcmp`
7775 // answering EQUAL: profiled, 30% of the working samples
7776 // sat in `memcmp` and 37% in the sort machinery.
7777 //
7778 // Sorting the integer keys is cheap. What each run needs
7779 // afterwards is ONE pass: if every value in it is equal,
7780 // input order already IS the stable answer, and proving
7781 // that costs n-1 comparisons rather than n log n. Only a
7782 // run that is not all-equal gets sorted.
7783 //
7784 // Single-term only. With a second ORDER BY column an
7785 // all-equal first term does not settle the row order --
7786 // the later terms still speak -- and the shortcut would
7787 // drop them.
7788 let all_keys = if keep_sorted {
7789 None
7790 } else {
7791 sort_keys_of(&tagged, terms[0].0)
7792 };
7793 let (worth_it, key_exact) = match all_keys.as_ref() {
7794 Some(PrefixKeys::Narrow(k, e)) => (*e || key_discriminates(k), *e),
7795 Some(PrefixKeys::Wide(k, e)) => (*e || key_discriminates(k), *e),
7796 None => (false, false),
7797 };
7798 let low_card = !keep_sorted && terms.len() == 1 && !key_exact && !worth_it;
7799 let keyed = all_keys.filter(|_| worth_it || low_card);
7800 if keep_sorted {
7801 // The collated permutation above already placed every
7802 // row. A draft let the byte-order fallback run after
7803 // it and undo the whole thing.
7804 } else if let Some(keys) = keyed {
7805 let exact = key_exact;
7806 let (first_col, first_desc, _) = terms[0];
7807 let row_cmp = |ia: u32, ib: u32| -> core::cmp::Ordering {
7808 let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
7809 for (col, desc, nf) in &terms {
7810 let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col))
7811 else {
7812 continue;
7813 };
7814 let ord = match (va, vb) {
7815 (Value::Text(x), Value::Text(y)) if !mysql => {
7816 let c = crate::orderby::str_cmp_prefix_first(x, y);
7817 if *desc { c.reverse() } else { c }
7818 }
7819 _ => {
7820 crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7821 }
7822 };
7823 if ord != core::cmp::Ordering::Equal {
7824 return ord;
7825 }
7826 }
7827 core::cmp::Ordering::Equal
7828 };
7829 let same_value = |ia: u32, ib: u32| -> bool {
7830 tagged[ia as usize].1.values.get(first_col)
7831 == tagged[ib as usize].1.values.get(first_col)
7832 };
7833 let how = PrefixSort {
7834 first_desc,
7835 low_card,
7836 exact,
7837 single_term: terms.len() == 1,
7838 };
7839 let order: Vec<u32> = match keys {
7840 PrefixKeys::Narrow(v, _) => {
7841 sort_prefix_permutation(v, &how, &row_cmp, &same_value)
7842 }
7843 PrefixKeys::Wide(v, _) => {
7844 sort_prefix_permutation(v, &how, &row_cmp, &same_value)
7845 }
7846 };
7847 let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7848 core::mem::take(&mut tagged).into_iter().map(Some).collect();
7849 tagged = order
7850 .iter()
7851 .map(|&i| {
7852 slots[i as usize]
7853 .take()
7854 .expect("the permutation names each row once")
7855 })
7856 .collect();
7857 } else {
7858 tagged.sort_by(|a, b| {
7859 for (i, (col, desc, nf)) in terms.iter().enumerate() {
7860 let va = a.1.values.get(*col);
7861 let vb = b.1.values.get(*col);
7862 let (Some(va), Some(vb)) = (va, vb) else {
7863 continue;
7864 };
7865 let _ = i;
7866 // v7.38.19 — two non-NULL strings, no MySQL fold, is
7867 // where a text sort spends every one of its ~7 M
7868 // comparisons, and the shared comparator cannot be
7869 // inlined into this loop: it carries NULL placement,
7870 // the fold, the NUMERIC bignum gate and the float
7871 // total order. Answering that one pair here is the
7872 // same answer by the same route — `value_cmp`'s
7873 // leading same-variant arm is `x.cmp(y)`, and the
7874 // raw comparator's last act is this reverse.
7875 let ord = match (va, vb) {
7876 (Value::Text(x), Value::Text(y)) if !mysql => {
7877 let c = crate::orderby::str_cmp_prefix_first(x, y);
7878 if *desc { c.reverse() } else { c }
7879 }
7880 _ => {
7881 crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7882 }
7883 };
7884 if ord != core::cmp::Ordering::Equal {
7885 return ord;
7886 }
7887 }
7888 core::cmp::Ordering::Equal
7889 });
7890 }
7891 } else {
7892 crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7893 }
7894 }
7895
7896 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7897 // past the truncated tail through every row that shares the
7898 // last-kept row's ORDER BY key. The tie check uses the
7899 // already-computed `(order_keys, row)` pairs so it matches
7900 // the sort comparator exactly. DISTINCT + WITH TIES falls
7901 // through to the no-ties path (PG also disallows their
7902 // combination; SPG silently drops the tie extension here so
7903 // the customer doesn't see a hard error mid-query — the
7904 // user-visible result is still correct, just narrower).
7905 let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7906 apply_offset_and_limit_tagged(
7907 &mut tagged,
7908 stmt.offset_literal(),
7909 stmt.limit_literal(),
7910 true,
7911 );
7912 tagged.into_iter().map(|(_, r)| r).collect()
7913 } else {
7914 // DISTINCT already de-duped pre-sort above.
7915 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7916 apply_offset_and_limit(
7917 &mut output_rows,
7918 stmt.offset_literal(),
7919 stmt.limit_literal(),
7920 );
7921 output_rows
7922 };
7923
7924 let columns: Vec<ColumnSchema> = projection
7925 .into_iter()
7926 .map(|p| p.to_column_schema())
7927 .collect();
7928
7929 Ok(QueryResult::Rows {
7930 columns,
7931 rows: output_rows,
7932 })
7933 }
7934
7935 /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7936 /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7937 /// select items for the surviving rows only — PG's Result-above-
7938 /// Limit shape, where SubPlan loops equal the OUTPUT row count
7939 /// (50) instead of the group count (24k).
7940 fn finish_agg_result(
7941 &self,
7942 mut agg: aggregate::AggResult,
7943 stmt: &SelectStatement,
7944 cancel: CancelToken<'_>,
7945 ) -> Result<QueryResult, EngineError> {
7946 apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7947 if !agg.deferred.is_empty() {
7948 apply_offset_and_limit(
7949 &mut agg.synth_rows,
7950 stmt.offset_literal(),
7951 stmt.limit_literal(),
7952 );
7953 let ctx = EvalContext::new(&agg.synth_schema, None);
7954 let mut memo = memoize::MemoizeCache::default();
7955 // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7956 // Deferred subqueries are referenced only by surviving
7957 // select-list rows (≤ LIMIT), so their correlation keys are
7958 // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7959 // each batchable subquery's group map over just those keys
7960 // via per-key index seek; the per-row splice loop below then
7961 // reuses the seeded map. A join-shaped or un-indexed inner
7962 // falls through to the all-keys batch inside the call (built
7963 // eagerly here instead of lazily on row 0 — same cost), so
7964 // it still pays the full scan, never the 715 ms per-row
7965 // direct eval; its index-nested-loop probe is the next
7966 // knife. Genuinely non-batchable shapes return None and are
7967 // left unseeded for the loop's per-row resolver, as before.
7968 for (_, expr) in &agg.deferred {
7969 let mut subs: Vec<&SelectStatement> = Vec::new();
7970 collect_scalar_subqueries(expr, &mut subs);
7971 for sub in subs {
7972 let repr = alloc::format!("{sub}");
7973 if memo.group_maps.contains_key(&repr) {
7974 continue;
7975 }
7976 if let Some(gm) = self.try_batch_correlated_scalar(
7977 sub,
7978 Some((&agg.synth_rows, &ctx)),
7979 cancel,
7980 )? {
7981 memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7982 }
7983 }
7984 }
7985 for (ri, srow) in agg.synth_rows.iter().enumerate() {
7986 cancel.check()?;
7987 for (col, expr) in &agg.deferred {
7988 let v =
7989 self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7990 if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7991 *cell = v;
7992 }
7993 }
7994 }
7995 }
7996 Ok(QueryResult::Rows {
7997 columns: agg.columns,
7998 rows: agg.rows,
7999 })
8000 }
8001
8002 /// v7.37 — streaming projection for the joined-non-aggregate
8003 /// shape (multi-table FROM, all projection items bound, no
8004 /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
8005 /// UNION). Walks the deferred join survivors and emits
8006 /// `&[&Value]` borrowed straight out of the source tables — no
8007 /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
8008 /// on the mailrs `PROJ` shape (about 4 ms saved).
8009 ///
8010 /// Returns `Ok(None)` when the shape doesn't qualify; the caller
8011 /// then falls back to the materialising path.
8012 /// v7.37 (round 831) — stream a joinless SELECT straight off the
8013 /// stored table, one row at a time, without ever building a row set.
8014 ///
8015 /// Returns `Ok(None)` for anything this cannot serve, and the caller
8016 /// falls through to the deferred-join path exactly as before: a
8017 /// missing table, or a cold tier whose hydration the fallback handles.
8018 /// Sort a single-table scan through the external sorter, so the
8019 /// answer's size is bounded by `work_mem` and not by the input.
8020 ///
8021 /// Sorting held every row twice — the scan's `Vec<Row>` and the
8022 /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
8023 /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
8024 /// enough ORDER BY took the server down, which is a liveness
8025 /// problem before it is a performance one.
8026 ///
8027 /// A SEPARATE walk rather than a change to `run_single_table_scan`,
8028 /// following what round 831 did for the joinless shape. That
8029 /// function is 552 lines whose projection loop is entangled with
8030 /// DISTINCT (which indexes back into the tagged vector) and with
8031 /// streaming top-N (whose boundary moves as the scan runs); both
8032 /// assume the projection has already happened when a row is
8033 /// pushed, which is exactly what spilling has to defer. Two earlier
8034 /// attempts tried to rework that loop and were reverted. Here the
8035 /// existing path is untouched and this one only claims shapes it
8036 /// can serve, so a decline costs nothing.
8037 ///
8038 /// Records are SOURCE rows, not projected ones: `finish` re-derives
8039 /// keys from what it decodes, and an ORDER BY key need not be in
8040 /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
8041 fn try_spill_sorted_scan(
8042 &self,
8043 stmt: &SelectStatement,
8044 from: &FromClause,
8045 cancel: CancelToken<'_>,
8046 ) -> Result<Option<QueryResult>, EngineError> {
8047 // Shapes this walk does not serve. Each one either needs the
8048 // whole tagged vector addressable (DISTINCT probes back into
8049 // it, WITH TIES re-reads its tail) or is already bounded
8050 // without spilling (a LIMIT makes the partial sort O(keep)).
8051 if !self.can_spill()
8052 || stmt.order_by.is_empty()
8053 || stmt.distinct
8054 || stmt.limit_with_ties
8055 || stmt.limit_literal().is_some()
8056 || !from.joins.is_empty()
8057 || from.primary.lateral_subquery.is_some()
8058 || from.primary.unnest_expr.is_some()
8059 || from.primary.generate_series_args.is_some()
8060 || select_has_window(stmt)
8061 {
8062 return Ok(None);
8063 }
8064 // A parent's rows are its children's. These walks scan the named
8065 // relation alone, so a partitioned or inherited parent comes back
8066 // short — and silently: the corpus caught `SELECT id FROM pr
8067 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8068 // parent's own rows instead of the partitions'. `ONLY` is exactly
8069 // the case that does not fan out, so it stays, which is the test
8070 // the FROM-clause fan-out itself makes.
8071 if !from.primary.only
8072 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8073 {
8074 return Ok(None);
8075 }
8076 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8077 return Ok(None);
8078 };
8079 // Cold-tier rows live outside `rows()`; this walk would drop
8080 // them silently, the same reason round 831's walk declines.
8081 if table.has_cold_rows_fast() {
8082 return Ok(None);
8083 }
8084
8085 let alias = from
8086 .primary
8087 .alias
8088 .as_deref()
8089 .unwrap_or(from.primary.name.as_str());
8090 let cols = table.schema().columns.clone();
8091 let sess = self.dml_session();
8092 let ctx = EvalContext::new(&cols, Some(alias))
8093 .with_catalog(self.active_catalog())
8094 .with_session(&sess);
8095 let projection = build_projection(
8096 &stmt.items,
8097 &cols,
8098 alias,
8099 self.speaks_mysql,
8100 Some(self.active_catalog()),
8101 )?;
8102 let order_by = stmt.order_by.clone();
8103 // The same one-shot resolution the general path does (round
8104 // 582): each ORDER BY column is bound once, not once per row.
8105 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8106 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8107 // Resolved BEFORE the scan, because it now decides what the sort
8108 // STORES and not just what it decodes (round 995).
8109 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8110
8111 // v7.38.22 — resolved HERE, because this path did not resolve
8112 // them at all.
8113 //
8114 // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8115 // "en_US.utf8"` in BYTE order on this path — and swallowed an
8116 // unknown collation name rather than raising — because the sorter
8117 // below compared with an empty collation slice. The materialising
8118 // path honoured both. Which answer a query got depended on which
8119 // path the planner took, and this is the path a plain single-table
8120 // SELECT takes.
8121 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
8122 // v7.39.12 — a correlated scalar subquery in ORDER BY is
8123 // resolved for the row before its key is built.
8124 //
8125 // Uncorrelated subqueries are replaced by a literal before
8126 // execution; a correlated one cannot be, so it reached the
8127 // per-row evaluator — the one place that cannot run a subquery
8128 // — and the statement raised "subquery reached row eval".
8129 // Reported by sentori against 7.39.11; see
8130 // `Engine::order_by_resolved_for_row`.
8131 //
8132 // The `any` runs once, here, so an ordinary ORDER BY pays one
8133 // bool per row and nothing else.
8134 let order_has_subquery = order_by
8135 .iter()
8136 .any(|o| crate::subquery::expr_has_subquery(&o.expr));
8137 let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
8138 let mut sorter = crate::extsort::ExternalSorter::new(
8139 self.temp_run_factory,
8140 self.session_work_mem_bytes(),
8141 cols.clone(),
8142 &descs,
8143 &order_colls,
8144 )
8145 .with_stats(&self.spill_stats)
8146 .with_pruned(&needed);
8147 let snapshot = self.current_snapshot();
8148 // One key buffer for the whole scan: `push` drains it and leaves
8149 // the capacity behind.
8150 let mut keys: Vec<OrderKey> = Vec::new();
8151 // r1024 — compile the predicate once for the scan.
8152 //
8153 // These two sorted-spill scans are the paths a single-table SELECT
8154 // with an ORDER BY takes, and they were the last row-returning ones
8155 // still walking the expression tree per row. r1023 did the
8156 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8157 // exactly this shape.
8158 //
8159 // Found from the profile's CALL TREE rather than its leaves. The
8160 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8161 // 261, `mod_op` 178 — and two attempts at reasoning out which
8162 // function asked for it were both wrong. The tree names the caller
8163 // chain, and it named this one.
8164 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8165 .where_
8166 .as_ref()
8167 .filter(|w| crate::eval::fully_compilable(w))
8168 .map(|w| crate::eval::compile_expr(w, &ctx));
8169 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8170 for (i, row) in table.scan_visible_from(0, &snapshot) {
8171 if i.is_multiple_of(256) {
8172 cancel.check()?;
8173 }
8174 if let Some(c) = &compiled_where {
8175 if !crate::eval::compiled::eval_compiled_pred(
8176 c,
8177 row,
8178 &ctx,
8179 &mut eval_stack,
8180 ctx.mysql_dialect,
8181 )? {
8182 continue;
8183 }
8184 } else if let Some(w) = &stmt.where_ {
8185 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8186 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8187 continue;
8188 }
8189 }
8190 keys.clear();
8191 // The same collations the sorter compares with, and the
8192 // re-derivation below is handed the same ones. `finish`'s
8193 // contract is that a key comes back the way it was pushed;
8194 // a collation is part of the way it was pushed.
8195 if order_has_subquery {
8196 // A substituted literal is no longer a bound column.
8197 let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
8198 crate::orderby::build_order_keys_bound(
8199 per_row.as_deref().unwrap_or(&order_by),
8200 &unbound,
8201 &order_colls,
8202 row,
8203 &ctx,
8204 &mut keys,
8205 )?;
8206 } else {
8207 crate::orderby::build_order_keys_bound(
8208 &order_by,
8209 &order_bound,
8210 &order_colls,
8211 row,
8212 &ctx,
8213 &mut keys,
8214 )?;
8215 }
8216 sorter.push(&mut keys, row)?;
8217 }
8218
8219 let key_ctx = &ctx;
8220 let rows = sorter.finish(
8221 |src, buf| {
8222 crate::orderby::build_order_keys_rederived(
8223 &order_by,
8224 &order_bound,
8225 &order_colls,
8226 src,
8227 key_ctx,
8228 buf,
8229 )
8230 },
8231 |src| {
8232 let mut values = Vec::with_capacity(projection.len());
8233 for p in &projection {
8234 values.push(
8235 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8236 );
8237 }
8238 Ok(Row::new(values))
8239 },
8240 )?;
8241
8242 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8243 Ok(Some(QueryResult::Rows { columns, rows }))
8244 }
8245
8246 /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
8247 /// handing each row to the consumer instead of collecting the answer.
8248 ///
8249 /// That walk bounds the SORT and then returns `QueryResult::Rows`,
8250 /// which holds every output row. Measured at `work_mem = 4 MB` over
8251 /// 200-byte rows, RSS above the server's own baseline while the
8252 /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
8253 /// at 400k — linear — while the spill underneath worked correctly
8254 /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
8255 /// removes each file, so a count taken afterwards reads 0 whatever
8256 /// happened, and an earlier reading of "no spill at all" was that
8257 /// blind witness). The growth is the collected result, not the sort.
8258 ///
8259 /// Emitting makes peak the budget, one buffer per run and a single
8260 /// row — the state a merge already holds at every step. It also
8261 /// frees each projected row as the next is built rather than
8262 /// accumulating them, which is where the time is: a profile of the
8263 /// collecting walk put the allocator at 586 samples, more than every
8264 /// sort comparison combined (420), against 19 for `push` itself.
8265 /// v7.37 (round 923) — which of a sort record's columns the output half
8266 /// reads. The record is the SOURCE row (round 836), so a narrow projection
8267 /// decoded every column: skipping one 200-byte text halves a decode
8268 /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
8269 ///
8270 /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
8271 /// column reads NULL. Answers only when every projection item is a bare
8272 /// column reference AND every ORDER BY key is a bound column; anything
8273 /// else returns empty, decoding everything as before.
8274 /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
8275 /// drops references from expression kinds it does not enumerate.
8276 ///
8277 /// ORDER BY columns are included — the merge re-derives keys from the
8278 /// decoded row on the spilled path, so pruning one would sort NULLs.
8279 pub(crate) fn sort_record_columns_needed(
8280 items: &[SelectItem],
8281 order_bound: &[Option<usize>],
8282 arity: usize,
8283 ctx: &EvalContext,
8284 ) -> Vec<bool> {
8285 let all_bare = items.iter().all(|i| {
8286 matches!(
8287 i,
8288 SelectItem::Expr {
8289 expr: Expr::Column(_),
8290 ..
8291 }
8292 )
8293 });
8294 if !all_bare || order_bound.iter().any(Option::is_none) {
8295 return Vec::new();
8296 }
8297 let mut mask = alloc::vec![false; arity];
8298 for item in items {
8299 if let SelectItem::Expr {
8300 expr: Expr::Column(c),
8301 ..
8302 } = item
8303 {
8304 match crate::eval::find_column_pos(c, ctx) {
8305 Some(p) if p < arity => mask[p] = true,
8306 _ => return Vec::new(),
8307 }
8308 }
8309 }
8310 for p in order_bound.iter().flatten() {
8311 if *p < arity {
8312 mask[*p] = true;
8313 } else {
8314 return Vec::new();
8315 }
8316 }
8317 mask
8318 }
8319
8320 /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
8321 /// of sorting.
8322 ///
8323 /// PG serves such an ordering from the index and never sorts. We sorted:
8324 /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
8325 /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
8326 /// the sorter's own round trip — `ExternalSorter::finish_each` →
8327 /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
8328 /// Every row is encoded into the sorter's arena and decoded back out,
8329 /// for an order the index already holds.
8330 ///
8331 /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
8332 /// because it was built for top-N. This is the unbounded sibling.
8333 ///
8334 /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
8335 /// from a btree, so walking one would silently drop those rows. That is
8336 /// exactly the defect r1020 fixed on the top-N path, where it had
8337 /// shipped.
8338 /// r1044 — the index this statement's ORDER BY can be WALKED on,
8339 /// instead of sorted, or `None`.
8340 ///
8341 /// Extracted so `EXPLAIN` can ask the same question the executor
8342 /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
8343 /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
8344 /// while the executor walked the primary key — 34.9 ms against
8345 /// 147.0 for the same query ordered by an unindexed column, so the
8346 /// walk was plainly running. Round 551 fixed a different case of
8347 /// this and wrote the reason down: EXPLAIN is the first thing any
8348 /// performance question opens, and an instrument that misnames the
8349 /// access path is worse than one that says nothing.
8350 ///
8351 /// The gate is here once. Two copies of it is how the plan and the
8352 /// executor come to disagree again.
8353 /// v7.39.13 — the shape refusals both ordered-walk gates make.
8354 ///
8355 /// One list, because two of them would be two answers to "can this
8356 /// statement walk an index", and a walk that runs where EXPLAIN says
8357 /// it does not is the defect r1044 exists to prevent.
8358 /// v7.39.13 — `WHERE lead = <literal> ORDER BY next [DESC] LIMIT n`
8359 /// behind an index on `(lead, next, …)`: one seek to the key prefix,
8360 /// then n steps inside it.
8361 ///
8362 /// Sentori's busiest read, and the one shape they have reported
8363 /// unchanged for three versions: `WHERE project_id = ? ORDER BY
8364 /// received_at DESC LIMIT 20`. PostgreSQL 18 answers it with
8365 /// `Limit -> Index Scan`; SPG planned `Sort -> Seq Scan` and sorted
8366 /// the table to return twenty rows, roughly 250x behind.
8367 ///
8368 /// The ordered walk that existed could only start at an index's
8369 /// LEADING column, so an index on `(project_id, received_at)` could
8370 /// serve `ORDER BY project_id` and nothing else. What was missing is
8371 /// below it: a tree walk bounded by a key prefix, which
8372 /// `Index::iter_prefix_desc` now provides.
8373 ///
8374 /// The equality conjunct only NARROWS the walk — the statement's own
8375 /// `WHERE` still runs per row — so picking the wrong conjunct can
8376 /// cost time and cannot change an answer.
8377 pub(crate) fn index_prefix_walk_target(
8378 &self,
8379 stmt: &SelectStatement,
8380 from: &FromClause,
8381 ) -> Option<(String, usize, alloc::vec::Vec<spg_storage::IndexKey>)> {
8382 if self.walk_shape_refused(stmt, from) {
8383 return None;
8384 }
8385 // One ORDER BY term for now: a second one would have to be the
8386 // next key column again, and the tree walks one direction.
8387 if stmt.order_by.len() != 1 || stmt.distinct {
8388 return None;
8389 }
8390 let table = self.active_catalog().get(&from.primary.name)?;
8391 let alias = from
8392 .primary
8393 .alias
8394 .as_deref()
8395 .unwrap_or(from.primary.name.as_str());
8396 let cols = &table.schema().columns;
8397 let order = &stmt.order_by[0];
8398 let Expr::Column(oc) = &order.expr else {
8399 return None;
8400 };
8401 if let Some(q) = &oc.qualifier
8402 && !q.eq_ignore_ascii_case(alias)
8403 {
8404 return None;
8405 }
8406 let order_pos = cols
8407 .iter()
8408 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8409 // The walk comes out in the tree's order, so it may only take an
8410 // ORDER BY whose order that IS — the same question the leading-
8411 // column gate asks, for the same reason.
8412 let order_col = cols.get(order_pos)?;
8413 if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8414 && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8415 {
8416 return None;
8417 }
8418 // A NULL key is not in the tree, and this walk has no separate
8419 // pass for those rows the way the leading-column one does.
8420 if order_col.nullable {
8421 return None;
8422 }
8423 let where_ = stmt.where_.as_ref()?;
8424 for index in table.indices() {
8425 if !matches!(index.kind, spg_storage::IndexKind::BTreeMulti(_))
8426 || index.expression.is_some()
8427 || index.partial_predicate.is_some()
8428 {
8429 continue;
8430 }
8431 // The ORDER BY column must be the key component that follows
8432 // the equality-bound prefix.
8433 if index.extra_column_positions.first() != Some(&order_pos) {
8434 continue;
8435 }
8436 let lead_pos = index.column_position;
8437 let lead_col = cols.get(lead_pos)?;
8438 // The prefix is compared with the tree's own ordering, so the
8439 // leading column has to be one the tree orders bytewise too.
8440 if crate::index_access::collated_column(lead_col, table.db_collation()).is_none()
8441 && !crate::collate::column_key_is_bytewise(lead_col, self.speaks_mysql)
8442 {
8443 continue;
8444 }
8445 let Some(key) = self.eq_literal_key_for(where_, lead_pos, cols, alias) else {
8446 continue;
8447 };
8448 return Some((index.name.clone(), order_pos, alloc::vec![key]));
8449 }
8450 None
8451 }
8452
8453 /// The index key a top-level `AND` conjunct binds `col_pos` to, when
8454 /// one of them is `col = <literal>` (either way round).
8455 ///
8456 /// Only literals: a column reference or a function would have to be
8457 /// evaluated per row, and this runs once for the whole statement.
8458 fn eq_literal_key_for(
8459 &self,
8460 where_: &Expr,
8461 col_pos: usize,
8462 cols: &[ColumnSchema],
8463 alias: &str,
8464 ) -> Option<spg_storage::IndexKey> {
8465 let col = cols.get(col_pos)?;
8466 let mut found: Option<spg_storage::IndexKey> = None;
8467 let mut stack: alloc::vec::Vec<&Expr> = alloc::vec![where_];
8468 while let Some(e) = stack.pop() {
8469 match e {
8470 Expr::Binary {
8471 lhs,
8472 op: spg_sql::ast::BinOp::And,
8473 rhs,
8474 } => {
8475 stack.push(lhs);
8476 stack.push(rhs);
8477 }
8478 Expr::Binary {
8479 lhs,
8480 op: spg_sql::ast::BinOp::Eq,
8481 rhs,
8482 } => {
8483 let names_col = |x: &Expr| match x {
8484 Expr::Column(c) => {
8485 c.name.eq_ignore_ascii_case(&col.name)
8486 && c.qualifier
8487 .as_ref()
8488 .is_none_or(|q| q.eq_ignore_ascii_case(alias))
8489 }
8490 _ => false,
8491 };
8492 let lit = if names_col(lhs) {
8493 Some(&**rhs)
8494 } else if names_col(rhs) {
8495 Some(&**lhs)
8496 } else {
8497 None
8498 };
8499 // v7.39.13 — a BARE literal means whatever the
8500 // COLUMN says it means, and
8501 // `literal_as_column_value` is the one place that
8502 // decision is made. Asking
8503 // `literal_expr_to_value` instead made this the
8504 // fifth copy of it, and it read every string
8505 // literal as text: `WHERE k = '\x07'` on a `bytea`
8506 // column built no key at all, so the walk declined
8507 // and the plan went back to sorting the table —
8508 // while the EQUALITY seek beside it, which does ask
8509 // the one funnel, used the very same index.
8510 //
8511 // Anything that is not a bare literal — a cast, a
8512 // negation — already carries its own type, and
8513 // `from_value_for_column` decides whether that type
8514 // keys for this column.
8515 let v = match lit {
8516 Some(Expr::Literal(l)) => {
8517 crate::index_access::literal_as_column_value(l, col, col_pos)
8518 }
8519 Some(other) => {
8520 crate::conversions::literal_expr_to_value(other.clone()).ok()
8521 }
8522 None => None,
8523 };
8524 if let Some(v) = v
8525 && !v.is_null()
8526 && let Some(k) = spg_storage::IndexKey::from_value_for_column(&v, col.ty)
8527 {
8528 found = Some(k);
8529 }
8530 }
8531 _ => {}
8532 }
8533 }
8534 found
8535 }
8536
8537 fn walk_shape_refused(&self, stmt: &SelectStatement, from: &FromClause) -> bool {
8538 // A non-literal count is refused: `LIMIT $1` is rewritten to a
8539 // literal by `resolve_limit_exprs` before dispatch, so anything
8540 // still carrying a placeholder here has not been through it.
8541 let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8542 matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8543 };
8544 if stmt.order_by.is_empty()
8545 || !stmt.distinct_on.is_empty()
8546 || stmt.limit_with_ties
8547 || !literal_count(&stmt.limit)
8548 || !literal_count(&stmt.offset)
8549 || stmt.having.is_some()
8550 || stmt.group_by.is_some()
8551 || !stmt.unions.is_empty()
8552 || !from.joins.is_empty()
8553 || from.primary.lateral_subquery.is_some()
8554 || from.primary.unnest_expr.is_some()
8555 || from.primary.as_of_segment.is_some()
8556 || from.primary.generate_series_args.is_some()
8557 || select_has_window(stmt)
8558 || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
8559 {
8560 return true;
8561 }
8562 if stmt
8563 .items
8564 .iter()
8565 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8566 {
8567 return true;
8568 }
8569 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8570 return true;
8571 };
8572 if table.has_cold_rows_fast() {
8573 return true;
8574 }
8575 !from.primary.only
8576 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8577 }
8578
8579 pub(crate) fn index_order_walk_target(
8580 &self,
8581 stmt: &SelectStatement,
8582 from: &FromClause,
8583 ) -> Option<(String, usize)> {
8584 // v7.39.11 — LIMIT / OFFSET join the walk instead of refusing it.
8585 //
8586 // Reported by sentori against 7.39.10 and measured on their own
8587 // busiest read: "the most recent N events for this project",
8588 // backed by an index on exactly that ordering. PostgreSQL 18
8589 // answered it with `Limit -> Index Scan`; SPG with
8590 // `Limit -> Sort -> Seq Scan`, 4.998 ms against 0.021 — the
8591 // whole table sorted to return twenty rows.
8592 //
8593 // The walk was built for this shape — `iter_desc`'s own doc says
8594 // "the ORDER BY <indexed col> DESC + LIMIT N executor path" —
8595 // and then the gate refused every statement that had a LIMIT, so
8596 // the one query it was written for could never reach it. The
8597 // capability was here; the routing was not.
8598 //
8599 // A non-literal count is refused: `LIMIT $1` is rewritten to a
8600 // literal by `resolve_limit_exprs` before dispatch, so anything
8601 // still carrying a placeholder here has not been through it.
8602 let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8603 matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8604 };
8605 if self.walk_shape_refused(stmt, from) {
8606 return None;
8607 }
8608 let table = self.active_catalog().get(&from.primary.name)?;
8609 let alias = from
8610 .primary
8611 .alias
8612 .as_deref()
8613 .unwrap_or(from.primary.name.as_str());
8614 let cols = &table.schema().columns;
8615 let order = &stmt.order_by[0];
8616 let Expr::Column(oc) = &order.expr else {
8617 return None;
8618 };
8619 if let Some(q) = &oc.qualifier
8620 && !q.eq_ignore_ascii_case(alias)
8621 {
8622 return None;
8623 }
8624 let order_pos = cols
8625 .iter()
8626 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8627 // r1047 — DISTINCT joins the walk when the projection IS the
8628 // order column, and only then. The index's keys are canonical
8629 // (r1039: representation equality is value equality — the
8630 // property every seek already depends on), so one key is one
8631 // distinct value and the walk can emit the first passing row of
8632 // each key group instead of hashing every row. On the release
8633 // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
8634 // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
8635 // with an ablation floor of 14.8, because the hash must
8636 // normalize and probe ALL the rows; the walk visits each key
8637 // once. A wider projection makes DISTINCT about the whole tuple,
8638 // not the key, so anything else still declines.
8639 if stmt.distinct {
8640 let only_the_order_column = stmt.items.len() == 1
8641 && match &stmt.items[0] {
8642 SelectItem::Expr {
8643 expr: Expr::Column(c),
8644 ..
8645 } => {
8646 c.name.eq_ignore_ascii_case(&oc.name)
8647 && match &c.qualifier {
8648 Some(q) => q.eq_ignore_ascii_case(alias),
8649 None => true,
8650 }
8651 }
8652 _ => false,
8653 };
8654 if !only_the_order_column {
8655 return None;
8656 }
8657 }
8658 // r1046 — a nullable key no longer refuses the walk; it changes
8659 // what the walk has to do. A NULL key is not in the btree, so
8660 // walking alone would silently drop those rows — the r1020
8661 // defect, which shipped once. The walk emits them separately, at
8662 // the end SQL puts them.
8663 //
8664 // Refusing was costing every nullable indexed column a 3.4x:
8665 // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
8666 // 72.0 ms with the column nullable and 20.2 with the same data
8667 // under NOT NULL. `NOT NULL` is not the default, so that was the
8668 // common case paying for the uncommon one.
8669 // v7.39.11 — the walk comes out in the tree's order, so it may
8670 // only take an ORDER BY whose order that IS.
8671 //
8672 // The B-tree walks in BYTE order unless the column's keys are
8673 // ICU sort keys. `try_pk_walk_top_n` has asked this since
8674 // v7.38.18; this gate never did, and the answer changed when an
8675 // index appeared. Measured on `alpha / Beta / GAMMA / delta`
8676 // over a MySQL-dialect session, `SELECT t FROM s ORDER BY t`:
8677 //
8678 // no index alpha Beta delta GAMMA (MySQL's own order)
8679 // indexed Beta GAMMA alpha delta (bytes)
8680 //
8681 // No row is wrong and nothing raises; only the order changes,
8682 // and it changes because an index exists. Ordering is the one
8683 // thing a walk contributes, so when it is the wrong ordering
8684 // there is nothing left to keep.
8685 let order_col = cols.get(order_pos)?;
8686 if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8687 && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8688 {
8689 return None;
8690 }
8691 // v7.39.11 — a composite B-tree LEADING on the ORDER BY column
8692 // walks it too, which is what `try_pk_walk_top_n` has always
8693 // done and what this gate did not know.
8694 //
8695 // Keys sort by the whole tuple, so the leading component comes
8696 // out in order — `Index::iter_asc` says so, and the materialising
8697 // top-N walk has relied on it since v7.38.1. The consequence of
8698 // the two gates disagreeing was the thing r1044 exists to
8699 // prevent: measured on a table indexed `(a, b)`, `SELECT a FROM
8700 // m ORDER BY a LIMIT 2` planned as `Limit -> Sort -> Seq Scan`
8701 // while the executor plainly walked the index — a projection
8702 // that divides by zero on the last row in key order returned two
8703 // rows instead of raising. EXPLAIN is the first thing any
8704 // performance question opens, and an instrument that misnames
8705 // the access path is worse than one that says nothing.
8706 let index = table
8707 .index_on(order_pos)
8708 .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8709 .or_else(|| {
8710 table.indices().iter().find(|i| {
8711 matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8712 && i.column_position == order_pos
8713 })
8714 })?;
8715 if index.expression.is_some() || index.partial_predicate.is_some() {
8716 return None;
8717 }
8718 // v7.39.11 — more than one ORDER BY term walks when the index
8719 // holds exactly that ordering.
8720 //
8721 // Keys sort by the whole tuple, so `iter_asc` over a composite
8722 // B-tree IS `ORDER BY a, b` — the walk needs no new machinery,
8723 // only permission. Reported by sentori against 7.39.10:
8724 // `ORDER BY a, b LIMIT 10` planned as `Seq Scan -> Sort` here
8725 // against an `Incremental Sort` over an index scan on
8726 // PostgreSQL 18, on a table indexed for it.
8727 //
8728 // Three things have to hold, and each of them is the tree's
8729 // limitation rather than a conservative choice:
8730 //
8731 // * the terms are the index's key columns, in its order, from
8732 // the leading one — a suffix or a permutation is a different
8733 // ordering;
8734 // * every term runs the same direction, because the tree is
8735 // walked one way for all of them. `(a, b DESC)` is what
8736 // PostgreSQL serves from an index whose SECOND key is
8737 // descending, and SPG's tree does not scan per column;
8738 // * every key column is NOT NULL. A NULL key is not in the
8739 // tree at all, and the separate pass that emits those rows
8740 // (r1046) knows how to place them for ONE column, not for a
8741 // tuple.
8742 if stmt.order_by.len() > 1 {
8743 let keys: Vec<usize> = core::iter::once(index.column_position)
8744 .chain(index.extra_column_positions.iter().copied())
8745 .collect();
8746 if stmt.order_by.len() > keys.len() {
8747 return None;
8748 }
8749 let desc = stmt.order_by[0].desc;
8750 for (term, &key_pos) in stmt.order_by.iter().zip(keys.iter()) {
8751 if term.desc != desc {
8752 return None;
8753 }
8754 let Expr::Column(c) = &term.expr else {
8755 return None;
8756 };
8757 if let Some(q) = &c.qualifier
8758 && !q.eq_ignore_ascii_case(alias)
8759 {
8760 return None;
8761 }
8762 let pos = cols
8763 .iter()
8764 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
8765 if pos != key_pos {
8766 return None;
8767 }
8768 let col = cols.get(pos)?;
8769 if col.nullable {
8770 return None;
8771 }
8772 if crate::index_access::collated_column(col, table.db_collation()).is_none()
8773 && !crate::collate::column_key_is_bytewise(col, self.speaks_mysql)
8774 {
8775 return None;
8776 }
8777 }
8778 }
8779 Some((index.name.clone(), order_pos))
8780 }
8781
8782 fn try_index_order_stream<F>(
8783 &self,
8784 stmt: &SelectStatement,
8785 from: &FromClause,
8786 cancel: CancelToken<'_>,
8787 emit: &mut F,
8788 ) -> Result<Option<usize>, EngineError>
8789 where
8790 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8791 {
8792 // r1044 — the shape gate lives in `index_order_walk_target`, so
8793 // `EXPLAIN` answers the same question. What stays here is the
8794 // part that RAISES (an illegal ORDER BY has to keep erroring
8795 // from where it did) and the bindings the walk needs.
8796 crate::orderby::check_order_by_legality(stmt)?;
8797 crate::orderby::check_order_by_positions(stmt)?;
8798 crate::window::reject_window_in_row_clauses(stmt)?;
8799 // v7.39.13 — the prefix walk first: it serves a shape the
8800 // leading-column walk cannot, and refuses everything that one
8801 // takes.
8802 let (order_pos, prefix) = match self.index_prefix_walk_target(stmt, from) {
8803 Some((_, pos, keys)) => (pos, Some(keys)),
8804 None => match self.index_order_walk_target(stmt, from) {
8805 Some((_, pos)) => (pos, None),
8806 None => return Ok(None),
8807 },
8808 };
8809 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8810 return Ok(None);
8811 };
8812 let alias = from
8813 .primary
8814 .alias
8815 .as_deref()
8816 .unwrap_or(from.primary.name.as_str());
8817 let cols = table.schema().columns.clone();
8818 let order = &stmt.order_by[0];
8819 // v7.39.11 — the same lookup the gate made; see
8820 // `index_order_walk_target`.
8821 let Some(index) = (if prefix.is_some() {
8822 // The prefix planner named an index whose FIRST extra key
8823 // column is the order column; the lookup below looks for one
8824 // whose LEADING column is, and would find the wrong tree.
8825 table.indices().iter().find(|i| {
8826 matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8827 && i.extra_column_positions.first() == Some(&order_pos)
8828 && i.expression.is_none()
8829 && i.partial_predicate.is_none()
8830 })
8831 } else {
8832 table
8833 .index_on(order_pos)
8834 .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8835 .or_else(|| {
8836 table.indices().iter().find(|i| {
8837 matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8838 && i.column_position == order_pos
8839 })
8840 })
8841 }) else {
8842 return Ok(None);
8843 };
8844
8845 let sess = self.dml_session();
8846 let ctx = EvalContext::new(&cols, Some(alias))
8847 .with_catalog(self.active_catalog())
8848 .with_session(&sess);
8849 let projection = build_projection(
8850 &stmt.items,
8851 &cols,
8852 alias,
8853 self.speaks_mysql,
8854 Some(self.active_catalog()),
8855 )?;
8856 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8857 emit(crate::StreamItem::Header(&columns))?;
8858 let bound_pos: Vec<Option<usize>> = projection
8859 .iter()
8860 .map(|p| match &p.expr {
8861 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8862 Ok(Some(pos)) => Some(pos),
8863 _ => None,
8864 },
8865 _ => None,
8866 })
8867 .collect();
8868
8869 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8870 .where_
8871 .as_ref()
8872 .filter(|w| crate::eval::fully_compilable(w))
8873 .map(|w| crate::eval::compile_expr(w, &ctx));
8874 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8875 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8876 let snapshot = self.current_snapshot();
8877
8878 // A btree holds one locator per row VERSION, so a row whose key was
8879 // updated can sit under two keys and a dead one can sit beside its
8880 // replacement. The visibility gate drops the dead; `seen` drops a
8881 // live row that the walk reaches twice, which would otherwise be a
8882 // duplicated output row rather than a slow one.
8883 let mut emitted_rows = alloc::vec![false; table.rows().len()];
8884
8885 // r1046 — the rows the index cannot hold.
8886 //
8887 // A NULL key is not in the btree, so the walk below never reaches
8888 // those rows; they are emitted here, at the end SQL puts them.
8889 // PG's default is NULLS LAST ascending and NULLS FIRST
8890 // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
8891 // the same rule `order_by_value_cmp_raw` applies to the sort this
8892 // replaces, so the two orders agree.
8893 //
8894 // Finding them costs one pass over the column. That pass is why
8895 // this is still worth doing: the sort it replaces encodes and
8896 // decodes every row, and the walk plus the pass measured 72.0 ms
8897 // down to about 22 on 400,000 rows.
8898 let nulls_first = order.nulls_first.unwrap_or(order.desc);
8899 // r1047 — under DISTINCT the walk emits the FIRST passing row of
8900 // each key group and skips the rest; the gate admits DISTINCT
8901 // only when the projection is the order column itself, so one
8902 // canonical key is one output row. NULL is one distinct value,
8903 // so the NULL pass stops at its first emit too.
8904 let distinct = stmt.distinct;
8905 let mut count = 0usize;
8906 let mut visited = 0usize;
8907 // v7.39.11 — OFFSET and LIMIT, applied as the walk goes.
8908 //
8909 // Both count PASSING rows, so a skipped row still has to run the
8910 // predicate and the projection — `stream_filter_project` is
8911 // `stream_project_row` without the emit, which is exactly that.
8912 // Stopping at `remaining == 0` is the whole point: twenty rows
8913 // off the end of an index instead of a sorted table.
8914 let mut to_skip = stmt.offset_literal().unwrap_or(0) as usize;
8915 let mut remaining: Option<usize> = stmt.limit_literal().map(|l| l as usize);
8916 let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
8917 eval_stack: &mut Vec<Value<'static>>,
8918 values: &mut Vec<Value<'static>>,
8919 visited: &mut usize,
8920 to_skip: &mut usize,
8921 remaining: &mut Option<usize>,
8922 emit: &mut F|
8923 -> Result<usize, EngineError> {
8924 if !cols[order_pos].nullable {
8925 return Ok(0);
8926 }
8927 // v7.39.11 — nothing to emit once the LIMIT is met, and
8928 // finding that out must not cost a scan.
8929 //
8930 // This pass looks for NULL-keyed rows by walking the whole
8931 // heap, because they are not in the tree. That is the price
8932 // r1046 measured and accepted for an UNBOUNDED order. With
8933 // a LIMIT the walk above has usually already produced every
8934 // row the caller asked for, and scanning 400,000 rows to
8935 // add none of them is the whole cost of the query: the
8936 // release sweep's `SELECT pad FROM t ORDER BY n LIMIT 10`
8937 // over a nullable indexed NUMERIC went 0.237 ms at 50,000
8938 // rows and 2.251 at 400,000 — linear, against PostgreSQL's
8939 // 0.155 and 0.182 — the moment this gate started accepting
8940 // LIMIT. The `remaining` check below sits after the
8941 // per-row filters, so it could never be reached.
8942 if *remaining == Some(0) {
8943 return Ok(0);
8944 }
8945 let mut n = 0usize;
8946 for (ri, row) in table.rows().iter().enumerate() {
8947 if !matches!(row.values.get(order_pos), Some(Value::Null)) {
8948 continue;
8949 }
8950 if emitted_rows.get(ri).copied().unwrap_or(true) {
8951 continue;
8952 }
8953 if !table.is_row_visible(ri, &snapshot) {
8954 continue;
8955 }
8956 *visited += 1;
8957 if visited.is_multiple_of(256) {
8958 cancel.check()?;
8959 }
8960 emitted_rows[ri] = true;
8961 if *remaining == Some(0) {
8962 break;
8963 }
8964 let passed = if *to_skip > 0 {
8965 let p = Self::stream_filter_project(
8966 row,
8967 stmt.where_.as_ref(),
8968 compiled_where.as_ref(),
8969 eval_stack,
8970 &projection,
8971 &bound_pos,
8972 &ctx,
8973 values,
8974 )?;
8975 if p {
8976 *to_skip -= 1;
8977 }
8978 false
8979 } else {
8980 Self::stream_project_row(
8981 row,
8982 stmt.where_.as_ref(),
8983 compiled_where.as_ref(),
8984 eval_stack,
8985 &projection,
8986 &bound_pos,
8987 &ctx,
8988 values,
8989 emit,
8990 )?
8991 };
8992 if passed {
8993 n += 1;
8994 if let Some(r) = remaining.as_mut() {
8995 *r -= 1;
8996 if *r == 0 {
8997 break;
8998 }
8999 }
9000 if distinct {
9001 break;
9002 }
9003 }
9004 }
9005 Ok(n)
9006 };
9007
9008 if nulls_first {
9009 count += emit_null_rows(
9010 &mut emitted_rows,
9011 &mut eval_stack,
9012 &mut values,
9013 &mut visited,
9014 &mut to_skip,
9015 &mut remaining,
9016 emit,
9017 )?;
9018 }
9019
9020 // v7.39.13 — a prefix walk when the statement binds the index's
9021 // leading column, the whole tree otherwise. The key is not read
9022 // by the loop, so the two shapes meet as posting lists.
9023 let walker: alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> =
9024 match prefix.as_ref().and_then(|p| {
9025 if order.desc {
9026 index.iter_prefix_desc(p).map(
9027 |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9028 alloc::boxed::Box::new(it.map(|(_, l)| l))
9029 },
9030 )
9031 } else {
9032 index.iter_prefix_asc(p).map(
9033 |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9034 alloc::boxed::Box::new(it.map(|(_, l)| l))
9035 },
9036 )
9037 }
9038 }) {
9039 Some(it) => it,
9040 None if order.desc => alloc::boxed::Box::new(index.iter_desc().map(|(_, l)| l)),
9041 None => alloc::boxed::Box::new(index.iter_asc().map(|(_, l)| l)),
9042 };
9043 'walk: for locators in walker {
9044 if remaining == Some(0) {
9045 break;
9046 }
9047 for loc in locators {
9048 let spg_storage::RowLocator::Hot(ri) = *loc else {
9049 continue;
9050 };
9051 if emitted_rows.get(ri).copied().unwrap_or(true) {
9052 continue;
9053 }
9054 if !table.is_row_visible(ri, &snapshot) {
9055 continue;
9056 }
9057 let Some(row) = table.rows().get(ri) else {
9058 continue;
9059 };
9060 visited += 1;
9061 if visited.is_multiple_of(256) {
9062 cancel.check()?;
9063 }
9064 emitted_rows[ri] = true;
9065 // v7.39.11 — a skipped row still runs the predicate and
9066 // the projection, because OFFSET counts rows that PASS;
9067 // it just does not reach the client.
9068 let passed = if to_skip > 0 {
9069 let p = Self::stream_filter_project(
9070 row,
9071 stmt.where_.as_ref(),
9072 compiled_where.as_ref(),
9073 &mut eval_stack,
9074 &projection,
9075 &bound_pos,
9076 &ctx,
9077 &mut values,
9078 )?;
9079 if p {
9080 to_skip -= 1;
9081 }
9082 false
9083 } else {
9084 Self::stream_project_row(
9085 row,
9086 stmt.where_.as_ref(),
9087 compiled_where.as_ref(),
9088 &mut eval_stack,
9089 &projection,
9090 &bound_pos,
9091 &ctx,
9092 &mut values,
9093 emit,
9094 )?
9095 };
9096 if passed {
9097 count += 1;
9098 if let Some(r) = remaining.as_mut() {
9099 *r -= 1;
9100 if *r == 0 {
9101 break 'walk;
9102 }
9103 }
9104 // One row per key group: the rest are the same value.
9105 if distinct {
9106 break;
9107 }
9108 }
9109 }
9110 }
9111
9112 if !nulls_first {
9113 count += emit_null_rows(
9114 &mut emitted_rows,
9115 &mut eval_stack,
9116 &mut values,
9117 &mut visited,
9118 &mut to_skip,
9119 &mut remaining,
9120 emit,
9121 )?;
9122 }
9123 Ok(Some(count))
9124 }
9125
9126 /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
9127 /// building an `OrderKey` vector per row.
9128 ///
9129 /// The row-returning sorted scan allocates twice per row: one
9130 /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
9131 /// projection. Counted over 400 k rows (r1030,
9132 /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
9133 /// allocations and 208 MB of traffic for an answer of four hundred
9134 /// thousand integers.
9135 ///
9136 /// The key half is pure ceremony on this shape.
9137 /// `sort_tagged_by_inline_int_key` already sorts indices rather than
9138 /// rows, so the per-row vector is built, has one integer taken out of
9139 /// it, and is then dragged through the permutation — it exists to carry
9140 /// a number the row's column already held. This lane carries the number
9141 /// instead, in a fixed-size array that lives inside the buffer element
9142 /// and allocates nothing. Same idea as the predicate VM's integer lane.
9143 ///
9144 /// Declines to `None` for anything it does not cover, and every caller
9145 /// falls through to the general path, so the gate list is the
9146 /// specification.
9147 ///
9148 /// Ties: equal keys keep scan order, as the stable sort on the general
9149 /// path does. Rows that tie on every ORDER BY term are entitled to any
9150 /// order among themselves either way — see `STABILITY.md`.
9151 fn try_int_key_sorted_stream<F>(
9152 &self,
9153 stmt: &SelectStatement,
9154 from: &FromClause,
9155 cancel: CancelToken<'_>,
9156 emit: &mut F,
9157 ) -> Result<Option<usize>, EngineError>
9158 where
9159 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9160 {
9161 /// Sort terms this lane carries inline. Four covers every ORDER BY
9162 /// in the endpoint sweep and in the dogfood corpus; wider ones fall
9163 /// through rather than growing the buffer element for everybody.
9164 const MAX_KEYS: usize = 4;
9165
9166 if stmt.order_by.is_empty()
9167 || stmt.order_by.len() > MAX_KEYS
9168 // v7.38.14 — DISTINCT is admitted when the projected set is
9169 // exactly the ORDER BY set, and only then. This lane sorts, and
9170 // when the sort key determines the projected row every duplicate
9171 // lands ADJACENT to its twin -- so the de-duplication is a
9172 // comparison with the previous row rather than a hash table, and
9173 // the reason this lane declined DISTINCT disappears with it. The
9174 // seen-set it could not offer held indices into a materialised
9175 // vector; there is no seen-set now.
9176 //
9177 // The gate is as narrow as the bare-GROUP-BY rewrite's for the
9178 // same reason: `ORDER BY a` over a projection of `a, b` does NOT
9179 // place duplicates of the PAIR adjacent, so set EQUALITY, never
9180 // overlap.
9181 || (stmt.distinct && !Self::distinct_is_adjacent_after_sort(stmt))
9182 || stmt.limit_with_ties
9183 || stmt.limit.is_some()
9184 || stmt.offset.is_some()
9185 || stmt.having.is_some()
9186 || stmt.group_by.is_some()
9187 || !stmt.unions.is_empty()
9188 || !from.joins.is_empty()
9189 || from.primary.lateral_subquery.is_some()
9190 || from.primary.unnest_expr.is_some()
9191 || from.primary.as_of_segment.is_some()
9192 || from.primary.generate_series_args.is_some()
9193 || select_has_window(stmt)
9194 || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
9195 {
9196 return Ok(None);
9197 }
9198 if stmt
9199 .items
9200 .iter()
9201 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9202 {
9203 return Ok(None);
9204 }
9205 crate::orderby::check_order_by_legality(stmt)?;
9206 crate::orderby::check_order_by_positions(stmt)?;
9207 crate::window::reject_window_in_row_clauses(stmt)?;
9208 let Some(table) = self.active_catalog().get(&from.primary.name) else {
9209 return Ok(None);
9210 };
9211 if table.has_cold_rows_fast() {
9212 return Ok(None);
9213 }
9214 if !from.primary.only
9215 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9216 {
9217 return Ok(None);
9218 }
9219 let alias = from
9220 .primary
9221 .alias
9222 .as_deref()
9223 .unwrap_or(from.primary.name.as_str());
9224 let cols = table.schema().columns.clone();
9225
9226 // Every ORDER BY term must be a NOT NULL integer column of this
9227 // table. NOT NULL is what lets the key be a bare integer: with
9228 // NULLs the lane would have to carry their ordering too, and
9229 // getting that subtly wrong is the r1020 defect.
9230 let mut key_pos = [0usize; MAX_KEYS];
9231 let mut descs = [false; MAX_KEYS];
9232 // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
9233 // which the AST records as `None`; `unwrap_or(desc)` is how the
9234 // rest of the engine resolves it.
9235 let mut nulls_first = [false; MAX_KEYS];
9236 let n_keys = stmt.order_by.len();
9237 for (slot, order) in stmt.order_by.iter().enumerate() {
9238 let Expr::Column(oc) = &order.expr else {
9239 return Ok(None);
9240 };
9241 if let Some(q) = &oc.qualifier
9242 && !q.eq_ignore_ascii_case(alias)
9243 {
9244 return Ok(None);
9245 }
9246 let Some(pos) = cols
9247 .iter()
9248 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
9249 else {
9250 return Ok(None);
9251 };
9252 if !matches!(
9253 cols[pos].ty,
9254 spg_storage::DataType::SmallInt
9255 | spg_storage::DataType::Int
9256 | spg_storage::DataType::BigInt
9257 ) {
9258 return Ok(None);
9259 }
9260 key_pos[slot] = pos;
9261 descs[slot] = order.desc;
9262 nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
9263 }
9264
9265 let sess = self.dml_session();
9266 let ctx = EvalContext::new(&cols, Some(alias))
9267 .with_catalog(self.active_catalog())
9268 .with_session(&sess);
9269 let projection = build_projection(
9270 &stmt.items,
9271 &cols,
9272 alias,
9273 self.speaks_mysql,
9274 Some(self.active_catalog()),
9275 )?;
9276 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9277 let bound_pos: Vec<Option<usize>> = projection
9278 .iter()
9279 .map(|p| match &p.expr {
9280 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9281 Ok(Some(pos)) => Some(pos),
9282 _ => None,
9283 },
9284 _ => None,
9285 })
9286 .collect();
9287 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9288 .where_
9289 .as_ref()
9290 .filter(|w| crate::eval::fully_compilable(w))
9291 .map(|w| crate::eval::compile_expr(w, &ctx));
9292
9293 // The same first-observable point the materialising planner fires,
9294 // placed after the gates so it fires exactly once: this lane runs
9295 // BEFORE that planner and would otherwise be a hole in the
9296 // panic-isolation and cancellation-race coverage rather than a
9297 // faster path through it.
9298 crate::injection_point!("planner_first_row_fetch", &stmt.from);
9299
9300 let mut eval_stack: Vec<Value<'static>> = Vec::new();
9301 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9302 let mut budget = ByteBudget::new(self.max_query_bytes);
9303 let snapshot = self.current_snapshot();
9304 // Keys, a NULL bit per key slot, and the row. The bitmask keeps
9305 // the element small: a nullable key still costs one bit rather
9306 // than a second array.
9307 let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
9308
9309 for (ri, row) in table.rows().iter().enumerate() {
9310 if ri.is_multiple_of(256) {
9311 cancel.check()?;
9312 }
9313 if !table.is_row_visible(ri, &snapshot) {
9314 continue;
9315 }
9316 // The key comes from the STORED row, before projection: an
9317 // ORDER BY column need not appear in the select list.
9318 let mut keys = [0i64; MAX_KEYS];
9319 let mut nulls = 0u8;
9320 let mut keyed = true;
9321 for slot in 0..n_keys {
9322 match row.values.get(key_pos[slot]) {
9323 Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
9324 Some(Value::Int(v)) => keys[slot] = i64::from(*v),
9325 Some(Value::BigInt(v)) => keys[slot] = *v,
9326 Some(Value::Null) | None => nulls |= 1 << slot,
9327 // An integer column holding something else is a row
9328 // this lane cannot order; hand the whole query back
9329 // rather than guess at it.
9330 _ => {
9331 keyed = false;
9332 break;
9333 }
9334 }
9335 }
9336 if !keyed {
9337 return Ok(None);
9338 }
9339 if !Self::stream_filter_project(
9340 row,
9341 stmt.where_.as_ref(),
9342 compiled_where.as_ref(),
9343 &mut eval_stack,
9344 &projection,
9345 &bound_pos,
9346 &ctx,
9347 &mut values,
9348 )? {
9349 continue;
9350 }
9351 budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
9352 sorted.push((keys, nulls, core::mem::take(&mut values)));
9353 values.reserve(projection.len());
9354 }
9355
9356 sorted.sort_by(|a, b| {
9357 use core::cmp::Ordering;
9358 for slot in 0..n_keys {
9359 let bit = 1u8 << slot;
9360 let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
9361 (true, true) => Ordering::Equal,
9362 // Where the NULLs go is already decided — `nulls_first`
9363 // resolved DESC's default when it was read. Reversing
9364 // this for DESC as well would apply the direction
9365 // twice and put them at the wrong end.
9366 (true, false) => {
9367 if nulls_first[slot] {
9368 Ordering::Less
9369 } else {
9370 Ordering::Greater
9371 }
9372 }
9373 (false, true) => {
9374 if nulls_first[slot] {
9375 Ordering::Greater
9376 } else {
9377 Ordering::Less
9378 }
9379 }
9380 (false, false) => {
9381 let o = a.0[slot].cmp(&b.0[slot]);
9382 if descs[slot] { o.reverse() } else { o }
9383 }
9384 };
9385 if ord != Ordering::Equal {
9386 return ord;
9387 }
9388 }
9389 Ordering::Equal
9390 });
9391
9392 emit(crate::StreamItem::Header(&columns))?;
9393 // v7.38.14 — DISTINCT, de-duplicated against the PREVIOUS row.
9394 //
9395 // The gate above only admits DISTINCT when the sort key determines
9396 // the projected row, so every duplicate is adjacent to its twin by
9397 // the time this loop runs and one comparison replaces a hash table
9398 // of every row seen. Equality is `values_eq_norm` with the same mask
9399 // the materialising path builds -- deliberately the same function,
9400 // because a de-duplication that disagreed with the one on the other
9401 // path would make the answer depend on which lane a query took.
9402 //
9403 // A query that did not ask for DISTINCT pays one already-false bool
9404 // test per row: the short-circuit means the comparison never runs
9405 // and `prev` is never written.
9406 let dedup_mask = fold_mask(&projection);
9407 let fold = FoldSpec::of(self.speaks_mysql, &dedup_mask);
9408 let mut count = 0usize;
9409 let mut prev: Option<&[Value<'static>]> = None;
9410 for (_, _, vals) in &sorted {
9411 if stmt.distinct
9412 && let Some(p) = prev
9413 && values_eq_norm(p, vals, fold)
9414 {
9415 continue;
9416 }
9417 emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
9418 count += 1;
9419 if stmt.distinct {
9420 prev = Some(vals);
9421 }
9422 }
9423 Ok(Some(count))
9424 }
9425
9426 /// v7.38.14 — would sorting place every duplicate next to its twin?
9427 ///
9428 /// True when the projected expressions and the ORDER BY expressions are the
9429 /// same SET. Then the sort key determines the projected row, so equal rows
9430 /// are adjacent afterwards and an adjacent comparison de-duplicates exactly
9431 /// as a hash would -- and, because both sort paths are stable, the survivor
9432 /// is the first-seen row, which is the one the hash keeps too.
9433 ///
9434 /// A wildcard's expansion is not known here, so it is not a set this can
9435 /// compare; an ordinal ORDER BY names a select-list position rather than a
9436 /// value and is left alone.
9437 fn distinct_is_adjacent_after_sort(stmt: &SelectStatement) -> bool {
9438 if stmt.order_by.is_empty() || !stmt.distinct_on.is_empty() {
9439 return false;
9440 }
9441 let mut projected: alloc::vec::Vec<&Expr> =
9442 alloc::vec::Vec::with_capacity(stmt.items.len());
9443 for item in &stmt.items {
9444 match item {
9445 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return false,
9446 SelectItem::Expr { expr, .. } => projected.push(expr),
9447 }
9448 }
9449 if projected.is_empty() {
9450 return false;
9451 }
9452 let keys: alloc::vec::Vec<&Expr> = stmt.order_by.iter().map(|o| &o.expr).collect();
9453 if keys
9454 .iter()
9455 .any(|k| matches!(k, Expr::Literal(spg_sql::ast::Literal::Integer(_))))
9456 {
9457 return false;
9458 }
9459 projected.iter().all(|p| keys.contains(p)) && keys.iter().all(|k| projected.contains(k))
9460 }
9461
9462 fn try_spill_sorted_stream<F>(
9463 &self,
9464 stmt: &SelectStatement,
9465 from: &FromClause,
9466 cancel: CancelToken<'_>,
9467 emit: &mut F,
9468 ) -> Result<Option<usize>, EngineError>
9469 where
9470 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9471 {
9472 // The shapes `try_spill_sorted_scan` declines, plus the ones the
9473 // streaming executor does not carry (a LIMIT is already bounded
9474 // by a partial sort; the rest need the answer addressable).
9475 if !self.can_spill()
9476 || stmt.order_by.is_empty()
9477 || stmt.distinct
9478 || stmt.limit_with_ties
9479 || stmt.limit.is_some()
9480 || stmt.offset.is_some()
9481 || stmt.having.is_some()
9482 || stmt.group_by.is_some()
9483 || !stmt.unions.is_empty()
9484 || !from.joins.is_empty()
9485 || from.primary.lateral_subquery.is_some()
9486 || from.primary.unnest_expr.is_some()
9487 || from.primary.as_of_segment.is_some()
9488 || from.primary.generate_series_args.is_some()
9489 || select_has_window(stmt)
9490 || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
9491 {
9492 return Ok(None);
9493 }
9494 if stmt
9495 .items
9496 .iter()
9497 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9498 {
9499 return Ok(None);
9500 }
9501 // Everything `exec_bare_select_cancel` does before it scans runs
9502 // BELOW this path, so a statement claimed here skips it. Three of
9503 // those were missed on the way in and each was caught by a
9504 // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
9505 // ORDER BY 2` sorted happily instead of raising 42P10), the
9506 // cancellation check by another, the partition fan-out by the
9507 // differential corpus. What is reconciled, item by item: with-ties
9508 // needs ORDER BY (gated above), USING/NATURAL and RLS join
9509 // rewrites (joins gated above), the single-table RLS predicate
9510 // (the dispatcher declines a policy-subject table before this is
9511 // reached), the meta-view dispatch (those names are not in the
9512 // catalog, so the lookup below declines). These three are calls,
9513 // so the message and SQLSTATE are the ones the fall-back gives —
9514 // `select_has_window` above reads the select list and ORDER BY but
9515 // not WHERE, which is the case the third one covers.
9516 crate::orderby::check_order_by_legality(stmt)?;
9517 crate::orderby::check_order_by_positions(stmt)?;
9518 crate::window::reject_window_in_row_clauses(stmt)?;
9519 // A parent's rows are its children's. These walks scan the named
9520 // relation alone, so a partitioned or inherited parent comes back
9521 // short — and silently: the corpus caught `SELECT id FROM pr
9522 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
9523 // parent's own rows instead of the partitions'. `ONLY` is exactly
9524 // the case that does not fan out, so it stays, which is the test
9525 // the FROM-clause fan-out itself makes.
9526 if !from.primary.only
9527 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9528 {
9529 return Ok(None);
9530 }
9531 let Some(table) = self.active_catalog().get(&from.primary.name) else {
9532 return Ok(None);
9533 };
9534 // Cold-tier rows live outside `rows()`; this walk would drop
9535 // them silently, the same reason round 831's walk declines.
9536 if table.has_cold_rows_fast() {
9537 return Ok(None);
9538 }
9539
9540 let alias = from
9541 .primary
9542 .alias
9543 .as_deref()
9544 .unwrap_or(from.primary.name.as_str());
9545 let cols = table.schema().columns.clone();
9546 let sess = self.dml_session();
9547 let ctx = EvalContext::new(&cols, Some(alias))
9548 .with_catalog(self.active_catalog())
9549 .with_session(&sess);
9550 let projection = build_projection(
9551 &stmt.items,
9552 &cols,
9553 alias,
9554 self.speaks_mysql,
9555 Some(self.active_catalog()),
9556 )?;
9557 let order_by = stmt.order_by.clone();
9558 // The same one-shot resolution the general path does (round
9559 // 582): each ORDER BY column is bound once, not once per row.
9560 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
9561 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
9562 // Resolved BEFORE the scan, because it now decides what the sort
9563 // STORES and not just what it decodes (round 995).
9564 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
9565
9566 // v7.38.22 — resolved HERE, because this path did not resolve
9567 // them at all.
9568 //
9569 // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
9570 // "en_US.utf8"` in BYTE order on this path — and swallowed an
9571 // unknown collation name rather than raising — because the sorter
9572 // below compared with an empty collation slice. The materialising
9573 // path honoured both. Which answer a query got depended on which
9574 // path the planner took, and this is the path a plain single-table
9575 // SELECT takes.
9576 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
9577 // v7.39.12 — a correlated scalar subquery in ORDER BY is
9578 // resolved for the row before its key is built.
9579 //
9580 // Uncorrelated subqueries are replaced by a literal before
9581 // execution; a correlated one cannot be, so it reached the
9582 // per-row evaluator — the one place that cannot run a subquery
9583 // — and the statement raised "subquery reached row eval".
9584 // Reported by sentori against 7.39.11; see
9585 // `Engine::order_by_resolved_for_row`.
9586 //
9587 // The `any` runs once, here, so an ordinary ORDER BY pays one
9588 // bool per row and nothing else.
9589 let order_has_subquery = order_by
9590 .iter()
9591 .any(|o| crate::subquery::expr_has_subquery(&o.expr));
9592 let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
9593 let mut sorter = crate::extsort::ExternalSorter::new(
9594 self.temp_run_factory,
9595 self.session_work_mem_bytes(),
9596 cols.clone(),
9597 &descs,
9598 &order_colls,
9599 )
9600 .with_stats(&self.spill_stats)
9601 .with_pruned(&needed);
9602 let snapshot = self.current_snapshot();
9603 // One key buffer for the whole scan: `push` drains it and leaves
9604 // the capacity behind.
9605 let mut keys: Vec<OrderKey> = Vec::new();
9606 // r1024 — compile the predicate once for the scan.
9607 //
9608 // These two sorted-spill scans are the paths a single-table SELECT
9609 // with an ORDER BY takes, and they were the last row-returning ones
9610 // still walking the expression tree per row. r1023 did the
9611 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
9612 // exactly this shape.
9613 //
9614 // Found from the profile's CALL TREE rather than its leaves. The
9615 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
9616 // 261, `mod_op` 178 — and two attempts at reasoning out which
9617 // function asked for it were both wrong. The tree names the caller
9618 // chain, and it named this one.
9619 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9620 .where_
9621 .as_ref()
9622 .filter(|w| crate::eval::fully_compilable(w))
9623 .map(|w| crate::eval::compile_expr(w, &ctx));
9624 let mut eval_stack: Vec<Value<'static>> = Vec::new();
9625 for (i, row) in table.scan_visible_from(0, &snapshot) {
9626 if i.is_multiple_of(256) {
9627 cancel.check()?;
9628 }
9629 if let Some(c) = &compiled_where {
9630 if !crate::eval::compiled::eval_compiled_pred(
9631 c,
9632 row,
9633 &ctx,
9634 &mut eval_stack,
9635 ctx.mysql_dialect,
9636 )? {
9637 continue;
9638 }
9639 } else if let Some(w) = &stmt.where_ {
9640 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
9641 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9642 continue;
9643 }
9644 }
9645 keys.clear();
9646 // The same collations the sorter compares with, and the
9647 // re-derivation below is handed the same ones. `finish`'s
9648 // contract is that a key comes back the way it was pushed;
9649 // a collation is part of the way it was pushed.
9650 if order_has_subquery {
9651 // A substituted literal is no longer a bound column.
9652 let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
9653 crate::orderby::build_order_keys_bound(
9654 per_row.as_deref().unwrap_or(&order_by),
9655 &unbound,
9656 &order_colls,
9657 row,
9658 &ctx,
9659 &mut keys,
9660 )?;
9661 } else {
9662 crate::orderby::build_order_keys_bound(
9663 &order_by,
9664 &order_bound,
9665 &order_colls,
9666 row,
9667 &ctx,
9668 &mut keys,
9669 )?;
9670 }
9671 sorter.push(&mut keys, row)?;
9672 }
9673
9674 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9675 emit(crate::StreamItem::Header(&columns))?;
9676
9677 let key_ctx = &ctx;
9678 let mut emitted_since_check = 0usize;
9679 let n = sorter.finish_each(
9680 |src, buf| {
9681 crate::orderby::build_order_keys_rederived(
9682 &order_by,
9683 &order_bound,
9684 &order_colls,
9685 src,
9686 key_ctx,
9687 buf,
9688 )
9689 },
9690 |src, values| {
9691 for p in &projection {
9692 values.push(
9693 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
9694 );
9695 }
9696 Ok(())
9697 },
9698 |cells| {
9699 // The merge is the long half of a big sort, and the scan's
9700 // check above stops running once it ends: a cancelled
9701 // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
9702 // anyway. Same stride as the scan.
9703 emitted_since_check += 1;
9704 if emitted_since_check >= 256 {
9705 emitted_since_check = 0;
9706 cancel.check()?;
9707 }
9708 emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
9709 },
9710 )?;
9711 Ok(Some(n))
9712 }
9713
9714 /// One row of the single-table streaming walk: the WHERE test, the
9715 /// projection, the emit. Returns whether a row was emitted.
9716 ///
9717 /// v7.39 (round 970) — factored out because the walk now has two ways
9718 /// to reach a row, the sequential scan and an index seek's candidate
9719 /// positions, and both must do IDENTICALLY this. A copy in each is how
9720 /// two paths for one job drift; this file already carries the cost of
9721 /// that lesson twice (rounds 823 and 961, both resolvers).
9722 ///
9723 /// `#[inline]` so the scan loop keeps the shape round 957 measured it
9724 /// in — a shared hot path pays for a new abstraction whether or not it
9725 /// uses it, and this one is on the scan.
9726 #[inline]
9727 #[allow(clippy::too_many_arguments)]
9728 fn stream_filter_project(
9729 row: &spg_storage::Row<'static>,
9730 where_: Option<&Expr>,
9731 // r1023 — the same WHERE, compiled once by the caller. `None` means
9732 // the expression did not qualify and `where_` is evaluated as before.
9733 compiled_where: Option<&crate::eval::CompiledExpr>,
9734 eval_stack: &mut Vec<Value<'static>>,
9735 projection: &[ProjectedItem],
9736 bound_pos: &[Option<usize>],
9737 ctx: &crate::eval::EvalContext<'_>,
9738 values: &mut Vec<Value<'static>>,
9739 ) -> Result<bool, EngineError> {
9740 // r1023 — this scan ran its predicate through the TREE INTERPRETER,
9741 // once per row, and it was the only row-returning path that did.
9742 // The aggregate path, `table_access`, and the PK walker all compile
9743 // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
9744 // server's live samples were `eval_expr` 99, `apply_binary` 81,
9745 // `mod_op` 29 — the interpreter, not delivery.
9746 //
9747 // The arithmetic accounted for it exactly. Over the wire, the same
9748 // filter costs 6.375 ms returning rows and 0.679 ms counting them;
9749 // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
9750 // which is what an interpreted predicate costs against the compiled
9751 // lane's 11.7. It was named "delivery after a filter" before this
9752 // profile, and it was never delivery.
9753 if let Some(c) = compiled_where {
9754 if !crate::eval::compiled::eval_compiled_pred(
9755 c,
9756 row,
9757 ctx,
9758 eval_stack,
9759 ctx.mysql_dialect,
9760 )? {
9761 return Ok(false);
9762 }
9763 } else if let Some(w) = where_ {
9764 let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
9765 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9766 return Ok(false);
9767 }
9768 }
9769 values.clear();
9770 for (p, bound) in projection.iter().zip(bound_pos) {
9771 values.push(match bound {
9772 Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
9773 None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
9774 });
9775 }
9776 Ok(true)
9777 }
9778
9779 /// The same filter and projection, then emit. Split from
9780 /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
9781 /// before it can emit them — a sort — runs the identical predicate and
9782 /// projection rather than a second copy of them.
9783 #[allow(clippy::too_many_arguments)]
9784 fn stream_project_row<F>(
9785 row: &spg_storage::Row<'static>,
9786 where_: Option<&Expr>,
9787 compiled_where: Option<&crate::eval::CompiledExpr>,
9788 eval_stack: &mut Vec<Value<'static>>,
9789 projection: &[ProjectedItem],
9790 bound_pos: &[Option<usize>],
9791 ctx: &crate::eval::EvalContext<'_>,
9792 values: &mut Vec<Value<'static>>,
9793 emit: &mut F,
9794 ) -> Result<bool, EngineError>
9795 where
9796 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9797 {
9798 if !Self::stream_filter_project(
9799 row,
9800 where_,
9801 compiled_where,
9802 eval_stack,
9803 projection,
9804 bound_pos,
9805 ctx,
9806 values,
9807 )? {
9808 return Ok(false);
9809 }
9810 emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
9811 Ok(true)
9812 }
9813
9814 fn try_stream_single_table<F>(
9815 &self,
9816 stmt: &SelectStatement,
9817 from: &FromClause,
9818 cancel: CancelToken<'_>,
9819 emit: &mut F,
9820 ) -> Result<Option<usize>, EngineError>
9821 where
9822 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9823 {
9824 let Some(table) = self.active_catalog().get(&from.primary.name) else {
9825 return Ok(None);
9826 };
9827 // Cold-tier rows live outside `rows()`; the materialising fallback
9828 // covers both tiers and this walk would silently drop them.
9829 if table.has_cold_rows_fast() {
9830 return Ok(None);
9831 }
9832 let alias = from
9833 .primary
9834 .alias
9835 .as_deref()
9836 .unwrap_or(from.primary.name.as_str());
9837 let cols = table.schema().columns.clone();
9838 let sess = self.dml_session();
9839 let ctx = EvalContext::new(&cols, Some(alias))
9840 .with_catalog(self.active_catalog())
9841 .with_session(&sess);
9842 let projection = build_projection(
9843 &stmt.items,
9844 &cols,
9845 alias,
9846 self.speaks_mysql,
9847 Some(self.active_catalog()),
9848 )?;
9849
9850 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9851 emit(crate::StreamItem::Header(&columns))?;
9852
9853 // v7.37 (round 957) — resolve each bare-column projection ONCE
9854 // instead of once per row. `find_column_pos`-style resolution is a
9855 // linear walk of the schema comparing column-name strings, and the
9856 // row loop below ran it for every cell of every row: measured at
9857 // 400k rows, binding it out of the loop took `SELECT pad` from
9858 // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
9859 //
9860 // ORDER BY has bound its keys this way since round 582
9861 // (`order_by_bound_positions`); the projection never did.
9862 //
9863 // `locate_column` is the same resolution `resolve_column` performs,
9864 // returning the site instead of the value, so the two cannot drift
9865 // apart the way a second hand-written resolver would. Anything it
9866 // declines — an expression, a whole-row reference, a name that does
9867 // not resolve — binds to `None` and takes the general path below,
9868 // errors included, so an empty table still reports nothing rather
9869 // than raising at bind time.
9870 let bound_pos: Vec<Option<usize>> = projection
9871 .iter()
9872 .map(|p| match &p.expr {
9873 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9874 Ok(Some(pos)) => Some(pos),
9875 _ => None,
9876 },
9877 _ => None,
9878 })
9879 .collect();
9880
9881 // One snapshot for the whole scan, as the materialising path takes.
9882 let snapshot = self.current_snapshot();
9883
9884 // v7.39 (round 970) — ask the indices BEFORE walking the table.
9885 //
9886 // This walk had no index step at all, and it is preferred over the
9887 // materialising path, which does have one (`pick_indexed_rows` ->
9888 // `try_index_seek`). So a primary-key point lookup — the commonest
9889 // statement there is — read every row: measured on 500k rows,
9890 // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
9891 // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
9892 // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
9893 //
9894 // The control that named it: `... OFFSET 0` — semantically the same
9895 // query — answered in 0.159 ms, because OFFSET is one of the shape
9896 // gates that declines this walk and sends the statement to the path
9897 // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
9898 // no semantics in common; what they share is making this function
9899 // stand down.
9900 //
9901 // The seek only NARROWS: every candidate still goes through the
9902 // full WHERE below, exactly as the mutation paths use it, so a
9903 // partial index match cannot change an answer. Positions come back
9904 // already visibility-filtered and already capped at a quarter of the
9905 // table (round 490), so a seek can never cost more than the scan it
9906 // replaces, and `None` means "walk the table" as before.
9907 //
9908 // Sorted because the scan would have produced table order and the
9909 // index produces key order. Without an ORDER BY neither is promised,
9910 // but a walk that silently reorders its answer when an index happens
9911 // to exist is a difference nobody asked for.
9912 let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
9913 crate::index_access::try_index_seek_positions(
9914 w,
9915 &cols,
9916 table,
9917 alias,
9918 &snapshot,
9919 self.speaks_mysql,
9920 )
9921 });
9922
9923 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9924 // r1023 — compile the predicate once for the whole scan. Same gate
9925 // every other path uses: `fully_compilable` or keep the interpreter,
9926 // so a shape the VM cannot take answers exactly as it did before.
9927 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9928 .where_
9929 .as_ref()
9930 .filter(|w| crate::eval::fully_compilable(w))
9931 .map(|w| crate::eval::compile_expr(w, &ctx));
9932 let mut eval_stack: Vec<Value<'static>> = Vec::new();
9933 let mut count: usize = 0;
9934 match seek_positions {
9935 Some(mut positions) => {
9936 positions.sort_unstable();
9937 for (n, pos) in positions.into_iter().enumerate() {
9938 if n.is_multiple_of(256) {
9939 cancel.check()?;
9940 }
9941 let Some(row) = table.rows().get(pos) else {
9942 continue;
9943 };
9944 if Self::stream_project_row(
9945 row,
9946 stmt.where_.as_ref(),
9947 compiled_where.as_ref(),
9948 &mut eval_stack,
9949 &projection,
9950 &bound_pos,
9951 &ctx,
9952 &mut values,
9953 emit,
9954 )? {
9955 count += 1;
9956 }
9957 }
9958 }
9959 None => {
9960 // v7.38.11 — the streaming scan is the path a client
9961 // reaches over the wire, so it is the one that has to
9962 // ask the BRIN summary which slots can be skipped. The
9963 // predicate still runs on every row that survives.
9964 let slots = stmt
9965 .where_
9966 .as_ref()
9967 .and_then(|w| crate::brin::candidate_slots(w, table))
9968 .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
9969 for (i, row) in table.scan_visible_slots(slots, &snapshot) {
9970 if i.is_multiple_of(256) {
9971 cancel.check()?;
9972 }
9973 if Self::stream_project_row(
9974 row,
9975 stmt.where_.as_ref(),
9976 compiled_where.as_ref(),
9977 &mut eval_stack,
9978 &projection,
9979 &bound_pos,
9980 &ctx,
9981 &mut values,
9982 emit,
9983 )? {
9984 count += 1;
9985 }
9986 }
9987 }
9988 }
9989 Ok(Some(count))
9990 }
9991
9992 pub(crate) fn try_exec_joined_streaming<F>(
9993 &self,
9994 stmt: &SelectStatement,
9995 cancel: CancelToken<'_>,
9996 emit: &mut F,
9997 ) -> Result<Option<usize>, EngineError>
9998 where
9999 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
10000 {
10001 // Shape gates — keep the streamable surface narrow on
10002 // purpose. The fall-back path still handles everything else.
10003 let Some(from) = &stmt.from else {
10004 return Ok(None);
10005 };
10006 // v7.37 (round 830) — decline anything a row-security policy binds
10007 // for this session. Policies are injected in
10008 // `exec_bare_select_cancel`, below this path, so a statement claimed
10009 // here would read the table unfiltered: measured, `SELECT val FROM
10010 // sec` returned all three rows to a session whose policy allows two,
10011 // while `SELECT upper(val) FROM sec` — declined by the shape gates
10012 // and so materialised — returned the correct two.
10013 //
10014 // Declining sends it to the path that enforces. Teaching this one to
10015 // inject the predicate itself would keep the streaming benefit for
10016 // RLS tables and is the better end state; it is not what a
10017 // correctness fix should carry, and the fall-back is exactly as
10018 // correct, only slower.
10019 if self.select_reads_policy_subject_table(stmt) {
10020 return Ok(None);
10021 }
10022 // r1058 — a WITH list this path never materialises: the CTE
10023 // name would be resolved as a physical relation and error
10024 // ("relation \"big\" does not exist" over the extended
10025 // protocol, caught by the perm-runner's wire legs). The
10026 // materialising fallback owns CTE execution.
10027 if !stmt.ctes.is_empty() {
10028 return Ok(None);
10029 }
10030 // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
10031 // tables` and kin) exist only as synth arms on the
10032 // materialising path; claiming one here errored "relation
10033 // does not exist" over the extended protocol for a query the
10034 // simple protocol answered. Prefix test only — a genuinely
10035 // missing relation must keep erroring in-path.
10036 if from.primary.name.starts_with("__spg_")
10037 || from
10038 .joins
10039 .iter()
10040 .any(|j| j.table.name.starts_with("__spg_"))
10041 {
10042 return Ok(None);
10043 }
10044 // r1058 — decline partitioned / inheritance parents, same
10045 // shape of bug as the RLS decline above: this path scans the
10046 // named table's own (empty) heap, so `SELECT id, region FROM
10047 // cust` on a partition parent streamed ZERO rows over the wire
10048 // while COUNT(*) — an aggregate, materialised below — said 3.
10049 // Caught by the perm-runner's server permutations; the
10050 // materialising fallback expands children correctly.
10051 if crate::partition::has_children(self.active_catalog(), &from.primary.name)
10052 || from
10053 .joins
10054 .iter()
10055 .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
10056 {
10057 return Ok(None);
10058 }
10059 // v7.39 (round 790) — single-table SELECTs stream too. This
10060 // gate said "joins only" because the path was written for
10061 // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
10062 // fell to the materialising fallback, which builds the whole
10063 // `Vec<Row<'static>>` and only then iterates it. Measured on
10064 // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
10065 // reached through a one-row JOIN — 2.6x, purely for lacking a
10066 // join. The deferred-join structure handles one source as the
10067 // degenerate stride-1 case, so the walk below is unchanged.
10068 let _single_table = from.joins.is_empty();
10069 // An ORDER BY that the bounded sort can serve streams; everything
10070 // else still falls to the materialising fallback below.
10071 // r1025 — an ordering the index already holds needs no sort at all.
10072 // Tried before the spill sort, which is the path it replaces.
10073 if !stmt.order_by.is_empty()
10074 && from.joins.is_empty()
10075 && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
10076 {
10077 return Ok(Some(n));
10078 }
10079 if !stmt.order_by.is_empty()
10080 && from.joins.is_empty()
10081 && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
10082 {
10083 return Ok(Some(n));
10084 }
10085 // r1031 — integer keys carried inline instead of an `OrderKey`
10086 // vector per row. Tried AFTER the spill sort on purpose: this lane
10087 // buffers the whole answer, so anything the spill path would take
10088 // must keep taking it rather than be turned back into an in-memory
10089 // sort that answers with a budget error.
10090 if !stmt.order_by.is_empty()
10091 && from.joins.is_empty()
10092 && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
10093 {
10094 return Ok(Some(n));
10095 }
10096 if !stmt.order_by.is_empty()
10097 || stmt.limit.is_some()
10098 || stmt.offset.is_some()
10099 || stmt.having.is_some()
10100 || stmt.group_by.is_some()
10101 || stmt.distinct
10102 || !stmt.unions.is_empty()
10103 || stmt.limit_with_ties
10104 {
10105 return Ok(None);
10106 }
10107 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
10108 return Ok(None);
10109 }
10110 // No window / SRF on the streaming path.
10111 if select_has_window(stmt) {
10112 return Ok(None);
10113 }
10114 if stmt
10115 .items
10116 .iter()
10117 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
10118 {
10119 return Ok(None);
10120 }
10121 // v7.37 (round 831) — a joinless FROM over a plain stored table
10122 // never needs the deferred structure, and building one costs the
10123 // whole table. `materialise_table_ref_filtered` clones every row
10124 // into a `Vec<Row<'static>>` before anything is filtered or
10125 // projected, so peak cost tracks the TABLE, not the result:
10126 // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
10127 // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
10128 // projection saving nothing, while an arithmetic projection — which
10129 // the shape gates decline, so it materialises through the ordinary
10130 // executor — cost +21 MB.
10131 //
10132 // Scanning in batches and releasing each one is what `cursor_fill`
10133 // already does for a lazy cursor, and it is the same walk: resume
10134 // from a slot, take visible rows, evaluate, hand them over, drop
10135 // them. Round 800's finding stands and is why this reads rows OUT
10136 // rather than seeding the join by index — touching the stored
10137 // `PersistentVec` in place makes the whole table resident, which is
10138 // worse than the copy. Each batch is copied, then freed.
10139 if from.joins.is_empty()
10140 && from.primary.unnest_expr.is_none()
10141 && from.primary.lateral_subquery.is_none()
10142 && from.primary.as_of_segment.is_none()
10143 && from.primary.generate_series_args.is_none()
10144 && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
10145 {
10146 return Ok(Some(n));
10147 }
10148 // Build the deferred join under the regular byte budget.
10149 let mut budget = ByteBudget::new(self.max_query_bytes);
10150 let deferred = {
10151 let mut needed = alloc::collections::BTreeSet::new();
10152 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10153 self.build_joined_filtered_rows(
10154 from,
10155 stmt.where_.as_ref(),
10156 cancel,
10157 if prunable { Some(&needed) } else { None },
10158 &mut budget,
10159 )?
10160 };
10161 let combined_schema = &deferred.combined_schema;
10162 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10163 // `::regclass` / enum cast in a joined projection or HAVING needs it.
10164 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10165 // the same predicate the unjoined shape carries.
10166 let joined_sess = self.dml_session();
10167 // v7.38.18 — and the DIALECT. This context carried the catalog and
10168 // the session and not the one field that decides how text
10169 // compares, so a joined row was evaluated in PostgreSQL
10170 // semantics inside a MySQL session.
10171 //
10172 // It showed up only where the two sides had DIFFERENT text types:
10173 // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10174 // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10175 // were fine and the same comparison inside one table was fine.
10176 // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10177 // so the wrong semantics were invisible until a CHAR's padding
10178 // had to be stripped and PostgreSQL's arm does not strip it.
10179 //
10180 // `with_engine` is what sets it; the next line already reaches
10181 // for `self.backslash_escapes`, so the dialect was in hand.
10182 let ctx = EvalContext::new(combined_schema, None)
10183 .with_catalog(self.active_catalog())
10184 .with_engine(self)
10185 .with_session(&joined_sess);
10186 let projection = build_projection(
10187 &stmt.items,
10188 combined_schema,
10189 "",
10190 self.speaks_mysql,
10191 Some(self.active_catalog()),
10192 )?;
10193 // Every projection item must be a bound qualified column —
10194 // anything that needs `eval_expr_with_correlated` keeps the
10195 // materialising path.
10196 let bound_pos = |e: &Expr| -> Option<usize> {
10197 match e {
10198 // v7.39 (round 822) — an UNQUALIFIED column resolves here
10199 // too. The `qualifier.is_some()` guard this replaces meant
10200 // `SELECT pad FROM big` — the commonest projection there is
10201 // — never reached the streaming walk: it fell out at this
10202 // gate and re-ran on the materialising path, after the
10203 // deferred join structure had already been built and paid
10204 // for. Measured (round 821, statement_timeout=120 over 400k
10205 // rows): `big.pad` and `b.pad` streamed and cancelled at
10206 // ~65k rows in 0.14 s, while bare `pad` ran to completion in
10207 // 0.80 s with the timeout never consulted. `find_column_pos`
10208 // has always handled the unqualified case (it falls through
10209 // to a by-name match), so the guard narrowed the gate for no
10210 // reason it recorded.
10211 Expr::Column(c) => eval::find_column_pos(c, &ctx),
10212 _ => None,
10213 }
10214 };
10215 let proj_decomposed: Vec<(usize, usize)> = {
10216 let mut out = Vec::with_capacity(projection.len());
10217 for p in &projection {
10218 let Some(abs) = bound_pos(&p.expr) else {
10219 return Ok(None);
10220 };
10221 let Some(k) = deferred
10222 .offsets
10223 .partition_point(|&o| o <= abs)
10224 .checked_sub(1)
10225 else {
10226 return Ok(None);
10227 };
10228 out.push((k, abs - deferred.offsets[k]));
10229 }
10230 out
10231 };
10232 // Emit columns once.
10233 let columns: Vec<ColumnSchema> = projection
10234 .iter()
10235 // v7.39 (read01 round 54) — keep the column's enum identity through
10236 // the projection (it lives outside the DataType lattice), or a
10237 // derived table / UNION / windowed result forgets it and any outer
10238 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
10239 .map(|p| p.to_column_schema())
10240 .collect();
10241 emit(crate::StreamItem::Header(&columns))?;
10242 let sources_ref = &deferred.sources;
10243 let stride = deferred.stride;
10244 let survivors_ref = &deferred.survivors;
10245 let n_surv = if stride == 0 {
10246 0
10247 } else {
10248 survivors_ref.len() / stride
10249 };
10250 // Reused per-row cell-ref scratch — pushes are zero-alloc
10251 // after the first row.
10252 let null_value = Value::Null;
10253 let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
10254 let mut count: usize = 0;
10255 for surv_i in 0..n_surv {
10256 if surv_i.is_multiple_of(256) {
10257 cancel.check()?;
10258 }
10259 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10260 cell_refs.clear();
10261 for &(k, col_in_src) in &proj_decomposed {
10262 let ri = tuple[k];
10263 let v: &Value = if ri == usize::MAX {
10264 &null_value
10265 } else {
10266 sources_ref[k]
10267 .get(ri)
10268 .and_then(|r| r.values.get(col_in_src))
10269 .unwrap_or(&null_value)
10270 };
10271 cell_refs.push(v);
10272 }
10273 emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
10274 count += 1;
10275 }
10276 Ok(Some(count))
10277 }
10278
10279 fn exec_joined_select(
10280 &self,
10281 stmt: &SelectStatement,
10282 from: &FromClause,
10283 cancel: CancelToken<'_>,
10284 ) -> Result<QueryResult, EngineError> {
10285 // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
10286 // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
10287 // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
10288 // FROM B WHERE B.k = A.k)` into
10289 // SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
10290 // WHERE B.k IS NULL
10291 // The general join executor builds a hash, probes every outer
10292 // tuple, materialises (left_padded_with_null) for every miss,
10293 // then runs the aggregate over the result set. For COUNT(*) we
10294 // only need the count — skip the tuple materialisation. Build
10295 // a HashSet of B's unique join values, scan A's PK index, and
10296 // increment the counter on each miss. PG's Merge Anti-Join
10297 // does roughly this; ours becomes a simple HashSet probe.
10298 if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
10299 return Ok(out);
10300 }
10301 // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
10302 // When ORDER BY is on an indexed primary column, walking the
10303 // btree in the requested direction lets the streamer break
10304 // after `LIMIT + OFFSET` survivors without ever materialising
10305 // the rest of the join — the 80 ms `mailrs_prod_not_exists`
10306 // plateau is exactly this shape.
10307 if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
10308 return Ok(out);
10309 }
10310 // v7.30.3 (mailrs round-26) — the bounded single-join path
10311 // first; peak memory scales with LIMIT instead of the table.
10312 if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
10313 return Ok(out);
10314 }
10315 // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
10316 // WHERE materialisation to the shared helper so the LATERAL
10317 // / UNNEST / regular-catalog paths route through one place.
10318 // (`build_joined_filtered_rows` carries LATERAL support as
10319 // of Phase 3.P0-41.) Downstream we still handle aggregate /
10320 // projection / ORDER BY / DISTINCT / LIMIT inline because
10321 // those depend on the SelectStatement's items list.
10322 let mut budget = ByteBudget::new(self.max_query_bytes);
10323 let deferred = {
10324 let mut needed = alloc::collections::BTreeSet::new();
10325 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10326 self.build_joined_filtered_rows(
10327 from,
10328 stmt.where_.as_ref(),
10329 cancel,
10330 if prunable { Some(&needed) } else { None },
10331 &mut budget,
10332 )?
10333 };
10334 let combined_schema = &deferred.combined_schema;
10335 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10336 // `::regclass` / enum cast in a joined projection or HAVING needs it.
10337 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10338 // the same predicate the unjoined shape carries.
10339 let joined_sess = self.dml_session();
10340 // v7.38.18 — and the DIALECT. This context carried the catalog and
10341 // the session and not the one field that decides how text
10342 // compares, so a joined row was evaluated in PostgreSQL
10343 // semantics inside a MySQL session.
10344 //
10345 // It showed up only where the two sides had DIFFERENT text types:
10346 // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10347 // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10348 // were fine and the same comparison inside one table was fine.
10349 // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10350 // so the wrong semantics were invisible until a CHAR's padding
10351 // had to be stripped and PostgreSQL's arm does not strip it.
10352 //
10353 // `with_engine` is what sets it; the next line already reaches
10354 // for `self.backslash_escapes`, so the dialect was in hand.
10355 let ctx = EvalContext::new(combined_schema, None)
10356 .with_catalog(self.active_catalog())
10357 .with_engine(self)
10358 .with_session(&joined_sess);
10359 // Aggregate path: handle GROUP BY / aggregate calls over the
10360 // joined+filtered rows.
10361 if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
10362 // v7.32 (P4 borrow channel, increment 2) — borrow each
10363 // surviving join tuple as a RowRef::Tuple; the aggregate
10364 // engine reads source cells by reference (bound fast path =
10365 // zero clone) instead of consuming materialised combined
10366 // Rows. This is where the +211k materialise_tuple_vals
10367 // clones disappear for the join+aggregate shape.
10368 let refs = deferred.row_refs();
10369 // v7.29 — a per-query memo so correlated scalar
10370 // subqueries batch-evaluate once (group map) instead of
10371 // executing per group.
10372 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
10373 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
10374 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
10375 .map_err(|err| match err {
10376 EngineError::Eval(ev) => ev,
10377 other => eval::EvalError::TypeMismatch {
10378 detail: alloc::format!("{other}"),
10379 },
10380 })
10381 };
10382 let agg = aggregate::run(
10383 stmt,
10384 crate::join::AggRows::Refs(&refs),
10385 combined_schema,
10386 None,
10387 Some(&agg_correlated),
10388 self.parallel_runner.0.as_deref(),
10389 Some(self.active_catalog()),
10390 Some(self),
10391 )?;
10392 return self.finish_agg_result(agg, stmt, cancel);
10393 }
10394
10395 let projection = build_projection(
10396 &stmt.items,
10397 combined_schema,
10398 "",
10399 self.speaks_mysql,
10400 Some(self.active_catalog()),
10401 )?;
10402 // v7.39 (round 734) — a set-returning projection over a JOIN.
10403 // This executor's projection loop treats every item as a scalar,
10404 // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
10405 // "function unnest(integer[]) does not exist" where PG expands
10406 // it. The row-set executor already carries the full SRF pipeline
10407 // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
10408 // sharding): materialise the joined survivors and hand over. The
10409 // WHERE is cleared — the join already applied it, and combined
10410 // columns resolve identically in both executors.
10411 if !self.srf_target_idxs(&projection).is_empty() {
10412 let refs = deferred.row_refs();
10413 let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
10414 let mut s2 = stmt.clone();
10415 s2.where_ = None;
10416 let schema = combined_schema.clone();
10417 return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
10418 }
10419 // v7.33 (P4 borrow channel, increment 3) — project directly off
10420 // the deferred row-index tuples instead of materialising an
10421 // intermediate combined Row per survivor. A bound qualified
10422 // column is read by reference (`RowRef::get` → `tuple_value`) and
10423 // cloned ONCE into the output row; the old `materialise()` (a full
10424 // combined Row plus a source→intermediate clone per referenced
10425 // cell, for every survivor) is gone. A row materialises on demand
10426 // only when a projection or ORDER BY expression needs the eval
10427 // path (subquery / function / arithmetic / unqualified column).
10428 // Same bind-once classification the aggregate input fast path uses
10429 // (`accumulate_groups`), reading the same `tuple_value` mapping the
10430 // differential gate already covers.
10431 let refs = deferred.row_refs();
10432 let bound_pos = |e: &Expr| -> Option<usize> {
10433 match e {
10434 Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
10435 _ => None,
10436 }
10437 };
10438 let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
10439 let all_proj_bound = proj_pos.iter().all(Option::is_some);
10440 // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
10441 // pre-decompose each bound projection position into
10442 // `(source_k, col_in_source)` so the per-row column read
10443 // skips the per-cell `tuple_value` partition_point + slice
10444 // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
10445 // calls) that walk dominated; this version reaches into
10446 // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
10447 let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
10448 .iter()
10449 .map(|p| {
10450 p.and_then(|abs| {
10451 let k = deferred
10452 .offsets
10453 .partition_point(|&o| o <= abs)
10454 .checked_sub(1)?;
10455 Some((k, abs - deferred.offsets[k]))
10456 })
10457 })
10458 .collect();
10459 // v7.39 (round 962) — which projection items are whole-row
10460 // references, and to which join source. The test is
10461 // `locate_column` declining the name, which is the SAME resolver
10462 // the evaluation path uses, so this cannot drift from it: a real
10463 // column carrying an alias's name resolves to a position and is
10464 // not reported here. The source index comes from the alias
10465 // prefix, the way the combined schema names its columns.
10466 let whole_row_src: Vec<Option<usize>> = projection
10467 .iter()
10468 .map(|p| {
10469 let Expr::Column(c) = &p.expr else {
10470 return None;
10471 };
10472 if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
10473 return None;
10474 }
10475 let prefix = alloc::format!("{name}.", name = c.name);
10476 let abs = deferred
10477 .combined_schema
10478 .iter()
10479 .position(|s| s.name.starts_with(&prefix))?;
10480 deferred
10481 .offsets
10482 .partition_point(|&o| o <= abs)
10483 .checked_sub(1)
10484 })
10485 .collect();
10486 // ORDER BY (when present) still evaluates against a materialised
10487 // Row — keep the order-key encoder correct rather than fork it.
10488 let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
10489 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
10490 let mut proj_memo = memoize::MemoizeCache::default();
10491 let sources_ref = &deferred.sources;
10492 let stride = deferred.stride;
10493 let survivors_ref = &deferred.survivors;
10494 let n_surv = survivors_ref.len() / stride.max(1);
10495 // v7.38 (read01 B8) — streaming top-N budget (see the sibling
10496 // single-table path). Bounds this JOIN projection's accumulator
10497 // to O(keep) for `ORDER BY … LIMIT k`.
10498 let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
10499 && !stmt.distinct
10500 && !stmt.limit_with_ties
10501 && !self.env_cfg().disable_topk
10502 {
10503 stmt.limit_literal().and_then(|l| {
10504 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
10505 (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
10506 })
10507 } else {
10508 None
10509 };
10510 // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
10511 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
10512 hashbrown::HashMap::new();
10513 let distinct_hb = hashbrown::DefaultHashBuilder::default();
10514 // v7.38.13 — which output positions must NOT fold. Built once per
10515 // scan from the projection, which carries the source column's
10516 // byte-wise-ness; see `FoldSpec`.
10517 let distinct_mask = fold_mask(&projection);
10518 for surv_i in 0..n_surv {
10519 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10520 let row = &refs[surv_i];
10521 let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
10522 Some(row.as_row())
10523 } else {
10524 None
10525 };
10526 let mut values = Vec::with_capacity(projection.len());
10527 for (i, p) in projection.iter().enumerate() {
10528 if let Some((k, col_in_src)) = proj_decomposed[i] {
10529 // v7.36 — direct (source_k, col) lookup, no
10530 // partition_point. tuple[k] is the row index in
10531 // sources[k]; LEFT-NULL slots are `usize::MAX`.
10532 let ri = tuple[k];
10533 let v: Value<'static> = if ri == usize::MAX {
10534 Value::Null
10535 } else {
10536 sources_ref[k]
10537 .get(ri)
10538 .and_then(|r| r.values.get(col_in_src))
10539 .cloned()
10540 .map(Value::into_owned)
10541 .unwrap_or(Value::Null)
10542 };
10543 values.push(v);
10544 } else if let Some(pos) = proj_pos[i] {
10545 // Bound but couldn't decompose (shouldn't normally
10546 // happen — keep as a safe path).
10547 values.push(
10548 row.get(pos)
10549 .cloned()
10550 .map(Value::into_owned)
10551 .unwrap_or(Value::Null),
10552 );
10553 } else if let Some(k) = whole_row_src[i]
10554 && tuple[k] == usize::MAX
10555 {
10556 // v7.39 (round 962) — a whole-row reference to a side
10557 // an OUTER join null-extended is NULL, not a
10558 // composite whose fields are all NULL. PG18.4 answers
10559 // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
10560 // an empty cell; round 961 answered `(,)`.
10561 //
10562 // The evaluator below cannot tell the two apart: it
10563 // reads the MATERIALISED combined row, where a
10564 // null-extended side is indistinguishable from a real
10565 // row whose every column is NULL — and that row is
10566 // `(,)` in PG too, so guessing by "all fields NULL"
10567 // would trade one wrong answer for another. The
10568 // tuple, which is still in hand here, does know:
10569 // `usize::MAX` is the sentinel the join writes for
10570 // exactly this.
10571 values.push(Value::Null);
10572 } else {
10573 // Eval path — `materialised` is Some whenever any
10574 // projection item is non-bound (need_eval_row true).
10575 // v7.24 (round-16 B) — select-list subqueries under a
10576 // JOIN go through the correlated-aware evaluator too.
10577 let mrow = materialised.as_deref().expect("materialised for eval");
10578 values.push(self.eval_expr_with_correlated(
10579 &p.expr,
10580 mrow,
10581 &ctx,
10582 cancel,
10583 Some(&mut proj_memo),
10584 )?);
10585 }
10586 }
10587 let out_row = Row::new(values);
10588 // v7.37.16 — streaming DISTINCT (see the scan-path twin):
10589 // probe on the projected row; duplicates skip the
10590 // build_order_keys eval and never enter `tagged`.
10591 if stmt.distinct {
10592 let bucket = seen_distinct
10593 .entry(norm_hash_row(
10594 &out_row,
10595 &distinct_hb,
10596 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10597 ))
10598 .or_default();
10599 if bucket.iter().any(|i| {
10600 row_eq_norm(
10601 &tagged[i].1,
10602 &out_row,
10603 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10604 )
10605 }) {
10606 continue;
10607 }
10608 bucket.push(tagged.len());
10609 }
10610 let order_keys = if stmt.order_by.is_empty() {
10611 Vec::new()
10612 } else {
10613 let mrow = materialised.as_deref().expect("materialised for order by");
10614 build_order_keys(&stmt.order_by, mrow, &ctx)?
10615 };
10616 budget.charge(approx_row_bytes(&out_row))?;
10617 tagged.push((order_keys, out_row));
10618 if let Some((k, descs)) = &topk_stream {
10619 topk_trim(&mut tagged, *k, descs);
10620 }
10621 }
10622 if !stmt.order_by.is_empty() {
10623 // v7.38 元机制 D acceptor — see other call site above.
10624 let keep = if self.env_cfg().disable_topk {
10625 None
10626 } else {
10627 stmt.limit_literal()
10628 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
10629 };
10630 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
10631 // v7.39 (round 688) — the join's ORDER BY resolves its keys
10632 // against `ctx`, which is built from `build_combined_schema`, so
10633 // this is where a declared collation reaches the sort. There was
10634 // exactly ONE resolver call in the engine before this — the
10635 // single-table scan's — which is why every other shape sorted by
10636 // bytes no matter what the schemas carried.
10637 let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
10638 crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
10639 }
10640 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
10641 apply_offset_and_limit(
10642 &mut output_rows,
10643 stmt.offset_literal(),
10644 stmt.limit_literal(),
10645 );
10646 let columns: Vec<ColumnSchema> = projection
10647 .into_iter()
10648 .map(|p| p.to_column_schema())
10649 .collect();
10650 Ok(QueryResult::Rows {
10651 columns,
10652 rows: output_rows,
10653 })
10654 }
10655}
10656
10657impl Engine {
10658 /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
10659 /// by id, decodes each row body against the table's current
10660 /// schema, applies the SELECT's projection + optional WHERE +
10661 /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
10662 /// / ORDER BY are unsupported on this path (STABILITY carve-
10663 /// out); operators wanting them should restore the segment
10664 /// into a regular table first.
10665 fn exec_select_as_of_segment(
10666 &self,
10667 stmt: &SelectStatement,
10668 from: &spg_sql::ast::FromClause,
10669 segment_id: u32,
10670 ) -> Result<QueryResult, EngineError> {
10671 // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
10672 // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
10673 if !from.joins.is_empty()
10674 || stmt.group_by.is_some()
10675 || stmt.having.is_some()
10676 || !stmt.unions.is_empty()
10677 || !stmt.order_by.is_empty()
10678 || stmt.offset.is_some()
10679 || stmt.distinct
10680 || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
10681 {
10682 return Err(EngineError::Unsupported(
10683 "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
10684 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
10685 .into(),
10686 ));
10687 }
10688 let table = self
10689 .active_catalog()
10690 .get(&from.primary.name)
10691 .ok_or_else(|| StorageError::TableNotFound {
10692 name: from.primary.name.clone(),
10693 })?;
10694 let schema = table.schema().clone();
10695 let schema_cols = &schema.columns;
10696 let alias = from
10697 .primary
10698 .alias
10699 .as_deref()
10700 .unwrap_or(from.primary.name.as_str());
10701 let ctx = self.ev_ctx(schema_cols, Some(alias));
10702 let seg = self
10703 .active_catalog()
10704 .cold_segment(segment_id)
10705 .ok_or_else(|| {
10706 EngineError::Unsupported(alloc::format!(
10707 "AS OF SEGMENT: cold segment {segment_id} not registered"
10708 ))
10709 })?;
10710 let mut out_rows: Vec<Row<'static>> = Vec::new();
10711 let mut limit_remaining: Option<usize> =
10712 stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
10713 for (_key, body) in seg.scan() {
10714 let (row, _consumed) =
10715 spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
10716 .map_err(EngineError::Storage)?;
10717 if let Some(where_expr) = &stmt.where_ {
10718 let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
10719 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
10720 continue;
10721 }
10722 }
10723 // Projection.
10724 let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
10725 out_rows.push(projected);
10726 if let Some(rem) = limit_remaining.as_mut() {
10727 if *rem == 0 {
10728 out_rows.pop();
10729 break;
10730 }
10731 *rem -= 1;
10732 }
10733 }
10734 // Output column schema: derive from SELECT items.
10735 let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
10736 Ok(QueryResult::Rows {
10737 columns,
10738 rows: out_rows,
10739 })
10740 }
10741
10742 /// v6.10.2 — simple-path WHERE eval that doesn't go through
10743 /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
10744 /// scan paths predicate against a snapshot frozen segment, no
10745 /// cross-row state.
10746 fn eval_expr_simple(
10747 &self,
10748 expr: &Expr,
10749 row: &Row<'static>,
10750 ctx: &EvalContext,
10751 ) -> Result<Value<'static>, EngineError> {
10752 let cancel = CancelToken::none();
10753 self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
10754 }
10755}
10756
10757// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
10758
10759/// One row-producing projection: an expression to evaluate, the resulting
10760/// column's user-visible name, its inferred type, and nullability.
10761#[derive(Debug, Clone)]
10762pub(crate) struct ProjectedItem {
10763 pub(crate) expr: Expr,
10764 pub(crate) output_name: String,
10765 pub(crate) ty: DataType,
10766 pub(crate) nullable: bool,
10767 /// v7.39 (read01 round 54) — a projected enum column keeps its enum
10768 /// identity. Enum-ness lives outside the DataType lattice (the value is a
10769 /// Text), so a projection that dropped this made the RESULT schema forget
10770 /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
10771 /// that schema, silently fell back to TEXT order instead of member order.
10772 pub(crate) user_enum_type: Option<String>,
10773 /// v7.39 (round 425) — a projected MySQL temporal column keeps its
10774 /// declared fractional-seconds precision, so the renderer can pad to
10775 /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
10776 /// a whole second). Like `user_enum_type` this lives outside the
10777 /// DataType lattice, so a projection that dropped it made the RESULT
10778 /// schema forget how wide the fraction should print.
10779 pub(crate) mysql_fsp: Option<u8>,
10780 /// v7.39 (round 688) — and its declared collation, the third thing to
10781 /// live outside the DataType lattice and the third to be lost the same
10782 /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
10783 /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
10784 /// projection rebuilt the output column and the ORDER BY resolves
10785 /// against THAT schema.
10786 pub(crate) collation_name: Option<String>,
10787 /// v7.38.13 — and whether this position must NOT fold when DISTINCT
10788 /// de-dups it. The fourth thing to live outside the DataType lattice
10789 /// and the fourth to be lost the same way: a column declared
10790 /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
10791 /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
10792 /// returns two.
10793 ///
10794 /// A BOOL rather than the `Collation` enum on purpose. The enum's
10795 /// storage default is `Binary`, but the FOLD default under MySQL is
10796 /// case-insensitive — carrying the enum would silently mean
10797 /// "exempt" for every projected expression that is not a column.
10798 /// This field states the question it answers.
10799 pub(crate) fold_exempt: bool,
10800 /// v7.38.18 — does this column's collation make trailing spaces
10801 /// insignificant? A separate question from `fold_exempt`:
10802 /// `utf8mb4_bin` is fold-exempt AND pads, `utf8mb4_0900_ai_ci`
10803 /// folds and does not. Read off the same column, at the same
10804 /// place, so the two masks cannot drift apart.
10805 pub(crate) pads: bool,
10806}
10807
10808impl ProjectedItem {
10809 /// v7.38.14 — the output column this projected item describes.
10810 ///
10811 /// There were TWENTY-ONE places converting a `ProjectedItem` into a
10812 /// `ColumnSchema`, each written as `ColumnSchema::new(..)` followed by a
10813 /// hand-picked list of attributes to copy after it, and the lists did not
10814 /// agree: six carried enum identity, the collation NAME and MySQL fsp; ten
10815 /// carried the first and last but not the name; five carried nothing at
10816 /// all. Not one carried `collation`, the enum every MySQL text comparison
10817 /// actually reads.
10818 ///
10819 /// That is how a declared collation vanished between a subquery and the
10820 /// query that selects from it: the inner SELECT's output schema claimed
10821 /// `ColumnSchema::new`'s default, which is `Binary` — a value downstream
10822 /// reads as "byte-wise ON PURPOSE" rather than as "unknown", so the loss
10823 /// presents as a deliberate declaration.
10824 ///
10825 /// One conversion, so a field added to either type has one place to be
10826 /// remembered instead of twenty-one.
10827 pub(crate) fn to_column_schema(&self) -> ColumnSchema {
10828 let mut c = ColumnSchema::new(self.output_name.clone(), self.ty, self.nullable);
10829 c.user_enum_type.clone_from(&self.user_enum_type);
10830 c.collation_name.clone_from(&self.collation_name);
10831 c.mysql_fsp = self.mysql_fsp;
10832 // `fold_exempt` is the projection's answer to the same question
10833 // `ColumnSchema::collation` answers downstream, and it was computed
10834 // from the source column. Keeping the two in step here is what stops
10835 // a de-duplication site further on from asking the schema and being
10836 // told the opposite of what the projection knew.
10837 c.collation = if self.fold_exempt {
10838 spg_storage::Collation::Binary
10839 } else {
10840 spg_storage::Collation::CaseInsensitive
10841 };
10842 c
10843 }
10844}
10845
10846/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
10847/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
10848/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
10849/// the spec's "two NULLs are not distinct"; the second is a tolerated
10850/// quirk for v1 (no NaN literals are reachable from the SQL surface).
10851/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
10852fn expr_is_aggregate_call(e: &Expr) -> bool {
10853 match e {
10854 Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
10855 Expr::AggregateOrdered { .. } => true,
10856 _ => false,
10857 }
10858}
10859
10860/// Collect distinct top-level aggregate call expressions (dedup by value). Does
10861/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
10862/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
10863/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
10864/// than today — never a regression on a working query).
10865fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
10866 if expr_is_aggregate_call(e) {
10867 if !out.iter().any(|x| x == e) {
10868 out.push(e.clone());
10869 }
10870 return;
10871 }
10872 match e {
10873 Expr::Binary { lhs, rhs, .. } => {
10874 collect_agg_exprs(lhs, out);
10875 collect_agg_exprs(rhs, out);
10876 }
10877 Expr::Unary { expr, .. }
10878 | Expr::Cast { expr, .. }
10879 | Expr::IsNull { expr, .. }
10880 | Expr::BoolTest { expr, .. }
10881 | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
10882 Expr::FunctionCall { args, .. } => {
10883 for a in args {
10884 collect_agg_exprs(a, out);
10885 }
10886 }
10887 Expr::Like { expr, pattern, .. } => {
10888 collect_agg_exprs(expr, out);
10889 collect_agg_exprs(pattern, out);
10890 }
10891 Expr::Extract { source, .. } => collect_agg_exprs(source, out),
10892 Expr::WindowFunction {
10893 args,
10894 partition_by,
10895 order_by,
10896 ..
10897 } => {
10898 for a in args {
10899 collect_agg_exprs(a, out);
10900 }
10901 for p in partition_by {
10902 collect_agg_exprs(p, out);
10903 }
10904 for (o, _, _) in order_by {
10905 collect_agg_exprs(o, out);
10906 }
10907 }
10908 _ => {}
10909 }
10910}
10911
10912/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
10913fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
10914 if expr_is_aggregate_call(e) {
10915 if let Some(idx) = aggs.iter().position(|x| x == e) {
10916 *e = Expr::Column(ColumnName {
10917 qualifier: None,
10918 name: alloc::format!("__agg{idx}"),
10919 });
10920 }
10921 return;
10922 }
10923 match e {
10924 Expr::Binary { lhs, rhs, .. } => {
10925 replace_agg_exprs(lhs, aggs);
10926 replace_agg_exprs(rhs, aggs);
10927 }
10928 Expr::Unary { expr, .. }
10929 | Expr::Cast { expr, .. }
10930 | Expr::IsNull { expr, .. }
10931 | Expr::BoolTest { expr, .. }
10932 | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
10933 Expr::FunctionCall { args, .. } => {
10934 for a in args {
10935 replace_agg_exprs(a, aggs);
10936 }
10937 }
10938 Expr::Like { expr, pattern, .. } => {
10939 replace_agg_exprs(expr, aggs);
10940 replace_agg_exprs(pattern, aggs);
10941 }
10942 Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
10943 Expr::WindowFunction {
10944 args,
10945 partition_by,
10946 order_by,
10947 ..
10948 } => {
10949 for a in args {
10950 replace_agg_exprs(a, aggs);
10951 }
10952 for p in partition_by {
10953 replace_agg_exprs(p, aggs);
10954 }
10955 for (o, _, _) in order_by {
10956 replace_agg_exprs(o, aggs);
10957 }
10958 }
10959 _ => {}
10960 }
10961}
10962
10963/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
10964/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
10965/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
10966/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
10967/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
10968/// Returns None outside the bounded subset (leaves current behaviour). Only fires
10969/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
10970/// window-only / aggregate-only queries.
10971fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
10972 if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
10973 return None;
10974 }
10975 // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
10976 if !stmt.unions.is_empty() {
10977 return None;
10978 }
10979 let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
10980 if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
10981 return None;
10982 }
10983 stmt.from.as_ref()?;
10984 // Collect the aggregate calls to hoist from projection + outer ORDER BY.
10985 let mut aggs: Vec<Expr> = Vec::new();
10986 for item in &stmt.items {
10987 if let SelectItem::Expr { expr, .. } = item {
10988 collect_agg_exprs(expr, &mut aggs);
10989 }
10990 }
10991 for ob in &stmt.order_by {
10992 collect_agg_exprs(&ob.expr, &mut aggs);
10993 }
10994 // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
10995 let mut inner_items: Vec<SelectItem> = Vec::new();
10996 for g in &group_cols {
10997 inner_items.push(SelectItem::Expr {
10998 expr: g.clone(),
10999 alias: None,
11000 });
11001 }
11002 for (i, a) in aggs.iter().enumerate() {
11003 inner_items.push(SelectItem::Expr {
11004 expr: a.clone(),
11005 alias: Some(alloc::format!("__agg{i}")),
11006 });
11007 }
11008 let inner = SelectStatement {
11009 items: inner_items,
11010 distinct: false,
11011 distinct_on: Vec::new(),
11012 unions: Vec::new(),
11013 order_by: Vec::new(),
11014 limit: None,
11015 offset: None,
11016 limit_with_ties: false,
11017 window_check_exprs: Vec::new(),
11018 ..stmt.clone()
11019 };
11020 let derived = TableRef {
11021 name: "__aggwin".into(),
11022 alias: Some("__aggwin".into()),
11023 only: false,
11024 as_of_segment: None,
11025 unnest_expr: None,
11026 unnest_column_aliases: Vec::new(),
11027 with_ordinality: false,
11028 generate_series_args: None,
11029 lateral_subquery: Some(alloc::boxed::Box::new(inner)),
11030 jsonb_each_text_arg: None,
11031 table_fn_call: None,
11032 rows_from: None,
11033 json_table: None,
11034 scalar_fn_item: false,
11035 };
11036 // Outer window query over the derived rows: aggregates → __aggN column refs.
11037 let mut outer_items = stmt.items.clone();
11038 for item in &mut outer_items {
11039 if let SelectItem::Expr { expr, alias } = item {
11040 // Preserve PG's column label for a bare aggregate projection.
11041 if alias.is_none()
11042 && let Expr::FunctionCall { name, .. } = expr
11043 && crate::aggregate::is_aggregate_name(name)
11044 {
11045 *alias = Some(name.to_ascii_lowercase());
11046 }
11047 replace_agg_exprs(expr, &aggs);
11048 }
11049 }
11050 let mut outer_order = stmt.order_by.clone();
11051 for ob in &mut outer_order {
11052 replace_agg_exprs(&mut ob.expr, &aggs);
11053 }
11054 let mut outer_distinct_on = stmt.distinct_on.clone();
11055 for e in &mut outer_distinct_on {
11056 replace_agg_exprs(e, &aggs);
11057 }
11058 Some(SelectStatement {
11059 locking: None,
11060 ctes: Vec::new(),
11061 distinct: stmt.distinct,
11062 distinct_on: outer_distinct_on,
11063 items: outer_items,
11064 from: Some(FromClause {
11065 primary: derived,
11066 joins: Vec::new(),
11067 }),
11068 where_: None,
11069 group_by: None,
11070 group_by_all: false,
11071 having: None,
11072 unions: Vec::new(),
11073 order_by: outer_order,
11074 limit: stmt.limit.clone(),
11075 offset: stmt.offset.clone(),
11076 limit_with_ties: stmt.limit_with_ties,
11077 window_check_exprs: Vec::new(),
11078 })
11079}
11080
11081/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
11082/// membership.
11083///
11084/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
11085/// there?", and all four answered by scanning the whole right side once per
11086/// left row. The cost was (left rows x right rows), which is why
11087/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
11088/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
11089/// row that does not pays for all of it. Over 100k left rows, raising the
11090/// right side from 100 to 10,000 took 35 ms to 2848.
11091///
11092/// This is the shape round 485 already solved for DISTINCT, and it reuses
11093/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
11094/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
11095/// every bucket with the exact comparator, so a collision costs time and
11096/// never an answer.
11097struct PeerIndex<'r> {
11098 bh: hashbrown::DefaultHashBuilder,
11099 buckets: hashbrown::HashMap<u64, Vec<usize>>,
11100 rows: &'r [Row<'static>],
11101 fold: FoldSpec<'r>,
11102}
11103
11104impl<'r> PeerIndex<'r> {
11105 fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
11106 // ONE hasher for the whole pass: the default builder is seeded per
11107 // instance, so a fresh one per row would put equal rows in different
11108 // buckets.
11109 let bh = hashbrown::DefaultHashBuilder::default();
11110 let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
11111 hashbrown::HashMap::with_capacity(rows.len());
11112 for (i, r) in rows.iter().enumerate() {
11113 buckets
11114 .entry(norm_hash_row(r, &bh, fold))
11115 .or_default()
11116 .push(i);
11117 }
11118 Self {
11119 bh,
11120 buckets,
11121 rows,
11122 fold,
11123 }
11124 }
11125
11126 fn contains(&self, r: &Row<'static>) -> bool {
11127 let h = norm_hash_row(r, &self.bh, self.fold);
11128 self.buckets
11129 .get(&h)
11130 .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
11131 }
11132
11133 /// Remove ONE occurrence, so the multiset forms cancel row for row the
11134 /// way the pool they replaced did.
11135 fn take_one(&mut self, r: &Row<'static>) -> bool {
11136 let h = norm_hash_row(r, &self.bh, self.fold);
11137 let Some(b) = self.buckets.get_mut(&h) else {
11138 return false;
11139 };
11140 let Some(pos) = b
11141 .iter()
11142 .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
11143 else {
11144 return false;
11145 };
11146 b.swap_remove(pos);
11147 true
11148 }
11149}
11150
11151pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
11152 dedup_by_row(rows, |r| r, fold)
11153}
11154
11155/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
11156/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
11157/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
11158/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
11159/// order is preserved, and correctness needs only the one-way guarantee
11160/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
11161/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
11162fn dedup_by_row<T>(
11163 items: Vec<T>,
11164 row_of: impl Fn(&T) -> &Row<'static>,
11165 fold: FoldSpec<'_>,
11166) -> Vec<T> {
11167 if items.len() <= 32 {
11168 let mut out: Vec<T> = Vec::with_capacity(items.len());
11169 for it in items {
11170 if !out
11171 .iter()
11172 .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
11173 {
11174 out.push(it);
11175 }
11176 }
11177 return out;
11178 }
11179 // ONE BuildHasher instance for the whole pass — the default builder
11180 // is randomly seeded PER INSTANCE, so a fresh one per row would give
11181 // equal rows different hashes and never dedup.
11182 let bh = hashbrown::DefaultHashBuilder::default();
11183 let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
11184 let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
11185 hashbrown::HashMap::with_capacity(items.len());
11186 for it in items {
11187 let h = norm_hash_row(row_of(&it), &bh, fold);
11188 let bucket = buckets.entry(h).or_default();
11189 if !bucket
11190 .iter()
11191 .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
11192 {
11193 bucket.push(out.len());
11194 out.push(it);
11195 }
11196 }
11197 out
11198}
11199
11200/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
11201/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
11202/// rows may collide (buckets are re-checked with the exact comparator).
11203///
11204/// Domain design mirrors `value_cmp`'s equivalence classes:
11205/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
11206/// shares one domain: a value that is an integer fitting i64 hashes the
11207/// i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
11208/// anything else hashes the f64 approximation computed by THE SAME
11209/// formula the value_cmp float arms use (`numeric_to_f64`), so
11210/// `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
11211/// hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
11212/// Known un-closable corner: an integer in [2^53, 2^63) can compare
11213/// Equal to a float via value_cmp's lossy f64 arm while hashing in the
11214/// exact-i64 domain — mixed int/float rows at that magnitude may miss a
11215/// dedup (PG itself compares int8↔float8 in the lossy float8 domain).
11216/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
11217/// compares them blank-insensitively; plain Text pairs that differ only
11218/// in trailing blanks merely collide and are separated exactly).
11219/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
11220/// hash their fields under a distinct tag.
11221/// - Everything value_cmp falls back to debug-format ordering for
11222/// (Json, arrays, vectors, geometry, ranges, …) shares one constant
11223/// bucket — degrades to the exact linear scan, never wrong.
11224fn norm_hash_row(
11225 row: &Row<'static>,
11226 bh: &hashbrown::DefaultHashBuilder,
11227 fold: FoldSpec<'_>,
11228) -> u64 {
11229 norm_hash_values(&row.values, bh, fold)
11230}
11231
11232/// v7.39 (round 485) — the same hash over a bare value slice, so the
11233/// DISTINCT probe can run against a reused buffer instead of demanding a
11234/// `Row` that has to be allocated first (see `values_eq_norm`).
11235fn norm_hash_values(
11236 values: &[Value<'static>],
11237 bh: &hashbrown::DefaultHashBuilder,
11238 fold: FoldSpec<'_>,
11239) -> u64 {
11240 use core::hash::{BuildHasher, Hash, Hasher};
11241 let mut h = bh.build_hasher();
11242 for (i, v) in values.iter().enumerate() {
11243 // v7.39 (round 410) — hash the folded key when the MySQL collation
11244 // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
11245 // `'A'` vs `'a '`) share a hash bucket.
11246 //
11247 // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
11248 // byte-wise column that folded here while the comparator did not
11249 // would scatter equal rows across buckets and stop de-duplicating
11250 // at all; the hash and the comparator have to read the same mask.
11251 if fold.folds(i)
11252 && let Some(folded) = mysql_dedup_fold(v, fold.pads_at(i))
11253 {
11254 folded.hash(&mut h);
11255 continue;
11256 }
11257 norm_hash_value(v, &mut h);
11258 }
11259 h.finish()
11260}
11261
11262/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
11263///
11264/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
11265const fn pow10_i128(p: u16) -> Option<i128> {
11266 const P: [i128; 39] = {
11267 let mut t = [1i128; 39];
11268 let mut i = 1;
11269 while i < 39 {
11270 t[i] = t[i - 1] * 10;
11271 i += 1;
11272 }
11273 t
11274 };
11275 if (p as usize) < P.len() {
11276 Some(P[p as usize])
11277 } else {
11278 None
11279 }
11280}
11281
11282fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
11283 const TAG_NULL: u8 = 0;
11284 const TAG_BOOL: u8 = 1;
11285 const TAG_NUM_I64: u8 = 2;
11286 const TAG_NUM_F64: u8 = 3;
11287 const TAG_TEXT: u8 = 4;
11288 const TAG_DATE: u8 = 6;
11289 const TAG_TIME: u8 = 7;
11290 const TAG_TIMESTAMP: u8 = 8;
11291 const TAG_TIMETZ: u8 = 10;
11292 const TAG_UUID: u8 = 11;
11293 const TAG_MONEY: u8 = 12;
11294 const TAG_BYTES: u8 = 13;
11295 const TAG_INTERVAL: u8 = 14;
11296 const TAG_CHAR1: u8 = 15;
11297 const TAG_OPAQUE: u8 = 255;
11298 // One shared writer for the numeric family: an integer value
11299 // representable as i64 goes exact (round-trip probe — no_std, so no
11300 // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
11301 // through 0i64, folding it into 0.0 as value_cmp requires.
11302 let num_f64 = |h: &mut H, x: f64| {
11303 if x.is_nan() {
11304 h.write_u8(TAG_NUM_F64);
11305 h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
11306 return;
11307 }
11308 const TWO63: f64 = 9_223_372_036_854_775_808.0;
11309 if (-TWO63..TWO63).contains(&x) {
11310 #[allow(clippy::cast_possible_truncation)]
11311 let n = x as i64;
11312 #[allow(clippy::cast_precision_loss)]
11313 if (n as f64) == x {
11314 h.write_u8(TAG_NUM_I64);
11315 h.write_i64(n);
11316 return;
11317 }
11318 }
11319 h.write_u8(TAG_NUM_F64);
11320 h.write_u64(x.to_bits());
11321 };
11322 match v {
11323 Value::Null => h.write_u8(TAG_NULL),
11324 Value::Bool(b) => {
11325 h.write_u8(TAG_BOOL);
11326 h.write_u8(u8::from(*b));
11327 }
11328 Value::SmallInt(n) => {
11329 h.write_u8(TAG_NUM_I64);
11330 h.write_i64(i64::from(*n));
11331 }
11332 Value::Int(n) => {
11333 h.write_u8(TAG_NUM_I64);
11334 h.write_i64(i64::from(*n));
11335 }
11336 Value::BigInt(n) => {
11337 h.write_u8(TAG_NUM_I64);
11338 h.write_i64(*n);
11339 }
11340 Value::Float(x) => num_f64(h, *x),
11341 Value::Numeric {
11342 scaled,
11343 scale,
11344 kind,
11345 } => match kind {
11346 spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
11347 spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
11348 spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
11349 spg_storage::NumericKind::Finite => {
11350 // Reduce trailing fractional zeros so 1.50 and 1.5 share a
11351 // representation, then: exact integers fitting i64 go to the
11352 // i64 domain; everything else uses numeric_to_f64 — the SAME
11353 // formula value_cmp's Numeric↔Float arm compares with.
11354 // r1044 — the reduction is required (`1.5` and `1.50` are
11355 // one value and must land in one bucket) and it used to
11356 // walk one digit at a time. That is O(scale), and scale
11357 // is not small in practice: `n / 100` on a NUMERIC
11358 // column stores `9.1900000000000000`, scale 16, so the
11359 // loop ran fourteen times PER ROW.
11360 //
11361 // Priced by ablation rather than guessed at — removing
11362 // the loop entirely took `SELECT DISTINCT n FROM t ORDER
11363 // BY n` over 400,000 rows from 52 ms to 14.8, against
11364 // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
11365 // tried first moved it not at all, which is why this one
11366 // was measured before it was written.
11367 //
11368 // Binary search over the same powers finds the whole
11369 // run of trailing zeros in at most six tests and one
11370 // division, instead of one test and one division per
11371 // digit.
11372 let (mut s, mut sc) = (*scaled, *scale);
11373 if sc > 0 && s != 0 {
11374 let mut lo: u16 = 0;
11375 let mut hi: u16 = sc;
11376 while lo < hi {
11377 let mid = (lo + hi).div_ceil(2);
11378 match pow10_i128(mid) {
11379 Some(p) if s % p == 0 => lo = mid,
11380 _ => hi = mid - 1,
11381 }
11382 }
11383 if lo > 0 {
11384 if let Some(p) = pow10_i128(lo) {
11385 s /= p;
11386 sc -= lo;
11387 }
11388 }
11389 }
11390 if sc == 0 {
11391 if let Ok(n) = i64::try_from(s) {
11392 h.write_u8(TAG_NUM_I64);
11393 h.write_i64(n);
11394 } else {
11395 num_f64(h, crate::orderby::numeric_to_f64(s, 0));
11396 }
11397 } else {
11398 num_f64(h, crate::orderby::numeric_to_f64(s, sc));
11399 }
11400 }
11401 },
11402 // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
11403 // value that also fits i128 reuses the Numeric path above so
11404 // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
11405 // any i128-representable value — constant bucket is safe.
11406 Value::NumericBig(b) => match b.to_i128() {
11407 Some(s) => norm_hash_value(
11408 &Value::Numeric {
11409 scaled: s,
11410 scale: b.scale(),
11411 kind: spg_storage::NumericKind::Finite,
11412 },
11413 h,
11414 ),
11415 None => h.write_u8(TAG_OPAQUE),
11416 },
11417 // value_cmp compares Text↔BpChar blank-insensitively (both sides
11418 // trimmed), so both hash the trimmed bytes. Text pairs differing
11419 // only in trailing blanks collide and are split exactly in-bucket.
11420 Value::Text(s) | Value::BpChar(s) => {
11421 h.write_u8(TAG_TEXT);
11422 h.write(s.trim_end_matches(' ').as_bytes());
11423 }
11424 Value::Char1(c) => {
11425 h.write_u8(TAG_CHAR1);
11426 h.write_u8(*c);
11427 }
11428 Value::Date(d) => {
11429 h.write_u8(TAG_DATE);
11430 h.write_i32(*d);
11431 }
11432 Value::Time(t) => {
11433 h.write_u8(TAG_TIME);
11434 h.write_i64(*t);
11435 }
11436 Value::Timestamp(t) => {
11437 h.write_u8(TAG_TIMESTAMP);
11438 h.write_i64(*t);
11439 }
11440 Value::TimeTz { us, offset_secs } => {
11441 h.write_u8(TAG_TIMETZ);
11442 h.write_i64(*us);
11443 h.write_i32(*offset_secs);
11444 }
11445 Value::Uuid(u) => {
11446 h.write_u8(TAG_UUID);
11447 h.write(u);
11448 }
11449 Value::Money(c) => {
11450 h.write_u8(TAG_MONEY);
11451 h.write_i64(*c);
11452 }
11453 Value::Bytes(b) => {
11454 h.write_u8(TAG_BYTES);
11455 h.write(b.as_ref());
11456 }
11457 Value::Interval {
11458 months,
11459 days,
11460 micros,
11461 kind,
11462 } => {
11463 h.write_u8(TAG_INTERVAL);
11464 h.write_i32(*months);
11465 h.write_i32(*days);
11466 h.write_i64(*micros);
11467 }
11468 // v7.37.16 — REAL joined the numeric value_cmp family (widened
11469 // to f64, same formulas as the arms), so it hashes in the shared
11470 // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
11471 // f32→f64 is exact, so equal-under-cmp implies equal bits here.
11472 Value::Real(x) => num_f64(h, f64::from(*x)),
11473 // Json (structural equality), vector families (float rendering),
11474 // arrays / geometry / net / ranges / composites (debug-format
11475 // fallback): one constant bucket — exact linear within.
11476 _ => h.write_u8(TAG_OPAQUE),
11477 }
11478}
11479
11480/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
11481/// treats numerically-equal exact values as one regardless of type or scale
11482/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
11483/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
11484/// `Row` `==` would keep them distinct.
11485/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
11486/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
11487/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
11488/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
11489/// the folded comparison key for a text value, None for anything else (which
11490/// keeps the byte-exact `value_cmp` path).
11491fn mysql_dedup_fold(v: &Value, pads: bool) -> Option<String> {
11492 match v {
11493 // v7.38.17 — CHAR's trailing spaces are padding; TEXT's are
11494 // data. The comment above named `utf8mb4_uca1400_ai_ci`, which
11495 // is MariaDB's default and PAD SPACE. SPG advertises MySQL 8.0,
11496 // whose default is NO PAD, so `'alpha'` and `'alpha '` are two
11497 // rows to a `SELECT DISTINCT` and one to `count(DISTINCT)` was
11498 // the same question answered twice.
11499 // v7.38.18 — a CHAR's padding is the TYPE's and never counts; a
11500 // TEXT's is the collation's, which `pads` carries per position.
11501 Value::BpChar(s) => Some(spg_storage::mysql_compare_fold_char(s)),
11502 Value::Text(s) if pads => Some(spg_storage::mysql_compare_fold_char(s)),
11503 Value::Text(s) => Some(spg_storage::mysql_compare_fold(s)),
11504 _ => None,
11505 }
11506}
11507
11508/// v7.39 (round 485) — how many projected rows the single-table scan
11509/// builds, and how many of those the DISTINCT probe throws away again.
11510///
11511/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
11512/// 21 % of all samples in malloc/free called straight from the scan
11513/// closure. The closure's one per-row allocation is the projected
11514/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
11515/// instructions later — but "most" is a guess until it is a number, so
11516/// these count it. (Round 480 was spent acting on an inference about a
11517/// branch that turned out never to run.)
11518/// v7.39 (round 488) — reachability counters for round 487's projection
11519/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
11520/// and a never-called-function probe rules out code layout — so the
11521/// question is whether that shape reaches this code at all, which is a
11522/// number, not an inference.
11523pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11524pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11525
11526pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11527pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
11528 core::sync::atomic::AtomicU64::new(0);
11529
11530/// v7.38.13 — how DISTINCT must compare one row of output.
11531///
11532/// The MySQL default collation folds case and trailing spaces when it
11533/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
11534/// must not fold — `e2e_mysql_collate_binary_round370` calls the
11535/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
11536/// one when the schema asked to keep them apart", and names DISTINCT as
11537/// one of the sites that has to honour it.
11538///
11539/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
11540/// value in a MySQL session, because a bool cannot see a column. The
11541/// GROUP BY path consults the schema and was right all along; the test
11542/// only ever exercised that spelling, so the DISTINCT hole was never
11543/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
11544///
11545/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
11546/// which is what a caller with no schema to offer gets.
11547#[derive(Clone, Copy)]
11548pub(crate) struct FoldSpec<'c> {
11549 mysql: bool,
11550 binary: &'c [bool],
11551 /// v7.38.18 — the padding mask, in lockstep with `binary`. Read the
11552 /// note on `folds`: a hash and its comparator must consult the same
11553 /// masks or equal rows scatter across buckets.
11554 pads: &'c [bool],
11555}
11556
11557impl<'c> FoldSpec<'c> {
11558 /// No column information — every Text position folds under MySQL.
11559 pub(crate) const fn dialect(mysql: bool) -> Self {
11560 Self {
11561 mysql,
11562 binary: &[],
11563 pads: &[],
11564 }
11565 }
11566
11567 /// The mask read off the output columns.
11568 pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
11569 Self {
11570 mysql,
11571 binary,
11572 pads: &[],
11573 }
11574 }
11575
11576 /// The masks read off the output columns — fold-exemption AND
11577 /// padding, which are different questions about the same collation.
11578 pub(crate) fn of_masks(mysql: bool, binary: &'c [bool], pads: &'c [bool]) -> Self {
11579 Self {
11580 mysql,
11581 binary,
11582 pads,
11583 }
11584 }
11585
11586 /// Does position `i` treat trailing spaces as insignificant?
11587 #[inline]
11588 fn pads_at(&self, i: usize) -> bool {
11589 self.pads.get(i).copied().unwrap_or(false)
11590 }
11591
11592 /// Does position `i` fold?
11593 #[inline]
11594 fn folds(&self, i: usize) -> bool {
11595 self.mysql && !self.binary.get(i).copied().unwrap_or(false)
11596 }
11597}
11598
11599/// The fold-exempt mask for a projection.
11600///
11601/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
11602/// projection rebuilds that schema through `ColumnSchema::new`, whose
11603/// collation default is `Binary` — a mask built from it would mark
11604/// EVERY column byte-wise and stop DISTINCT folding at all.
11605/// The padding mask for a projection, read off the same items as
11606/// [`fold_mask`] so the two cannot come from different places.
11607pub(crate) fn pad_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11608 projection.iter().map(|p| p.pads).collect()
11609}
11610
11611pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11612 projection.iter().map(|p| p.fold_exempt).collect()
11613}
11614
11615/// v7.38.14 — the same mask, from an OUTPUT SCHEMA instead of a
11616/// projection.
11617///
11618/// Some de-duplication sites hold `Vec<ColumnSchema>` and never see the
11619/// `ProjectedItem`s it came from. `ProjectedItem::fold_exempt` is built
11620/// from exactly this test (`select.rs`, `build_projection`), so the two
11621/// must keep answering identically -- a site that decided "byte-wise" one
11622/// way while its neighbour decided the other is how the answer came to
11623/// depend on which executor ran the query.
11624///
11625/// The direction matters: `Collation::Binary` is `ColumnSchema::new`'s
11626/// DEFAULT, so a schema rebuilt without carrying the field reads as
11627/// "byte-wise on purpose" here. That is a real trap and it has caught
11628/// five fields so far; it is why S4 of this release exists.
11629/// v7.38.18 — the padding mask from output columns, the sibling of
11630/// [`fold_mask_of_columns`]. Whether a column folds and whether it
11631/// pads are different questions about the same collation.
11632pub(crate) fn pad_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11633 columns
11634 .iter()
11635 .map(|c| crate::collate::pads_space(c.collation_name.as_deref()))
11636 .collect()
11637}
11638
11639pub(crate) fn fold_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11640 columns
11641 .iter()
11642 .map(|c| matches!(c.collation, spg_storage::Collation::Binary))
11643 .collect()
11644}
11645
11646pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
11647 values_eq_norm(&a.values, &b.values, fold)
11648}
11649
11650/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
11651/// DISTINCT probe can compare a reused projection buffer against a kept
11652/// row without building a `Row` for it.
11653pub(crate) fn values_eq_norm(
11654 a: &[Value<'static>],
11655 b: &[Value<'static>],
11656 fold: FoldSpec<'_>,
11657) -> bool {
11658 a.len() == b.len()
11659 && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
11660 if fold.folds(i)
11661 && let (Some(fx), Some(fy)) = (
11662 mysql_dedup_fold(x, fold.pads_at(i)),
11663 mysql_dedup_fold(y, fold.pads_at(i)),
11664 )
11665 {
11666 return fx == fy;
11667 }
11668 crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
11669 })
11670}
11671
11672/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
11673/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
11674/// order via the byte values; vectors are not sortable.
11675pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
11676 // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
11677 // so values sharing a ≥6-byte common prefix (`product_001` vs
11678 // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
11679 // order by their exact bytes instead of the old lossy f64 coarse key.
11680 // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
11681 // matches PG's default C / binary text collation. Every other type
11682 // keeps the lossless-enough `f64` fast path below.
11683 if let Value::Text(s) = v {
11684 return Ok(OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())));
11685 }
11686 // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
11687 // byte order (PG bpcharcmp under C collation), so mixed-pad values of
11688 // the same logical string order equal.
11689 if let Value::BpChar(s) = v {
11690 return Ok(OrderKey::Text(crate::orderby::CompactText::new(
11691 s.trim_end_matches(' '),
11692 )));
11693 }
11694 // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
11695 // carry the parsed value and compare it structurally (see
11696 // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
11697 if let Value::Json(s) = v {
11698 return Ok(match crate::json::parse(s) {
11699 Ok(jv) => OrderKey::Json(jv),
11700 Err(_) => OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())),
11701 });
11702 }
11703 // v7.37 — byte-orderable types PG sorts byte-wise but that have no
11704 // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
11705 // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
11706 // matching PG's network ordering.
11707 match v {
11708 Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
11709 // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
11710 Value::NumericBig(b) => {
11711 return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11712 spg_storage::NumericKey::from_big(b),
11713 )));
11714 }
11715 Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
11716 Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11717 Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11718 Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
11719 Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
11720 let mut key = alloc::vec::Vec::with_capacity(18);
11721 key.push(*family);
11722 key.extend_from_slice(addr);
11723 key.push(*bits);
11724 return Ok(OrderKey::Bytes(key));
11725 }
11726 _ => {}
11727 }
11728 // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
11729 // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
11730 // own OrderKey so integer arrays sort numerically; a NULL element rides to
11731 // the end via the +INF sentinel.
11732 let inf = || OrderKey::NullBig;
11733 let arr = match v {
11734 Value::IntArray(a) => Some(
11735 a.iter()
11736 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11737 .collect(),
11738 ),
11739 Value::SmallIntArray(a) => Some(
11740 a.iter()
11741 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11742 .collect(),
11743 ),
11744 Value::BigIntArray(a) => Some(
11745 a.iter()
11746 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11747 .collect(),
11748 ),
11749 Value::BoolArray(a) => Some(
11750 a.iter()
11751 .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
11752 .collect(),
11753 ),
11754 Value::TextArray(a) => Some(
11755 a.iter()
11756 .map(|o| {
11757 o.as_ref()
11758 .map_or_else(inf, |s| OrderKey::Text(crate::orderby::CompactText::new(s)))
11759 })
11760 .collect(),
11761 ),
11762 #[allow(clippy::cast_precision_loss)]
11763 Value::FloatArray(a) => Some(
11764 a.iter()
11765 .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
11766 .collect(),
11767 ),
11768 // r1040 — array elements take the same exact key their scalar
11769 // form does; an f64 projection here would order `{0.1}` against
11770 // `{0.1000000000000000001}` by luck.
11771 Value::NumericArray(a) => Some(
11772 a.iter()
11773 .map(|o| {
11774 o.map_or_else(inf, |(m, s)| {
11775 OrderKey::Numeric(alloc::boxed::Box::new(
11776 spg_storage::NumericKey::from_numeric(
11777 m,
11778 s,
11779 spg_storage::NumericKind::Finite,
11780 ),
11781 ))
11782 })
11783 })
11784 .collect(),
11785 ),
11786 Value::DateArray(a) => Some(
11787 a.iter()
11788 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11789 .collect(),
11790 ),
11791 _ => None,
11792 };
11793 if let Some(elements) = arr {
11794 return Ok(OrderKey::Array(elements));
11795 }
11796 // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
11797 // right, which is exactly the lexicographic element order an Array key
11798 // already gives: `(2,'b') < (9,'a')` because the leading field decides.
11799 if let Value::Composite(fields) = v {
11800 let elements = fields
11801 .iter()
11802 .map(|(_, fv)| value_to_order_key(fv))
11803 .collect::<Result<alloc::vec::Vec<_>, _>>()?;
11804 return Ok(OrderKey::Array(elements));
11805 }
11806 // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
11807 // Projecting these to f64 (the historic path) silently collapses BigInt /
11808 // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
11809 // the wrong order for large ids and microsecond timestamps.
11810 match v {
11811 Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11812 Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
11813 Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11814 // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
11815 // integer (days / micros / cents / calendar year); TIMETZ by the
11816 // UTC-equivalent micros (local wall - offset) so the same physical
11817 // instant in different zones sorts equal.
11818 Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
11819 Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
11820 Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
11821 Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
11822 // v7.39.13 — the UTC instant is only HALF the key.
11823 //
11824 // This ordered by the instant alone, so the values that share
11825 // one were called equal and a stable sort then returned them in
11826 // insertion order — an answer, not a tie-break. Measured on
11827 // PostgreSQL 18.6 against this engine, six rows, one column:
11828 //
11829 // ```text
11830 // PG 18.6 SPG 7.39.12
11831 // 07:00:00+01 07:00:00+01
11832 // 06:59:59+00 06:59:59+00
11833 // 09:00:00+02 07:00:00+00 <- the four that share
11834 // 07:00:00+00 02:00:00-05 07:00 UTC, in the
11835 // 02:00:00-05 09:00:00+02 order they were written
11836 // 01:00:00-06 01:00:00-06
11837 // ```
11838 //
11839 // PostgreSQL breaks the tie by OFFSET DESCENDING, and
11840 // `'07:00:00+00' = '02:00:00-05'` is FALSE there. Shifting the
11841 // instant left by 32 bits leaves room for the offset underneath
11842 // it — `i128` holds both exactly, where `i64` could not — and
11843 // `compare` in `eval::binop` orders the same pair the same way,
11844 // from the same measurement.
11845 Value::TimeTz { us, offset_secs } => {
11846 return Ok(OrderKey::Int(i128::from(spg_storage::timetz_sort_key(
11847 *us,
11848 *offset_secs,
11849 ))));
11850 }
11851 Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
11852 _ => {}
11853 }
11854 let num = match v {
11855 // Callers without NULLS FIRST/LAST context (array elements,
11856 // histogram sampling) put NULL last, as before.
11857 Value::Null => return Ok(OrderKey::NullBig),
11858 // v7.17.0 Phase 3.P0-38 — range ordering is not supported
11859 // in v7.17.0 (needs lex-then-inclusivity tiebreak).
11860 Value::Range { .. } => {
11861 return Err(EngineError::Unsupported(
11862 "ORDER BY of a range value is not supported in v7.17.0".into(),
11863 ));
11864 }
11865 // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
11866 Value::Hstore(_) => {
11867 return Err(EngineError::Unsupported(
11868 "ORDER BY of a hstore value is not supported".into(),
11869 ));
11870 }
11871 // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
11872 Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
11873 return Err(EngineError::Unsupported(
11874 "ORDER BY of a 2D array is not supported in v7.17.0".into(),
11875 ));
11876 }
11877 // r1039/r1040 — the exact canonical key, not an f64 projection.
11878 //
11879 // r1039 fixed the three specials, which carry a canonical zero in
11880 // `scaled` and so all sorted as the number 0. The projection
11881 // itself was the rest of the defect: "precision losses here only
11882 // matter for tie-breaks well past 15 significant digits" was the
11883 // comment, and the measurement disagreed — f64 called
11884 // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
11885 // returned them in insertion order. Three of ten values came back
11886 // in the wrong place against PG18.4.
11887 Value::Numeric {
11888 scaled,
11889 scale,
11890 kind,
11891 } => {
11892 return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11893 spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
11894 )));
11895 }
11896 Value::Float(x) => *x,
11897 // v7.37.16 — REAL sorts by its exact f64 widening (it had no
11898 // arm and fell through to the unsupported error).
11899 Value::Real(x) => f64::from(*x),
11900 Value::Bool(b) => {
11901 if *b {
11902 1.0
11903 } else {
11904 0.0
11905 }
11906 }
11907 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
11908 return Err(EngineError::Unsupported(
11909 "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
11910 ));
11911 }
11912 // v7.37 — PG orders INTERVAL by its total time, treating a month as
11913 // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
11914 // f64 is exact for any interval under ~285 years, and only ORDER BY
11915 // tie-breaks past that magnitude lose precision. Matches the
11916 // min/max(interval) comparator in aggregate.rs.
11917 #[allow(clippy::cast_precision_loss)]
11918 Value::Interval {
11919 months,
11920 days,
11921 micros,
11922 kind,
11923 } => {
11924 let total = i128::from(*months) * 30 * 86_400_000_000
11925 + i128::from(*days) * 86_400_000_000
11926 + i128::from(*micros);
11927 total as f64
11928 }
11929 Value::Json(_) => {
11930 return Err(EngineError::Unsupported(
11931 "ORDER BY of a JSON value is not supported — cast the document to text first"
11932 .into(),
11933 ));
11934 }
11935 // v7.5.0 — Value is #[non_exhaustive]; future variants need
11936 // an explicit ORDER BY mapping. Surface as Unsupported until
11937 // engine support is added.
11938 _ => {
11939 return Err(EngineError::Unsupported(
11940 "ORDER BY of this value type is not supported".into(),
11941 ));
11942 }
11943 };
11944 Ok(OrderKey::Num(num))
11945}
11946
11947/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
11948/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
11949/// `EngineError` so the projection-build path keeps `UnknownQualifier`
11950/// vs `ColumnNotFound` distinct.
11951/// PG's name for the physical row identity. It is reserved there — no table
11952/// can have a column called this — which is what lets `*` skip it by name.
11953pub(crate) const CTID_COLUMN: &str = "ctid";
11954
11955/// v7.39 (round 512) — PG's system columns, in the order they are appended.
11956/// All six are reserved names there, which is what lets `*` skip them and
11957/// lets a scan tell them from a user column without a flag.
11958pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
11959
11960/// Is this name one of them?
11961pub(crate) fn is_system_column(name: &str) -> bool {
11962 SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
11963}
11964
11965/// Where the scan's appended system columns begin, if this schema carries
11966/// them: the trailing six, named in order. A catalog view with a column of
11967/// its own called `xmin` does not match, which is the point.
11968fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
11969 let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
11970 cols[start..]
11971 .iter()
11972 .zip(SYSTEM_COLUMNS)
11973 .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
11974 .then_some(start)
11975}
11976
11977/// v7.39 (round 540) — which positions `*` must skip.
11978///
11979/// The rule stays round 512's — the synthetic columns are the trailing
11980/// six of a relation's block, matched by POSITION so a genuine `xmin`
11981/// column is not lost — but a JOINED schema names its columns
11982/// `alias.column` and lays the peers out end to end, so a peer's six sit
11983/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
11984/// "trailing six" test back on the block it was written for.
11985fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11986 let mut skip = alloc::vec![false; cols.len()];
11987 fn qualifier(n: &str) -> Option<&str> {
11988 n.rsplit_once('.').map(|(q, _)| q)
11989 }
11990 fn bare(n: &str) -> &str {
11991 n.rsplit('.').next().unwrap_or(n)
11992 }
11993 let mut i = 0;
11994 while i < cols.len() {
11995 let q = qualifier(&cols[i].name);
11996 let mut end = i;
11997 while end < cols.len() && qualifier(&cols[end].name) == q {
11998 end += 1;
11999 }
12000 if let Some(start) = (end - i)
12001 .checked_sub(SYSTEM_COLUMNS.len())
12002 .map(|off| i + off)
12003 && cols[start..end]
12004 .iter()
12005 .zip(SYSTEM_COLUMNS)
12006 .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
12007 {
12008 for s in skip.iter_mut().take(end).skip(start) {
12009 *s = true;
12010 }
12011 }
12012 i = end;
12013 }
12014 skip
12015}
12016
12017/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
12018/// read? Only then is the column materialised.
12019pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
12020 let mut found = false;
12021 crate::expr_analysis::visit_expr_columns_and_subqueries(
12022 e,
12023 &mut |c| {
12024 if is_system_column(&c.name) {
12025 found = true;
12026 }
12027 },
12028 &mut |_| {},
12029 );
12030 found
12031}
12032
12033fn references_ctid(stmt: &SelectStatement) -> bool {
12034 let in_expr = expr_references_ctid;
12035 stmt.items.iter().any(|i| match i {
12036 SelectItem::Expr { expr, .. } => in_expr(expr),
12037 _ => false,
12038 }) || stmt.where_.as_ref().is_some_and(in_expr)
12039 || stmt.order_by.iter().any(|o| in_expr(&o.expr))
12040 || stmt
12041 .group_by
12042 .as_ref()
12043 .is_some_and(|g| g.iter().any(in_expr))
12044 || stmt.having.as_ref().is_some_and(in_expr)
12045}
12046
12047/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
12048/// is a name the projection has to TYPE before any row exists.
12049///
12050/// Evaluation has answered this since round T9 (`resolve_column` builds a
12051/// `Value::Composite` of every column), but the typing side below had no
12052/// such branch and raised `column "t" does not exist` first — so the
12053/// feature was unreachable through a projection. Measured against PG18.4:
12054/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
12055///
12056/// The type is `Jsonb` + a composite marker, which is exactly how a
12057/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
12058/// the value travels as a `Value::Composite` and renders in the canonical
12059/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
12060/// so the marker names the alias and no rehydration keys off it — the
12061/// value arrives already built.
12062fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
12063 let mut s = ColumnSchema::new(
12064 alloc::string::String::from(alias),
12065 spg_storage::DataType::Jsonb,
12066 true,
12067 );
12068 s.user_composite_type = Some(alloc::string::String::from(alias));
12069 s
12070}
12071
12072/// v7.39.3 — `mysql` makes the name comparison case-INSENSITIVE, which
12073/// is MySQL's rule for column names (measured on 9.7.2: `mycol`,
12074/// `MYCOL` and a backquoted `MyCol` all resolve the same column).
12075///
12076/// SPG compared byte for byte and its lexer folds an UNQUOTED
12077/// identifier, so a table restored from a `mysqldump` — where every
12078/// identifier is backquoted and keeps its case — had every mixed-case
12079/// column unreachable from ordinary unquoted SQL. Same "two spellings,
12080/// two things" defect v7.39.1 closed for relation names.
12081pub(crate) fn resolve_projection_column<'a>(
12082 c: &ColumnName,
12083 schema_cols: &'a [ColumnSchema],
12084 table_alias: &str,
12085 mysql: bool,
12086) -> Result<Cow<'a, ColumnSchema>, EngineError> {
12087 let same = |a: &str, b: &str| {
12088 if mysql {
12089 a.eq_ignore_ascii_case(b)
12090 } else {
12091 a == b
12092 }
12093 };
12094 if let Some(q) = &c.qualifier {
12095 let composite = alloc::format!("{q}.{name}", name = c.name);
12096 if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &composite)) {
12097 return Ok(Cow::Borrowed(s));
12098 }
12099 // Single-table case: the qualifier may equal the active alias —
12100 // then look for the bare column name.
12101 if same(q, table_alias)
12102 && let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name))
12103 {
12104 return Ok(Cow::Borrowed(s));
12105 }
12106 // For multi-table schemas the qualifier is unknown only if no
12107 // column bears the "<q>." prefix. For single-table, the alias
12108 // mismatch alone is enough.
12109 let prefix = alloc::format!("{q}.");
12110 let qualifier_known =
12111 same(q, table_alias) || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
12112 if !qualifier_known {
12113 return Err(EngineError::Eval(EvalError::UnknownQualifier {
12114 qualifier: q.clone(),
12115 column: c.name.clone(),
12116 }));
12117 }
12118 return Err(EngineError::Eval(EvalError::ColumnNotFound {
12119 name: c.name.clone(),
12120 }));
12121 }
12122 if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name)) {
12123 return Ok(Cow::Borrowed(s));
12124 }
12125 let suffix = alloc::format!(".{name}", name = c.name);
12126 let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
12127 let first = matches.next();
12128 let extra = matches.next();
12129 match (first, extra) {
12130 (Some(s), None) => Ok(Cow::Borrowed(s)),
12131 (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
12132 detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
12133 })),
12134 // The whole-row reference, checked LAST so a real column carrying
12135 // the alias's name still wins — the same precedence
12136 // `resolve_column` applies on the evaluation side.
12137 //
12138 // Two schema shapes reach here. A single-table (or subquery, or
12139 // CTE) scan carries its alias and bare column names, so the name
12140 // has to equal the alias. A JOIN's combined schema carries no
12141 // alias at all and qualifies every column `alias.col`, so the
12142 // alias is identified by the prefix instead — which is exactly
12143 // how `whole_row_composite` picks the fields out on the
12144 // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
12145 // answers `(7,z)` on PG18.4 and errored here until this arm
12146 // covered the joined shape too.
12147 _ if !table_alias.is_empty() && c.name == table_alias => {
12148 Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
12149 }
12150 _ if table_alias.is_empty() && {
12151 let prefix = alloc::format!("{name}.", name = c.name);
12152 schema_cols.iter().any(|s| s.name.starts_with(&prefix))
12153 } =>
12154 {
12155 Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
12156 }
12157 _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
12158 name: c.name.clone(),
12159 })),
12160 }
12161}
12162
12163/// v7.40.0 — a column the grouping-set rewrite injected purely to sort
12164/// on, and which must not reach the client. Two families: `__grp_ord_*`
12165/// carries a branch's `grouping()` mask (round 135), `__grp_key_*`
12166/// carries a key the rollup orders by that the query did not project —
12167/// without it `SELECT SUM(qty) … GROUP BY qty WITH ROLLUP` answered
12168/// `column "qty" does not exist`, because a UNION's ORDER BY can only
12169/// name output columns.
12170fn is_synthetic_group_col(name: &str) -> bool {
12171 name.starts_with("__grp_ord_") || name.starts_with("__grp_key_")
12172}
12173
12174/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
12175/// parser to carry per-branch GROUPING() masks into a grouping-set query's
12176/// ORDER BY. They must never reach the output. No-op unless such a column is
12177/// present, so the common path is untouched.
12178/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
12179///
12180/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
12181/// a `LIMIT 2` that should have answered two groups answered one.
12182fn apply_deferred_limit(
12183 rows: alloc::vec::Vec<Row<'static>>,
12184 deferred: &(
12185 Option<spg_sql::ast::LimitExpr>,
12186 Option<spg_sql::ast::LimitExpr>,
12187 ),
12188) -> alloc::vec::Vec<Row<'static>> {
12189 let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
12190 Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
12191 _ => None,
12192 };
12193 let mut rows = rows;
12194 if let Some(off) = count(&deferred.1) {
12195 rows = rows.split_off(off.min(rows.len()));
12196 }
12197 if let Some(lim) = count(&deferred.0) {
12198 rows.truncate(lim);
12199 }
12200 rows
12201}
12202
12203fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
12204 let QueryResult::Rows { columns, rows } = result else {
12205 return result;
12206 };
12207 if !columns.iter().any(|c| is_synthetic_group_col(&c.name)) {
12208 return QueryResult::Rows { columns, rows };
12209 }
12210 let keep: Vec<usize> = columns
12211 .iter()
12212 .enumerate()
12213 .filter(|(_, c)| !is_synthetic_group_col(&c.name))
12214 .map(|(i, _)| i)
12215 .collect();
12216 let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
12217 let new_rows: Vec<Row<'static>> = rows
12218 .into_iter()
12219 .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
12220 .collect();
12221 QueryResult::Rows {
12222 columns: new_cols,
12223 rows: new_rows,
12224 }
12225}
12226
12227/// v7.39 (round 487) — bind every projection item that is a bare column
12228/// reference to its position, once per query.
12229///
12230/// `#[inline(never)]` and out of line on purpose. Round 486 established
12231/// that adding code inside these scan bodies moves neighbouring hot
12232/// functions around under fat LTO: the first version of this had the loop
12233/// inline in `run_single_table_scan` and four aggregate shapes that never
12234/// touch that function — `full_agg`, `join_agg`, `group_500k`,
12235/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
12236/// the same machine. Keeping it out of line kept them still.
12237#[inline(never)]
12238fn bind_direct_columns(
12239 projection: &[ProjectedItem],
12240 ctx: &eval::EvalContext<'_>,
12241) -> Vec<Option<usize>> {
12242 projection
12243 .iter()
12244 .map(|p| match &p.expr {
12245 Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
12246 // Same exclusion `compile_into` makes: a composite column
12247 // has to be rehydrated from stored JSON, which is not a
12248 // cell read.
12249 ctx.columns
12250 .get(*pos)
12251 .is_none_or(|sc| sc.user_composite_type.is_none())
12252 }),
12253 _ => None,
12254 })
12255 .collect()
12256}
12257
12258/// v7.39 (round 505) — the name an un-aliased projected expression reports.
12259///
12260/// PG18 names a call for its function and everything else `?column?`;
12261/// measured with `\gdesc`. SPG used to print the parsed expression back
12262/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
12263/// name-keyed row access found nothing under `upper`.
12264///
12265/// The MySQL half is NOT this rule and is deliberately left alone here:
12266/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
12267/// which needs the parser to hand over spans the AST does not carry yet.
12268/// Until it does, a MySQL session keeps the printed form — closer to what
12269/// MariaDB answers than `?column?` would be.
12270pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
12271 if mysql {
12272 return expr.to_string();
12273 }
12274 spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
12275}
12276
12277pub(crate) fn build_projection(
12278 items: &[SelectItem],
12279 schema_cols: &[ColumnSchema],
12280 table_alias: &str,
12281 mysql: bool,
12282 cat: Option<&Catalog>,
12283) -> Result<Vec<ProjectedItem>, EngineError> {
12284 build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0, cat)
12285}
12286
12287/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
12288/// invisible to `*`.
12289///
12290/// The windowed-SELECT path appends a synthetic `__win_N` column per window
12291/// function so the rewritten projection can reference the computed values as
12292/// ordinary columns. `*` then expanded them too, and
12293/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
12294/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
12295/// silent one: the row simply had one more field than the client asked for.
12296///
12297/// Hidden by POSITION rather than by name, for the reason round 512 recorded
12298/// about the system columns: a name test looks safe until a real column
12299/// happens to carry the name. These are appended last, so the count is what
12300/// identifies them.
12301pub(crate) fn build_projection_hiding_tail(
12302 items: &[SelectItem],
12303 schema_cols: &[ColumnSchema],
12304 table_alias: &str,
12305 mysql: bool,
12306 hidden_tail: usize,
12307 // v7.38.19 — the catalog, so a user-defined function's DECLARED
12308 // return type reaches the projection. Without it `describe_expr`
12309 // cannot type `f_sql()` and the column falls back to text, which is
12310 // what psql reads to decide alignment: `SELECT 7::bigint, f_sql()`
12311 // right-aligned one cell and left-aligned the other while both held
12312 // a bigint. Reported by sentori against 7.38.18 (their §2.2), who
12313 // also established that the EXECUTOR was never confused -- CTAS off
12314 // the same expression gives a bigint column, and arithmetic on it
12315 // works. Only the type travelling in the RowDescription was wrong.
12316 cat: Option<&Catalog>,
12317) -> Result<Vec<ProjectedItem>, EngineError> {
12318 let visible = schema_cols.len().saturating_sub(hidden_tail);
12319 // v7.39 (round 462) — a join's combined schema qualifies every column
12320 // `alias.col` so the deferred-join cell lookups resolve by composite
12321 // name. That is an internal convention, and `*` was handing it to the
12322 // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
12323 // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
12324 // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
12325 // already learned this for `q.*`; plain `*` never got the same rule.
12326 //
12327 // The signal is the schema itself, not the call site: only a combined
12328 // join schema arrives with no table alias AND every column qualified.
12329 // A single-table schema carries its alias, an empty schema has nothing
12330 // to strip, and a synthetic schema's names carry no dot.
12331 let joined_schema = table_alias.is_empty()
12332 && !schema_cols.is_empty()
12333 && schema_cols.iter().all(|c| c.name.contains('.'));
12334 let bare_name = |name: &str| -> String {
12335 if !joined_schema {
12336 return name.to_string();
12337 }
12338 match name.split_once('.') {
12339 Some((_, rest)) if !rest.is_empty() => rest.to_string(),
12340 _ => name.to_string(),
12341 }
12342 };
12343 let mut out = Vec::new();
12344 for item in items {
12345 match item {
12346 SelectItem::Wildcard => {
12347 // v7.39 (round 511) — `*` never expands a system column, as
12348 // PG's does not. They join the schema only when the statement
12349 // asked for them, so this matters for the mixed shape
12350 // `SELECT *, ctid FROM t`.
12351 //
12352 // v7.39 (round 512) — by POSITION, not by name. Matching on
12353 // the name alone looked safe because PG reserves them, and it
12354 // is not: `pg_replication_slots` genuinely has a column called
12355 // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
12356 // Only the trailing six, in the order the scan appends them,
12357 // are the synthetic ones.
12358 let sys_skip = synthetic_system_positions(schema_cols);
12359 for (idx, col) in schema_cols.iter().enumerate() {
12360 if sys_skip[idx] || idx >= visible {
12361 continue;
12362 }
12363 out.push(ProjectedItem {
12364 expr: Expr::Column(ColumnName {
12365 qualifier: None,
12366 name: col.name.clone(),
12367 }),
12368 output_name: bare_name(&col.name),
12369 ty: col.ty,
12370 nullable: col.nullable,
12371 user_enum_type: col.user_enum_type.clone(),
12372 mysql_fsp: col.mysql_fsp,
12373 collation_name: col.collation_name.clone(),
12374 fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12375 pads: crate::collate::pads_space(col.collation_name.as_deref()),
12376 });
12377 }
12378 }
12379 // v7.39 (round 128) — `q.*` expands to every column belonging to
12380 // the qualifier `q`. Single-table schemas carry bare column names
12381 // reachable via `table_alias`; a join's combined schema carries
12382 // `alias.col` names, so a column belongs to `q` when its name has
12383 // the `q.` prefix. PG labels the expanded columns by their bare
12384 // name, so the `alias.` prefix is stripped from the output name.
12385 SelectItem::QualifiedWildcard(q) => {
12386 let prefix = alloc::format!("{q}.");
12387 let single_table = !table_alias.is_empty() && q == table_alias;
12388 let mut matched = 0usize;
12389 for col in &schema_cols[..visible] {
12390 let belongs =
12391 col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
12392 if !belongs {
12393 continue;
12394 }
12395 matched += 1;
12396 let output_name = col
12397 .name
12398 .strip_prefix(&prefix)
12399 .unwrap_or(&col.name)
12400 .to_string();
12401 out.push(ProjectedItem {
12402 expr: Expr::Column(ColumnName {
12403 qualifier: None,
12404 name: col.name.clone(),
12405 }),
12406 output_name,
12407 ty: col.ty,
12408 nullable: col.nullable,
12409 user_enum_type: col.user_enum_type.clone(),
12410 mysql_fsp: col.mysql_fsp,
12411 collation_name: col.collation_name.clone(),
12412 fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12413 pads: crate::collate::pads_space(col.collation_name.as_deref()),
12414 });
12415 }
12416 if matched == 0 {
12417 // `q.*` names no column, so the reference IS the star.
12418 return Err(EngineError::Eval(EvalError::UnknownQualifier {
12419 qualifier: q.clone(),
12420 column: alloc::string::String::from("*"),
12421 }));
12422 }
12423 }
12424 SelectItem::Expr { expr, alias } => {
12425 // Plain column ref keeps full schema info (real type +
12426 // nullability). For compound expressions try the
12427 // describe-side function-return-type table first
12428 // (e.g. `SELECT now()` → Timestamptz, `SELECT
12429 // concat(…)` → Text). Falls back to nullable Text
12430 // for shapes the describe path can't resolve.
12431 if let Expr::Column(c) = expr {
12432 let sch = resolve_projection_column(c, schema_cols, table_alias, mysql)?;
12433 let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
12434 out.push(ProjectedItem {
12435 expr: expr.clone(),
12436 output_name,
12437 ty: sch.ty,
12438 nullable: sch.nullable,
12439 // v7.39 (read01 round 54) — a bare enum column keeps
12440 // its enum identity through the projection.
12441 user_enum_type: sch.user_enum_type.clone(),
12442 mysql_fsp: sch.mysql_fsp,
12443 collation_name: sch.collation_name.clone(),
12444 // v7.38.13 — and its byte-wise-ness. This is the
12445 // site `SELECT DISTINCT t FROM t` arrives at.
12446 fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
12447 pads: crate::collate::pads_space(sch.collation_name.as_deref()),
12448 });
12449 } else if let Some(shape) = describe::describe_expr_in(expr, schema_cols, cat) {
12450 let output_name = alias
12451 .clone()
12452 .unwrap_or_else(|| default_output_name(expr, mysql));
12453 out.push(ProjectedItem {
12454 expr: expr.clone(),
12455 // v7.38.18 — a projected EXPRESSION has no column collation
12456 // to read, so it takes the session default, which is MySQL
12457 // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12458 pads: false,
12459 output_name,
12460 ty: shape.ty,
12461 // v7.39 (round 258) — a projected EXPRESSION keeps its
12462 // enum identity too, not just a bare column. `FROM
12463 // (VALUES ('happy'::mood), …) t(m)` lowers to constant
12464 // SELECTs, so the derived column arrived here as a cast
12465 // and lost the enum — making the outer ORDER BY / min /
12466 // max / array_agg sort by the label's TEXT.
12467 nullable: shape.nullable,
12468 user_enum_type: None,
12469 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12470 // A bare column reference keeps its collation; any
12471 // other expression produces a new value and has none.
12472 collation_name: match expr {
12473 Expr::Column(c) => schema_cols
12474 .iter()
12475 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12476 .and_then(|sc| sc.collation_name.clone()),
12477 _ => None,
12478 },
12479 fold_exempt: match expr {
12480 Expr::Column(c) => schema_cols
12481 .iter()
12482 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12483 .is_some_and(|sc| {
12484 matches!(sc.collation, spg_storage::Collation::Binary)
12485 }),
12486 // Not a column: no declared collation to honour,
12487 // so the session default applies and it folds.
12488 _ => false,
12489 },
12490 });
12491 } else {
12492 let output_name = alias
12493 .clone()
12494 .unwrap_or_else(|| default_output_name(expr, mysql));
12495 out.push(ProjectedItem {
12496 expr: expr.clone(),
12497 // v7.38.18 — a projected EXPRESSION has no column collation
12498 // to read, so it takes the session default, which is MySQL
12499 // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12500 pads: false,
12501 output_name,
12502 // A user ENUM has no DataType of its own, so
12503 // `describe_expr` cannot type `'ok'::mood` and the
12504 // item lands HERE, defaulting to text — which is why
12505 // pg_typeof answered `text` and a derived table sorted
12506 // enum values by their label.
12507 ty: DataType::Text,
12508 nullable: true,
12509 user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
12510 .map(alloc::string::String::from),
12511 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12512 collation_name: match expr {
12513 Expr::Column(c) => schema_cols
12514 .iter()
12515 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12516 .and_then(|sc| sc.collation_name.clone()),
12517 _ => None,
12518 },
12519 fold_exempt: match expr {
12520 Expr::Column(c) => schema_cols
12521 .iter()
12522 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12523 .is_some_and(|sc| {
12524 matches!(sc.collation, spg_storage::Collation::Binary)
12525 }),
12526 // Not a column: no declared collation to honour,
12527 // so the session default applies and it folds.
12528 _ => false,
12529 },
12530 });
12531 }
12532 }
12533 }
12534 }
12535 Ok(out)
12536}
12537
12538// ---- v4.12 window-function helpers ----
12539// The (partition-key, order-key, original-index) tuple shape used
12540// across these helpers is intrinsic to the planner. Factoring it
12541// into a typedef adds indirection without making the code clearer,
12542// so several lints are allowed inline on the affected functions
12543// rather than module-wide.
12544
12545/// v4.22: pick more specific column types from observed rows when
12546/// the projection builder defaulted to Text (the v1.x behavior for
12547/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
12548/// land an Int column in the CTE storage table rather than failing
12549/// the insert with "expected TEXT, got INT".
12550pub(crate) fn infer_column_types(
12551 columns: &[ColumnSchema],
12552 rows: &[Row<'static>],
12553) -> Vec<ColumnSchema> {
12554 let mut out = columns.to_vec();
12555 for (col_idx, col) in out.iter_mut().enumerate() {
12556 if col.ty != DataType::Text {
12557 continue;
12558 }
12559 let mut inferred: Option<DataType> = None;
12560 let mut all_null = true;
12561 for row in rows {
12562 let Some(v) = row.values.get(col_idx) else {
12563 continue;
12564 };
12565 let ty = match v {
12566 Value::Null => continue,
12567 Value::SmallInt(_) => DataType::SmallInt,
12568 Value::Int(_) => DataType::Int,
12569 Value::BigInt(_) => DataType::BigInt,
12570 Value::Float(_) => DataType::Float,
12571 Value::Bool(_) => DataType::Bool,
12572 Value::Vector(_) => DataType::Vector {
12573 dim: 0,
12574 encoding: VecEncoding::F32,
12575 },
12576 // v7.38 (read01 U16) — carry array values through with an
12577 // array type so a recursive CTE that projects an array
12578 // (e.g. a SEARCH/CYCLE ord / path column) types the working
12579 // column as an array, not Text.
12580 Value::TextArray(_) => DataType::TextArray,
12581 Value::IntArray(_) => DataType::IntArray,
12582 Value::BigIntArray(_) => DataType::BigIntArray,
12583 Value::SmallIntArray(_) => DataType::SmallIntArray,
12584 Value::FloatArray(_) => DataType::FloatArray,
12585 Value::BoolArray(_) => DataType::BoolArray,
12586 // v7.39 (GUC knife 2) — an interval projection describes
12587 // as INTERVAL (typed drivers read the RowDescription OID).
12588 Value::Interval { .. } => DataType::Interval,
12589 _ => DataType::Text,
12590 };
12591 all_null = false;
12592 inferred = Some(match inferred {
12593 None => ty,
12594 Some(prev) if prev == ty => prev,
12595 Some(_) => DataType::Text,
12596 });
12597 }
12598 if let Some(t) = inferred {
12599 col.ty = t;
12600 col.nullable = true;
12601 } else if all_null {
12602 col.nullable = true;
12603 }
12604 }
12605 out
12606}
12607
12608/// Numeric widening rank for UNION type resolution (higher = wider).
12609fn numeric_rank(t: DataType) -> Option<u8> {
12610 match t {
12611 DataType::SmallInt => Some(1),
12612 DataType::Int => Some(2),
12613 DataType::BigInt => Some(3),
12614 DataType::Numeric { .. } => Some(4),
12615 DataType::Float => Some(5),
12616 _ => None,
12617 }
12618}
12619
12620/// Resolve the common result type for a UNION / VALUES column from the
12621/// set of concrete (non-NULL) branch types, following the safe subset
12622/// of PG's type resolution:
12623/// * all-numeric → the widest numeric (int ∪ bigint → bigint, … ∪
12624/// numeric → numeric, … ∪ float → float);
12625/// * DATE ∪ TIMESTAMP → TIMESTAMP;
12626/// * exactly one concrete non-TEXT type mixed with TEXT literals →
12627/// that concrete type (the TEXT cells get parsed into it).
12628/// Returns `None` for anything ambiguous, so the caller leaves the
12629/// column untouched rather than risk a wrong or failing coercion.
12630fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
12631 // NB: types are collected from RUNTIME values, which are coarser
12632 // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
12633 // a single-concrete-type fast path must NOT overwrite the column
12634 // type — it would downgrade tstz to ts. NULL-only unification (PG:
12635 // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
12636 // row's pg_typeof) needs schema-level resolution — recorded, not
12637 // attempted here.
12638 if types.len() < 2 {
12639 return None;
12640 }
12641 if types.iter().all(|t| numeric_rank(*t).is_some()) {
12642 return types
12643 .iter()
12644 .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
12645 .copied();
12646 }
12647 let non_text: Vec<&DataType> = types
12648 .iter()
12649 .filter(|t| !matches!(t, DataType::Text))
12650 .collect();
12651 // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
12652 // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
12653 // if any is timestamp the result is timestamp (ts ∪ date). All values are
12654 // the same UTC-micros instant, so widening date/ts to tstz is lossless.
12655 if non_text.iter().all(|t| {
12656 matches!(
12657 t,
12658 DataType::Date | DataType::Timestamp | DataType::Timestamptz
12659 )
12660 }) && non_text
12661 .iter()
12662 .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
12663 {
12664 if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
12665 return Some(DataType::Timestamptz);
12666 }
12667 return Some(DataType::Timestamp);
12668 }
12669 // A single concrete non-TEXT type mixed with TEXT literals.
12670 if non_text.len() == 1 {
12671 return Some(*non_text[0]);
12672 }
12673 // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
12674 // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
12675 // text): resolve the concrete set first (PG treats the unknown-
12676 // typed string literals as castable to whatever the knowns
12677 // resolve to), then the TEXT cells parse into that target — the
12678 // caller's coercion dry-run still abandons the column if any
12679 // literal doesn't parse.
12680 if !non_text.is_empty() && non_text.len() < types.len() {
12681 let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
12682 return resolve_union_common_type(&concrete);
12683 }
12684 None
12685}
12686
12687/// Coerce every cell of a UNION / VALUES result column to one common
12688/// type (see [`resolve_union_common_type`]). Conservative: a column
12689/// whose branches already agree, or whose types don't resolve, or where
12690/// any cell fails to coerce, is left exactly as it was — this never
12691/// turns a previously-working query into an error.
12692fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
12693 for col_idx in 0..columns.len() {
12694 let mut seen: Vec<DataType> = Vec::new();
12695 for row in rows.iter() {
12696 if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
12697 if !seen.contains(&dt) {
12698 seen.push(dt);
12699 }
12700 }
12701 }
12702 // v7.37.16 — a single concrete runtime type under a TEXT-typed
12703 // column means the column type came off a NULL (or unknown-text)
12704 // branch: NULL literals describe as TEXT (`L::Null → Text`), so
12705 // `VALUES (NULL),(1.5)` left the column "text" while every
12706 // non-NULL cell is numeric. Adopt the concrete type — schema
12707 // only, no cell changes. tstz-safe by construction: a real
12708 // timestamptz column's schema type is Timestamptz, not Text, so
12709 // the coarser runtime type (Value::Timestamp) can't downgrade it
12710 // through this arm; and a real text column's non-NULL cells are
12711 // Text, which keeps seen == [Text] and skips it.
12712 if seen.len() == 1
12713 && matches!(columns[col_idx].ty, DataType::Text)
12714 && !matches!(seen[0], DataType::Text)
12715 {
12716 columns[col_idx].ty = seen[0];
12717 continue;
12718 }
12719 let Some(target) = resolve_union_common_type(&seen) else {
12720 continue;
12721 };
12722 // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
12723 // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
12724 // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
12725 // existing numeric cell untouched and only promote integers (to scale 0)
12726 // rather than rescaling everything to the widest scale.
12727 let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
12728 // Dry-run the coercion; abandon the whole column if any fails.
12729 let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
12730 let mut ok = true;
12731 for row in rows.iter() {
12732 match row.values.get(col_idx) {
12733 Some(Value::Numeric { .. }) if scale_preserving_numeric => {
12734 coerced.push(Some(row.values[col_idx].clone()));
12735 }
12736 Some(v) => {
12737 let cell_target = if scale_preserving_numeric {
12738 DataType::Numeric {
12739 precision: 0,
12740 scale: 0,
12741 }
12742 } else {
12743 target
12744 };
12745 match crate::conversions::coerce_value(
12746 v.clone(),
12747 cell_target,
12748 &columns[col_idx].name,
12749 col_idx,
12750 ) {
12751 Ok(cv) => coerced.push(Some(cv)),
12752 Err(_) => {
12753 ok = false;
12754 break;
12755 }
12756 }
12757 }
12758 None => coerced.push(None),
12759 }
12760 }
12761 if !ok {
12762 continue;
12763 }
12764 for (row, cv) in rows.iter_mut().zip(coerced) {
12765 if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
12766 *slot = nv;
12767 }
12768 }
12769 columns[col_idx].ty = target;
12770 }
12771}
12772
12773/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
12774/// dedup inside the recursive iteration. Crude but deterministic
12775/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
12776fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
12777 let mut out = Vec::new();
12778 for v in &row.values {
12779 // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
12780 // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
12781 // like PG (and like GROUP BY, which already normalizes). The old
12782 // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
12783 // the exact-decimal family through one scale-stripped canonical form.
12784 match v {
12785 Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12786 Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12787 Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12788 Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
12789 other => {
12790 let s = alloc::format!("{other:?}|");
12791 out.extend_from_slice(s.as_bytes());
12792 }
12793 }
12794 }
12795 out
12796}
12797
12798/// Append a scale-independent canonical key for an exact-decimal value: strip
12799/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
12800/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
12801fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
12802 while scale > 0 && scaled % 10 == 0 {
12803 scaled /= 10;
12804 scale -= 1;
12805 }
12806 let s = alloc::format!("\u{1}{scaled}e-{scale}|");
12807 out.extend_from_slice(s.as_bytes());
12808}
12809
12810/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
12811/// (uncorrelated; outer refs were substituted upstream), then zip
12812/// them in parallel, NULL-padding shorter arrays to the longest
12813/// (PG's ROWS FROM shorthand). Shared by the primary-position
12814/// executor and the join-position materialiser, which both detect
12815/// the parser's `__unnest_zip` marker call.
12816pub(crate) fn unnest_zip_rows(
12817 args: &[Expr],
12818) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
12819 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12820 let ctx = EvalContext::new(&empty_schema, None);
12821 let dummy_row = Row::new(alloc::vec::Vec::new());
12822 let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
12823 let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
12824 alloc::vec::Vec::with_capacity(args.len());
12825 for a in args {
12826 let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
12827 // v7.39.13 — the element menu the rest of the workspace already
12828 // has, not a third copy of a shortened one.
12829 //
12830 // This arm listed Text, Int and BigInt and refused everything
12831 // else, so `unnest(uuid[], text[])` raised while
12832 // `unnest(uuid[])` — a different path — did not. A shipped
12833 // endpoint of a customer's returned 500 on every call because
12834 // of it. `array_elements` and `array_element_type` are the two
12835 // halves of the menu that `array_element_at`'s own comment
12836 // describes: "previously only matched Text/Int/BigInt arrays
12837 // and errored on every other element type". Same sentence,
12838 // third arm.
12839 let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = if matches!(v, Value::Null) {
12840 (DataType::Text, alloc::vec::Vec::new())
12841 } else if let Some(items) = crate::eval::values::array_elements(&v) {
12842 let dt = v
12843 .data_type()
12844 .and_then(crate::describe::array_element_type)
12845 .unwrap_or(DataType::Text);
12846 (dt, items)
12847 } else {
12848 return Err(EngineError::Unsupported(alloc::format!(
12849 "unnest() expects array arguments, got {}",
12850 crate::conversions::pg_type_name_for_error_opt(v.data_type())
12851 )));
12852 };
12853 dtypes.push(dt);
12854 columns.push(items);
12855 }
12856 let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
12857 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
12858 for i in 0..max_len {
12859 let vals: alloc::vec::Vec<Value<'static>> = columns
12860 .iter()
12861 .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
12862 .collect();
12863 rows.push(Row::new(vals));
12864 }
12865 Ok((dtypes, rows))
12866}
12867
12868/// Detect the parser's multi-arg unnest marker on an unnest_expr.
12869pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
12870 match expr {
12871 Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
12872 _ => None,
12873 }
12874}
12875
12876/// Evaluate generate_series arguments (uncorrelated — outer refs
12877/// were substituted upstream where applicable) and build the row
12878/// stream. Dispatches on the start value's shape and rejects
12879/// mixed-shape calls early (e.g. start = timestamp, stop =
12880/// integer) so the caller gets a clean error rather than a panic.
12881/// Shared by the primary-position executor and the join-position
12882/// materialiser.
12883pub(crate) fn generate_series_rows(
12884 args: &[Expr],
12885 cancel: &CancelToken<'_>,
12886) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12887 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12888 let ctx = EvalContext::new(&empty_schema, None);
12889 let dummy_row = Row::new(alloc::vec::Vec::new());
12890 let mut arg_values: alloc::vec::Vec<Value<'static>> =
12891 alloc::vec::Vec::with_capacity(args.len());
12892 for a in args {
12893 arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
12894 }
12895 generate_series_from_values(arg_values, args, cancel)
12896}
12897
12898/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
12899/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
12900/// full integer / numeric / timestamp overload set with the FROM-clause path.
12901/// Before this split the target-list arm reimplemented only the integer case,
12902/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
12903/// NULL for the timestamp column instead of the series. `arg_values` are the
12904/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
12905/// timestamp type resolution (it inspects the argument expressions' types).
12906pub(crate) fn generate_series_from_values(
12907 mut arg_values: alloc::vec::Vec<Value<'static>>,
12908 args: &[Expr],
12909 cancel: &CancelToken<'_>,
12910) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12911 // PG: a NULL bound or step yields zero rows (also keeps the
12912 // NULL-padded lateral probe alive — schema without data).
12913 if arg_values.iter().any(|v| matches!(v, Value::Null)) {
12914 return Ok((DataType::BigInt, alloc::vec::Vec::new()));
12915 }
12916 // PG resolves `generate_series(date, date, interval)` to the
12917 // timestamp/timestamptz overload by implicitly casting each date
12918 // bound up to a timestamp at midnight (verified vs live PG18.4:
12919 // date args yield rows anchored at 00:00:00). SPG's TZ-naive
12920 // timestamp model renders the same instants, so fold any Date
12921 // bound to its midnight Timestamp (canonical `days *
12922 // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
12923 // the shape match so the existing timestamp arm drives the walk.
12924 // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
12925 // `generate_series(date, date, interval)` has no date overload, and among
12926 // the two candidates PG prefers the timestamptz one (timestamptz is the
12927 // preferred type of the datetime category), so the column comes back
12928 // `timestamp with time zone` — the rows render with a `+00` offset. A
12929 // timestamptz bound obviously lands there too. Only genuinely
12930 // timestamp-typed bounds keep the TZ-naive result type.
12931 let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12932 let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
12933 || args.iter().any(|a| {
12934 crate::describe::describe_expr(a, &empty_cols)
12935 .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
12936 });
12937 for v in &mut arg_values {
12938 if let Value::Date(d) = *v {
12939 *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
12940 }
12941 }
12942 match arg_values.as_slice() {
12943 [Value::Timestamp(start), Value::Timestamp(stop), step] => {
12944 let interval_step = match step {
12945 Value::Interval { .. } => step.clone(),
12946 // v7.38 (read01) — PG resolves an unknown-type string step
12947 // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
12948 // a bare text step by parsing it the same way `::interval` does.
12949 Value::Text(s) => crate::conversions::coerce_value(
12950 Value::text(s.as_ref()),
12951 DataType::Interval,
12952 "",
12953 0,
12954 )
12955 .map_err(|_| {
12956 EngineError::Unsupported(alloc::format!(
12957 "generate_series(timestamp, timestamp, …): \
12958 could not parse step {s:?} as INTERVAL"
12959 ))
12960 })?,
12961 other => {
12962 return Err(EngineError::Unsupported(alloc::format!(
12963 "generate_series(timestamp, timestamp, …): \
12964 step must be INTERVAL, got {}",
12965 crate::conversions::pg_type_name_for_error_opt(other.data_type())
12966 )));
12967 }
12968 };
12969 let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
12970 Ok((
12971 if tz {
12972 DataType::Timestamptz
12973 } else {
12974 DataType::Timestamp
12975 },
12976 rows,
12977 ))
12978 }
12979 [start, stop, step]
12980 if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
12981 {
12982 let s = value_to_i64(start);
12983 let e = value_to_i64(stop);
12984 let st = value_to_i64(step);
12985 // PG types the series by the argument type: int4 args → int4
12986 // elements, int8 (bigint) args → int8. Any BigInt operand widens.
12987 let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
12988 let rows = generate_series_integers(s, e, st, wide, cancel)?;
12989 Ok((
12990 if wide {
12991 DataType::BigInt
12992 } else {
12993 DataType::Int
12994 },
12995 rows,
12996 ))
12997 }
12998 [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
12999 let s = value_to_i64(start);
13000 let e = value_to_i64(stop);
13001 let wide = value_is_bigint(start) || value_is_bigint(stop);
13002 let rows = generate_series_integers(s, e, 1, wide, cancel)?;
13003 Ok((
13004 if wide {
13005 DataType::BigInt
13006 } else {
13007 DataType::Int
13008 },
13009 rows,
13010 ))
13011 }
13012 // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
13013 // series in exact numeric arithmetic; NaN / infinity bounds and a
13014 // zero step get dedicated wordings, and a mixed int/numeric call
13015 // resolves here via the implicit int→numeric cast.
13016 [_, _] | [_, _, _]
13017 if arg_values
13018 .iter()
13019 .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
13020 && arg_values.iter().all(|v| {
13021 matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
13022 }) =>
13023 {
13024 use spg_storage::NumericKind as K;
13025 let words: [(&str, &str); 3] = [
13026 (
13027 "start value cannot be NaN",
13028 "start value cannot be infinity",
13029 ),
13030 ("stop value cannot be NaN", "stop value cannot be infinity"),
13031 ("step size cannot be NaN", "step size cannot be infinity"),
13032 ];
13033 for (i, v) in arg_values.iter().enumerate() {
13034 if let Value::Numeric { kind, .. } = v {
13035 if *kind != K::Finite {
13036 let (nan_w, inf_w) = words[i];
13037 return Err(EngineError::Unsupported(
13038 if *kind == K::NaN { nan_w } else { inf_w }.into(),
13039 ));
13040 }
13041 }
13042 }
13043 let big =
13044 |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
13045 let start = big(&arg_values[0]);
13046 let stop = big(&arg_values[1]);
13047 let step = if arg_values.len() == 3 {
13048 big(&arg_values[2])
13049 } else {
13050 spg_storage::bignum::BigNumeric::from_i128(1, 0)
13051 };
13052 if step.is_zero() {
13053 return Err(EngineError::Unsupported(
13054 "step size cannot equal zero".into(),
13055 ));
13056 }
13057 let descending = step.parts().0;
13058 let mut rows = alloc::vec::Vec::new();
13059 let mut cur = start;
13060 const MAX_ROWS: usize = 10_000_000;
13061 loop {
13062 cancel.check()?;
13063 let c = cur.cmp(&stop);
13064 if descending {
13065 if c == core::cmp::Ordering::Less {
13066 break;
13067 }
13068 } else if c == core::cmp::Ordering::Greater {
13069 break;
13070 }
13071 if rows.len() >= MAX_ROWS {
13072 return Err(EngineError::Unsupported(alloc::format!(
13073 "generate_series() result exceeds {MAX_ROWS} rows"
13074 )));
13075 }
13076 rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
13077 cur.clone()
13078 )]));
13079 cur = cur.add(&step);
13080 }
13081 Ok((
13082 DataType::Numeric {
13083 precision: 0,
13084 scale: 0,
13085 },
13086 rows,
13087 ))
13088 }
13089 _ => Err(EngineError::Unsupported(alloc::format!(
13090 "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
13091 argument shapes; got {}",
13092 arg_values
13093 .iter()
13094 .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
13095 .collect::<alloc::vec::Vec<_>>()
13096 .join(", ")
13097 ))),
13098 }
13099}
13100
13101/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
13102/// Step direction follows the sign: positive step iterates upward
13103/// (stops when current > stop); negative iterates downward; zero
13104/// errors. Caller-facing row stream is `BigInt`-typed so a single
13105/// projection schema covers SmallInt / Int / BigInt callers.
13106fn generate_series_integers(
13107 start: i64,
13108 stop: i64,
13109 step: i64,
13110 wide: bool,
13111 cancel: &CancelToken<'_>,
13112) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13113 if step == 0 {
13114 return Err(EngineError::Unsupported(
13115 "step size cannot equal zero".into(),
13116 ));
13117 }
13118 let mut out = alloc::vec::Vec::new();
13119 let mut cur = start;
13120 // Hard cap to keep a runaway call from eating all memory. PG
13121 // has no such cap but does honour query timeout; SPG's cancel
13122 // token will fire too — this is a defense-in-depth backstop.
13123 const MAX_ROWS: usize = 10_000_000;
13124 loop {
13125 cancel.check()?;
13126 if step > 0 && cur > stop {
13127 break;
13128 }
13129 if step < 0 && cur < stop {
13130 break;
13131 }
13132 out.push(Row::new(alloc::vec![if wide {
13133 Value::BigInt(cur)
13134 } else {
13135 Value::Int(cur as i32)
13136 }]));
13137 if out.len() > MAX_ROWS {
13138 return Err(EngineError::Unsupported(alloc::format!(
13139 "generate_series(): exceeded {MAX_ROWS} rows; \
13140 narrow start/stop or use a larger step"
13141 )));
13142 }
13143 cur = match cur.checked_add(step) {
13144 Some(n) => n,
13145 None => break,
13146 };
13147 }
13148 Ok(out)
13149}
13150
13151/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
13152/// `Value::Interval { months, micros }` per the caller's guard;
13153/// each iteration adds the interval via `apply_binary_interval`
13154/// so month-shifting handles short-month rollover (PG semantics).
13155fn generate_series_timestamps(
13156 start: i64,
13157 stop: i64,
13158 step: Value,
13159 cancel: &CancelToken<'_>,
13160) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13161 let (months, days, micros) = match &step {
13162 Value::Interval {
13163 months,
13164 days,
13165 micros,
13166 kind,
13167 } => (*months, *days, *micros),
13168 _ => unreachable!("caller guards step.is_interval"),
13169 };
13170 if months == 0 && days == 0 && micros == 0 {
13171 return Err(EngineError::Unsupported(
13172 "generate_series(): INTERVAL step cannot be zero".into(),
13173 ));
13174 }
13175 let ascending = months > 0 || days > 0 || micros > 0;
13176 let mut out = alloc::vec::Vec::new();
13177 let mut cur = Value::Timestamp(start);
13178 const MAX_ROWS: usize = 10_000_000;
13179 loop {
13180 cancel.check()?;
13181 let cur_t = match cur {
13182 Value::Timestamp(t) => t,
13183 _ => unreachable!("loop invariant: cur is Timestamp"),
13184 };
13185 if ascending && cur_t > stop {
13186 break;
13187 }
13188 if !ascending && cur_t < stop {
13189 break;
13190 }
13191 out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
13192 if out.len() > MAX_ROWS {
13193 return Err(EngineError::Unsupported(alloc::format!(
13194 "generate_series(): exceeded {MAX_ROWS} rows; \
13195 narrow start/stop or use a larger step"
13196 )));
13197 }
13198 let next = eval::apply_binary_interval(
13199 spg_sql::ast::BinOp::Add,
13200 &cur,
13201 &Value::Interval {
13202 months,
13203 days,
13204 micros,
13205 kind: spg_storage::IntervalKind::Finite,
13206 },
13207 )
13208 .map_err(EngineError::Eval)?;
13209 cur = match next {
13210 Some(v) => v,
13211 None => break,
13212 };
13213 }
13214 Ok(out)
13215}
13216
13217/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
13218/// WITH TIES` requires an `ORDER BY`. Without one, there's no
13219/// way to identify "ties" deterministically, so PG errors at
13220/// plan time. SPG mirrors that surface so the same DDL / app
13221/// behaviour holds on cutover.
13222fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
13223 if stmt.limit_with_ties && stmt.order_by.is_empty() {
13224 return Err(EngineError::Unsupported(alloc::string::String::from(
13225 "WITH TIES cannot be specified without ORDER BY clause",
13226 )));
13227 }
13228 Ok(())
13229}
13230
13231/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
13232/// (case-insensitive). Used by `exec_select_cancel`'s
13233/// projection loop to detect Set-Returning-Function rows that
13234/// need per-row expansion. Only the top-level call counts —
13235/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
13236/// projection's perspective; it would surface as an "unknown
13237/// function" mismatch downstream, which is what we want
13238/// (multi-SRF / nested SRF is documented carve-out for v7.19).
13239fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
13240 top_level_srf_kind(expr).is_some()
13241}
13242
13243/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
13244/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
13245/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
13246/// source row.
13247#[derive(Clone, Copy, PartialEq, Eq)]
13248pub(crate) enum SrfKind {
13249 Unnest,
13250 /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
13251 /// list. It used to be handled ONLY by the parser's lift into FROM, so a
13252 /// second one in the same list came back as "unknown function".
13253 GenerateSeries,
13254 GenerateSubscripts,
13255 /// `_text` variants unwrap scalars to their lexeme; the plain forms render
13256 /// every value as compact JSON text.
13257 ArrayElements {
13258 as_text: bool,
13259 },
13260 PathQuery,
13261 RegexpMatches,
13262 Each {
13263 as_text: bool,
13264 },
13265 ObjectKeys,
13266}
13267
13268/// Case-insensitive match against any of `names`.
13269fn name_is(name: &str, names: &[&str]) -> bool {
13270 names.iter().any(|n| name.eq_ignore_ascii_case(n))
13271}
13272
13273pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
13274 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13275 return None;
13276 };
13277 let n = args.len();
13278 // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
13279 // SELECT list (it returned an array there before) and shares the unnest
13280 // expansion machinery.
13281 if n == 1 && name.eq_ignore_ascii_case("unnest") {
13282 return Some(SrfKind::Unnest);
13283 }
13284 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
13285 return Some(SrfKind::GenerateSeries);
13286 }
13287 if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
13288 return Some(SrfKind::GenerateSubscripts);
13289 }
13290 // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
13291 // per element / match in the SELECT list; they collapsed to a single row
13292 // (a TextArray, or an "unknown function" error for `each`) before.
13293 if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
13294 return Some(SrfKind::ArrayElements { as_text: false });
13295 }
13296 if n == 1
13297 && name_is(
13298 name,
13299 &["jsonb_array_elements_text", "json_array_elements_text"],
13300 )
13301 {
13302 return Some(SrfKind::ArrayElements { as_text: true });
13303 }
13304 // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
13305 if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
13306 return Some(SrfKind::PathQuery);
13307 }
13308 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
13309 return Some(SrfKind::RegexpMatches);
13310 }
13311 if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
13312 return Some(SrfKind::Each { as_text: false });
13313 }
13314 if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
13315 return Some(SrfKind::Each { as_text: true });
13316 }
13317 if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
13318 return Some(SrfKind::ObjectKeys);
13319 }
13320 None
13321}
13322
13323/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
13324/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
13325/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
13326/// rows, as in PG).
13327pub(crate) fn top_level_srf_output(
13328 expr: &spg_sql::ast::Expr,
13329 row: &Row<'static>,
13330 ctx: &EvalContext<'_>,
13331) -> Result<Vec<Value<'static>>, EngineError> {
13332 let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
13333 (top_level_srf_kind(expr), expr)
13334 else {
13335 return Err(EngineError::Unsupported(
13336 "expected a SELECT-list SRF call".into(),
13337 ));
13338 };
13339 match kind {
13340 SrfKind::Unnest => {
13341 // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
13342 // the elements DIRECTLY: the old path built the whole
13343 // Value::Array (one eval + a clone per element) only for
13344 // array_value_to_elements to clone every element back out.
13345 // Any other argument shape (a column, a function result)
13346 // keeps the build-then-split path.
13347 if let spg_sql::ast::Expr::Array(items) = &args[0] {
13348 return items
13349 .iter()
13350 .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
13351 .collect();
13352 }
13353 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13354 array_value_to_elements(&arr)
13355 }
13356 SrfKind::GenerateSeries => {
13357 // v7.39 (read01 round 96) — evaluate the args against the actual
13358 // row, then hand off to the shared core so the numeric and
13359 // timestamp/timestamptz overloads work here too (this arm used to
13360 // handle only integers, silently NULLing a temporal/numeric series
13361 // when it shared a target list with another SRF).
13362 let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
13363 for a in args {
13364 arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13365 }
13366 let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
13367 Ok(rows
13368 .into_iter()
13369 .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
13370 .collect())
13371 }
13372 SrfKind::GenerateSubscripts => {
13373 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13374 let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13375 if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
13376 return Ok(Vec::new());
13377 }
13378 let len = array_value_to_elements(&arr)?.len();
13379 Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
13380 }
13381 // One Value per array element (`_text` → text / SQL NULL, plain → the
13382 // element's compact JSON text) — the element list the FROM-clause form
13383 // materialises.
13384 SrfKind::ArrayElements { as_text } => {
13385 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13386 if matches!(arg, Value::Null) {
13387 return Ok(Vec::new());
13388 }
13389 let items =
13390 crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13391 Ok(items
13392 .into_iter()
13393 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13394 .collect())
13395 }
13396 // The scalar form already yields a TextArray of the keys (or errors on
13397 // a non-object, like PG); expand it into rows.
13398 SrfKind::ObjectKeys => {
13399 let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
13400 array_value_to_elements(&v)
13401 }
13402 // One row per match, each a text[] of the pattern's capture groups.
13403 SrfKind::RegexpMatches => {
13404 let vals: Vec<Value<'static>> = args
13405 .iter()
13406 .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
13407 .collect::<Result<_, _>>()?;
13408 crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
13409 }
13410 // One composite `(key, value)` row per object member (plain → jsonb
13411 // value, `_text` → text / SQL NULL).
13412 SrfKind::Each { as_text } => {
13413 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13414 if matches!(arg, Value::Null) {
13415 return Ok(Vec::new());
13416 }
13417 let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13418 Ok(pairs
13419 .into_iter()
13420 .map(|(k, v)| {
13421 let val = if as_text {
13422 v.map(Value::text).unwrap_or(Value::Null)
13423 } else {
13424 v.map(Value::json).unwrap_or(Value::Null)
13425 };
13426 Value::Composite(alloc::vec![
13427 ("key".to_string(), Value::text(k)),
13428 ("value".to_string(), val),
13429 ])
13430 })
13431 .collect())
13432 }
13433 // One Value per matched JSON value.
13434 SrfKind::PathQuery => {
13435 let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13436 let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13437 // v7.39 — optional vars document (3rd arg).
13438 let vars = match args.get(2) {
13439 Some(a) => {
13440 let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
13441 crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
13442 }
13443 None => None,
13444 };
13445 match crate::json::path_query_vars(&doc, &path, vars.as_ref())
13446 .map_err(EngineError::Eval)?
13447 {
13448 Value::Null => Ok(Vec::new()),
13449 Value::TextArray(items) => Ok(items
13450 .into_iter()
13451 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13452 .collect()),
13453 other => Ok(alloc::vec![other]),
13454 }
13455 }
13456 }
13457}
13458
13459/// v7.19 P5 — turn an array-typed `Value` into the element list
13460/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
13461/// = (no rows)`). Non-array values fall through to a type-mismatch
13462/// error.
13463pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
13464 // v7.39 (round 236) — PG unnests a multidimensional array into its
13465 // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
13466 // rows). SPG stores 2-D arrays as their own variants, which fell
13467 // through to the type-mismatch arm below.
13468 if let Some(flat) = crate::eval::values::flatten_2d(v) {
13469 return array_value_to_elements(&flat);
13470 }
13471 // v7.39.11 — every array-family value, through the one element
13472 // menu. The arms below name int / bigint / text / json and stop, so
13473 // `SELECT unnest(ARRAY[1,2]::smallint[])` raised "expects an array
13474 // argument, got smallint[]" — the type it had just been given —
13475 // and so did every catalog vector. Found while closing sentori's
13476 // §4 against 7.39.10; the FROM-clause unnest has the same arm.
13477 if crate::eval::values::array_len(v).is_some() {
13478 if let Some(elems) = crate::eval::values::array_elements(v) {
13479 return Ok(elems);
13480 }
13481 }
13482 match v {
13483 Value::Null => Ok(Vec::new()),
13484 Value::TextArray(items) => Ok(items
13485 .iter()
13486 .map(|opt| {
13487 opt.as_ref()
13488 .map(|s| Value::text(s.clone()))
13489 .unwrap_or(Value::Null)
13490 })
13491 .collect()),
13492 Value::IntArray(items) => Ok(items
13493 .iter()
13494 .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
13495 .collect()),
13496 Value::BigIntArray(items) => Ok(items
13497 .iter()
13498 .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
13499 .collect()),
13500 // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
13501 // range per canonical span.
13502 Value::Multirange { kind, ranges } => Ok(ranges
13503 .iter()
13504 .map(|s| Value::Range {
13505 kind: *kind,
13506 lower: s.lower.clone(),
13507 upper: s.upper.clone(),
13508 lower_inc: s.lower_inc,
13509 upper_inc: s.upper_inc,
13510 empty: false,
13511 })
13512 .collect()),
13513 other => Err(EngineError::Eval(EvalError::TypeMismatch {
13514 detail: alloc::format!(
13515 "unnest() expects an array argument, got {}",
13516 crate::conversions::pg_type_name_for_error_opt(other.data_type())
13517 ),
13518 })),
13519 }
13520}
13521
13522impl Engine {
13523 /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
13524 /// the SELECT's FROM / JOIN graph, re-parse each view's body
13525 /// source, and prepend it as a synthetic CTE on the
13526 /// returned SelectStatement. Returns `None` when no view
13527 /// references are found (caller proceeds with the original
13528 /// statement); returns `Some(rewritten)` otherwise (caller
13529 /// re-runs exec_select_cancel on the rewritten form so the
13530 /// regular CTE materialiser handles it).
13531 fn expand_views_in_select(
13532 &self,
13533 stmt: &SelectStatement,
13534 ) -> Result<Option<SelectStatement>, EngineError> {
13535 let cat = self.active_catalog();
13536 let mut referenced: Vec<String> = Vec::new();
13537 if let Some(from) = &stmt.from {
13538 collect_view_refs(&from.primary, cat, &mut referenced);
13539 for j in &from.joins {
13540 collect_view_refs(&j.table, cat, &mut referenced);
13541 }
13542 }
13543 // Don't expand a view name that's already shadowed by a
13544 // CTE on the same SELECT — the CTE wins per PG.
13545 referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
13546 if referenced.is_empty() {
13547 return Ok(None);
13548 }
13549 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
13550 for name in &referenced {
13551 let view = cat.view(name).ok_or_else(|| {
13552 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13553 "view {name:?} disappeared mid-expansion"
13554 )))
13555 })?;
13556 let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
13557 EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
13558 })?;
13559 let Statement::Select(body) = parsed else {
13560 return Err(EngineError::Unsupported(alloc::format!(
13561 "view {name:?} body is not a SELECT (catalog corruption)"
13562 )));
13563 };
13564 new_ctes.push(spg_sql::ast::Cte {
13565 name: name.clone(),
13566 body: spg_sql::ast::CteBody::Select(body),
13567 recursive: false,
13568 column_overrides: view.columns.clone(),
13569 search: None,
13570 cycle: None,
13571 });
13572 }
13573 let mut out = stmt.clone();
13574 // Prepend so view CTEs are visible to caller-supplied CTEs.
13575 new_ctes.extend(out.ctes);
13576 out.ctes = new_ctes;
13577 Ok(Some(out))
13578 }
13579
13580 /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
13581 /// any partition-parent table, rewrite the SELECT so each parent
13582 /// reference resolves to a CTE whose body is a `UNION ALL` over the
13583 /// children that pass the WHERE-derived partition-key range. Returns
13584 /// `None`(no rewrite needed)when no parent is referenced or all
13585 /// references are shadowed by a same-name CTE.
13586 ///
13587 /// Pruning vocabulary at v7.37.6-B:
13588 /// * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
13589 /// and `<key> BETWEEN literal AND literal`.
13590 /// * Anything outside that(OR / nested IN / function call on the
13591 /// key)defaults to "no pruning" — every child + DEFAULT lands
13592 /// in the UNION. Correctness is preserved; only the plan size
13593 /// widens.
13594 fn expand_partition_parents_in_select(
13595 &self,
13596 stmt: &SelectStatement,
13597 ) -> Result<Option<SelectStatement>, EngineError> {
13598 let cat = self.active_catalog();
13599 let Some(from) = &stmt.from else {
13600 return Ok(None);
13601 };
13602 let mut parent_refs: Vec<String> = Vec::new();
13603 collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
13604 for j in &from.joins {
13605 collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
13606 }
13607 // Drop names shadowed by a CTE on the same SELECT(PG semantics
13608 // — same as view expansion above).
13609 parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
13610 if parent_refs.is_empty() {
13611 return Ok(None);
13612 }
13613 // Synthesise a CTE name per parent so the existing
13614 // "CTE shadows a real table" guard doesn't fire (the parent
13615 // IS a real table in the catalog, unlike VIEW expansion's
13616 // case). The FROM-clause TableRef walker below rewrites
13617 // every parent reference to point at the synthetic CTE.
13618 let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
13619 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
13620 let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
13621 for parent_name in &parent_refs {
13622 // No children = no rewrite. The parent itself is a real
13623 // (empty-rows) table — the regular FROM-resolution path
13624 // will scan it and return 0 rows, matching the
13625 // "partition parent with no children" plan. Skipping the
13626 // CTE here also avoids `SELECT * FROM parent` re-entering
13627 // this rewrite on the synthetic body (infinite recursion).
13628 let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
13629 continue;
13630 };
13631 new_ctes.push(spg_sql::ast::Cte {
13632 name: synth_name(parent_name),
13633 body: spg_sql::ast::CteBody::Select(body),
13634 recursive: false,
13635 column_overrides: Vec::new(),
13636 search: None,
13637 cycle: None,
13638 });
13639 expanded_parents.push(parent_name.clone());
13640 }
13641 if expanded_parents.is_empty() {
13642 return Ok(None);
13643 }
13644 let mut out = stmt.clone();
13645 if let Some(from) = out.from.as_mut() {
13646 rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
13647 for j in &mut from.joins {
13648 rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
13649 }
13650 }
13651 new_ctes.extend(out.ctes);
13652 out.ctes = new_ctes;
13653 Ok(Some(out))
13654 }
13655
13656 /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
13657 /// Children include every overlap-hit `Range` plus(always)the
13658 /// `Default` child(if any). Returns `Ok(None)` when no children
13659 /// would survive — caller skips the CTE injection and lets the
13660 /// parent fall through to the regular(empty-rows)scan path,
13661 /// avoiding the infinite recursion that an empty-body CTE
13662 /// referencing the parent name would trigger.
13663 /// v7.37.16 (16.10) — public helper invoked from explain.rs to
13664 /// surface "which children survive the WHERE-clause prune" in
13665 /// EXPLAIN output. Returns `None` when `parent_name` isn't
13666 /// actually a partition parent; otherwise returns the list of
13667 /// children the planner would scan (same algorithm as
13668 /// [`Self::build_partition_parent_union_body`] but without the
13669 /// SQL re-parse).
13670 /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
13671 /// expression (the PG-shaped EXPLAIN's scan builder has no full
13672 /// SelectStatement in hand). Wraps the original by synthesising a
13673 /// minimal statement carrying just the predicate.
13674 pub(crate) fn explain_partition_kept_children_by_where(
13675 &self,
13676 parent_name: &str,
13677 where_: Option<&spg_sql::ast::Expr>,
13678 ) -> Option<Vec<alloc::string::String>> {
13679 let mut synth = SelectStatement::default();
13680 synth.where_ = where_.cloned();
13681 self.explain_partition_kept_children(parent_name, &synth)
13682 }
13683
13684 pub(crate) fn explain_partition_kept_children(
13685 &self,
13686 parent_name: &str,
13687 outer: &SelectStatement,
13688 ) -> Option<Vec<alloc::string::String>> {
13689 use spg_storage::PartitionRole;
13690 let cat = self.active_catalog();
13691 let parent = cat.get(parent_name)?;
13692 let (key_position, parent_kind) = match &parent.schema().partition_role {
13693 Some(PartitionRole::Parent {
13694 key_column_positions,
13695 kind,
13696 ..
13697 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13698 _ => return None,
13699 };
13700 let key_col_name = parent.schema().columns[key_position].name.clone();
13701 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13702 Some(expr) => extract_key_range(expr, &key_col_name),
13703 None => (None, None),
13704 };
13705 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13706 Some(expr) => extract_key_eq_value(expr, &key_col_name),
13707 None => None,
13708 };
13709 let children = crate::partition::children_of_parent(cat, parent_name);
13710 let mut kept: Vec<alloc::string::String> = Vec::new();
13711 let mut default_child: Option<alloc::string::String> = None;
13712 for child_name in &children {
13713 let Some(child) = cat.get(child_name) else {
13714 continue;
13715 };
13716 match &child.schema().partition_role {
13717 Some(PartitionRole::Range { lower, upper, .. }) => {
13718 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13719 kept.push(child_name.clone());
13720 }
13721 }
13722 Some(PartitionRole::List { values, .. }) => match &eq_value {
13723 Some(v) => {
13724 if values.iter().any(|b| b.equals_value(v)) {
13725 kept.push(child_name.clone());
13726 }
13727 }
13728 None => kept.push(child_name.clone()),
13729 },
13730 Some(PartitionRole::Hash {
13731 modulus, remainder, ..
13732 }) => match &eq_value {
13733 Some(v) => {
13734 let h = crate::partition::pg_compatible_hash(v);
13735 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13736 kept.push(child_name.clone());
13737 }
13738 }
13739 None => kept.push(child_name.clone()),
13740 },
13741 Some(PartitionRole::Default { .. }) => {
13742 default_child = Some(child_name.clone());
13743 }
13744 _ => {}
13745 }
13746 }
13747 let _ = parent_kind;
13748 if let Some(d) = default_child {
13749 if kept.is_empty() || eq_value.is_none() {
13750 kept.push(d);
13751 }
13752 }
13753 Some(kept)
13754 }
13755
13756 fn build_partition_parent_union_body(
13757 &self,
13758 parent_name: &str,
13759 outer: &SelectStatement,
13760 ) -> Result<Option<SelectStatement>, EngineError> {
13761 use spg_storage::PartitionRole;
13762 let cat = self.active_catalog();
13763 let parent = cat.get(parent_name).ok_or_else(|| {
13764 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13765 "partition parent {parent_name:?} disappeared mid-expansion"
13766 )))
13767 })?;
13768 let (key_position, parent_kind) = match &parent.schema().partition_role {
13769 Some(PartitionRole::Parent {
13770 key_column_positions,
13771 kind,
13772 ..
13773 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13774 // v7.39 (round 645) — an INHERITANCE parent, which has no
13775 // role of its own: the relationship is recorded only in the
13776 // children. Three things differ from a partition parent and
13777 // all three are in this body.
13778 //
13779 // * The parent HOLDS ROWS, so it is a term of the union —
13780 // `FROM ONLY`, or expanding it would recurse.
13781 // * There is no partition key, so there is nothing to
13782 // prune: every child is a term.
13783 // * A child may declare columns of its own, so the terms
13784 // name the PARENT's columns rather than `*`. PG's
13785 // `SELECT * FROM parent` returns the parent's shape.
13786 //
13787 // Answered from this match rather than a branch before it —
13788 // round 644 measured what an extra early return beside an
13789 // existing test costs in this file.
13790 _ if crate::partition::has_inheritance_children(cat, parent_name) => {
13791 let cols = parent
13792 .schema()
13793 .columns
13794 .iter()
13795 .map(|c| quote_ident_for_sql(&c.name))
13796 .collect::<Vec<_>>()
13797 .join(", ");
13798 let carry_sys = references_ctid(outer);
13799 let sys = if carry_sys {
13800 let mut t = alloc::string::String::new();
13801 for s in SYSTEM_COLUMNS {
13802 t.push_str(", ");
13803 t.push_str(s);
13804 }
13805 t
13806 } else {
13807 alloc::string::String::new()
13808 };
13809 let mut body = alloc::format!(
13810 "SELECT {cols}{sys} FROM ONLY {}",
13811 quote_ident_for_sql(parent_name)
13812 );
13813 for child in crate::partition::children_of_parent(cat, parent_name) {
13814 body.push_str(&alloc::format!(
13815 " UNION ALL SELECT {cols}{sys} FROM {}",
13816 quote_ident_for_sql(&child)
13817 ));
13818 }
13819 return parse_select_or_corrupt(&body).map(Some);
13820 }
13821 _ => {
13822 return Err(EngineError::Unsupported(alloc::format!(
13823 "partition expansion: {parent_name:?} is not a parent"
13824 )));
13825 }
13826 };
13827 let key_col_name = parent.schema().columns[key_position].name.clone();
13828 // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
13829 // off the WHERE; for LIST / HASH we extract a single `=`
13830 // literal (and the rest of the planner falls back to "keep
13831 // every child" — same conservative path as 16.1/16.2).
13832 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13833 Some(expr) => extract_key_range(expr, &key_col_name),
13834 None => (None, None),
13835 };
13836 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13837 Some(expr) => extract_key_eq_value(expr, &key_col_name),
13838 None => None,
13839 };
13840 let children = crate::partition::children_of_parent(cat, parent_name);
13841 let mut kept: Vec<String> = Vec::new();
13842 let mut default_child: Option<String> = None;
13843 // First pass — apply per-strategy gates, defer DEFAULT until
13844 // we know whether some non-DEFAULT child matched.
13845 for child_name in &children {
13846 let Some(child) = cat.get(child_name) else {
13847 continue;
13848 };
13849 match &child.schema().partition_role {
13850 Some(PartitionRole::Range { lower, upper, .. }) => {
13851 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13852 kept.push(child_name.clone());
13853 }
13854 }
13855 // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
13856 // = <lit>`, only the child whose values contain that
13857 // literal survives. Otherwise (no equality predicate
13858 // or planner couldn't extract one) keep the child
13859 // conservatively.
13860 Some(PartitionRole::List { values, .. }) => match &eq_value {
13861 Some(v) => {
13862 if values.iter().any(|b| b.equals_value(v)) {
13863 kept.push(child_name.clone());
13864 }
13865 }
13866 None => kept.push(child_name.clone()),
13867 },
13868 // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
13869 // we know the residue class deterministically, so
13870 // only the matching REMAINDER child survives.
13871 Some(PartitionRole::Hash {
13872 modulus, remainder, ..
13873 }) => match &eq_value {
13874 Some(v) => {
13875 let h = crate::partition::pg_compatible_hash(v);
13876 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13877 kept.push(child_name.clone());
13878 }
13879 }
13880 None => kept.push(child_name.clone()),
13881 },
13882 Some(PartitionRole::Default { .. }) => {
13883 default_child = Some(child_name.clone());
13884 }
13885 _ => {}
13886 }
13887 }
13888 // PG-style DEFAULT semantics: the DEFAULT child must be
13889 // scanned iff some row could fall outside every concrete
13890 // child's bound predicate. We approximate that as "no
13891 // concrete child matched" (== full prune) — strictly
13892 // conservative for LIST / HASH (DEFAULT also catches rows
13893 // outside the union of value-sets / residues), and matches
13894 // PG for the equality case where we *do* know the routing
13895 // outcome.
13896 let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
13897 if let Some(d) = default_child {
13898 if kept.is_empty() {
13899 kept.push(d);
13900 } else if eq_value.is_none() {
13901 // Without an equality literal, the DEFAULT child may
13902 // still hold matching rows (e.g. LIKE on TEXT keys
13903 // for which a LIST partition exists). Keep it.
13904 kept.push(d);
13905 }
13906 }
13907 // Build the UNION ALL body text and re-parse — keeps the
13908 // rewrite expressible in surface SQL so the engine's existing
13909 // parser path handles the AST shape uniformly.
13910 if kept.is_empty() {
13911 // No children survive — caller falls back to scanning the
13912 // (empty) parent table. Returning None here is what
13913 // prevents the synthetic CTE from referring back to the
13914 // parent name and re-entering this rewrite pass.
13915 let _ = parent_name;
13916 return Ok(None);
13917 }
13918 // v7.39 (round 622, S05a) — the system columns of the CHILD the row
13919 // actually lives in.
13920 //
13921 // The parent is read through a synthetic CTE, so a `tableoid` on it
13922 // resolved against that CTE: every row of every child reported
13923 // `__spg_partition_pm`, an internal name no user ever typed, where
13924 // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
13925 // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
13926 // one asks "which partition is this row in", answering 0 rows where
13927 // PG answers 1. `ctid` had the same shape: it numbered the CTE's
13928 // output, so rows in different children got distinct ctids instead
13929 // of each child's own physical position.
13930 //
13931 // Naming them in the term is what carries them: the child scan
13932 // materialises its own six because the statement now references
13933 // them, and they land in SYSTEM_COLUMNS order right after the user
13934 // columns — the exact layout the positional `*` skip already
13935 // expects. Only done when the outer statement asks for one, so a
13936 // plain `SELECT * FROM parent` scans exactly what it scanned.
13937 let carry_sys = references_ctid(outer);
13938 let mut body = alloc::string::String::new();
13939 for (i, child_name) in kept.iter().enumerate() {
13940 if i > 0 {
13941 body.push_str(" UNION ALL ");
13942 }
13943 body.push_str("SELECT *");
13944 if carry_sys {
13945 for sys in SYSTEM_COLUMNS {
13946 body.push_str(", ");
13947 body.push_str(sys);
13948 }
13949 }
13950 body.push_str(" FROM ");
13951 body.push_str("e_ident_for_sql(child_name));
13952 }
13953 parse_select_or_corrupt(&body).map(Some)
13954 }
13955}
13956
13957/// Rewrite a `TableRef` pointing at a partition parent so it
13958/// references the synthetic CTE created by the expansion. If the
13959/// original ref had no alias, preserve the parent name as an alias
13960/// so column references like `events_partitioned.received_at`
13961/// keep resolving.
13962fn rewrite_partition_parent_table_ref(
13963 t: &mut spg_sql::ast::TableRef,
13964 parents: &[alloc::string::String],
13965 synth_name: &impl Fn(&str) -> alloc::string::String,
13966) {
13967 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13968 return;
13969 }
13970 // v7.39 (round 644) — an ONLY reference stays pointed at the parent
13971 // itself. The rewrite is keyed on the NAME, so in
13972 // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
13973 // parent list and this then rewrote BOTH — including the one that
13974 // asked not to descend. PG answers 0 for that join; SPG answered 2.
13975 // Folded into the existing test — see the note in
13976 // `collect_partition_parent_refs` for what a separate one cost.
13977 if t.only || !parents.iter().any(|p| p == &t.name) {
13978 return;
13979 }
13980 if t.alias.is_none() {
13981 t.alias = Some(t.name.clone());
13982 }
13983 t.name = synth_name(&t.name);
13984}
13985
13986/// Walk a `TableRef` and push its `name` if it resolves to a partition
13987/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
13988/// `generate_series_args` references — those aren't catalog tables.
13989fn collect_partition_parent_refs(
13990 t: &spg_sql::ast::TableRef,
13991 cat: &spg_storage::Catalog,
13992 out: &mut Vec<alloc::string::String>,
13993) {
13994 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13995 return;
13996 }
13997 // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
13998 // The keyword used to be absorbed at parse time, so this fanned out
13999 // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
14000 // answered 2 where PG answers 0.
14001 //
14002 // Folded into the existing test rather than given an early return of
14003 // its own: as two extra lines in this function's body it cost
14004 // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
14005 // outside the panel. Rounds 641 and 643 met the same wall from the
14006 // other two directions — adding to a hot function and taking away
14007 // from a cold one. What goes in a body near the row loop is a
14008 // codegen decision whatever its shape.
14009 if !t.only && crate::partition::has_children(cat, &t.name) {
14010 out.push(t.name.clone());
14011 }
14012}
14013
14014/// v7.37.6-B partition-key range derived from a WHERE expression.
14015/// `i64` microseconds since epoch with the same sign convention as
14016/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
14017/// / `=`),`false` ⇒ exclusive(`>` / `<`).
14018#[derive(Debug, Clone, Copy)]
14019pub(crate) struct PartitionFilterBound {
14020 pub micros: i64,
14021 pub inclusive: bool,
14022}
14023
14024/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
14025/// shapes; tighten the running lo / hi as we go. Anything outside that
14026/// (OR / nested calls / non-key columns)is ignored — caller treats
14027/// `None` as "no constraint on that side."
14028fn extract_key_range(
14029 expr: &spg_sql::ast::Expr,
14030 key_col: &str,
14031) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
14032 let mut lo: Option<PartitionFilterBound> = None;
14033 let mut hi: Option<PartitionFilterBound> = None;
14034 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14035 while let Some(e) = stack.pop() {
14036 match e {
14037 spg_sql::ast::Expr::Binary {
14038 lhs,
14039 op: spg_sql::ast::BinOp::And,
14040 rhs,
14041 } => {
14042 stack.push(lhs);
14043 stack.push(rhs);
14044 }
14045 // BETWEEN is desugared at parse time into `lhs >= low AND
14046 // lhs <= high`, so it lands here as two regular Binary
14047 // arms via the AND walker above.
14048 spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
14049 let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
14050 (Some(lhs.as_ref()), rhs.as_ref(), false)
14051 } else if is_column_ref(rhs, key_col) {
14052 (Some(rhs.as_ref()), lhs.as_ref(), true)
14053 } else {
14054 (None, lhs.as_ref(), false)
14055 };
14056 if col_ref.is_none() {
14057 continue;
14058 }
14059 let Some(lit) = literal_to_micros(lit_side) else {
14060 continue;
14061 };
14062 use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
14063 let effective_op = if swapped {
14064 match op {
14065 Lt => Gt,
14066 LtEq => GtEq,
14067 Gt => Lt,
14068 GtEq => LtEq,
14069 other => *other,
14070 }
14071 } else {
14072 *op
14073 };
14074 match effective_op {
14075 Eq => {
14076 tighten_lo(
14077 &mut lo,
14078 PartitionFilterBound {
14079 micros: lit,
14080 inclusive: true,
14081 },
14082 );
14083 tighten_hi(
14084 &mut hi,
14085 PartitionFilterBound {
14086 micros: lit,
14087 inclusive: true,
14088 },
14089 );
14090 }
14091 GtEq => {
14092 tighten_lo(
14093 &mut lo,
14094 PartitionFilterBound {
14095 micros: lit,
14096 inclusive: true,
14097 },
14098 );
14099 }
14100 Gt => {
14101 tighten_lo(
14102 &mut lo,
14103 PartitionFilterBound {
14104 micros: lit,
14105 inclusive: false,
14106 },
14107 );
14108 }
14109 LtEq => {
14110 tighten_hi(
14111 &mut hi,
14112 PartitionFilterBound {
14113 micros: lit,
14114 inclusive: true,
14115 },
14116 );
14117 }
14118 Lt => {
14119 tighten_hi(
14120 &mut hi,
14121 PartitionFilterBound {
14122 micros: lit,
14123 inclusive: false,
14124 },
14125 );
14126 }
14127 _ => {}
14128 }
14129 }
14130 _ => {}
14131 }
14132 }
14133 (lo, hi)
14134}
14135
14136fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14137 match slot {
14138 None => *slot = Some(new),
14139 Some(cur) => {
14140 if new.micros > cur.micros
14141 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14142 {
14143 *slot = Some(new);
14144 }
14145 }
14146 }
14147}
14148
14149fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14150 match slot {
14151 None => *slot = Some(new),
14152 Some(cur) => {
14153 if new.micros < cur.micros
14154 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14155 {
14156 *slot = Some(new);
14157 }
14158 }
14159 }
14160}
14161
14162fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
14163 if let spg_sql::ast::Expr::Column(c) = e {
14164 c.name.eq_ignore_ascii_case(key_col)
14165 } else {
14166 false
14167 }
14168}
14169
14170/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
14171/// `key_col = <literal>` predicate out for LIST/HASH partition
14172/// pruning. Returns `None` when no equality literal can be lifted
14173/// (planner then keeps every child — correctness preserved). The
14174/// returned `Value<'static>` is an owned coercion so the caller can
14175/// outlive any AST node it was extracted from.
14176pub(crate) fn extract_key_eq_value(
14177 expr: &spg_sql::ast::Expr,
14178 key_col: &str,
14179) -> Option<spg_storage::Value<'static>> {
14180 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14181 while let Some(e) = stack.pop() {
14182 match e {
14183 spg_sql::ast::Expr::Binary {
14184 lhs,
14185 op: spg_sql::ast::BinOp::And,
14186 rhs,
14187 } => {
14188 stack.push(lhs);
14189 stack.push(rhs);
14190 }
14191 spg_sql::ast::Expr::Binary {
14192 lhs,
14193 op: spg_sql::ast::BinOp::Eq,
14194 rhs,
14195 } => {
14196 let lit_side = if is_column_ref(lhs, key_col) {
14197 rhs.as_ref()
14198 } else if is_column_ref(rhs, key_col) {
14199 lhs.as_ref()
14200 } else {
14201 continue;
14202 };
14203 let cloned = lit_side.clone();
14204 let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
14205 continue;
14206 };
14207 // Coerce to an owned Value<'static> so the caller
14208 // can hold it past the WHERE expression's lifetime.
14209 let owned: spg_storage::Value<'static> = match v {
14210 spg_storage::Value::Text(s) => {
14211 spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
14212 }
14213 spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
14214 spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
14215 spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
14216 spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
14217 spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
14218 spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
14219 spg_storage::Value::Null => spg_storage::Value::Null,
14220 // Anything else (Vector / Json / Bytes / Numeric /
14221 // arrays / interval / …) isn't a current partition
14222 // key type; skip without pruning.
14223 _ => continue,
14224 };
14225 return Some(owned);
14226 }
14227 _ => {}
14228 }
14229 }
14230 None
14231}
14232
14233/// Coerce a literal Expr(after the parser folded sequence calls etc.)
14234/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
14235/// pruning and routing agree on the literal vocabulary. Returns
14236/// `None` when the literal isn't recognised(planner then skips
14237/// pruning on that branch — correctness preserved).
14238fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
14239 let cloned = e.clone();
14240 let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
14241 match value {
14242 spg_storage::Value::Timestamp(m) => Some(m),
14243 spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
14244 spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
14245 _ => None,
14246 }
14247}
14248
14249/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
14250/// satisfying the WHERE-derived filter range. PG-style half-open:
14251/// child upper exclusive. Filter inclusivity is honoured per-bound.
14252fn range_satisfies_filter(
14253 range_lo: &spg_storage::PartitionBound,
14254 range_hi: &spg_storage::PartitionBound,
14255 filter_lo: Option<&PartitionFilterBound>,
14256 filter_hi: Option<&PartitionFilterBound>,
14257) -> bool {
14258 use spg_storage::PartitionBound;
14259 // For each filter side, reject children that can't host any row
14260 // matching the predicate.
14261 if let Some(lo) = filter_lo {
14262 // child upper bound vs filter lower:
14263 // if filter is x >= L, child rejects iff child.hi <= L
14264 // if filter is x > L, child rejects iff child.hi <= L
14265 // (child.hi exclusive, so equality with L still rejects)
14266 match range_hi {
14267 PartitionBound::MinValue => return false,
14268 PartitionBound::MaxValue => {}
14269 PartitionBound::TimestampTz(hi) => {
14270 if *hi <= lo.micros {
14271 return false;
14272 }
14273 }
14274 // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
14275 // matched against TIMESTAMPTZ filters here; keep child
14276 // (conservative: don't prune).
14277 PartitionBound::BigInt(_)
14278 | PartitionBound::Int(_)
14279 | PartitionBound::SmallInt(_)
14280 | PartitionBound::Date(_)
14281 | PartitionBound::Text(_) => {}
14282 }
14283 }
14284 if let Some(hi) = filter_hi {
14285 // child lower bound vs filter upper:
14286 // if filter is x <= U, child rejects iff child.lo > U
14287 // if filter is x < U, child rejects iff child.lo >= U
14288 match range_lo {
14289 PartitionBound::MaxValue => return false,
14290 PartitionBound::MinValue => {}
14291 PartitionBound::TimestampTz(lo) => {
14292 let rejects = if hi.inclusive {
14293 *lo > hi.micros
14294 } else {
14295 *lo >= hi.micros
14296 };
14297 if rejects {
14298 return false;
14299 }
14300 }
14301 PartitionBound::BigInt(_)
14302 | PartitionBound::Int(_)
14303 | PartitionBound::SmallInt(_)
14304 | PartitionBound::Date(_)
14305 | PartitionBound::Text(_) => {}
14306 }
14307 }
14308 true
14309}
14310
14311fn quote_ident_for_sql(name: &str) -> alloc::string::String {
14312 // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
14313 // identifier, otherwise quoted). Conservative: always quote so
14314 // children with reserved names round-trip safely through the
14315 // CTE-body parse.
14316 let mut out = alloc::string::String::with_capacity(name.len() + 2);
14317 out.push('"');
14318 for c in name.chars() {
14319 if c == '"' {
14320 out.push('"');
14321 }
14322 out.push(c);
14323 }
14324 out.push('"');
14325 out
14326}
14327
14328fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
14329 let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
14330 EngineError::Unsupported(alloc::format!(
14331 "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
14332 ))
14333 })?;
14334 let Statement::Select(body) = parsed else {
14335 return Err(EngineError::Unsupported(alloc::format!(
14336 "partition expansion: generated SQL {sql:?} is not a SELECT"
14337 )));
14338 };
14339 Ok(body)
14340}
14341
14342/// v7.39 (read01 round 65/66) — the column shape a set-returning function
14343/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
14344/// yields ONE column named after the call's alias when there is one (`FROM
14345/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
14346/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
14347fn setof_column_shape_from(
14348 declared: &str,
14349 name: &str,
14350 alias: Option<&str>,
14351 got: &[ColumnSchema],
14352) -> alloc::vec::Vec<ColumnSchema> {
14353 let upper = declared.to_ascii_uppercase();
14354 if upper.starts_with("TABLE(") {
14355 let raw = &declared["TABLE(".len()..declared.len() - 1];
14356 return raw
14357 .split(',')
14358 .zip(got.iter())
14359 .map(|(decl, g)| {
14360 let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
14361 ColumnSchema::new(cname.to_string(), g.ty, true)
14362 })
14363 .collect();
14364 }
14365 let cname = alias.unwrap_or(name);
14366 got.first()
14367 .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
14368 .unwrap_or_default()
14369}
14370
14371/// The plpgsql twin: the interpreter hands back raw value rows, so the types
14372/// come off the first row.
14373fn setof_column_shape(
14374 declared: &str,
14375 name: &str,
14376 alias: Option<&str>,
14377 first_row: Option<&alloc::vec::Vec<Value<'static>>>,
14378) -> alloc::vec::Vec<ColumnSchema> {
14379 let got: alloc::vec::Vec<ColumnSchema> = first_row
14380 .map(|r| {
14381 r.iter()
14382 .enumerate()
14383 .map(|(i, v)| {
14384 ColumnSchema::new(
14385 alloc::format!("col{i}"),
14386 v.data_type().unwrap_or(DataType::Text),
14387 true,
14388 )
14389 })
14390 .collect()
14391 })
14392 .unwrap_or_default();
14393 setof_column_shape_from(declared, name, alias, &got)
14394}
14395
14396/// v7.39 (read01 round 67) — expand every set-returning call in a target list
14397/// for ONE input row, PG's ProjectSet semantics.
14398///
14399/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
14400/// output has as many rows as the LONGEST of them, and a shorter one is padded
14401/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
14402/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
14403/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
14404/// is zero rows, not one NULL row.
14405///
14406/// Non-SRF items repeat, evaluated once per output row from the same input row.
14407/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
14408/// used to reach the scalar function dispatcher, which reported the aggregate as
14409/// an *unknown function* — the same "symptom two layers above the cause" shape
14410/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
14411/// sees a call, not the clause it came from. The statement knows.
14412/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
14413/// clause may appear.
14414///
14415/// PG rejects `FOR UPDATE` on exactly the shapes that have no
14416/// identifiable base row to lock, each with its own wording. SPG
14417/// accepted all of them and locked nothing, so a query that PG refuses
14418/// outright came back looking like it had taken locks.
14419///
14420/// Every wording read off live PG 18.4.
14421impl crate::Engine {
14422 /// v7.39.2 — a column name in WHERE / ORDER BY / GROUP BY / HAVING
14423 /// that names nothing is refused before the scan, not when a row
14424 /// reaches it.
14425 ///
14426 /// The projection resolves its names eagerly; a predicate only meets
14427 /// them per row. So on an EMPTY table `SELECT a FROM t WHERE nosuch
14428 /// = 1` answered zero rows and no error, and the same statement over
14429 /// a table with one row raised. Measured on PostgreSQL 18.6 and
14430 /// MySQL 9.7.2: both refuse it whatever the row count. A typo in a
14431 /// predicate therefore passed a test written against an empty
14432 /// fixture and failed in production — or, worse, ran nightly over an
14433 /// empty window and reported nothing.
14434 ///
14435 /// Deliberately narrow: ONE plain base table, nothing else. A join,
14436 /// a CTE, a set operation, a lateral or function source, or a
14437 /// subquery in the clause all bring a second scope into which a name
14438 /// may legitimately resolve, and refusing one of those would be a
14439 /// worse defect than the one this closes. Those shapes keep the
14440 /// old behaviour; the walk below does not descend into a subquery
14441 /// for the same reason.
14442 /// v7.39.2 — refuse a call whose argument count no overload accepts,
14443 /// BEFORE the scan rather than per row.
14444 ///
14445 /// `SELECT lower(t, n) FROM t` answered zero rows and no error over
14446 /// an EMPTY table and raised the moment the table had one row in it,
14447 /// because the arity check lives inside the row-time dispatch. It is
14448 /// the same shape as the unknown-column-in-a-predicate defect closed
14449 /// earlier in this release, and it hides in the same place: a query
14450 /// written against an empty fixture passes its test.
14451 ///
14452 /// The accepted counts come from `eval::arity::REFUSED_ARITIES`,
14453 /// which is derived by asking the dispatch itself offline and can
14454 /// only ever UNDER-refuse — see that file for why the two other
14455 /// candidate oracles were refuted.
14456 /// v7.39.3 — MySQL's column names are case-insensitive; PostgreSQL's
14457 /// quoted ones are not. See `EvalContext::col_eq`.
14458 fn col_name_eq(&self, a: &str, b: &str) -> bool {
14459 if self.speaks_mysql {
14460 a.eq_ignore_ascii_case(b)
14461 } else {
14462 a == b
14463 }
14464 }
14465
14466 pub(crate) fn validate_function_arity(
14467 &self,
14468 stmt: &SelectStatement,
14469 ) -> Result<(), EngineError> {
14470 let mut calls: Vec<(alloc::string::String, Vec<Expr>)> = Vec::new();
14471 for it in &stmt.items {
14472 if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14473 collect_function_calls(expr, &mut calls);
14474 }
14475 }
14476 if let Some(w) = &stmt.where_ {
14477 collect_function_calls(w, &mut calls);
14478 }
14479 for o in &stmt.order_by {
14480 collect_function_calls(&o.expr, &mut calls);
14481 }
14482 // The columns a name in this statement could resolve to. Only
14483 // plain base tables; anything else and the types are not
14484 // statically knowable, so nothing is refused early.
14485 let cat = self.active_catalog();
14486 let mut cols: Vec<ColumnSchema> = Vec::new();
14487 if let Some(from) = &stmt.from {
14488 for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14489 if let Some(table) = cat.get(&t.name) {
14490 cols.extend(table.schema().columns.iter().cloned());
14491 }
14492 }
14493 }
14494 for (name, args) in calls {
14495 let Ok(i) = crate::eval::arity::REFUSED_ARITIES
14496 .binary_search_by(|(n, _)| (*n).cmp(name.as_str()))
14497 else {
14498 continue;
14499 };
14500 if !crate::eval::arity::REFUSED_ARITIES[i]
14501 .1
14502 .contains(&args.len())
14503 {
14504 continue;
14505 }
14506 // v7.39.2 — PostgreSQL names the SIGNATURE it could not
14507 // match, and before the scan there are no values to read a
14508 // type from. Where every argument's type is knowable
14509 // statically — a column of a source table, or a literal —
14510 // the sentence is PostgreSQL's exactly; where one is not,
14511 // this leaves the call to the row-time raise, which has the
14512 // values. Refusing early with a WORSE message would trade
14513 // one defect for another.
14514 let mut types: Vec<alloc::string::String> = Vec::new();
14515 for a in &args {
14516 let Some(t) = static_arg_type(a, &cols) else {
14517 types.clear();
14518 break;
14519 };
14520 types.push(t);
14521 }
14522 if types.len() != args.len() {
14523 continue;
14524 }
14525 return Err(EngineError::Eval(EvalError::WrongArity {
14526 name,
14527 types: types.join(", "),
14528 }));
14529 }
14530 Ok(())
14531 }
14532
14533 pub(crate) fn validate_clause_columns(
14534 &self,
14535 stmt: &SelectStatement,
14536 ) -> Result<(), EngineError> {
14537 let Some(from) = &stmt.from else {
14538 return Ok(());
14539 };
14540 if !stmt.ctes.is_empty() {
14541 return Ok(());
14542 }
14543 // v7.39.2 — every source, not just the first. A join is checkable
14544 // for the same reason one table is: with no CTE and no
14545 // subquery-shaped source, a bare name has to come from one of
14546 // them. Refusing the check for joins left `SELECT … FROM a JOIN b
14547 // … WHERE nosuch = 1` labelled `'field list'` where MySQL 9.7.2
14548 // says `'where clause'`.
14549 let plain = |t: &spg_sql::ast::TableRef| -> bool {
14550 t.unnest_expr.is_none()
14551 && t.generate_series_args.is_none()
14552 && t.lateral_subquery.is_none()
14553 && t.jsonb_each_text_arg.is_none()
14554 && t.table_fn_call.is_none()
14555 && t.rows_from.is_none()
14556 && t.json_table.is_none()
14557 && !t.scalar_fn_item
14558 };
14559 let cat = self.active_catalog();
14560 let mut sources: Vec<(String, &spg_storage::Table)> = Vec::new();
14561 for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14562 if !plain(t) {
14563 return Ok(());
14564 }
14565 let Some(table) = cat.get(&t.name) else {
14566 return Ok(());
14567 };
14568 sources.push((t.alias.clone().unwrap_or_else(|| t.name.clone()), table));
14569 }
14570 let known = |c: &spg_sql::ast::ColumnName| -> bool {
14571 // A system column is not in a table's list and is a perfectly
14572 // good predicate: `WHERE ctid = '(0,4)'::tid` and `WHERE
14573 // tableoid::regclass::text = 'pm_a'` are both real, and the
14574 // first draft of this check refused them. The e2e suite said
14575 // so immediately, which is what it is for.
14576 if is_system_column(&c.name) {
14577 return true;
14578 }
14579 if let Some(q) = &c.qualifier {
14580 // A qualifier must name one of this statement's sources,
14581 // and that source must carry the column. An alias
14582 // REPLACES the written name, which is PostgreSQL's rule
14583 // and MySQL's: `FROM pg_cast c WHERE pg_cast.oid <> 0`
14584 // is an error on both.
14585 return match sources.iter().find(|(a, _)| a == q) {
14586 Some((_, t)) => t
14587 .schema()
14588 .columns
14589 .iter()
14590 .any(|sc| self.col_name_eq(&sc.name, &c.name)),
14591 None => false,
14592 };
14593 }
14594 sources
14595 .iter()
14596 .any(|(_, t)| {
14597 t.schema()
14598 .columns
14599 .iter()
14600 .any(|sc| self.col_name_eq(&sc.name, &c.name))
14601 })
14602 // An output name the statement itself defines: ORDER BY,
14603 // GROUP BY and HAVING may all name one.
14604 || stmt.items.iter().any(|it| match it {
14605 SelectItem::Expr { expr, alias } => {
14606 alias.as_deref() == Some(c.name.as_str())
14607 || matches!(expr, Expr::Column(pc) if pc.name == c.name)
14608 }
14609 _ => false,
14610 })
14611 };
14612 // v7.39.2 — the CLAUSE travels with the reference, because MySQL
14613 // names it: `Unknown column 'x' in 'where clause'`, `'order
14614 // clause'`, `'group statement'`, `'having clause'`. Measured on
14615 // 9.7.2, and a driver's error handling reads the sentence as well
14616 // as the number. PostgreSQL says only `column "x" does not
14617 // exist`, with no clause, so its wording is unchanged.
14618 //
14619 // This walk is the only place the clause is still known: by the
14620 // time a row-time resolver meets the name, the expression has
14621 // been detached from the statement that held it.
14622 let mut refs: Vec<(spg_sql::ast::ColumnName, &'static str)> = Vec::new();
14623 let mut push = |e: &Expr, ctx: &'static str, out: &mut Vec<_>| {
14624 let mut here: Vec<spg_sql::ast::ColumnName> = Vec::new();
14625 collect_plain_column_refs(e, &mut here);
14626 out.extend(here.into_iter().map(|c| (c, ctx)));
14627 };
14628 if let Some(w) = &stmt.where_ {
14629 push(w, "where clause", &mut refs);
14630 }
14631 if let Some(g) = &stmt.group_by {
14632 for e in g {
14633 push(e, "group statement", &mut refs);
14634 }
14635 }
14636 if let Some(h) = &stmt.having {
14637 push(h, "having clause", &mut refs);
14638 }
14639 for o in &stmt.order_by {
14640 push(&o.expr, "order clause", &mut refs);
14641 }
14642 // v7.39.2 — and the join predicates, which MySQL calls the `on
14643 // clause`. Measured on 9.7.2: `Unknown column 'j1.nosuch' in 'on
14644 // clause'`, qualifier and all.
14645 for j in &from.joins {
14646 if let Some(on) = &j.on {
14647 push(on, "on clause", &mut refs);
14648 }
14649 }
14650 for (c, ctx) in &refs {
14651 if !known(c) {
14652 if self.speaks_mysql {
14653 // The QUALIFIER travels with it: MySQL 9.7.2 answers
14654 // `Unknown column 'j1.nosuch' in 'on clause'`, not the
14655 // bare name. Measured.
14656 let shown = match &c.qualifier {
14657 Some(q) => alloc::format!("{q}.{}", c.name),
14658 None => c.name.clone(),
14659 };
14660 return Err(EngineError::Eval(EvalError::TypeMismatch {
14661 detail: alloc::format!("Unknown column '{shown}' in '{ctx}'"),
14662 }));
14663 }
14664 // PostgreSQL 18.6 names the missing TABLE when the
14665 // qualifier is the part that resolves to nothing
14666 // (`missing FROM-clause entry for table "pg_cast"`) and
14667 // the COLUMN otherwise. Raising the column error for both
14668 // dropped the table name a caller matches on.
14669 if let Some(q) = &c.qualifier
14670 && !sources.iter().any(|(a, _)| a == q)
14671 {
14672 return Err(EngineError::Eval(EvalError::UnknownQualifier {
14673 qualifier: q.clone(),
14674 column: c.name.clone(),
14675 }));
14676 }
14677 // v7.39.2 — and a qualified reference whose qualifier
14678 // DOES resolve prints the whole thing, unquoted:
14679 // `column ea.no_such does not exist` (measured on PG
14680 // 18.6). The bare `column "no_such" does not exist` drops
14681 // the alias a caller matches on, which is what the
14682 // sqlx round-20 pin says.
14683 if let Some(q) = &c.qualifier {
14684 return Err(EngineError::Eval(EvalError::QualifiedColumnNotFound {
14685 qualifier: q.clone(),
14686 column: c.name.clone(),
14687 }));
14688 }
14689 return Err(EngineError::Eval(EvalError::ColumnNotFound {
14690 name: c.name.clone(),
14691 }));
14692 }
14693 }
14694 Ok(())
14695 }
14696}
14697
14698/// v7.39.2 — the column references of an expression, NOT descending into
14699/// a subquery.
14700///
14701/// A correlated subquery resolves its names against an outer scope this
14702/// walk cannot see, so descending would refuse valid queries. Missing a
14703/// typo inside one is the safe direction; refusing a good query is not.
14704/// v7.39.2 — the type PostgreSQL would name for an argument, when it
14705/// can be known without a row: a column of a source table, or a
14706/// literal. `None` for anything else, which is what keeps the pre-scan
14707/// refusal from printing a worse sentence than the row-time one.
14708pub(crate) fn static_arg_type(e: &Expr, cols: &[ColumnSchema]) -> Option<alloc::string::String> {
14709 use spg_sql::ast::Literal as L;
14710 match e {
14711 Expr::Column(c) => cols
14712 .iter()
14713 .find(|s| s.name.eq_ignore_ascii_case(&c.name))
14714 .map(|s| crate::conversions::pg_type_name_for_error(s.ty)),
14715 // A bare literal has no type yet on PostgreSQL — it names it
14716 // `unknown` in this very sentence — except where the lexeme
14717 // fixes one.
14718 Expr::Literal(L::String(_)) | Expr::Literal(L::Null) => {
14719 Some(alloc::string::String::from("unknown"))
14720 }
14721 Expr::Literal(L::Integer(_)) => Some(alloc::string::String::from("integer")),
14722 Expr::Literal(L::Bool(_)) => Some(alloc::string::String::from("boolean")),
14723 _ => None,
14724 }
14725}
14726
14727/// v7.39.2 — the function calls of an expression, name and argument
14728/// count, NOT descending into a subquery (its scope is its own).
14729fn collect_function_calls(e: &Expr, out: &mut Vec<(alloc::string::String, Vec<Expr>)>) {
14730 match e {
14731 Expr::FunctionCall { name, args } => {
14732 out.push((name.to_ascii_lowercase(), args.clone()));
14733 for a in args {
14734 collect_function_calls(a, out);
14735 }
14736 }
14737 Expr::Binary { lhs, rhs, .. } => {
14738 collect_function_calls(lhs, out);
14739 collect_function_calls(rhs, out);
14740 }
14741 Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14742 collect_function_calls(expr, out);
14743 }
14744 _ => {}
14745 }
14746}
14747
14748fn collect_plain_column_refs(e: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
14749 match e {
14750 Expr::Column(c) => out.push(c.clone()),
14751 Expr::Binary { lhs, rhs, .. } => {
14752 collect_plain_column_refs(lhs, out);
14753 collect_plain_column_refs(rhs, out);
14754 }
14755 Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14756 collect_plain_column_refs(expr, out);
14757 }
14758 Expr::FunctionCall { args, .. } => {
14759 for a in args {
14760 collect_plain_column_refs(a, out);
14761 }
14762 }
14763 _ => {}
14764 }
14765}
14766
14767fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
14768 let Some(lock) = &stmt.locking else {
14769 return Ok(());
14770 };
14771 let verb = lock_clause_verb(lock.strength);
14772 let refuse = |what: &str| {
14773 Err(EngineError::Unsupported(alloc::format!(
14774 "{verb} is not allowed with {what}"
14775 )))
14776 };
14777 if !stmt.unions.is_empty() {
14778 return refuse("UNION/INTERSECT/EXCEPT");
14779 }
14780 if stmt.distinct || !stmt.distinct_on.is_empty() {
14781 return refuse("DISTINCT clause");
14782 }
14783 if stmt.group_by.is_some() || stmt.group_by_all {
14784 return refuse("GROUP BY clause");
14785 }
14786 let has_agg = stmt.items.iter().any(|it| match it {
14787 spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
14788 _ => false,
14789 });
14790 if has_agg {
14791 return refuse("aggregate functions");
14792 }
14793 // `FOR UPDATE OF t` must name a relation that is actually in FROM.
14794 for want in &lock.of_tables {
14795 if !locking_from_names(stmt)
14796 .iter()
14797 .any(|n| n.eq_ignore_ascii_case(want))
14798 {
14799 return Err(EngineError::Unsupported(alloc::format!(
14800 "relation \"{want}\" in {verb} clause not found in FROM clause"
14801 )));
14802 }
14803 }
14804 Ok(())
14805}
14806
14807/// How PG names the clause in its diagnostics.
14808const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
14809 use spg_sql::ast::LockStrength as LS;
14810 match s {
14811 LS::Update => "FOR UPDATE",
14812 LS::NoKeyUpdate => "FOR NO KEY UPDATE",
14813 LS::Share => "FOR SHARE",
14814 LS::KeyShare => "FOR KEY SHARE",
14815 }
14816}
14817
14818/// Every relation name (or alias) the FROM clause exposes.
14819fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
14820 let mut out = alloc::vec::Vec::new();
14821 if let Some(f) = &stmt.from {
14822 let mut push = |t: &spg_sql::ast::TableRef| {
14823 if let Some(a) = &t.alias {
14824 out.push(a.clone());
14825 }
14826 out.push(t.name.clone());
14827 };
14828 push(&f.primary);
14829 for j in &f.joins {
14830 push(&j.table);
14831 }
14832 }
14833 out
14834}
14835
14836fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
14837 use spg_sql::ast::Expr;
14838 if let Some(w) = &stmt.where_
14839 && aggregate::contains_aggregate(w)
14840 {
14841 return Err(EngineError::Unsupported(
14842 "aggregate functions are not allowed in WHERE".into(),
14843 ));
14844 }
14845 let mut nested = false;
14846 let mut check = |e: &Expr| {
14847 let mut probe = e.clone();
14848 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14849 let args = match n {
14850 Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
14851 _ => return false,
14852 };
14853 if args.iter().any(aggregate::contains_aggregate) {
14854 nested = true;
14855 }
14856 false
14857 });
14858 };
14859 for it in &stmt.items {
14860 if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14861 check(expr);
14862 }
14863 }
14864 if let Some(h) = &stmt.having {
14865 check(h);
14866 }
14867 for o in &stmt.order_by {
14868 check(&o.expr);
14869 }
14870 if nested {
14871 return Err(EngineError::Unsupported(
14872 "aggregate function calls cannot be nested".into(),
14873 ));
14874 }
14875 Ok(())
14876}
14877
14878/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
14879/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
14880/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
14881/// to a set and then applies the enclosing expression once per element. SPG only
14882/// ever recognised an SRF that WAS the item, so everything above died on
14883/// "unknown function unnest" — the set-returning call, wrapped in anything at
14884/// all, fell through to the scalar function dispatcher which has no such name.
14885///
14886/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
14887/// rewritten to read that column, and the rewritten expression is evaluated once
14888/// per output row against the input row extended with the lifted values. The
14889/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
14890/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
14891/// executors (the single-table scan, the synthetic-table pipeline, and the
14892/// unnest FROM path) each evaluated the key as an ordinary expression, where the
14893/// literal `n` is just the constant n — the same sort key for every row. The
14894/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
14895/// back in input order, not in a wrong order. Statement prep resolves the common
14896/// case, but only when the SELECT item is an expression — a `*` is not one, and
14897/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
14898/// spelling landed on exactly the shape prep could not resolve.
14899///
14900/// A set-returning item is left alone: copying it into ORDER BY would make the
14901/// key "the whole set", evaluated once per INPUT row.
14902fn resolve_positional_order_by(
14903 order_by: &[spg_sql::ast::OrderBy],
14904 projection: &[ProjectedItem],
14905) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
14906 order_by
14907 .iter()
14908 .filter_map(|o| {
14909 let mut o = o.clone();
14910 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
14911 && *n >= 1
14912 && let Ok(idx) = usize::try_from(*n - 1)
14913 && let Some(item) = projection.get(idx)
14914 && !expr_contains_builtin_srf(&item.expr)
14915 {
14916 // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
14917 // item is itself an integer LITERAL must not be
14918 // substituted textually: the literal would read as an
14919 // ordinal again downstream, and `SELECT 10 … ORDER BY
14920 // 1` died with "position 10 is not in select list"
14921 // where PG happily returns the rows. Ordering by a
14922 // constant orders nothing, so the key drops.
14923 if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
14924 return None;
14925 }
14926 o.expr = item.expr.clone();
14927 }
14928 Some(o)
14929 })
14930 .collect()
14931}
14932
14933/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
14934/// this expression? Statement preparation (`resolve_order_by_position`) runs
14935/// before any catalog is in hand, and it only needs to know "is this item's value
14936/// a set", which the builtin SRFs answer syntactically.
14937pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
14938 let mut found = false;
14939 let mut probe = e.clone();
14940 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14941 if is_top_level_unnest(n) {
14942 found = true;
14943 return true;
14944 }
14945 false
14946 });
14947 found
14948}
14949
14950/// v7.39 (round 599) — everything about a target-list SRF that does not
14951/// depend on the row.
14952///
14953/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
14954/// each SRF-bearing projection expression, walked and rewrote the tree,
14955/// formatted a `__srf_N` name per node, and copied the whole column schema.
14956/// A counting allocator put the path at 24 allocations per input row for a
14957/// single-element `unnest`, against 0 for the same scan without one — 211 MB
14958/// where the plain scan took 4.3 — and the shape held whatever the array
14959/// contained, which is what invariant work looks like.
14960struct SrfPlan {
14961 /// The lifted SRF calls, in slot order.
14962 nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
14963 /// Per projection position, the expression with its SRF calls replaced
14964 /// by `__srf_N` column references. `None` means the item has none.
14965 rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
14966 /// The input schema followed by one column per slot. Only the slots'
14967 /// TYPES vary per row, and they are patched in place.
14968 ext_cols: alloc::vec::Vec<ColumnSchema>,
14969 /// v7.39 (round 743) — the rewritten projection COMPILED against the
14970 /// extended schema, once per plan. The per-output-row evaluation ran
14971 /// the interpreter (~560 ns/row on the unnest panel cell); the Step
14972 /// VM reads the `__srf_N` slots as plain columns. `None` = that item
14973 /// is not fully compilable and keeps the interpreter.
14974 compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
14975 base_cols: usize,
14976}
14977
14978fn build_srf_plan(
14979 engine: &Engine,
14980 projection: &[ProjectedItem],
14981 srf_idxs: &[usize],
14982 ctx: &EvalContext<'_>,
14983) -> Result<SrfPlan, EngineError> {
14984 // Lift every SRF node out of every item that contains one.
14985 let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
14986 let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
14987 let mut reject: Option<EngineError> = None;
14988 for &i in srf_idxs {
14989 let mut e = projection[i].expr.clone();
14990 crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
14991 if reject.is_some() {
14992 return true;
14993 }
14994 // PG refuses a set-returning function inside a conditional: the set
14995 // would have to be produced before anyone knows whether the branch
14996 // is even taken.
14997 let conditional = match n {
14998 spg_sql::ast::Expr::Case { .. } => Some("CASE"),
14999 spg_sql::ast::Expr::FunctionCall { name, .. }
15000 if name.eq_ignore_ascii_case("coalesce") =>
15001 {
15002 Some("COALESCE")
15003 }
15004 _ => None,
15005 };
15006 if let Some(kind) = conditional
15007 && engine.expr_contains_srf(n)
15008 {
15009 reject = Some(EngineError::Unsupported(alloc::format!(
15010 "set-returning functions are not allowed in {kind}"
15011 )));
15012 return true;
15013 }
15014 if !engine.is_srf_node(n) {
15015 return false;
15016 }
15017 let slot = nodes.len();
15018 nodes.push(n.clone());
15019 *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
15020 qualifier: None,
15021 name: alloc::format!("__srf_{slot}"),
15022 });
15023 true
15024 });
15025 rewritten[i] = Some(e);
15026 }
15027 if let Some(err) = reject {
15028 return Err(err);
15029 }
15030 let base_cols = ctx.columns.len();
15031 let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
15032 for slot in 0..nodes.len() {
15033 ext_cols.push(ColumnSchema::new(
15034 alloc::format!("__srf_{slot}"),
15035 DataType::Text,
15036 true,
15037 ));
15038 }
15039 // v7.39 (round 743) — compile the rewritten items against the
15040 // EXTENDED schema. The slot columns' declared type is a per-row
15041 // patched detail the compiled column read does not consult.
15042 let compiled: Vec<Option<eval::CompiledExpr>> = {
15043 let mut ext_ctx = ctx.clone();
15044 ext_ctx.columns = &ext_cols;
15045 projection
15046 .iter()
15047 .enumerate()
15048 .map(|(i, p)| {
15049 let e = rewritten[i].as_ref().unwrap_or(&p.expr);
15050 if eval::fully_compilable(e) {
15051 Some(eval::compile_expr(e, &ext_ctx))
15052 } else {
15053 None
15054 }
15055 })
15056 .collect()
15057 };
15058 Ok(SrfPlan {
15059 nodes,
15060 rewritten,
15061 ext_cols,
15062 compiled,
15063 base_cols,
15064 })
15065}
15066
15067/// One input row expanded through a plan built once for the whole scan.
15068/// v7.39 (round 621) — expand a projection whose target list contains
15069/// set-returning items, remembering which INPUT row each output row came from.
15070///
15071/// The three materialised-source tails — `FROM unnest(…)`, `FROM
15072/// generate_series(…)`, and the one that serves VALUES / a derived table /
15073/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
15074/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
15075/// v(x)` answered `function unnest(integer[]) does not exist` on all the
15076/// others, for a query PG answers. Sharing the expansion is the point: a
15077/// fourth copy would have been the fourth place to forget.
15078fn expand_projection_srfs(
15079 engine: &Engine,
15080 projection: &[ProjectedItem],
15081 srf_idxs: &[usize],
15082 filtered: &[Row<'static>],
15083 ctx: &EvalContext<'_>,
15084) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
15085 let mut out = alloc::vec::Vec::with_capacity(filtered.len());
15086 let mut src = alloc::vec::Vec::with_capacity(filtered.len());
15087 // v7.39 (round 726) — ONE plan for the whole scan. The per-row
15088 // spelling rebuilt it for every input row: a full clone of the
15089 // rewritten projection trees and the extended schema, 50k times on
15090 // the panel's unnest cell.
15091 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15092 // v7.39 (round 733) — shard the expansion. Each shard clones the
15093 // plan (its ext_cols slot types are per-row mutable) and builds a
15094 // MINIMAL context — EvalContext is not Sync — which is sound only
15095 // when every expression involved is pure: the whole projection and
15096 // every SRF argument must be fully_compilable, or the row loop
15097 // stays serial with the full session context.
15098 // The projection is judged in its REWRITTEN form — the SRF call
15099 // itself is never compilable, but after the lift it is a plain
15100 // `__srf_N` column reference.
15101 let all_pure = projection
15102 .iter()
15103 .enumerate()
15104 .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
15105 && plan.nodes.iter().all(|n| match n {
15106 Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
15107 other => eval::fully_compilable(other),
15108 });
15109 if all_pure
15110 && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
15111 && let Some(r) = engine.parallel_runner.0.as_deref()
15112 {
15113 let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
15114 let chunk = filtered.len().div_ceil(n_shards);
15115 type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
15116 let schema_cols = ctx.columns;
15117 let alias = ctx.table_alias;
15118 let mysql = ctx.mysql_dialect;
15119 let style = ctx.render_style;
15120 let plan_ref = &plan;
15121 let results = r.run_shards(n_shards, &|si| {
15122 let lo = si * chunk;
15123 let hi = ((si + 1) * chunk).min(filtered.len());
15124 let mut sctx = eval::EvalContext::new(schema_cols, alias);
15125 sctx.mysql_dialect = mysql;
15126 sctx.render_style = style;
15127 // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
15128 // compiled programs); each shard rebuilds it, which also
15129 // recompiles against the shard's own context. Build errors
15130 // were already surfaced by the outer build above.
15131 let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
15132 Ok(p) => p,
15133 Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
15134 };
15135 let mut run = || -> ShardOut {
15136 let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
15137 let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
15138 for (i, row) in filtered[lo..hi].iter().enumerate() {
15139 let expanded =
15140 expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
15141 sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
15142 o.extend(expanded);
15143 }
15144 Ok((o, sidx))
15145 };
15146 alloc::boxed::Box::new(run())
15147 });
15148 for boxed in results {
15149 let shard = boxed
15150 .downcast::<ShardOut>()
15151 .expect("runner echoes the closure's box");
15152 let (o, sidx) = (*shard)?;
15153 out.extend(o);
15154 src.extend(sidx);
15155 }
15156 return Ok((out, src));
15157 }
15158 for (i, row) in filtered.iter().enumerate() {
15159 let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
15160 src.extend(core::iter::repeat_n(i, expanded.len()));
15161 out.extend(expanded);
15162 }
15163 Ok((out, src))
15164}
15165
15166/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
15167///
15168/// A key that names a select-list item reads it out of the EXPANDED row,
15169/// because PG sorts after the expansion. A key that names a source column the
15170/// query does not project is evaluated against the input row that output row
15171/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
15172fn srf_order_key(
15173 ob: &spg_sql::ast::OrderBy,
15174 out_col: Option<usize>,
15175 out: &Row<'static>,
15176 src: &Row<'static>,
15177 ctx: &EvalContext<'_>,
15178) -> Result<Value<'static>, EngineError> {
15179 match out_col {
15180 Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
15181 None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
15182 }
15183}
15184
15185fn expand_srf_row_with(
15186 engine: &Engine,
15187 plan: &mut SrfPlan,
15188 projection: &[ProjectedItem],
15189 row: &Row<'static>,
15190 ctx: &EvalContext<'_>,
15191) -> Result<Vec<Row<'static>>, EngineError> {
15192 let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
15193 for n in &plan.nodes {
15194 lists.push(engine.srf_values(n, row, ctx)?);
15195 }
15196 let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
15197 // Only the slots' element types depend on the row; the names and the
15198 // input schema around them do not.
15199 for (slot, list) in lists.iter().enumerate() {
15200 plan.ext_cols[plan.base_cols + slot].ty = list
15201 .iter()
15202 .find_map(|v| v.data_type())
15203 .unwrap_or(DataType::Text);
15204 }
15205 let mut ext_ctx = ctx.clone();
15206 ext_ctx.columns = &plan.ext_cols;
15207 let mut out = Vec::with_capacity(n_rows);
15208 // v7.39 (round 726) — the base columns are the SAME for every
15209 // expanded row; clone them once and rewrite only the SRF slots per
15210 // k. The old form cloned the whole input row per OUTPUT row — for
15211 // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
15212 // TEXT column the projection never reads.
15213 let base_len = row.values.len();
15214 let mut ext_vals = row.values.clone();
15215 ext_vals.resize(base_len + lists.len(), Value::Null);
15216 let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15217 for k in 0..n_rows {
15218 for (slot, list) in lists.iter().enumerate() {
15219 // Past the end of THIS srf's rows → NULL (PG pads).
15220 ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
15221 }
15222 let ext_row = Row::new(core::mem::take(&mut ext_vals));
15223 let mut vals = Vec::with_capacity(projection.len());
15224 for (i, p) in projection.iter().enumerate() {
15225 // v7.39 (round 743) — compiled when possible; the
15226 // interpreter for the rest, with its exact wording.
15227 vals.push(match &plan.compiled[i] {
15228 Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
15229 .map_err(EngineError::Eval)?,
15230 None => {
15231 let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
15232 eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
15233 }
15234 });
15235 }
15236 ext_vals = ext_row.values;
15237 out.push(Row::new(vals));
15238 }
15239 Ok(out)
15240}
15241
15242/// The one-shot spelling, for the callers that expand a single row.
15243/// v7.39 (round 600) — which output column each ORDER BY key names, for a
15244/// query whose target list contains a set-returning function.
15245///
15246/// The keys used to be built from the INPUT row, before the SRF expanded, so
15247/// anything that named the SRF's own output was evaluated as a scalar call:
15248/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
15249/// "function unnest(integer[]) does not exist", and so did the spellings that
15250/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
15251/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
15252/// back in input order. PG sorts AFTER the expansion, so a key that names a
15253/// select-list item reads that item's value out of the expanded row.
15254///
15255/// `None` keeps the key on the input row, which is where an ORDER BY naming
15256/// a column the query does not project has to be evaluated.
15257/// v7.38.19 — the output column an ORDER BY term reads, when reading it
15258/// is provably the same as building a key from the input row.
15259///
15260/// A sort key is a COPY of the sort column, made because the source row
15261/// is gone by the time the sort runs — only the projection survives. On
15262/// `SELECT s_long FROM t ORDER BY s_long` that copy is of data the
15263/// projected row already holds, and on 400,000 rows of 192-character
15264/// text it is 400,000 allocations, 400,000 frees and 77 MB of copying.
15265/// A profile of that cell put the allocator at 2,025 leaf samples of the
15266/// working set, second only to the comparison chain.
15267///
15268/// The condition is narrow on purpose. `srf_order_output_cols` resolves
15269/// an ORDER BY term the way SQL does — a positional ordinal, or a name
15270/// matching the select list — and SQL resolves against the select list
15271/// BEFORE the input columns. The key path resolves against the INPUT
15272/// columns. For `SELECT g AS id … ORDER BY id` on a table that also has
15273/// an `id`, those are different columns, and swapping one for the other
15274/// would change answers rather than timings.
15275///
15276/// So this takes only the case where the two cannot disagree: a bare
15277/// unqualified column name, matching exactly one output item, whose own
15278/// expression is that same column. The projected cell then IS the input
15279/// cell, and the key would have been its copy.
15280/// True when comparing two of this column's VALUES gives the same order
15281/// as comparing the sort KEYS built from them.
15282///
15283/// It does not hold widely. A user ENUM stores its label as text but
15284/// orders by DECLARATION position; an array orders element-wise; a
15285/// domain or composite carries its own rules. For those the two paths
15286/// answer differently, and a sort that skipped the key would silently
15287/// reorder the result. This is the short list where they agree.
15288fn value_order_is_key_order(col: &ColumnSchema) -> bool {
15289 use spg_storage::DataType as T;
15290 col.user_enum_type.is_none()
15291 && col.user_domain_type.is_none()
15292 && col.user_composite_type.is_none()
15293 && col.collation_name.is_none()
15294 && col.collation == spg_storage::Collation::Binary
15295 && matches!(
15296 col.ty,
15297 T::SmallInt | T::Int | T::BigInt | T::Text | T::Varchar(_) | T::Bool | T::Uuid
15298 )
15299}
15300
15301/// The full ORDER BY comparison between two rows, named by index.
15302///
15303/// v7.38.19 — what a permutation sort falls back to when its key ties.
15304fn row_cmp_by_index(
15305 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15306 terms: &[(usize, bool, Option<bool>)],
15307 colls: &[Option<crate::collate::Collated>],
15308 mysql: bool,
15309 ia: u32,
15310 ib: u32,
15311) -> core::cmp::Ordering {
15312 let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
15313 for (i, (col, desc, nf)) in terms.iter().enumerate() {
15314 let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col)) else {
15315 continue;
15316 };
15317 let ord = match (va, vb) {
15318 (Value::Text(x), Value::Text(y)) => match colls.get(i).and_then(Option::as_ref) {
15319 Some(c) => {
15320 let o = c.compare(x, y);
15321 if *desc { o.reverse() } else { o }
15322 }
15323 None if !mysql => {
15324 let o = crate::orderby::str_cmp_prefix_first(x, y);
15325 if *desc { o.reverse() } else { o }
15326 }
15327 None => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15328 },
15329 _ => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15330 };
15331 if ord != core::cmp::Ordering::Equal {
15332 return ord;
15333 }
15334 }
15335 core::cmp::Ordering::Equal
15336}
15337
15338/// Whether ordering these rows by BYTES is what the collation in force
15339/// would have answered anyway.
15340///
15341/// v7.38.19 — a collated sort used to be shut out of the keyed path
15342/// entirely, and the cost of that showed up the moment the byte path
15343/// got fast: on the same fixture, the same binary took 92 ms under `C`
15344/// and 371 ms under `en_US`, so declaring a collation had become a
15345/// four-fold tax on a query that sorts md5 hex.
15346///
15347/// It need not be. For several locales `[0-9a-z]` orders exactly as
15348/// bytes do -- `collate::ascii_byte_order` carries that fact, and the
15349/// test beside it re-derives the whole allowlist by sorting a corpus
15350/// twice rather than asserting it. So when the collation is one of
15351/// those AND every value in every sort column is drawn from that
15352/// alphabet, the byte answer IS the collated answer.
15353///
15354/// Both halves are required. A collation outside the list can put `z`
15355/// between `s` and `t`; a value outside the alphabet can be `Ápple`,
15356/// which no locale in the list orders by its bytes. Either one and this
15357/// returns false, and the sort takes the collator's own path.
15358fn byte_order_answers_the_collation(
15359 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15360 terms: &[(usize, bool, Option<bool>)],
15361 colls: &[Option<crate::collate::Collated>],
15362) -> bool {
15363 if colls.iter().all(Option::is_none) {
15364 return true;
15365 }
15366 if !colls
15367 .iter()
15368 .flatten()
15369 .all(crate::collate::Collated::ascii_byte_order)
15370 {
15371 return false;
15372 }
15373 tagged.iter().all(|(_, row)| {
15374 terms.iter().all(|(col, _, _)| match row.values.get(*col) {
15375 // Only TEXT is collation-sensitive; a number or a NULL
15376 // orders the same under every collation there is.
15377 Some(Value::Text(t)) => crate::collate::is_ascii_alnum_lower(t),
15378 _ => true,
15379 })
15380 })
15381}
15382
15383/// An eight-byte key for each row's sort column, paired with the row's
15384/// index — or `None` when the column cannot give one on every row.
15385///
15386/// v7.38.19 — the pair is what the sort array holds instead of the row.
15387/// Two kinds of column can supply it:
15388///
15389/// * an INTEGER, whose whole value fits. Flipping the sign bit maps
15390/// the signed order onto the unsigned one, so the key is EXACT and
15391/// a comparison never has to look at the row at all.
15392/// * TEXT, as the first eight bytes big-endian, zero-padded. That
15393/// orders the same as the string — two that differ inside those
15394/// bytes differ at the same index either way, and one shorter than
15395/// eight pads with zeros exactly where `[u8]`'s own comparison runs
15396/// out — but it is a PREFIX, so equal keys must still ask the full
15397/// comparator.
15398///
15399/// The `None` is the safety of it: a NULL or any other type has no
15400/// faithful eight-byte key, so such a column takes the ordinary path
15401/// rather than being given a made-up one.
15402/// The prefix keys for a sort, at the width the DATA asks for.
15403///
15404/// v7.40.1 — the width used to be eight bytes for every text column, and
15405/// the panel's two text cells priced both halves of that choice against
15406/// PostgreSQL 18.6, in memory on both legs, 400,000 rows:
15407///
15408/// ```text
15409/// short text (9 bytes, shared prefix) SPG 96.6 PG 71.0 1.36x behind
15410/// long text (192 bytes, byte 0 decides) SPG 70.9 PG 72.4 parity
15411/// ```
15412///
15413/// `'k' || lpad(n, 8, '0')` is nine bytes, so an eight-byte prefix drops
15414/// the last digit: ten rows share every key, forty thousand tie-runs
15415/// each fall back to the full comparator, and each of those reads at
15416/// random into a 400,000-element array. The md5 column decides on byte
15417/// zero and never ties, which is why only one of the two cells lost.
15418///
15419/// Widened to sixteen bytes and measured -- same window, two binaries
15420/// named by md5, order digests identical:
15421///
15422/// ```text
15423/// short text 104.9 -> 56.9 ms 1.84x faster (and 0.80x of PG)
15424/// long text 74.9 -> 90.7 ms 1.21x SLOWER
15425/// ```
15426///
15427/// So a fixed width is the wrong shape either way: `(u128, u32)` is 32
15428/// bytes against `(u64, u32)`'s 16, and a column that already decided on
15429/// byte zero pays double the sort's memory traffic for eight bytes it
15430/// never reads. That is the tax a shared hot path levies on the workload
15431/// it does not help.
15432///
15433/// The width comes from the longest value instead, which is exact and
15434/// free -- it is one pass the loop below already makes. Every value at
15435/// sixteen bytes or under makes the wide key the WHOLE key, so `exact`
15436/// is true and the tie fallback with its random reads disappears
15437/// altogether; anything longer keeps the narrow key and pays nothing.
15438enum PrefixKeys {
15439 Narrow(Vec<(u64, u32)>, bool),
15440 Wide(Vec<(u128, u32)>, bool),
15441}
15442
15443fn sort_keys_of(
15444 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15445 col: usize,
15446) -> Option<PrefixKeys> {
15447 let n = u32::try_from(tagged.len()).ok()?;
15448 let is_text = match tagged.first()?.1.values.get(col)? {
15449 Value::Text(_) => true,
15450 Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => false,
15451 _ => return None,
15452 };
15453 if !is_text {
15454 let mut out: Vec<(u64, u32)> = Vec::with_capacity(tagged.len());
15455 for (i, row) in (0..n).zip(tagged.iter()) {
15456 let key = match row.1.values.get(col) {
15457 Some(Value::SmallInt(v)) => (i64::from(*v) as u64) ^ (1 << 63),
15458 Some(Value::Int(v)) => (i64::from(*v) as u64) ^ (1 << 63),
15459 Some(Value::BigInt(v)) => (*v as u64) ^ (1 << 63),
15460 _ => return None,
15461 };
15462 out.push((key, i));
15463 }
15464 return Some(PrefixKeys::Narrow(out, true));
15465 }
15466 // One pass, and it answers both questions: the bytes of every key,
15467 // and whether the longest of them fits the wide one.
15468 let mut wide: Vec<(u128, u32)> = Vec::with_capacity(tagged.len());
15469 let mut longest = 0usize;
15470 for (i, row) in (0..n).zip(tagged.iter()) {
15471 let Some(Value::Text(t)) = row.1.values.get(col) else {
15472 return None;
15473 };
15474 let bytes = t.as_bytes();
15475 longest = longest.max(bytes.len());
15476 let mut k = [0u8; 16];
15477 let take = bytes.len().min(16);
15478 k[..take].copy_from_slice(&bytes[..take]);
15479 wide.push((u128::from_be_bytes(k), i));
15480 }
15481 if longest <= 16 {
15482 return Some(PrefixKeys::Wide(wide, true));
15483 }
15484 // Longer than the wide key: the narrow one costs half the memory
15485 // traffic and decides exactly as much, since neither is the whole
15486 // value. Built from the wide keys rather than reading the rows again.
15487 let narrow = wide
15488 .into_iter()
15489 .map(|(k, i)| ((k >> 64) as u64, i))
15490 .collect();
15491 Some(PrefixKeys::Narrow(narrow, false))
15492}
15493
15494/// Sort a prefix-key permutation, whatever the key's width.
15495///
15496/// v7.40.1 -- extracted so the two widths share one body. `low_card`
15497/// keeps the run-at-a-time shortcut and `exact` keeps the "a tie means
15498/// the values are equal" one; both are the caller's to decide.
15499struct PrefixSort {
15500 /// The first ORDER BY term is descending.
15501 first_desc: bool,
15502 /// The key does not discriminate, so sort it and settle each run of
15503 /// equal keys in one pass instead of n log n comparisons.
15504 low_card: bool,
15505 /// The key IS the value, so a tie means the values are equal.
15506 exact: bool,
15507 /// One ORDER BY term, so nothing else can speak after a tie.
15508 single_term: bool,
15509}
15510
15511fn sort_prefix_permutation<K: Copy + Ord>(
15512 mut order: Vec<(K, u32)>,
15513 how: &PrefixSort,
15514 row_cmp: &dyn Fn(u32, u32) -> core::cmp::Ordering,
15515 same_value: &dyn Fn(u32, u32) -> bool,
15516) -> Vec<u32> {
15517 let PrefixSort {
15518 first_desc,
15519 low_card,
15520 exact,
15521 single_term,
15522 } = *how;
15523 if low_card {
15524 // Integer sort first, then one pass per run.
15525 order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
15526 let c = pa.cmp(&pb);
15527 let c = if first_desc { c.reverse() } else { c };
15528 c.then_with(|| ia.cmp(&ib))
15529 });
15530 let mut lo = 0;
15531 while lo < order.len() {
15532 let mut hi = lo + 1;
15533 while hi < order.len() && order[hi].0 == order[lo].0 {
15534 hi += 1;
15535 }
15536 if hi - lo > 1 {
15537 let head = order[lo].1;
15538 let uniform = order[lo + 1..hi].iter().all(|&(_, i)| same_value(head, i));
15539 if !uniform {
15540 order[lo..hi]
15541 .sort_by(|&(_, ia), &(_, ib)| row_cmp(ia, ib).then_with(|| ia.cmp(&ib)));
15542 }
15543 // A uniform run is already in index order, which IS the
15544 // stable answer.
15545 }
15546 lo = hi;
15547 }
15548 } else {
15549 order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
15550 let c = pa.cmp(&pb);
15551 let c = if first_desc { c.reverse() } else { c };
15552 if c != core::cmp::Ordering::Equal {
15553 return c;
15554 }
15555 // An EXACT key that ties means the values are equal, so only
15556 // the remaining terms can speak. A prefix that ties has
15557 // decided nothing yet and the first term must be asked again,
15558 // which `row_cmp` does by walking every term from the start.
15559 if exact && single_term {
15560 return ia.cmp(&ib);
15561 }
15562 row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
15563 });
15564 }
15565 order.into_iter().map(|(_, i)| i).collect()
15566}
15567
15568/// Whether a PREFIX key is worth sorting a permutation on.
15569///
15570/// v7.38.19 — it is not always, and the panel says so in one cell. The
15571/// `text (26 values)` fixture is two hundred identical characters drawn
15572/// from twenty-six letters, so every eight-byte prefix inside a letter
15573/// is the same and 15,000 rows tie on it. Each tie then pays the prefix
15574/// compare, a two-hundred-byte comparison, AND a random read into a
15575/// 400,000-element array — while sorting the rows in place keeps the
15576/// partition contiguous. Measured: 160 ms sorting rows, 247 ms sorting
15577/// the permutation, on the very fixture built to be degenerate.
15578///
15579/// So the permutation is taken when the key DECIDES, and a sample says
15580/// whether it does. An exact key always decides; a prefix has to earn
15581/// it.
15582fn key_discriminates<K: Copy + Ord>(keys: &[(K, u32)]) -> bool {
15583 const SAMPLE: usize = 1024;
15584 let step = (keys.len() / SAMPLE).max(1);
15585 let mut seen: Vec<K> = keys
15586 .iter()
15587 .step_by(step)
15588 .take(SAMPLE)
15589 .map(|&(k, _)| k)
15590 .collect();
15591 let taken = seen.len();
15592 if taken < 8 {
15593 return true;
15594 }
15595 seen.sort_unstable();
15596 seen.dedup();
15597 seen.len() * 2 >= taken
15598}
15599
15600fn order_by_output_cols_if_identical(
15601 order_by: &[spg_sql::ast::OrderBy],
15602 projection: &[ProjectedItem],
15603 schema_cols: &[ColumnSchema],
15604) -> Option<Vec<usize>> {
15605 if order_by.is_empty() {
15606 return None;
15607 }
15608 let mut out = Vec::with_capacity(order_by.len());
15609 for ob in order_by {
15610 let Expr::Column(c) = &ob.expr else {
15611 return None;
15612 };
15613 if c.qualifier.is_some() {
15614 return None;
15615 }
15616 let mut hit = None;
15617 for (i, p) in projection.iter().enumerate() {
15618 if !p.output_name.eq_ignore_ascii_case(&c.name) {
15619 continue;
15620 }
15621 if hit.is_some() {
15622 return None; // ambiguous — SQL would reject it too
15623 }
15624 // The item must BE that column, not merely be named for it.
15625 let Expr::Column(pc) = &p.expr else {
15626 return None;
15627 };
15628 if !pc.name.eq_ignore_ascii_case(&c.name) {
15629 return None;
15630 }
15631 let sc = schema_cols
15632 .iter()
15633 .find(|s| s.name.eq_ignore_ascii_case(&c.name))?;
15634 if !value_order_is_key_order(sc) {
15635 return None;
15636 }
15637 hit = Some(i);
15638 }
15639 out.push(hit?);
15640 }
15641 Some(out)
15642}
15643
15644fn srf_order_output_cols(
15645 order_by: &[spg_sql::ast::OrderBy],
15646 projection: &[ProjectedItem],
15647) -> Vec<Option<usize>> {
15648 order_by
15649 .iter()
15650 .map(|ob| {
15651 // A positive ordinal is the Nth output column, directly.
15652 // `resolve_positional_order_by` deliberately leaves an ordinal
15653 // pointing at a set-returning item alone — copying the call into
15654 // ORDER BY would have made the key "the whole set" back when keys
15655 // came from the input row. Reading the expanded row's column is
15656 // what it should have meant, and is what this does.
15657 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
15658 && *n >= 1
15659 && let Ok(idx) = usize::try_from(*n - 1)
15660 && idx < projection.len()
15661 {
15662 return Some(idx);
15663 }
15664 // An unqualified name matching exactly one output name. SQL
15665 // resolves ORDER BY against the select list first, so this wins
15666 // over an input column of the same name — which is the whole
15667 // point of `SELECT g AS id … ORDER BY id`.
15668 if let Expr::Column(c) = &ob.expr
15669 && c.qualifier.is_none()
15670 {
15671 let mut hit = None;
15672 for (i, p) in projection.iter().enumerate() {
15673 if p.output_name.eq_ignore_ascii_case(&c.name) {
15674 if hit.is_some() {
15675 hit = None;
15676 break;
15677 }
15678 hit = Some(i);
15679 }
15680 }
15681 if hit.is_some() {
15682 return hit;
15683 }
15684 }
15685 // Or the same expression as a select-list item — which is what
15686 // `ORDER BY 1` becomes once `resolve_positional_order_by` has
15687 // run, and what a repeated `ORDER BY unnest(…)` is.
15688 projection.iter().position(|p| p.expr == ob.expr)
15689 })
15690 .collect()
15691}
15692
15693fn expand_srf_row(
15694 engine: &Engine,
15695 projection: &[ProjectedItem],
15696 srf_idxs: &[usize],
15697 row: &Row<'static>,
15698 ctx: &EvalContext<'_>,
15699) -> Result<Vec<Row<'static>>, EngineError> {
15700 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15701 expand_srf_row_with(engine, &mut plan, projection, row, ctx)
15702}
15703
15704impl Engine {
15705 /// The rows one target-list SRF yields for an input row. `None` from
15706 /// `srf_target_idxs` means the expression is not set-returning at all.
15707 fn srf_values(
15708 &self,
15709 expr: &spg_sql::ast::Expr,
15710 row: &Row<'static>,
15711 ctx: &EvalContext<'_>,
15712 ) -> Result<Vec<Value<'static>>, EngineError> {
15713 if top_level_srf_kind(expr).is_some() {
15714 return top_level_srf_output(expr, row, ctx);
15715 }
15716 // A user set-returning function. Its body runs through the real
15717 // executor, like every function body since round 63.
15718 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
15719 return Err(EngineError::Unsupported(
15720 "expected a SELECT-list SRF call".into(),
15721 ));
15722 };
15723 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15724 for a in args {
15725 vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
15726 }
15727 let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
15728 // v7.39 (read01 round 68) — in a target list a multi-column function is
15729 // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
15730 // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
15731 // what it is for. A single-column function contributes its bare value.
15732 Ok(rows
15733 .into_iter()
15734 .map(|r| {
15735 if r.values.len() == 1 {
15736 r.values.into_iter().next().unwrap_or(Value::Null)
15737 } else {
15738 Value::Composite(
15739 cols.iter()
15740 .map(|c| c.name.clone())
15741 .zip(r.values)
15742 .collect::<alloc::vec::Vec<_>>(),
15743 )
15744 }
15745 })
15746 .collect())
15747 }
15748
15749 /// Is THIS node a set-returning call: one of the builtin kinds, or a user
15750 /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
15751 fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
15752 if is_top_level_unnest(e) {
15753 return true;
15754 }
15755 let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
15756 return false;
15757 };
15758 self.active_catalog().functions_named(name).iter().any(|f| {
15759 let r = f.returns.trim().to_ascii_uppercase();
15760 r.starts_with("SETOF") || r.starts_with("TABLE(")
15761 })
15762 }
15763
15764 /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
15765 fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
15766 let mut found = false;
15767 let mut probe = e.clone();
15768 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
15769 if self.is_srf_node(n) {
15770 found = true;
15771 return true;
15772 }
15773 false
15774 });
15775 found
15776 }
15777
15778 /// Which projection items CONTAIN a set-returning call. Before round 78 this
15779 /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
15780 /// ordinary scalar call all the way down to the function dispatcher, which
15781 /// then reported `unnest` as an unknown function.
15782 fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
15783 projection
15784 .iter()
15785 .enumerate()
15786 .filter(|(_, p)| self.expr_contains_srf(&p.expr))
15787 .map(|(i, _)| i)
15788 .collect()
15789 }
15790}
15791
15792impl Engine {
15793 /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
15794 /// no `(f(args)).*` item.
15795 fn lower_record_expansion(
15796 &self,
15797 stmt: &SelectStatement,
15798 ) -> Result<Option<SelectStatement>, EngineError> {
15799 use spg_sql::ast::{Expr, SelectItem};
15800 let is_marker = |it: &SelectItem| {
15801 matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
15802 if name == "__record_expand")
15803 };
15804 if !stmt.items.iter().any(is_marker) {
15805 return Ok(None);
15806 }
15807 let mut out = stmt.clone();
15808 let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
15809 let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
15810 for (n, item) in stmt.items.iter().enumerate() {
15811 if !is_marker(item) {
15812 items.push(item.clone());
15813 continue;
15814 }
15815 let SelectItem::Expr {
15816 expr: Expr::FunctionCall { args, .. },
15817 ..
15818 } = item
15819 else {
15820 unreachable!("checked by is_marker");
15821 };
15822 let Some(Expr::FunctionCall {
15823 name: fname,
15824 args: fargs,
15825 }) = args.first()
15826 else {
15827 return Err(EngineError::Unsupported(
15828 "(<expr>).* expands a function's record — it needs a function call".into(),
15829 ));
15830 };
15831 let cols = self.setof_declared_columns(fname)?;
15832 let alias = alloc::format!("__rec{n}");
15833 let mut tref = bare_table_ref_named(&alias);
15834 tref.table_fn_call = Some(alloc::boxed::Box::new((
15835 fname.to_ascii_lowercase(),
15836 fargs.clone(),
15837 )));
15838 tref.alias = Some(alias.clone());
15839 lateral_refs.push(tref);
15840 for c in cols {
15841 items.push(SelectItem::Expr {
15842 expr: Expr::Column(spg_sql::ast::ColumnName {
15843 qualifier: Some(alias.clone()),
15844 name: c,
15845 }),
15846 alias: None,
15847 });
15848 }
15849 }
15850 out.items = items;
15851 // The function joins the FROM. With no FROM it BECOMES the FROM; with one
15852 // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
15853 // (the arguments may reference the outer row — the round-69 correlation).
15854 for tref in lateral_refs {
15855 match &mut out.from {
15856 None => {
15857 out.from = Some(spg_sql::ast::FromClause {
15858 primary: tref,
15859 joins: alloc::vec::Vec::new(),
15860 });
15861 }
15862 Some(from) => from.joins.push(spg_sql::ast::FromJoin {
15863 kind: spg_sql::ast::JoinKind::Cross,
15864 table: tref,
15865 on: None,
15866 using_cols: None,
15867 natural: false,
15868 }),
15869 }
15870 }
15871 Ok(Some(out))
15872 }
15873
15874 /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
15875 /// v text)` names them; a `SETOF <scalar>` is one column named after the
15876 /// function.
15877 fn setof_declared_columns(
15878 &self,
15879 name: &str,
15880 ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
15881 let cat = self.active_catalog();
15882 let overloads = cat.functions_named(name);
15883 let def = overloads.first().ok_or_else(|| {
15884 EngineError::Unsupported(alloc::format!("function {name} does not exist"))
15885 })?;
15886 let declared = def.returns.trim();
15887 let upper = declared.to_ascii_uppercase();
15888 if upper.starts_with("TABLE(") {
15889 let raw = &declared["TABLE(".len()..declared.len() - 1];
15890 return Ok(raw
15891 .split(',')
15892 .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
15893 .collect());
15894 }
15895 Ok(alloc::vec![name.to_string()])
15896 }
15897}
15898
15899/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
15900/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
15901/// COLUMNS list (data-independent), NESTED children inlined in
15902/// declaration order (PG's flattened output shape).
15903/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
15904/// correlated JSON_TABLE's static schema without evaluating its doc.
15905pub(crate) fn json_table_schema_pub(
15906 cols: &[spg_sql::ast::JsonTableColumn],
15907) -> alloc::vec::Vec<ColumnSchema> {
15908 json_table_schema(cols)
15909}
15910
15911fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
15912 use spg_sql::ast::JsonTableColumn as C;
15913 let mut out = alloc::vec::Vec::new();
15914 for c in cols {
15915 match c {
15916 C::Ordinality { name } => {
15917 out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
15918 }
15919 C::Regular {
15920 name, ty, exists, ..
15921 } => {
15922 let dt = if *exists {
15923 DataType::Bool
15924 } else {
15925 crate::conversions::column_type_to_data_type(*ty)
15926 };
15927 out.push(ColumnSchema::new(name.clone(), dt, true));
15928 }
15929 C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
15930 }
15931 }
15932 out
15933}
15934
15935/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
15936/// JSON_TABLE column's declared type (the DEFAULT expr may be a
15937/// string literal like `'none'` that must land as the column type).
15938fn coerce_json_table_default(
15939 v: Value<'static>,
15940 ty: spg_sql::ast::ColumnTypeName,
15941 name: &str,
15942) -> Result<Value<'static>, EngineError> {
15943 if v.is_null() {
15944 return Ok(Value::Null);
15945 }
15946 let dt = crate::conversions::column_type_to_data_type(ty);
15947 crate::conversions::coerce_value(v, dt, name, 0)
15948}
15949
15950/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
15951fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
15952 use crate::json::JsonValue as J;
15953 match v {
15954 Value::Null => J::Null,
15955 Value::Bool(b) => J::Bool(*b),
15956 Value::SmallInt(n) => J::Number(f64::from(*n)),
15957 Value::Int(n) => J::Number(f64::from(*n)),
15958 Value::BigInt(n) => J::Number(*n as f64),
15959 Value::Float(x) => J::Number(*x),
15960 Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
15961 other => J::String(crate::eval::value_to_text(other)),
15962 }
15963}
15964
15965fn bare_table_ref_named(name: &str) -> TableRef {
15966 TableRef {
15967 name: name.to_string(),
15968 alias: None,
15969 only: false,
15970 as_of_segment: None,
15971 unnest_expr: None,
15972 unnest_column_aliases: alloc::vec::Vec::new(),
15973 with_ordinality: false,
15974 generate_series_args: None,
15975 lateral_subquery: None,
15976 jsonb_each_text_arg: None,
15977 table_fn_call: None,
15978 rows_from: None,
15979 json_table: None,
15980 scalar_fn_item: false,
15981 }
15982}
15983
15984impl Engine {
15985 /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
15986 /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
15987 /// entries are the array-able SRFs, already lowered by the parser into their
15988 /// scalar array form.
15989 fn rows_from_rows(
15990 &self,
15991 primary: &TableRef,
15992 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
15993 let entries = primary
15994 .rows_from
15995 .as_ref()
15996 .expect("caller guards rows_from.is_some()");
15997 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
15998 let ctx = self.ev_ctx(&empty, None);
15999 let dummy = Row::new(alloc::vec::Vec::new());
16000 let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
16001 let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
16002 for (name, args) in entries {
16003 let (vals, colname) = if name == "__array" {
16004 // The parser lowered this one to `<array expr>`; its rows are the
16005 // array's elements.
16006 let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
16007 (
16008 array_value_to_elements(&arr)?,
16009 alloc::string::String::from("unnest"),
16010 )
16011 } else {
16012 let call = spg_sql::ast::Expr::FunctionCall {
16013 name: name.clone(),
16014 args: args.clone(),
16015 };
16016 (self.srf_values(&call, &dummy, &ctx)?, name.clone())
16017 };
16018 let ty = vals
16019 .first()
16020 .and_then(spg_storage::Value::data_type)
16021 .unwrap_or(DataType::Text);
16022 cols.push(ColumnSchema::new(colname, ty, true));
16023 lists.push(vals);
16024 }
16025 let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
16026 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
16027 for k in 0..n {
16028 let mut vals: alloc::vec::Vec<Value<'static>> =
16029 alloc::vec::Vec::with_capacity(lists.len() + 1);
16030 for l in &lists {
16031 vals.push(l.get(k).cloned().unwrap_or(Value::Null));
16032 }
16033 rows.push(Row::new(vals));
16034 }
16035 if primary.with_ordinality {
16036 cols.push(ColumnSchema::new(
16037 "ordinality".to_string(),
16038 DataType::BigInt,
16039 false,
16040 ));
16041 rows = rows
16042 .into_iter()
16043 .enumerate()
16044 .map(|(i, r)| {
16045 let mut v = r.values;
16046 v.push(Value::BigInt(i as i64 + 1));
16047 Row::new(v)
16048 })
16049 .collect();
16050 }
16051 Ok((rows, cols))
16052 }
16053}
16054
16055/// v7.39 (round 232) — PG names the offending set operation in its
16056/// arity / type-mismatch messages ("each UNION query must have the same
16057/// number of columns"). `UNION ALL` is still spelled UNION there.
16058fn set_op_name(kind: UnionKind) -> &'static str {
16059 match kind {
16060 UnionKind::All | UnionKind::Distinct => "UNION",
16061 UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
16062 UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
16063 }
16064}
16065
16066/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
16067/// type: a bare string or NULL literal that no context has typed yet. SPG
16068/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
16069/// be the syntax. A wildcard or a non-literal expression is never unknown.
16070/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
16071/// LABELS as text (the wire render) but the value is an oid-carrying
16072/// dual, so a UNION with a numeric column must not be refused on the
16073/// label (pg_dump: `SELECT classid … UNION ALL SELECT
16074/// 'pg_opfamily'::regclass …`).
16075fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
16076 fn is_regcast(e: &Expr) -> bool {
16077 matches!(
16078 e,
16079 Expr::Cast {
16080 target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
16081 ..
16082 }
16083 )
16084 }
16085 stmt.items
16086 .iter()
16087 .map(|item| match item {
16088 SelectItem::Expr { expr, .. } => is_regcast(expr),
16089 _ => false,
16090 })
16091 .collect()
16092}
16093
16094fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
16095 stmt.items
16096 .iter()
16097 .map(|item| match item {
16098 SelectItem::Expr { expr, .. } => matches!(
16099 expr,
16100 Expr::Literal(spg_sql::ast::Literal::String(_))
16101 | Expr::Literal(spg_sql::ast::Literal::Null)
16102 ),
16103 _ => false,
16104 })
16105 .collect()
16106}
16107
16108/// v7.39 (round 233) — retype one branch column's cells, reporting the
16109/// conversion failure the way PG does rather than leaving the column
16110/// half-converted. Used when the other branch typed an untyped literal.
16111fn coerce_branch_column(
16112 rows: &mut [Row<'static>],
16113 col_idx: usize,
16114 target: DataType,
16115 col_name: &str,
16116) -> Result<(), EngineError> {
16117 for row in rows.iter_mut() {
16118 let Some(slot) = row.values.get_mut(col_idx) else {
16119 continue;
16120 };
16121 if matches!(slot, Value::Null) {
16122 continue;
16123 }
16124 *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
16125 }
16126 Ok(())
16127}
16128
16129/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
16130/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
16131/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
16132/// reference to q's output columns substituted by the underlying column.
16133///
16134/// Admission is deliberately narrow — anything that changes cardinality,
16135/// order, or scope stays on the materialising path:
16136/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
16137/// FROM with no ordinality or positional column aliases, and no
16138/// subquery anywhere its expressions (an inner scope could reference
16139/// q too — descending is a later knife);
16140/// * inner: one stored table, bare-column projection only, no
16141/// CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
16142/// * every outer column reference must resolve inside q's output list —
16143/// a name that does not is an ERROR today, and flattening would
16144/// silently legalise it against the base table.
16145fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16146 use spg_sql::ast::SelectItem;
16147 let inner = primary.lateral_subquery.as_deref()?;
16148 // Outer shape.
16149 if !stmt.ctes.is_empty()
16150 || !stmt.unions.is_empty()
16151 || stmt.distinct
16152 || !stmt.distinct_on.is_empty()
16153 || !stmt.window_check_exprs.is_empty()
16154 || stmt.locking.is_some()
16155 || primary.with_ordinality
16156 || !primary.unnest_column_aliases.is_empty()
16157 {
16158 return None;
16159 }
16160 // Inner shape.
16161 if !inner.ctes.is_empty()
16162 || !inner.unions.is_empty()
16163 || inner.distinct
16164 || !inner.distinct_on.is_empty()
16165 || inner.group_by.is_some()
16166 || inner.group_by_all
16167 || inner.having.is_some()
16168 || !inner.order_by.is_empty()
16169 || inner.limit.is_some()
16170 || inner.offset.is_some()
16171 || !inner.window_check_exprs.is_empty()
16172 || inner.locking.is_some()
16173 {
16174 return None;
16175 }
16176 let ifrom = inner.from.as_ref()?;
16177 let it = &ifrom.primary;
16178 if !ifrom.joins.is_empty()
16179 || it.name.is_empty()
16180 || it.lateral_subquery.is_some()
16181 || it.unnest_expr.is_some()
16182 || it.generate_series_args.is_some()
16183 || it.as_of_segment.is_some()
16184 || it.jsonb_each_text_arg.is_some()
16185 || it.table_fn_call.is_some()
16186 || it.rows_from.is_some()
16187 || it.json_table.is_some()
16188 || it.with_ordinality
16189 || !it.unnest_column_aliases.is_empty()
16190 {
16191 return None;
16192 }
16193 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16194 return None;
16195 }
16196 // The output map: q's visible name -> the underlying column.
16197 let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
16198 let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
16199 alloc::collections::BTreeMap::new();
16200 for item in &inner.items {
16201 let SelectItem::Expr { expr, alias } = item else {
16202 return None;
16203 };
16204 let Expr::Column(c) = expr else {
16205 return None;
16206 };
16207 if let Some(q) = c.qualifier.as_deref()
16208 && !q.eq_ignore_ascii_case(&inner_alias)
16209 {
16210 return None;
16211 }
16212 let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
16213 // A duplicated output name would make substitution ambiguous.
16214 if map
16215 .insert(out_name.to_ascii_lowercase(), c.clone())
16216 .is_some()
16217 {
16218 return None;
16219 }
16220 }
16221 if map.is_empty() {
16222 return None;
16223 }
16224 let derived_alias = primary
16225 .alias
16226 .clone()
16227 .unwrap_or_else(|| primary.name.clone())
16228 .to_ascii_lowercase();
16229 // Substitute in a clone; bail (None) on the first reference the map
16230 // cannot answer.
16231 let mut out = stmt.clone();
16232 let ok = core::cell::Cell::new(true);
16233 let mut subst = |e: &mut Expr| -> bool {
16234 match e {
16235 Expr::Column(c) => {
16236 match c.qualifier.as_deref() {
16237 Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
16238 None => {}
16239 Some(_) => {
16240 ok.set(false);
16241 return true;
16242 }
16243 }
16244 match map.get(&c.name.to_ascii_lowercase()) {
16245 Some(target) => *c = target.clone(),
16246 None => ok.set(false),
16247 }
16248 true
16249 }
16250 // Any subquery could reference q from its own scope;
16251 // descending is a later knife — bail for now.
16252 Expr::ScalarSubquery(_)
16253 | Expr::Exists { .. }
16254 | Expr::InSubquery { .. }
16255 | Expr::RowInSubquery { .. }
16256 | Expr::RowCmpSubquery { .. } => {
16257 ok.set(false);
16258 true
16259 }
16260 _ => false,
16261 }
16262 };
16263 for item in &mut out.items {
16264 match item {
16265 SelectItem::Expr { expr, .. } => {
16266 crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
16267 }
16268 // `SELECT * FROM (…) q` means q's columns, in q's order.
16269 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
16270 }
16271 }
16272 if let Some(w) = &mut out.where_ {
16273 crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
16274 }
16275 if let Some(gs) = &mut out.group_by {
16276 for g in gs {
16277 crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
16278 }
16279 }
16280 if let Some(h) = &mut out.having {
16281 crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
16282 }
16283 for o in &mut out.order_by {
16284 crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
16285 }
16286 for d in &mut out.distinct_on {
16287 crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
16288 }
16289 if !ok.get() {
16290 return None;
16291 }
16292 // FROM becomes the stored table; the filters conjoin.
16293 out.from = Some(spg_sql::ast::FromClause {
16294 primary: it.clone(),
16295 joins: Vec::new(),
16296 });
16297 out.where_ = match (inner.where_.clone(), out.where_.take()) {
16298 (Some(a), Some(b)) => Some(Expr::Binary {
16299 lhs: alloc::boxed::Box::new(a),
16300 op: spg_sql::ast::BinOp::And,
16301 rhs: alloc::boxed::Box::new(b),
16302 }),
16303 (Some(a), None) => Some(a),
16304 (None, b) => b,
16305 };
16306 Some(out)
16307}
16308
16309/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
16310/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
16311/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
16312/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
16313/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
16314/// DISTINCT, an SRF, or an unprovable inner shape stays put.
16315fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16316 use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
16317 let inner = primary.lateral_subquery.as_deref()?;
16318 // Outer: exactly `SELECT count(*)`, nothing else.
16319 if !stmt.ctes.is_empty()
16320 || !stmt.unions.is_empty()
16321 || stmt.distinct
16322 || !stmt.distinct_on.is_empty()
16323 || stmt.where_.is_some()
16324 || stmt.group_by.is_some()
16325 || stmt.having.is_some()
16326 || !stmt.order_by.is_empty()
16327 || stmt.limit.is_some()
16328 || stmt.offset.is_some()
16329 || stmt.items.len() != 1
16330 {
16331 return None;
16332 }
16333 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16334 return None;
16335 };
16336 let E::FunctionCall { name, args } = expr else {
16337 return None;
16338 };
16339 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16340 return None;
16341 }
16342 // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
16343 let Some(LimitExpr::Literal(k)) = &inner.offset else {
16344 return None;
16345 };
16346 let k = i64::from(*k);
16347 if inner.limit.is_some() || inner.order_by.is_empty() {
16348 return None;
16349 }
16350 let mut counted = inner.clone();
16351 counted.order_by = Vec::new();
16352 counted.offset = None;
16353 // The stripped inner must now be a provable simple shape (its
16354 // items become irrelevant — count(*) reads none of them — but an
16355 // SRF item would change the row count, so the flatten predicate's
16356 // scrutiny still applies).
16357 let base = matview_flatten_probe(&counted)?;
16358 let mut out = stmt.clone();
16359 out.items = alloc::vec![SelectItem::Expr {
16360 expr: E::FunctionCall {
16361 name: String::from("greatest"),
16362 args: alloc::vec![
16363 E::Binary {
16364 lhs: alloc::boxed::Box::new(E::FunctionCall {
16365 name: String::from("count_star"),
16366 args: alloc::vec![],
16367 }),
16368 op: spg_sql::ast::BinOp::Sub,
16369 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16370 },
16371 E::Literal(spg_sql::ast::Literal::Integer(0)),
16372 ],
16373 },
16374 alias: Some(String::from("count")),
16375 }];
16376 out.from = Some(spg_sql::ast::FromClause {
16377 primary: base,
16378 joins: Vec::new(),
16379 });
16380 out.where_ = counted.where_.clone();
16381 Some(out)
16382}
16383
16384/// The inner-shape probe `try_count_over_offset` shares with the
16385/// flatten: single stored table, no modifiers, no subqueries, no SRF
16386/// items. Returns the base TableRef.
16387fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
16388 use spg_sql::ast::SelectItem;
16389 if !inner.ctes.is_empty()
16390 || !inner.unions.is_empty()
16391 || inner.distinct
16392 || !inner.distinct_on.is_empty()
16393 || inner.group_by.is_some()
16394 || inner.group_by_all
16395 || inner.having.is_some()
16396 || !inner.order_by.is_empty()
16397 || inner.limit.is_some()
16398 || inner.offset.is_some()
16399 || !inner.window_check_exprs.is_empty()
16400 || inner.locking.is_some()
16401 {
16402 return None;
16403 }
16404 let ifrom = inner.from.as_ref()?;
16405 let it = &ifrom.primary;
16406 if !ifrom.joins.is_empty()
16407 || it.name.is_empty()
16408 || it.lateral_subquery.is_some()
16409 || it.unnest_expr.is_some()
16410 || it.generate_series_args.is_some()
16411 || it.as_of_segment.is_some()
16412 || it.jsonb_each_text_arg.is_some()
16413 || it.table_fn_call.is_some()
16414 || it.rows_from.is_some()
16415 || it.json_table.is_some()
16416 || it.with_ordinality
16417 {
16418 return None;
16419 }
16420 for item in &inner.items {
16421 match item {
16422 SelectItem::Expr { expr, .. } => {
16423 if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
16424 return None;
16425 }
16426 }
16427 SelectItem::Wildcard => {}
16428 SelectItem::QualifiedWildcard(_) => return None,
16429 }
16430 }
16431 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16432 return None;
16433 }
16434 Some(it.clone())
16435}
16436
16437/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
16438/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
16439/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
16440/// constant-LENGTH array literal unnests to exactly k rows per input
16441/// row (NULL elements are rows too). One SRF item only, elements
16442/// subquery-free, and the stripped inner must pass the same probe the
16443/// count-over-offset rewrite uses.
16444fn try_count_over_const_unnest(
16445 stmt: &SelectStatement,
16446 primary: &TableRef,
16447) -> Option<SelectStatement> {
16448 use spg_sql::ast::{Expr as E, SelectItem};
16449 let inner = primary.lateral_subquery.as_deref()?;
16450 if !stmt.ctes.is_empty()
16451 || !stmt.unions.is_empty()
16452 || stmt.distinct
16453 || !stmt.distinct_on.is_empty()
16454 || stmt.where_.is_some()
16455 || stmt.group_by.is_some()
16456 || stmt.having.is_some()
16457 || !stmt.order_by.is_empty()
16458 || stmt.limit.is_some()
16459 || stmt.offset.is_some()
16460 || stmt.items.len() != 1
16461 {
16462 return None;
16463 }
16464 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16465 return None;
16466 };
16467 let E::FunctionCall { name, args } = expr else {
16468 return None;
16469 };
16470 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16471 return None;
16472 }
16473 // Inner: exactly one item, and it is unnest(ARRAY[...]).
16474 if inner.items.len() != 1
16475 || !inner.order_by.is_empty()
16476 || inner.limit.is_some()
16477 || inner.offset.is_some()
16478 {
16479 return None;
16480 }
16481 let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
16482 return None;
16483 };
16484 let E::FunctionCall {
16485 name: fname,
16486 args: fargs,
16487 } = item
16488 else {
16489 return None;
16490 };
16491 if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
16492 return None;
16493 }
16494 let E::Array(elems) = &fargs[0] else {
16495 return None;
16496 };
16497 if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
16498 return None;
16499 }
16500 let k = elems.len() as i64;
16501 // The stripped inner (the SRF item replaced by a plain constant)
16502 // must be the provable simple shape.
16503 let mut counted = inner.clone();
16504 counted.items = alloc::vec![SelectItem::Expr {
16505 expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
16506 alias: None,
16507 }];
16508 let base = matview_flatten_probe(&counted)?;
16509 let mut out = stmt.clone();
16510 out.items = alloc::vec![SelectItem::Expr {
16511 expr: E::Binary {
16512 lhs: alloc::boxed::Box::new(E::FunctionCall {
16513 name: String::from("count_star"),
16514 args: alloc::vec![],
16515 }),
16516 op: spg_sql::ast::BinOp::Mul,
16517 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16518 },
16519 alias: Some(String::from("count")),
16520 }];
16521 out.from = Some(spg_sql::ast::FromClause {
16522 primary: base,
16523 joins: Vec::new(),
16524 });
16525 out.where_ = counted.where_.clone();
16526 Some(out)
16527}