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 )
306 });
307 match seek_positions {
308 Some(mut positions) => {
309 // Table order, which is the order the scan
310 // would have produced.
311 positions.sort_unstable();
312 for (n, pos) in positions.into_iter().enumerate() {
313 if n.is_multiple_of(256) {
314 cancel.check()?;
315 }
316 let Some(row) = table.rows().get(pos) else {
317 continue;
318 };
319 if passes(row)? {
320 filtered.push(row);
321 }
322 }
323 }
324 None => {
325 for (i, row) in table.scan_visible(&snap) {
326 if i.is_multiple_of(256) {
327 cancel.check()?;
328 }
329 if passes(row)? {
330 filtered.push(row);
331 }
332 }
333 }
334 }
335 }
336 }
337 } else {
338 let deferred = self.build_joined_filtered_rows(
339 from,
340 stmt.where_.as_ref(),
341 cancel,
342 None,
343 &mut ByteBudget::new(self.max_query_bytes),
344 )?;
345 // A join's survivors are row-index tuples over its sources, so
346 // there is no single row to borrow — this branch owns them.
347 owned_rows = deferred.materialise();
348 rows_are_owned = true;
349 schema_cols_owned = deferred.combined_schema;
350 alias_opt = None;
351 }
352 if rows_are_owned {
353 filtered = owned_rows.iter().collect();
354 }
355 let schema_cols = &schema_cols_owned;
356 let ctx = self.ev_ctx(schema_cols, alias_opt);
357 let alias = alias_opt.unwrap_or("");
358 let n_rows = filtered.len();
359 // The window pipeline reads `&[&Row<'static>]`, and `filtered`
360 // already is one whichever branch produced it — the separate
361 // `filtered_refs` this used to build was the collect that made
362 // owning the rows look necessary.
363
364 // 2) Collect unique window function nodes from projection.
365 let mut window_nodes: Vec<Expr> = Vec::new();
366 for item in &stmt.items {
367 if let SelectItem::Expr { expr, .. } = item {
368 collect_window_nodes(expr, &mut window_nodes);
369 }
370 }
371 // v7.39 (round 592) — and from ORDER BY, which may name a window the
372 // select list never mentions. The order-key builder below rewrites
373 // window calls to `__win_N` columns, and a call that was never
374 // collected has no column to become.
375 for o in &stmt.order_by {
376 collect_window_nodes(&o.expr, &mut window_nodes);
377 }
378
379 // 3) For each window, compute per-row value.
380 // Index: same order as window_nodes; for row i, win_vals[w][i].
381 let mut win_vals: Vec<Vec<Value<'static>>> = Vec::with_capacity(window_nodes.len());
382 for wnode in &window_nodes {
383 let Expr::WindowFunction {
384 name,
385 args,
386 partition_by,
387 order_by,
388 frame,
389 null_treatment,
390 filter,
391 } = wnode
392 else {
393 unreachable!("collect_window_nodes pushes only WindowFunction");
394 };
395 // Compute (partition_key, order_key, original_index) for each row.
396 // v7.39 (round 593) — a key that is a plain column sits at the same
397 // position in every row, but was resolved BY NAME for each one. A
398 // per-library profile of `lag(id) OVER (ORDER BY id)` put
399 // `resolve_column` at 5.8% of the query on its own, with
400 // `rehydrate_cell` and the `eval_expr` dispatch behind it. Resolve
401 // once; anything that is not a plain column keeps the resolver.
402 let p_bound: Vec<Option<usize>> = partition_by
403 .iter()
404 .map(|e| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
405 .collect();
406 let o_bound: Vec<Option<usize>> = order_by
407 .iter()
408 .map(|(e, _, _)| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
409 .collect();
410 let arg_bound = args
411 .first()
412 .and_then(|a| crate::orderby::bound_column_position(a, schema_cols, alias_opt));
413 // v7.39 (round 690) — a window's ORDER BY over a column that
414 // declares a collation sorts by it, the same as a top-level
415 // ORDER BY. Resolved from the bound position, so only a bare
416 // column gets one; an expression produces a new value and the
417 // derivation that would give IT a collation is unbuilt.
418 let o_colls: Vec<Option<alloc::string::String>> = o_bound
419 .iter()
420 .map(|p| {
421 p.and_then(|pos| schema_cols.get(pos))
422 .and_then(|sc| sc.collation_name.clone())
423 .filter(|n| crate::collate::is_supported(n))
424 })
425 .collect();
426 let mut indexed: Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)> =
427 Vec::with_capacity(n_rows);
428 // v7.39 (round 731) — single bound INT partition key, no window
429 // ORDER BY: group on the i64 directly. The generic build paid
430 // two heap Vecs per row (pkey + empty okey) plus a canonical
431 // string encode per row just to bucket 500k rows into 100
432 // groups; the whole per-row key apparatus disappears here.
433 // Neither key Vec is read downstream on this path: the hash
434 // grouping replaces partition_key_cmp, and okey is empty by
435 // construction.
436 let int_pkey_fast = order_by.is_empty()
437 && partition_by.len() == 1
438 && p_bound[0].is_some_and(|pos| {
439 matches!(
440 schema_cols.get(pos).map(|c| c.ty),
441 Some(
442 spg_storage::DataType::Int
443 | spg_storage::DataType::BigInt
444 | spg_storage::DataType::SmallInt
445 )
446 )
447 });
448 // v7.39 (round 979) — the same idea for a single bound INT
449 // window ORDER BY: sort on the i64 instead of on a heap vector
450 // per row.
451 //
452 // Measured at 400k rows (round 978, ablation, answer checked
453 // byte-for-byte against the general path on a key column that
454 // is a permutation): `row_number() OVER (ORDER BY k)` went
455 // 157.057-157.868 ms to 31.253-31.679, which is 79.8% and puts
456 // it on top of the `OVER ()` baseline — the sort essentially
457 // disappears. Round 977 had already shown the cost was
458 // key-shaped rather than row-shaped: the sort's share was
459 // 132.0 ms on a three-integer table and 132.5 with a 200-byte
460 // column added, and a per-row COPY does scale with width
461 // (round 976 measured that at +36 ns/row/200 bytes).
462 //
463 // Gated to ROW_NUMBER, which is the one function that reads
464 // neither key vector — it numbers the order it is handed.
465 // `rank` and `dense_rank` compare adjacent entries' order keys
466 // in `compute_window_partition`, so leaving those vectors
467 // empty would silently give every row rank 1. A wider version
468 // would carry the i64 in the entry and teach those two to use
469 // it; this one is the part that can be shown correct by
470 // construction.
471 let int_okey_fast = partition_by.is_empty()
472 && order_by.len() == 1
473 && frame.is_none()
474 && filter.is_none()
475 && matches!(null_treatment, spg_sql::ast::NullTreatment::Respect)
476 && name.eq_ignore_ascii_case("row_number")
477 && o_bound[0].is_some_and(|pos| {
478 matches!(
479 schema_cols.get(pos).map(|c| c.ty),
480 Some(
481 spg_storage::DataType::Int
482 | spg_storage::DataType::BigInt
483 | spg_storage::DataType::SmallInt
484 )
485 )
486 });
487 // Set when a cell in that column turns out not to be an
488 // integer after all. The declared type says it should be, but
489 // "should" is not a thing to sort 400k rows on, so the general
490 // path takes over and this build is discarded.
491 let mut int_okey_bailed = false;
492 if int_okey_fast {
493 let pos = o_bound[0].expect("gated bound");
494 let desc = order_by[0].1;
495 // PG orders NULLs last ascending and first descending
496 // unless the query says otherwise.
497 let nulls_first = order_by[0].2.unwrap_or(desc);
498 let mut keyed: Vec<(bool, i64, usize)> = Vec::with_capacity(n_rows);
499 for (i, row) in filtered.iter().enumerate() {
500 match row.values.get(pos) {
501 Some(Value::Int(n)) => keyed.push((false, i64::from(*n), i)),
502 Some(Value::BigInt(n)) => keyed.push((false, *n, i)),
503 Some(Value::SmallInt(n)) => keyed.push((false, i64::from(*n), i)),
504 Some(Value::Null) | None => keyed.push((true, 0, i)),
505 Some(_) => {
506 int_okey_bailed = true;
507 break;
508 }
509 }
510 }
511 if !int_okey_bailed {
512 // `null_rank` puts NULLs on the side the query asked
513 // for; the row's original index breaks every tie, so
514 // equal keys keep the order the scan produced — what
515 // the stable sort below would have given them.
516 let null_rank = |is_null: bool| -> u8 { u8::from(is_null != nulls_first) };
517 keyed.sort_unstable_by(|a, b| {
518 null_rank(a.0)
519 .cmp(&null_rank(b.0))
520 .then_with(|| {
521 if a.0 {
522 core::cmp::Ordering::Equal
523 } else if desc {
524 b.1.cmp(&a.1)
525 } else {
526 a.1.cmp(&b.1)
527 }
528 })
529 .then_with(|| a.2.cmp(&b.2))
530 });
531 for (_, _, i) in keyed {
532 indexed.push((Vec::new(), Vec::new(), i));
533 }
534 } else {
535 indexed.clear();
536 }
537 }
538 if int_okey_fast && !int_okey_bailed {
539 // Ordered above; nothing else to build.
540 } else if int_pkey_fast {
541 let pos = p_bound[0].expect("gated bound");
542 let mut slot: hashbrown::HashMap<Option<i64>, usize> = hashbrown::HashMap::new();
543 let mut groups: Vec<Vec<usize>> = Vec::new();
544 for (i, row) in filtered.iter().enumerate() {
545 let k: Option<i64> = match row.values.get(pos) {
546 Some(Value::BigInt(n)) => Some(*n),
547 Some(Value::Int(n)) => Some(i64::from(*n)),
548 Some(Value::SmallInt(n)) => Some(i64::from(*n)),
549 _ => None,
550 };
551 match slot.get(&k) {
552 Some(&gi) => groups[gi].push(i),
553 None => {
554 slot.insert(k, groups.len());
555 groups.push(alloc::vec![i]);
556 }
557 }
558 }
559 // The downstream partition-boundary scan compares pkeys
560 // of ADJACENT entries, so the key must ride along — one
561 // single-element Vec per row (half the generic build's
562 // allocations, no string encode).
563 for g in groups {
564 for i in g {
565 let k: Value<'static> = match filtered[i].values.get(pos) {
566 Some(v) => v.clone(),
567 None => Value::Null,
568 };
569 indexed.push((alloc::vec![k], Vec::new(), i));
570 }
571 }
572 } else {
573 for (i, row) in filtered.iter().enumerate() {
574 let pkey: Vec<Value<'static>> = partition_by
575 .iter()
576 .enumerate()
577 .map(
578 |(k, p)| match p_bound[k].and_then(|pos| row.values.get(pos)) {
579 Some(v) => Ok(v.clone()),
580 None => eval::eval_expr(p, row, &ctx),
581 },
582 )
583 .collect::<Result<_, _>>()?;
584 // v7.39 (read01 round 54) — a window's ORDER BY over an enum
585 // column must sort by MEMBER order (enumsortorder), not the
586 // label's text. Enum values are Text at runtime, so the raw
587 // value key sorted alphabetically — `row_number() OVER (ORDER
588 // BY mood)` numbered the rows happy,ok,sad. Substitute the
589 // member ordinal, the same key the top-level ORDER BY uses.
590 // (Closes the enum-order knife's recorded window residual.)
591 let okey: Vec<(Value, bool, Option<bool>)> = order_by
592 .iter()
593 .enumerate()
594 .map(|(k, (e, desc, nf))| -> Result<_, EngineError> {
595 let v = match o_bound[k].and_then(|pos| row.values.get(pos)) {
596 Some(v) => v.clone(),
597 None => eval::eval_expr(e, row, &ctx)?,
598 };
599 let v = match crate::orderby::enum_order_ordinal(e, &v, &ctx) {
600 Some(ord) => Value::Float(ord),
601 None => v,
602 };
603 Ok((v, *desc, *nf))
604 })
605 .collect::<Result<_, _>>()?;
606 indexed.push((pkey, okey, i));
607 }
608 }
609 // Sort by (partition_key, order_key). Partition key uses
610 // a stable encoded form; order key respects ASC/DESC.
611 // v7.39 (round 731) — with NO window ORDER BY the sort's only
612 // job was putting same-partition rows next to each other, and a
613 // 500k-row comparison sort is a spectacular way to hash-group:
614 // the panel's `sum(id) OVER (PARTITION BY g)` spent ~100 ms
615 // here. Group by encoded key instead, preserving row order
616 // inside each group — exactly what the stable sort preserved,
617 // so every function (row_number included) answers the same.
618 if int_okey_fast && !int_okey_bailed {
619 // Already ordered by the i64 key above.
620 } else if int_pkey_fast {
621 // Already grouped above; same-partition rows are adjacent
622 // in original row order.
623 } else if order_by.is_empty() && !partition_by.is_empty() {
624 let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
625 let mut groups: Vec<
626 Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)>,
627 > = Vec::new();
628 let mut keybuf = String::new();
629 for entry in indexed.drain(..) {
630 keybuf.clear();
631 for v in &entry.0 {
632 crate::aggregate::push_canonical_key(&mut keybuf, v);
633 }
634 match slot.get(keybuf.as_str()) {
635 Some(&gi) => groups[gi].push(entry),
636 None => {
637 slot.insert(keybuf.clone(), groups.len());
638 groups.push(alloc::vec![entry]);
639 }
640 }
641 }
642 for g in groups {
643 indexed.extend(g);
644 }
645 } else {
646 indexed.sort_by(|a, b| {
647 let p_cmp = partition_key_cmp(&a.0, &b.0);
648 if p_cmp != core::cmp::Ordering::Equal {
649 return p_cmp;
650 }
651 crate::window::order_key_cmp_in(&a.1, &b.1, &o_colls)
652 });
653 }
654 // Per-partition compute.
655 let mut out_vals: Vec<Value<'static>> = alloc::vec![Value::Null; n_rows];
656 let mut p_start = 0;
657 while p_start < indexed.len() {
658 let mut p_end = p_start + 1;
659 while p_end < indexed.len()
660 && partition_key_cmp(&indexed[p_start].0, &indexed[p_end].0)
661 == core::cmp::Ordering::Equal
662 {
663 p_end += 1;
664 }
665 // Compute the function within this partition slice.
666 compute_window_partition(
667 name,
668 args,
669 arg_bound,
670 !order_by.is_empty(),
671 frame.as_ref(),
672 *null_treatment,
673 filter.as_deref(),
674 &indexed[p_start..p_end],
675 &filtered,
676 &ctx,
677 &mut out_vals,
678 )?;
679 p_start = p_end;
680 }
681 win_vals.push(out_vals);
682 }
683
684 // 4) Build extended schema: original columns + synthetic.
685 let mut ext_cols = schema_cols.clone();
686 for i in 0..window_nodes.len() {
687 ext_cols.push(ColumnSchema::new(
688 alloc::format!("__win_{i}"),
689 DataType::Text, // type doesn't matter for projection eval
690 true,
691 ));
692 }
693 // 6) Rewrite the projection: WindowFunction nodes → Column(__win_N).
694 let mut rewritten_items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
695 for item in &stmt.items {
696 let new_item = match item {
697 SelectItem::Wildcard => SelectItem::Wildcard,
698 SelectItem::QualifiedWildcard(q) => SelectItem::QualifiedWildcard(q.clone()),
699 SelectItem::Expr { expr, alias } => {
700 let mut e = expr.clone();
701 rewrite_window_to_columns(&mut e, &window_nodes);
702 // The rewrite swaps the window call for a synthetic
703 // `__win_N` column, and the projection then reported
704 // THAT as the column name — `SELECT count(*) OVER ()`
705 // answered `__win_0`, an internal name, where PG18
706 // answers `count`. Pin the name while the call the
707 // column is named for is still in hand.
708 let alias = if alias.is_none() && e != *expr {
709 Some(default_output_name(expr, self.backslash_escapes))
710 } else {
711 alias.clone()
712 };
713 SelectItem::Expr { expr: e, alias }
714 }
715 };
716 rewritten_items.push(new_item);
717 }
718
719 // 7) Project into final rows. JOIN case uses None so the
720 // qualifier check in `resolve_column` falls through to the
721 // composite `alias.col` schema lookup; single-table case
722 // keeps the bare alias so `bare_col` resolution still
723 // works for the projection's per-row column references.
724 // v7.39 (read01 round 54) — build through `ev_ctx`, the canonical
725 // constructor: it threads the catalog (plus render style / tz / GUCs)
726 // that a bare `EvalContext::new` drops. Without the catalog the OUTER
727 // `ORDER BY <enum col>` of a windowed query sorted by TEXT — the
728 // window values were right, the row order silently was not.
729 let ext_ctx = self.ev_ctx(&ext_cols, alias_opt);
730 let projection = build_projection_hiding_tail(
731 &rewritten_items,
732 &ext_cols,
733 alias,
734 self.backslash_escapes,
735 window_nodes.len(),
736 )?;
737 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(n_rows);
738 // v7.39 (round 592) — the extended row (input columns plus the window
739 // values) used to be materialised for EVERY input row and kept until
740 // the projection had run: the input values cloned into a fresh Vec,
741 // then grown once to take the window columns. A counting allocator put
742 // the window path at 4 allocations a row where a plain derived table
743 // takes 1, and named all four — the input row, the clone, the growth,
744 // and the projected row. Only the last has to exist afterwards, so the
745 // extended row is one buffer refilled per row.
746 let mut ext_row: Row<'static> =
747 Row::new(Vec::with_capacity(schema_cols.len() + window_nodes.len()));
748 for i in 0..n_rows {
749 if i.is_multiple_of(256) {
750 cancel.check()?;
751 }
752 ext_row.values.clear();
753 ext_row.values.extend(filtered[i].values.iter().cloned());
754 for w in 0..window_nodes.len() {
755 ext_row.values.push(win_vals[w][i].clone());
756 }
757 let row = &ext_row;
758 let mut values = Vec::with_capacity(projection.len());
759 for p in &projection {
760 values.push(eval::eval_expr(&p.expr, row, &ext_ctx)?);
761 }
762 let order_keys = if stmt.order_by.is_empty() {
763 Vec::new()
764 } else {
765 let mut keys = Vec::with_capacity(stmt.order_by.len());
766 for o in &stmt.order_by {
767 let mut e = o.expr.clone();
768 rewrite_window_to_columns(&mut e, &window_nodes);
769 let key = eval::eval_expr(&e, row, &ext_ctx)?;
770 // v7.39 (read01 round 54) — this path builds its order keys
771 // itself instead of going through `build_order_keys`, so it
772 // skipped the enum-ordinal substitution: the OUTER
773 // `ORDER BY <enum col>` of a windowed query sorted by the
774 // label's TEXT, not by member order. The window values were
775 // right and only the row order was wrong — silently.
776 match crate::orderby::enum_order_ordinal(&e, &key, &ext_ctx) {
777 Some(ord) => keys.push(value_to_order_key(&Value::Float(ord))?),
778 None => keys.push(value_to_order_key(&key)?),
779 }
780 }
781 keys
782 };
783 tagged.push((order_keys, Row::new(values)));
784 }
785 // ORDER BY + LIMIT/OFFSET on the projected rows.
786 if !stmt.order_by.is_empty() {
787 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
788 sort_by_keys(&mut tagged, &descs);
789 }
790 let mut out_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
791 // v7.37 D.41 — `SELECT DISTINCT` over a window projection: the window
792 // pipeline builds one output row per input row, so DISTINCT must dedup the
793 // projected rows (PG evaluates window functions before DISTINCT). Applied
794 // after ORDER BY (duplicate rows share sort keys, so order is preserved)
795 // and before LIMIT.
796 if stmt.distinct {
797 out_rows = dedup_rows(out_rows, self.backslash_escapes);
798 }
799 apply_offset_and_limit(&mut out_rows, stmt.offset_literal(), stmt.limit_literal());
800 let final_cols: Vec<ColumnSchema> = projection
801 .into_iter()
802 .map(|p| {
803 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
804 c.user_enum_type = p.user_enum_type;
805 c.collation_name = p.collation_name;
806 c.mysql_fsp = p.mysql_fsp;
807 c
808 })
809 .collect();
810 Ok(QueryResult::Rows {
811 columns: final_cols,
812 rows: out_rows,
813 })
814 }
815
816 /// v4.11: materialise each CTE into a temp table inside a
817 /// cloned catalog, then run the body SELECT against a fresh
818 /// engine instance that owns the enriched catalog. The clone
819 /// is moderately expensive — only paid by CTE-bearing queries.
820 /// Subqueries inside CTE bodies / the main body resolve as
821 /// usual; `clock_fn` is propagated so `NOW()` lines up.
822 /// v7.16.2 — mailrs round-10 A.3. Materialise the
823 /// `information_schema.*` / `pg_catalog.*` virtual views
824 /// the SELECT references, then re-execute the SELECT
825 /// against an enriched catalog where those views are real
826 /// tables. Same pattern as `exec_with_ctes`. The temp
827 /// engine carries `meta_views_materialised = true` so its
828 /// own meta-dispatch short-circuits — without that we'd
829 /// infinite-recurse since the temp catalog's view name
830 /// still starts with `__spg_info_` and re-triggers the
831 /// check.
832 pub(crate) fn exec_select_with_meta_views(
833 &self,
834 stmt: &SelectStatement,
835 cancel: CancelToken<'_>,
836 ) -> Result<QueryResult, EngineError> {
837 let catalog = self.meta_view_catalog(stmt)?;
838 let mut temp = Engine::restore(catalog);
839 if let Some(c) = self.clock {
840 temp = temp.with_clock(c);
841 }
842 if let Some(f) = self.salt_fn {
843 temp = temp.with_salt_fn(f);
844 }
845 // v7.39 (round 522) — the temp engine holds the materialised
846 // catalog and, until now, nothing of the SESSION. So every
847 // session-scoped answer changed the moment a system view
848 // appeared in the FROM clause: `SELECT current_user` said
849 // `unmei` and `SELECT current_user FROM pg_class` said `admin`;
850 // `current_setting('work_mem')` fell back to the boot default
851 // after a SET; `application_name` read empty. A privilege check
852 // written against a catalog join was reading a different
853 // identity than the same check written without one.
854 //
855 // Carry what a session can be observed through — its parameters
856 // (which is also where the session user lives), the role store
857 // the privilege builtins read, the dialect, and the rendering
858 // settings a timestamp is spelled with.
859 temp.session_params.clone_from(&self.session_params);
860 temp.users.clone_from(&self.users);
861 temp.backslash_escapes = self.backslash_escapes;
862 temp.mysql_strict = self.mysql_strict;
863 temp.render_style = self.render_style;
864 temp.tz_offset_fn = self.tz_offset_fn;
865 temp.tz_localize_fn = self.tz_localize_fn;
866 temp.tz_abbrev_fn = self.tz_abbrev_fn;
867 temp.meta_views_materialised = true;
868 temp.exec_select_cancel(stmt, cancel)
869 }
870
871 /// v7.39 (round 462) — the catalog a meta-view SELECT resolves
872 /// against: this engine's catalog with every `__spg_*` view the
873 /// statement references materialised into it.
874 ///
875 /// Split out of `exec_select_with_meta_views` so Describe can reach
876 /// the same shapes execution reaches. Describe used to look the FROM
877 /// relation up in the plain catalog, where a system view does not
878 /// exist, and reported "no columns" for every one of them — so an
879 /// extended-protocol client reading `pg_stat_user_tables` got rows
880 /// with no column metadata. Sharing the materialisation means a
881 /// view added here is described correctly the day it is added.
882 pub(crate) fn meta_view_catalog(&self, stmt: &SelectStatement) -> Result<Catalog, EngineError> {
883 let mut needed: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
884 collect_meta_view_names(stmt, &mut needed);
885 let mut catalog = self.active_catalog().clone();
886 for view in &needed {
887 if catalog.get(view).is_some() {
888 continue;
889 }
890 match view.as_str() {
891 "__spg_info_columns" => {
892 let (schema, rows) = synth_information_schema_columns(
893 self.active_catalog(),
894 self.backslash_escapes,
895 );
896 materialise_meta_view(&mut catalog, view, schema, rows)?;
897 }
898 "__spg_info_tables" => {
899 let (schema, rows) = synth_information_schema_tables(self.active_catalog());
900 materialise_meta_view(&mut catalog, view, schema, rows)?;
901 }
902 "__spg_pg_class" => {
903 let (schema, rows) = synth_pg_class(
904 self.active_catalog(),
905 i64::try_from(self.vacuum_oldest_active()).unwrap_or(i64::MAX),
906 );
907 materialise_meta_view(&mut catalog, view, schema, rows)?;
908 }
909 "__spg_pg_attribute" => {
910 let (schema, rows) = synth_pg_attribute(self.active_catalog());
911 materialise_meta_view(&mut catalog, view, schema, rows)?;
912 }
913 // v7.17.0 Phase 3.P0-50 — pg_catalog.pg_type for
914 // sqlx / SQLAlchemy / Diesel / pgAdmin lookups.
915 "__spg_pg_type" => {
916 let (schema, rows) = synth_pg_type(self.active_catalog());
917 materialise_meta_view(&mut catalog, view, schema, rows)?;
918 }
919 // v7.39 (round 621) — pg_catalog.pg_operator, which did not
920 // exist at all.
921 "__spg_pg_operator" => {
922 let (schema, rows) = synth_pg_operator(self.active_catalog());
923 materialise_meta_view(&mut catalog, view, schema, rows)?;
924 }
925 // v7.17.0 Phase 3.P0-51 — pg_catalog.pg_proc for
926 // function-name introspection (ORM / pgAdmin).
927 "__spg_pg_proc" => {
928 let (schema, rows) = synth_pg_proc(self.active_catalog());
929 materialise_meta_view(&mut catalog, view, schema, rows)?;
930 }
931 // v7.24 (round-16 D) — pg_catalog.pg_trigger. The
932 // round-16 "why doesn't prod fire the trigger"
933 // question was unanswerable because triggers had NO
934 // introspection surface; tgname/tgenabled plus the
935 // pragmatic relname/timing/events/function columns
936 // make "is it registered and enabled" a one-liner.
937 "__spg_pg_trigger" => {
938 let (schema, rows) = synth_pg_trigger(self.active_catalog());
939 materialise_meta_view(&mut catalog, view, schema, rows)?;
940 }
941 // v7.17.0 Phase 3.P0-52 — pg_catalog.pg_namespace
942 // (schema list for admin tools' tree views).
943 "__spg_pg_namespace" => {
944 let (schema, rows) = synth_pg_namespace(self.active_catalog());
945 materialise_meta_view(&mut catalog, view, schema, rows)?;
946 }
947 // v7.39 — pg_tables convenience view (was a pgwire
948 // canned response that ignored projections).
949 "__spg_pg_tables" => {
950 let (schema, rows) =
951 crate::system_catalog::synth_pg_tables(self.active_catalog());
952 materialise_meta_view(&mut catalog, view, schema, rows)?;
953 }
954 // v7.37.24 (24.1) — pg_catalog.pg_enum (label list
955 // for ENUM types; sqlx / ORM enum codecs read this).
956 "__spg_pg_enum" => {
957 let (schema, rows) =
958 crate::system_catalog::synth_pg_enum(self.active_catalog());
959 materialise_meta_view(&mut catalog, view, schema, rows)?;
960 }
961 // v7.37.21 (21.13) — pg_catalog.pg_replication_slots
962 // (shape-stable empty until 21.12 persists slot state).
963 // v7.39 (round 277) — session-scoped prepared statements.
964 "__spg_pg_prepared_statements" => {
965 let (schema, rows) = crate::system_catalog::synth_pg_prepared_statements(
966 &self.prepared_statements,
967 );
968 materialise_meta_view(&mut catalog, view, schema, rows)?;
969 }
970 "__spg_pg_replication_slots" => {
971 let (schema, rows) =
972 crate::system_catalog::synth_pg_replication_slots(self.active_catalog());
973 materialise_meta_view(&mut catalog, view, schema, rows)?;
974 }
975 // v7.37.21 (21.13-b) — pg_catalog.pg_publication
976 // (one row per CREATE PUBLICATION).
977 "__spg_pg_publication" => {
978 let (schema, rows) = crate::system_catalog::synth_pg_publication(self);
979 materialise_meta_view(&mut catalog, view, schema, rows)?;
980 }
981 // v7.37.21 (21.13-c) — pg_catalog.pg_subscription
982 // (one row per CREATE SUBSCRIPTION; subconninfo
983 // redacted so dashboards can't leak credentials).
984 "__spg_pg_subscription" => {
985 let (schema, rows) = crate::system_catalog::synth_pg_subscription(self);
986 materialise_meta_view(&mut catalog, view, schema, rows)?;
987 }
988 // v7.37.22 (22.x-stat-db) — pg_catalog.pg_stat_database
989 // (one row for SPG's single database; counters are
990 // shape-stable 0 until wiring lands).
991 "__spg_pg_stat_database" => {
992 let (schema, rows) = crate::system_catalog::synth_pg_stat_database(
993 self,
994 self.stat_tup_inserted,
995 self.stat_tup_updated,
996 self.stat_tup_deleted,
997 );
998 materialise_meta_view(&mut catalog, view, schema, rows)?;
999 }
1000 // v7.37.22 (22.14) — pg_catalog.pg_stat_user_tables
1001 // (per-table churn counters; live_tup = row count).
1002 "__spg_pg_stat_user_tables" => {
1003 // r192 — DML counters come from the engine-side
1004 // non-transactional map, not the (tx-shadowed)
1005 // catalog tables.
1006 let (schema, rows) = crate::system_catalog::synth_pg_stat_user_tables(
1007 self.active_catalog(),
1008 &self.table_write_stats,
1009 );
1010 materialise_meta_view(&mut catalog, view, schema, rows)?;
1011 }
1012 // v7.37.22 (22.15) — pg_catalog.pg_stat_user_indexes
1013 // (per-index usage counters; flag unused indexes).
1014 "__spg_pg_stat_user_indexes" => {
1015 let (schema, rows) =
1016 crate::system_catalog::synth_pg_stat_user_indexes(self.active_catalog());
1017 materialise_meta_view(&mut catalog, view, schema, rows)?;
1018 }
1019 // v7.37.22 (22.16) — pg_catalog.pg_stat_bgwriter.
1020 "__spg_pg_stat_bgwriter" => {
1021 let (schema, rows) =
1022 crate::system_catalog::synth_pg_stat_bgwriter(self.active_catalog());
1023 materialise_meta_view(&mut catalog, view, schema, rows)?;
1024 }
1025 // v7.38 (read01 P3.14) — pg_catalog.pg_stat_checkpointer /
1026 // pg_stat_wal shell views (shape-stable, counters pending).
1027 "__spg_pg_stat_checkpointer" => {
1028 let (schema, rows) =
1029 crate::system_catalog::synth_pg_stat_checkpointer(self.active_catalog());
1030 materialise_meta_view(&mut catalog, view, schema, rows)?;
1031 }
1032 "__spg_pg_stat_wal" => {
1033 let (schema, rows) =
1034 crate::system_catalog::synth_pg_stat_wal(self.active_catalog());
1035 materialise_meta_view(&mut catalog, view, schema, rows)?;
1036 }
1037 // v7.38 (read01 P3.15) — pg_catalog.pg_stat_slru /
1038 // pg_stat_subscription_stats shell views.
1039 "__spg_pg_stat_slru" => {
1040 let (schema, rows) =
1041 crate::system_catalog::synth_pg_stat_slru(self.active_catalog());
1042 materialise_meta_view(&mut catalog, view, schema, rows)?;
1043 }
1044 "__spg_pg_stat_subscription_stats" => {
1045 let (schema, rows) = crate::system_catalog::synth_pg_stat_subscription_stats(
1046 self.active_catalog(),
1047 );
1048 materialise_meta_view(&mut catalog, view, schema, rows)?;
1049 }
1050 // v7.37.22 (22.17) — pg_catalog.pg_stat_archiver.
1051 "__spg_pg_stat_archiver" => {
1052 let (schema, rows) =
1053 crate::system_catalog::synth_pg_stat_archiver(self.active_catalog());
1054 materialise_meta_view(&mut catalog, view, schema, rows)?;
1055 }
1056 // v7.37.21 (21.13-d) — pg_catalog.pg_stat_replication.
1057 "__spg_pg_stat_replication" => {
1058 let (schema, rows) =
1059 crate::system_catalog::synth_pg_stat_replication(self.active_catalog());
1060 materialise_meta_view(&mut catalog, view, schema, rows)?;
1061 }
1062 // v7.37.24 (24.13) — pg_catalog.pg_am.
1063 "__spg_pg_am" => {
1064 let (schema, rows) = crate::system_catalog::synth_pg_am(self.active_catalog());
1065 materialise_meta_view(&mut catalog, view, schema, rows)?;
1066 }
1067 // v7.37.22 (22.18) — pg_catalog.pg_stat_io (PG 16+).
1068 "__spg_pg_stat_io" => {
1069 let (schema, rows) =
1070 crate::system_catalog::synth_pg_stat_io(self.active_catalog());
1071 materialise_meta_view(&mut catalog, view, schema, rows)?;
1072 }
1073 // v7.37.22 (22.19) — pg_catalog.pg_stat_user_functions.
1074 "__spg_pg_stat_user_functions" => {
1075 let (schema, rows) =
1076 crate::system_catalog::synth_pg_stat_user_functions(self.active_catalog());
1077 materialise_meta_view(&mut catalog, view, schema, rows)?;
1078 }
1079 // v7.39 (round 287) — pg_catalog.pg_largeobject{,_metadata}.
1080 "__spg_pg_largeobject" => {
1081 let (schema, rows) =
1082 crate::system_catalog::synth_pg_largeobject(self.active_catalog());
1083 materialise_meta_view(&mut catalog, view, schema, rows)?;
1084 }
1085 "__spg_pg_largeobject_metadata" => {
1086 let (schema, rows) =
1087 crate::system_catalog::synth_pg_largeobject_metadata(self.active_catalog());
1088 materialise_meta_view(&mut catalog, view, schema, rows)?;
1089 }
1090 // v7.37.23 (23.7-a) — pg_catalog.pg_statistic_ext.
1091 "__spg_pg_statistic_ext" => {
1092 let (schema, rows) =
1093 crate::system_catalog::synth_pg_statistic_ext(self.active_catalog());
1094 materialise_meta_view(&mut catalog, view, schema, rows)?;
1095 }
1096 // v7.37.24 (24.15) — pg_catalog.pg_statistic.
1097 "__spg_pg_statistic" => {
1098 let (schema, rows) =
1099 crate::system_catalog::synth_pg_statistic(self.active_catalog());
1100 materialise_meta_view(&mut catalog, view, schema, rows)?;
1101 }
1102 // v7.37.22 (22.20) — pg_catalog.pg_stat_progress_vacuum.
1103 "__spg_pg_stat_progress_vacuum" => {
1104 let (schema, rows) =
1105 crate::system_catalog::synth_pg_stat_progress_vacuum(self.active_catalog());
1106 materialise_meta_view(&mut catalog, view, schema, rows)?;
1107 }
1108 // v7.37.22 (22.21) — pg_catalog.pg_stat_progress_create_index.
1109 "__spg_pg_stat_progress_create_index" => {
1110 let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_create_index(
1111 self.active_catalog(),
1112 );
1113 materialise_meta_view(&mut catalog, view, schema, rows)?;
1114 }
1115 // v7.37.22 (22.22) — pg_catalog.pg_stat_progress_analyze.
1116 "__spg_pg_stat_progress_analyze" => {
1117 let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_analyze(
1118 self.active_catalog(),
1119 );
1120 materialise_meta_view(&mut catalog, view, schema, rows)?;
1121 }
1122 // v7.37.24 (24.16) — pg_catalog.pg_inherits
1123 // (partition parent → child OID mapping).
1124 "__spg_pg_inherits" => {
1125 let (schema, rows) =
1126 crate::system_catalog::synth_pg_inherits(self.active_catalog());
1127 materialise_meta_view(&mut catalog, view, schema, rows)?;
1128 }
1129 // v7.39 (round 650) — the text-search catalogs, filled
1130 // with what SPG actually has rather than PG's thirty.
1131 "__spg_pg_ts_config_map" => {
1132 let (schema, rows) =
1133 crate::system_catalog::synth_pg_ts_config_map(self.active_catalog());
1134 materialise_meta_view(&mut catalog, view, schema, rows)?;
1135 }
1136 "__spg_pg_ts_config" => {
1137 let (schema, rows) =
1138 crate::system_catalog::synth_pg_ts_config(self.active_catalog());
1139 materialise_meta_view(&mut catalog, view, schema, rows)?;
1140 }
1141 "__spg_pg_ts_dict" => {
1142 let (schema, rows) =
1143 crate::system_catalog::synth_pg_ts_dict(self.active_catalog());
1144 materialise_meta_view(&mut catalog, view, schema, rows)?;
1145 }
1146 "__spg_pg_ts_parser" => {
1147 let (schema, rows) =
1148 crate::system_catalog::synth_pg_ts_parser(self.active_catalog());
1149 materialise_meta_view(&mut catalog, view, schema, rows)?;
1150 }
1151 "__spg_pg_ts_template" => {
1152 let (schema, rows) =
1153 crate::system_catalog::synth_pg_ts_template(self.active_catalog());
1154 materialise_meta_view(&mut catalog, view, schema, rows)?;
1155 }
1156 // v7.37.24 (24.17) — pg_catalog.pg_depend
1157 // (dependency graph; shape-stable empty since
1158 // SPG's drop enforcement is per-kind, not per-object).
1159 "__spg_pg_depend" => {
1160 let (schema, rows) =
1161 crate::system_catalog::synth_pg_depend(self.active_catalog());
1162 materialise_meta_view(&mut catalog, view, schema, rows)?;
1163 }
1164 // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1165 // ORM reflection + pg_dump read the deparsed default text).
1166 "__spg_pg_attrdef" => {
1167 let (schema, rows) =
1168 crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1169 materialise_meta_view(&mut catalog, view, schema, rows)?;
1170 }
1171 // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1172 "__spg_pg_policy" => {
1173 let (schema, rows) =
1174 crate::system_catalog::synth_pg_policy(self.active_catalog());
1175 materialise_meta_view(&mut catalog, view, schema, rows)?;
1176 }
1177 "__spg_pg_policies" => {
1178 let (schema, rows) =
1179 crate::system_catalog::synth_pg_policies(self.active_catalog());
1180 materialise_meta_view(&mut catalog, view, schema, rows)?;
1181 }
1182 // v7.37.24 (24.14) — pg_catalog.pg_collation.
1183 "__spg_pg_collation" => {
1184 let (schema, rows) =
1185 crate::system_catalog::synth_pg_collation(self.active_catalog());
1186 materialise_meta_view(&mut catalog, view, schema, rows)?;
1187 }
1188 // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1189 "__spg_pg_tablespace" => {
1190 let (schema, rows) =
1191 crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1192 materialise_meta_view(&mut catalog, view, schema, rows)?;
1193 }
1194 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1195 // for pgAdmin / DataGrip "indexes per table" listings.
1196 "__spg_pg_indexes" => {
1197 let (schema, rows) = synth_pg_indexes(self.active_catalog());
1198 materialise_meta_view(&mut catalog, view, schema, rows)?;
1199 }
1200 // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1201 // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1202 "__spg_pg_description" => {
1203 let (schema, rows) =
1204 crate::system_catalog::synth_pg_description(self.active_catalog());
1205 materialise_meta_view(&mut catalog, view, schema, rows)?;
1206 }
1207 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1208 // for index introspection by ORM compilers.
1209 "__spg_pg_index" => {
1210 let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1211 materialise_meta_view(&mut catalog, view, schema, rows)?;
1212 }
1213 // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1214 // for FK / UNIQUE / PK / CHECK introspection.
1215 "__spg_pg_constraint" => {
1216 let (schema, rows) = synth_pg_constraint(self.active_catalog());
1217 materialise_meta_view(&mut catalog, view, schema, rows)?;
1218 }
1219 // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1220 // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1221 "__spg_pg_sequence" => {
1222 let (schema, rows) = synth_pg_sequence(self.active_catalog());
1223 materialise_meta_view(&mut catalog, view, schema, rows)?;
1224 }
1225 // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1226 // pg_roles / pg_user. SPG is single-database so
1227 // pg_database surfaces just `postgres`; pg_roles
1228 // / pg_user walk the engine's UserStore.
1229 "__spg_pg_database" => {
1230 let (schema, rows) = synth_pg_database(self);
1231 materialise_meta_view(&mut catalog, view, schema, rows)?;
1232 }
1233 "__spg_pg_roles" => {
1234 let (schema, rows) = synth_pg_roles(self);
1235 materialise_meta_view(&mut catalog, view, schema, rows)?;
1236 }
1237 // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1238 // same roles, with PG's own `use*` column names. It used to
1239 // publish pg_roles' columns under this name.
1240 "__spg_pg_user" => {
1241 let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1242 materialise_meta_view(&mut catalog, view, schema, rows)?;
1243 }
1244 // v7.39 (read01 round 58) — role membership.
1245 "__spg_pg_auth_members" => {
1246 let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1247 materialise_meta_view(&mut catalog, view, schema, rows)?;
1248 }
1249 // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1250 // pg_views surfaces every CREATE VIEW result; SPG
1251 // ships one row per declared view from the catalog.
1252 "__spg_pg_views" => {
1253 let (schema, rows) = synth_pg_views(self.active_catalog());
1254 materialise_meta_view(&mut catalog, view, schema, rows)?;
1255 }
1256 // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1257 // catalogued query-rewrite RULE.
1258 "__spg_pg_rules" => {
1259 let (schema, rows) =
1260 crate::system_catalog::synth_pg_rules(self.active_catalog());
1261 materialise_meta_view(&mut catalog, view, schema, rows)?;
1262 }
1263 // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1264 // catalogue `pg_get_ruledef(oid)` resolves against.
1265 "__spg_pg_rewrite" => {
1266 let (schema, rows) =
1267 crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1268 materialise_meta_view(&mut catalog, view, schema, rows)?;
1269 }
1270 // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1271 // and PG's own column names.
1272 "__spg_pg_matviews" => {
1273 let (schema, rows) =
1274 crate::system_catalog::synth_pg_matviews(self.active_catalog());
1275 materialise_meta_view(&mut catalog, view, schema, rows)?;
1276 }
1277 // pg_catalog.pg_extension — native capability list
1278 // (mailrs embed round-12).
1279 // v7.39 (round 546) — the catalogs SPG has real content
1280 // for, from the facts it already holds.
1281 "__spg_pg_db_role_setting" => {
1282 let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1283 materialise_meta_view(&mut catalog, view, schema, rows)?;
1284 }
1285 "__spg_pg_language" => {
1286 let (schema, rows) = crate::system_catalog::synth_pg_language();
1287 materialise_meta_view(&mut catalog, view, schema, rows)?;
1288 }
1289 "__spg_pg_sequences" => {
1290 let (schema, rows) =
1291 crate::system_catalog::synth_pg_sequences(self.active_catalog());
1292 materialise_meta_view(&mut catalog, view, schema, rows)?;
1293 }
1294 "__spg_pg_range" => {
1295 let (schema, rows) = crate::system_catalog::synth_pg_range();
1296 materialise_meta_view(&mut catalog, view, schema, rows)?;
1297 }
1298 "__spg_pg_partitioned_table" => {
1299 let (schema, rows) =
1300 crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1301 materialise_meta_view(&mut catalog, view, schema, rows)?;
1302 }
1303 "__spg_pg_authid" => {
1304 let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1305 materialise_meta_view(&mut catalog, view, schema, rows)?;
1306 }
1307 "__spg_pg_group" => {
1308 let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1309 materialise_meta_view(&mut catalog, view, schema, rows)?;
1310 }
1311 "__spg_pg_shadow" => {
1312 let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1313 materialise_meta_view(&mut catalog, view, schema, rows)?;
1314 }
1315 // v7.39 (round 544) — pg_cast, probed from the real
1316 // cast implementation.
1317 "__spg_pg_cast" => {
1318 let (schema, rows) = crate::system_catalog::synth_pg_cast();
1319 materialise_meta_view(&mut catalog, view, schema, rows)?;
1320 }
1321 // v7.39 (round 541) — an empty catalog that exists.
1322 "__spg_pg_foreign_table" => {
1323 let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1324 materialise_meta_view(&mut catalog, view, schema, rows)?;
1325 }
1326 "__spg_pg_extension" => {
1327 let (schema, rows) = synth_pg_extension();
1328 materialise_meta_view(&mut catalog, view, schema, rows)?;
1329 }
1330 // v7.39 (round 502) — the timezone catalogues.
1331 "__spg_pg_timezone_names" => {
1332 let (schema, rows) = synth_pg_timezone_names(self);
1333 materialise_meta_view(&mut catalog, view, schema, rows)?;
1334 }
1335 "__spg_pg_timezone_abbrevs" => {
1336 let (schema, rows) = synth_pg_timezone_abbrevs(self);
1337 materialise_meta_view(&mut catalog, view, schema, rows)?;
1338 }
1339 // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1340 "__spg_pg_settings" => {
1341 let (schema, rows) = synth_pg_settings(self);
1342 materialise_meta_view(&mut catalog, view, schema, rows)?;
1343 }
1344 // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1345 // v7.39 (read01 round 51) — information_schema.role_table_grants
1346 // and .table_privileges. Both report the owner's seven implicit
1347 // table privileges; SPG's single role owns everything.
1348 // v7.39 (read01 round 59) — information_schema.column_privileges.
1349 "__spg_info_column_privileges" => {
1350 let (schema, rows) =
1351 crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1352 materialise_meta_view(&mut catalog, view, schema, rows)?;
1353 }
1354 "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1355 let grantee = self.current_role().to_string();
1356 let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1357 self.active_catalog(),
1358 &grantee,
1359 );
1360 materialise_meta_view(&mut catalog, view, schema, rows)?;
1361 }
1362 "__spg_info_key_column_usage" => {
1363 let (schema, rows) = synth_info_key_column_usage(self.active_catalog());
1364 materialise_meta_view(&mut catalog, view, schema, rows)?;
1365 }
1366 // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1367 "__spg_info_referential_constraints" => {
1368 let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1369 materialise_meta_view(&mut catalog, view, schema, rows)?;
1370 }
1371 // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1372 "__spg_info_statistics" => {
1373 let (schema, rows) = synth_info_statistics(self.active_catalog());
1374 materialise_meta_view(&mut catalog, view, schema, rows)?;
1375 }
1376 // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1377 "__spg_info_routines" => {
1378 let (schema, rows) = synth_info_routines();
1379 materialise_meta_view(&mut catalog, view, schema, rows)?;
1380 }
1381 // v7.37.24 (24.3) — information_schema.attributes.
1382 "__spg_info_attributes" => {
1383 let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1384 self.active_catalog(),
1385 );
1386 materialise_meta_view(&mut catalog, view, schema, rows)?;
1387 }
1388 // v7.37.24 (24.2) — information_schema.domains.
1389 "__spg_info_domains" => {
1390 let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1391 self.active_catalog(),
1392 );
1393 materialise_meta_view(&mut catalog, view, schema, rows)?;
1394 }
1395 // v7.37.24 (24.9) — information_schema.schemata.
1396 "__spg_info_schemata" => {
1397 let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1398 self.active_catalog(),
1399 );
1400 materialise_meta_view(&mut catalog, view, schema, rows)?;
1401 }
1402 // v7.37.24 (24.9) — information_schema.views.
1403 "__spg_info_views" => {
1404 let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1405 self.active_catalog(),
1406 );
1407 materialise_meta_view(&mut catalog, view, schema, rows)?;
1408 }
1409 // v7.37.24 (24.9) — information_schema.table_constraints.
1410 "__spg_info_table_constraints" => {
1411 let (schema, rows) =
1412 crate::system_catalog::synth_information_schema_table_constraints(
1413 self.active_catalog(),
1414 );
1415 materialise_meta_view(&mut catalog, view, schema, rows)?;
1416 }
1417 // v7.37.17 — information_schema.constraint_column_usage.
1418 "__spg_info_constraint_column_usage" => {
1419 let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1420 self.active_catalog(),
1421 );
1422 materialise_meta_view(&mut catalog, view, schema, rows)?;
1423 }
1424 // v7.37.17 — information_schema.triggers.
1425 "__spg_info_triggers" => {
1426 let (schema, rows) =
1427 crate::system_catalog::synth_info_triggers(self.active_catalog());
1428 materialise_meta_view(&mut catalog, view, schema, rows)?;
1429 }
1430 // v7.37.17 — information_schema.check_constraints.
1431 "__spg_info_check_constraints" => {
1432 let (schema, rows) =
1433 crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1434 materialise_meta_view(&mut catalog, view, schema, rows)?;
1435 }
1436 // v7.37.17 — information_schema.sequences.
1437 "__spg_info_sequences" => {
1438 let (schema, rows) =
1439 crate::system_catalog::synth_info_sequences(self.active_catalog());
1440 materialise_meta_view(&mut catalog, view, schema, rows)?;
1441 }
1442 // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1443 "__spg_mysql_user" => {
1444 let (schema, rows) = synth_mysql_user(self);
1445 materialise_meta_view(&mut catalog, view, schema, rows)?;
1446 }
1447 "__spg_mysql_db" => {
1448 let (schema, rows) = synth_mysql_db();
1449 materialise_meta_view(&mut catalog, view, schema, rows)?;
1450 }
1451 // v7.39 (round 541) — the catalogs PG has that SPG is
1452 // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1453 other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1454 let (schema, rows) =
1455 crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1456 materialise_meta_view(&mut catalog, view, schema, rows)?;
1457 }
1458 _ => {
1459 return Err(EngineError::Unsupported(alloc::format!(
1460 "meta view {view:?} is not yet materialisable; \
1461 v7.16.2 covers information_schema.columns / .tables \
1462 and pg_catalog.pg_class / pg_attribute; \
1463 v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1464 pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1465 pg_user / pg_views / pg_matviews / pg_settings"
1466 )));
1467 }
1468 }
1469 }
1470 Ok(catalog)
1471 }
1472
1473 pub(crate) fn exec_with_ctes(
1474 &self,
1475 stmt: &SelectStatement,
1476 cancel: CancelToken<'_>,
1477 ) -> Result<QueryResult, EngineError> {
1478 cancel.check()?;
1479 // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1480 // bodies are supported here. Writable CTEs on a SELECT
1481 // outer require `&mut self` and route through the
1482 // top-level `exec_select_cancel_mut` entry; sentori
1483 // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1484 // INSERT, not a SELECT, so this restriction is harmless
1485 // in practice.
1486 if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1487 // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1488 // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1489 // of a statement, not nested inside a subquery; this path is
1490 // reached exactly when one is nested. The old text described SPG's
1491 // own executor plumbing ("the top-level mutable entry"), which
1492 // means nothing to a client.
1493 return Err(EngineError::Unsupported(
1494 "WITH clause containing a data-modifying statement must be at the top level".into(),
1495 ));
1496 }
1497 let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1498 // Strip CTEs from the body before running on the temp engine
1499 // so we don't recurse forever.
1500 let mut body = stmt.clone();
1501 body.ctes = Vec::new();
1502 let mut temp = Engine::restore(catalog);
1503 if let Some(c) = self.clock {
1504 temp = temp.with_clock(c);
1505 }
1506 if let Some(f) = self.salt_fn {
1507 temp = temp.with_salt_fn(f);
1508 }
1509 temp.exec_select_cancel(&body, cancel)
1510 }
1511
1512 /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1513 /// `&self` SELECT path. Caller guarantees no modifying CTE
1514 /// bodies are present.
1515 pub(crate) fn materialise_ctes_readonly(
1516 &self,
1517 ctes: &[spg_sql::ast::Cte],
1518 cancel: CancelToken<'_>,
1519 ) -> Result<crate::Catalog, EngineError> {
1520 cancel.check()?;
1521 let mut catalog = self.active_catalog().clone();
1522 for cte in ctes {
1523 let body_select = cte.body.as_select().ok_or_else(|| {
1524 EngineError::Unsupported(alloc::format!(
1525 "data-modifying CTE not supported on this SELECT entry"
1526 ))
1527 })?;
1528 // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1529 // (PG scoping: the WITH name wins for the outer query and later
1530 // CTEs, while THIS body still sees the real table — a
1531 // non-recursive body's self-name is the table, probe P2). This
1532 // materialiser works on a CLONE, so the shadow is simply: run
1533 // the body against the untouched clone, then drop the real
1534 // table from the clone before installing the CTE's temp. A
1535 // RECURSIVE self-reference is the CTE itself (P6), so there the
1536 // drop happens before the iterating materialiser runs.
1537 let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1538 let synthetic = spg_sql::ast::Cte {
1539 name: cte.name.clone(),
1540 body: spg_sql::ast::CteBody::Select(body_select.clone()),
1541 recursive: true,
1542 column_overrides: cte.column_overrides.clone(),
1543 search: None,
1544 cycle: None,
1545 };
1546 if catalog.get(&cte.name).is_some() {
1547 let _ = catalog.drop_table(&cte.name);
1548 }
1549 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1550 } else {
1551 let mut cte_engine = Engine::restore(catalog.clone());
1552 if let Some(c) = self.clock {
1553 cte_engine = cte_engine.with_clock(c);
1554 }
1555 if let Some(f) = self.salt_fn {
1556 cte_engine = cte_engine.with_salt_fn(f);
1557 }
1558 let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1559 let QueryResult::Rows { columns, rows } = body_result else {
1560 return Err(EngineError::Unsupported(alloc::format!(
1561 "CTE {:?} body did not return rows",
1562 cte.name
1563 )));
1564 };
1565 (columns, rows)
1566 };
1567 let inferred = infer_column_types(&columns, &rows);
1568 let mut columns = inferred;
1569 if !cte.column_overrides.is_empty() {
1570 if cte.column_overrides.len() != columns.len() {
1571 return Err(EngineError::Unsupported(alloc::format!(
1572 "CTE {:?} column list has {} names but body returns {} columns",
1573 cte.name,
1574 cte.column_overrides.len(),
1575 columns.len()
1576 )));
1577 }
1578 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1579 col.name.clone_from(name);
1580 }
1581 }
1582 let schema = TableSchema::new(cte.name.clone(), columns);
1583 // v7.39 (round 156) — the body ran against the untouched clone;
1584 // from here on the CTE name resolves to the temp (PG scoping).
1585 if catalog.get(&cte.name).is_some() {
1586 let _ = catalog.drop_table(&cte.name);
1587 }
1588 catalog.create_table(schema).map_err(EngineError::Storage)?;
1589 let table = catalog
1590 .get_mut(&cte.name)
1591 .expect("just-created CTE table must exist");
1592 for row in rows {
1593 table.insert(row).map_err(EngineError::Storage)?;
1594 }
1595 }
1596 Ok(catalog)
1597 }
1598
1599 /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1600 /// Retained for non-DML callers; the DML path (writable CTE on
1601 /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1602 /// `dml.rs` which installs the CTE temps directly on the
1603 /// active catalog so the outer statement's writes hit real
1604 /// tables.
1605 #[allow(dead_code)]
1606 pub(crate) fn materialise_ctes(
1607 &mut self,
1608 ctes: &[spg_sql::ast::Cte],
1609 cancel: CancelToken<'_>,
1610 ) -> Result<crate::Catalog, EngineError> {
1611 cancel.check()?;
1612 // v7.37.43-T4.4 — modifying CTEs need to write through the
1613 // SAME catalog as the outer statement, not a clone (PG's
1614 // writable CTE puts all modifications in one transaction).
1615 // For the read-only case the original logic cloned, but
1616 // since the outer statement also goes through the cloned
1617 // engine and ALL writes must converge, we now drive the
1618 // accumulator off `self.active_catalog().clone()` and
1619 // commit the modifying writes directly to `self`'s active
1620 // catalog so the surface is consistent.
1621 let mut catalog = self.active_catalog().clone();
1622 // v7.39 (round 149) — a modifying CTE body's target must be a
1623 // real relation, never a sibling CTE (PG: relation does not
1624 // exist); checked before any alias lands in the accumulator.
1625 for cte in ctes {
1626 let body_target = match &cte.body {
1627 spg_sql::ast::CteBody::Select(_) => None,
1628 spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1629 spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1630 spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1631 spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1632 };
1633 if let Some(t) = body_target
1634 && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1635 && catalog.get(t).is_none()
1636 {
1637 return Err(EngineError::Storage(
1638 spg_storage::StorageError::TableNotFound { name: t.into() },
1639 ));
1640 }
1641 }
1642 for cte in ctes {
1643 if catalog.get(&cte.name).is_some() {
1644 return Err(EngineError::Unsupported(alloc::format!(
1645 "CTE name {:?} shadows an existing table; rename the CTE",
1646 cte.name
1647 )));
1648 }
1649 let (columns, rows) = match &cte.body {
1650 // v7.39 (round 145) — see the sibling site: only a body that
1651 // truly self-references takes the iterating materialiser.
1652 spg_sql::ast::CteBody::Select(body)
1653 if cte.recursive && select_refers_to(body, &cte.name) =>
1654 {
1655 // Recursive CTE — the existing helper takes a
1656 // SELECT body and the snapshot catalog.
1657 let synthetic = spg_sql::ast::Cte {
1658 name: cte.name.clone(),
1659 body: spg_sql::ast::CteBody::Select(body.clone()),
1660 recursive: true,
1661 column_overrides: cte.column_overrides.clone(),
1662 search: None,
1663 cycle: None,
1664 };
1665 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1666 }
1667 spg_sql::ast::CteBody::Select(body) => {
1668 // v7.25 (round-17) — run against the accumulated
1669 // catalog so later CTEs can reference earlier
1670 // ones in the same WITH clause.
1671 let mut cte_engine = Engine::restore(catalog.clone());
1672 if let Some(c) = self.clock {
1673 cte_engine = cte_engine.with_clock(c);
1674 }
1675 if let Some(f) = self.salt_fn {
1676 cte_engine = cte_engine.with_salt_fn(f);
1677 }
1678 let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1679 let QueryResult::Rows { columns, rows } = body_result else {
1680 return Err(EngineError::Unsupported(alloc::format!(
1681 "CTE {:?} body did not return rows",
1682 cte.name
1683 )));
1684 };
1685 (columns, rows)
1686 }
1687 spg_sql::ast::CteBody::Insert(body) => {
1688 self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1689 }
1690 spg_sql::ast::CteBody::Update(body) => {
1691 self.exec_modifying_cte_update(&cte.name, body, cancel)?
1692 }
1693 spg_sql::ast::CteBody::Delete(body) => {
1694 self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1695 }
1696 spg_sql::ast::CteBody::Merge(body) => {
1697 self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1698 }
1699 };
1700 // v4.22: the projection builder labels any non-column
1701 // expression as Text — including literal SELECT 1.
1702 // Promote each column's type to whatever the rows
1703 // actually carry so the CTE storage table accepts them.
1704 let inferred = infer_column_types(&columns, &rows);
1705 let mut columns = inferred;
1706 if !cte.column_overrides.is_empty() {
1707 if cte.column_overrides.len() != columns.len() {
1708 return Err(EngineError::Unsupported(alloc::format!(
1709 "CTE {:?} column list has {} names but body returns {} columns",
1710 cte.name,
1711 cte.column_overrides.len(),
1712 columns.len()
1713 )));
1714 }
1715 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1716 col.name.clone_from(name);
1717 }
1718 }
1719 let schema = TableSchema::new(cte.name.clone(), columns);
1720 catalog.create_table(schema).map_err(EngineError::Storage)?;
1721 let table = catalog
1722 .get_mut(&cte.name)
1723 .expect("just-created CTE table must exist");
1724 for row in rows {
1725 table.insert(row).map_err(EngineError::Storage)?;
1726 }
1727 }
1728 Ok(catalog)
1729 }
1730
1731 /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1732 /// against `self` (so the mutation lands in the active catalog
1733 /// inside the current transaction) and captures the RETURNING
1734 /// projection — column schema + rows — to materialise as the
1735 /// CTE alias's table. An INSERT without RETURNING produces a
1736 /// 0-row table with a synthetic single-column placeholder
1737 /// (matches PG: the CTE alias is still defined, but referencing
1738 /// it from the outer query without RETURNING raises a
1739 /// column-resolution error at scan time).
1740 fn exec_modifying_cte_insert(
1741 &mut self,
1742 cte_name: &str,
1743 body: &spg_sql::ast::InsertStatement,
1744 _cancel: CancelToken<'_>,
1745 ) -> Result<
1746 (
1747 Vec<spg_storage::ColumnSchema>,
1748 Vec<spg_storage::Row<'static>>,
1749 ),
1750 EngineError,
1751 > {
1752 // round 151 — a WITH-headed body keeps its own ctes; the body
1753 // statement routes through its writable-CTE entry (outer CTEs
1754 // are never copied into bodies, so no recursion risk).
1755 let body = body.clone();
1756 let result = self.exec_insert(body)?;
1757 match result {
1758 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1759 QueryResult::CommandOk { .. } => {
1760 // No RETURNING — emit a sentinel single-column
1761 // schema with zero rows so the alias is defined.
1762 let placeholder = spg_storage::ColumnSchema::new(
1763 alloc::format!("{cte_name}_returning_absent"),
1764 spg_storage::DataType::Text,
1765 true,
1766 );
1767 Ok((alloc::vec![placeholder], Vec::new()))
1768 }
1769 }
1770 }
1771
1772 /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1773 /// as INSERT above.
1774 fn exec_modifying_cte_update(
1775 &mut self,
1776 cte_name: &str,
1777 body: &spg_sql::ast::UpdateStatement,
1778 cancel: CancelToken<'_>,
1779 ) -> Result<
1780 (
1781 Vec<spg_storage::ColumnSchema>,
1782 Vec<spg_storage::Row<'static>>,
1783 ),
1784 EngineError,
1785 > {
1786 let body = body.clone();
1787 let result = self.exec_update_cancel(&body, cancel)?;
1788 match result {
1789 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1790 QueryResult::CommandOk { .. } => {
1791 let placeholder = spg_storage::ColumnSchema::new(
1792 alloc::format!("{cte_name}_returning_absent"),
1793 spg_storage::DataType::Text,
1794 true,
1795 );
1796 Ok((alloc::vec![placeholder], Vec::new()))
1797 }
1798 }
1799 }
1800
1801 /// v7.37.43-T4.4 — execute a DELETE CTE body.
1802 fn exec_modifying_cte_delete(
1803 &mut self,
1804 cte_name: &str,
1805 body: &spg_sql::ast::DeleteStatement,
1806 cancel: CancelToken<'_>,
1807 ) -> Result<
1808 (
1809 Vec<spg_storage::ColumnSchema>,
1810 Vec<spg_storage::Row<'static>>,
1811 ),
1812 EngineError,
1813 > {
1814 let body = body.clone();
1815 let result = self.exec_delete_cancel(&body, cancel)?;
1816 match result {
1817 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1818 QueryResult::CommandOk { .. } => {
1819 let placeholder = spg_storage::ColumnSchema::new(
1820 alloc::format!("{cte_name}_returning_absent"),
1821 spg_storage::DataType::Text,
1822 true,
1823 );
1824 Ok((alloc::vec![placeholder], Vec::new()))
1825 }
1826 }
1827 }
1828
1829 /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1830 fn exec_modifying_cte_merge(
1831 &mut self,
1832 cte_name: &str,
1833 body: &spg_sql::ast::MergeStatement,
1834 cancel: CancelToken<'_>,
1835 ) -> Result<
1836 (
1837 Vec<spg_storage::ColumnSchema>,
1838 Vec<spg_storage::Row<'static>>,
1839 ),
1840 EngineError,
1841 > {
1842 let body = body.clone();
1843 let result = self.exec_merge_cancel(&body, cancel)?;
1844 match result {
1845 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1846 QueryResult::CommandOk { .. } => {
1847 let placeholder = spg_storage::ColumnSchema::new(
1848 alloc::format!("{cte_name}_returning_absent"),
1849 spg_storage::DataType::Text,
1850 true,
1851 );
1852 Ok((alloc::vec![placeholder], Vec::new()))
1853 }
1854 }
1855 }
1856
1857 /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1858 /// UNION (or UNION ALL) of an anchor that does not reference
1859 /// the CTE name, and one or more recursive terms that do. The
1860 /// anchor runs first; each subsequent iteration runs the
1861 /// recursive term against a temp catalog where the CTE name is
1862 /// bound to the *previous* iteration's output. Iteration stops
1863 /// when the recursive term yields no rows; UNION (DISTINCT)
1864 /// deduplicates against the accumulated result, UNION ALL does
1865 /// not. A hard cap on total rows prevents runaway queries.
1866 #[allow(clippy::too_many_lines)]
1867 pub(crate) fn materialise_recursive_cte(
1868 &self,
1869 cte: &spg_sql::ast::Cte,
1870 base_catalog: &Catalog,
1871 cancel: CancelToken<'_>,
1872 ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1873 const MAX_TOTAL_ROWS: usize = 1_000_000;
1874 const MAX_ITERATIONS: usize = 100_000;
1875 cancel.check()?;
1876 // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1877 // a modifying recursive CTE is parser-rejectable but we
1878 // guard here defensively.
1879 let body_select = cte.body.as_select().ok_or_else(|| {
1880 EngineError::Unsupported(alloc::format!(
1881 "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1882 cte.name
1883 ))
1884 })?;
1885 if body_select.unions.is_empty() {
1886 return Err(EngineError::Unsupported(alloc::format!(
1887 "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1888 cte.name
1889 )));
1890 }
1891 // Anchor: the body's leading SELECT, with unions stripped.
1892 let mut anchor = body_select.clone();
1893 let all_union_terms = core::mem::take(&mut anchor.unions);
1894 anchor.ctes = Vec::new();
1895 // v7.37 D.42 — split the UNION members: those that do NOT reference the
1896 // CTE are additional ANCHOR terms, only the ones that do recurse. A
1897 // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1898 // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1899 // treating the non-recursive `SELECT r2` as a recursive term made it
1900 // re-emit its constant row every iteration → runaway loop.
1901 let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1902 .into_iter()
1903 .partition(|(_, t)| !select_refers_to(t, &cte.name));
1904 let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1905 let QueryResult::Rows {
1906 columns: anchor_cols,
1907 rows: mut anchor_rows,
1908 } = anchor_result
1909 else {
1910 return Err(EngineError::Unsupported(alloc::format!(
1911 "WITH RECURSIVE {:?}: anchor did not return rows",
1912 cte.name
1913 )));
1914 };
1915 // Append every non-recursive UNION member's rows to the anchor set.
1916 for (_, term) in &anchor_terms {
1917 let mut term = term.clone();
1918 term.ctes = Vec::new();
1919 if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
1920 anchor_rows.extend(rows);
1921 }
1922 }
1923 // The projection builder labels non-column expressions Text;
1924 // refine column types from the anchor's actual values so the
1925 // intermediate iter-catalog tables accept them.
1926 let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
1927 if !cte.column_overrides.is_empty() {
1928 if cte.column_overrides.len() != columns.len() {
1929 return Err(EngineError::Unsupported(alloc::format!(
1930 "CTE {:?} column list has {} names but anchor returns {} columns",
1931 cte.name,
1932 cte.column_overrides.len(),
1933 columns.len()
1934 )));
1935 }
1936 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1937 col.name.clone_from(name);
1938 }
1939 }
1940 let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
1941 let mut working_set: Vec<Row<'static>> = anchor_rows;
1942 let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
1943 // Track at least one "all UNION ALL" flag — if every union
1944 // kind is ALL we skip the dedup step (faster + matches PG).
1945 let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
1946 if !all_union_all {
1947 for r in &all_rows {
1948 seen.insert(encode_row_key(r));
1949 }
1950 }
1951 // v7.39 (round 598) — the engine and its catalog are built ONCE.
1952 // Each iteration used to clone the catalog, create the CTE table,
1953 // and construct a whole `Engine` — which initialises 82 fields — to
1954 // hold that round's working set. A counting allocator put the loop
1955 // at 63 allocations and 104 kB per iteration, or 1 GB for a
1956 // 10,000-row recursive CTE, and none of it varied with how much
1957 // else was in the catalog: the per-round rebuild WAS the cost. The
1958 // table is emptied and refilled instead.
1959 let mut iter_catalog = base_catalog.clone();
1960 let schema = TableSchema::new(cte.name.clone(), columns.clone());
1961 iter_catalog
1962 .create_table(schema)
1963 .map_err(EngineError::Storage)?;
1964 let mut iter_engine = Engine::restore(iter_catalog);
1965 if let Some(c) = self.clock {
1966 iter_engine = iter_engine.with_clock(c);
1967 }
1968 if let Some(f) = self.salt_fn {
1969 iter_engine = iter_engine.with_salt_fn(f);
1970 }
1971 // The recursive terms are cloned once too — the clone stripped the
1972 // CTE list off each of them, per term per iteration.
1973 let recursive_terms: Vec<SelectStatement> = union_terms
1974 .iter()
1975 .map(|(_, t)| {
1976 let mut t = t.clone();
1977 t.ctes = Vec::new();
1978 t
1979 })
1980 .collect();
1981 // v7.39 (round 618) — plan every recursive term once. Taken only if
1982 // ALL of them plan, so a query never runs half on each path.
1983 let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
1984 .iter()
1985 .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
1986 .collect();
1987 let fast_ctx = term_plans.as_ref().map(|plans| {
1988 let alias = plans[0].alias.clone();
1989 (alias, ())
1990 });
1991 for iter in 0..MAX_ITERATIONS {
1992 cancel.check()?;
1993 if working_set.is_empty() {
1994 break;
1995 }
1996 if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
1997 // The worktable IS the working set: no table to empty and
1998 // refill, and no query execution per round.
1999 let mut next_set: Vec<Row<'static>> = Vec::new();
2000 for plan in plans {
2001 let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2002 for row in &working_set {
2003 cancel.check()?;
2004 if let Some(w) = plan.where_ {
2005 let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2006 if !matches!(v, Value::Bool(true)) {
2007 continue;
2008 }
2009 }
2010 let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2011 for it in &plan.items {
2012 vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2013 }
2014 let out = Row::new(vals);
2015 if !all_union_all {
2016 let key = encode_row_key(&out);
2017 if !seen.insert(key) {
2018 continue;
2019 }
2020 }
2021 next_set.push(out);
2022 }
2023 }
2024 if next_set.is_empty() {
2025 break;
2026 }
2027 all_rows.extend(next_set.iter().cloned());
2028 working_set = next_set;
2029 if all_rows.len() > MAX_TOTAL_ROWS {
2030 return Err(EngineError::Unsupported(alloc::format!(
2031 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2032 cte.name
2033 )));
2034 }
2035 if iter + 1 == MAX_ITERATIONS {
2036 return Err(EngineError::Unsupported(alloc::format!(
2037 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2038 cte.name
2039 )));
2040 }
2041 continue;
2042 }
2043 {
2044 // Truncated rather than dropped and recreated: the table's
2045 // own structure is what dropping it throws away, and it is
2046 // identical every round.
2047 let cat = iter_engine.base_catalog_mut();
2048 let table = cat.get_mut(&cte.name).expect("created above");
2049 table.truncate();
2050 for row in &working_set {
2051 table.insert(row.clone()).map_err(EngineError::Storage)?;
2052 }
2053 }
2054 // Run each recursive term in sequence and collect new rows.
2055 let mut next_set: Vec<Row<'static>> = Vec::new();
2056 for term in &recursive_terms {
2057 let r = iter_engine.exec_select_cancel(term, cancel)?;
2058 let QueryResult::Rows {
2059 columns: rc,
2060 rows: rs,
2061 } = r
2062 else {
2063 return Err(EngineError::Unsupported(alloc::format!(
2064 "WITH RECURSIVE {:?}: recursive term did not return rows",
2065 cte.name
2066 )));
2067 };
2068 if rc.len() != columns.len() {
2069 return Err(EngineError::Unsupported(alloc::format!(
2070 "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2071 cte.name,
2072 rc.len(),
2073 columns.len()
2074 )));
2075 }
2076 for row in rs {
2077 if !all_union_all {
2078 let key = encode_row_key(&row);
2079 if !seen.insert(key) {
2080 continue;
2081 }
2082 }
2083 next_set.push(row);
2084 }
2085 }
2086 if next_set.is_empty() {
2087 break;
2088 }
2089 all_rows.extend(next_set.iter().cloned());
2090 working_set = next_set;
2091 if all_rows.len() > MAX_TOTAL_ROWS {
2092 return Err(EngineError::Unsupported(alloc::format!(
2093 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2094 cte.name
2095 )));
2096 }
2097 if iter + 1 == MAX_ITERATIONS {
2098 return Err(EngineError::Unsupported(alloc::format!(
2099 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2100 cte.name
2101 )));
2102 }
2103 }
2104 Ok((columns, all_rows))
2105 }
2106
2107 pub(crate) fn resolve_select_subqueries(
2108 &self,
2109 stmt: &mut SelectStatement,
2110 cancel: CancelToken<'_>,
2111 ) -> Result<(), EngineError> {
2112 for item in &mut stmt.items {
2113 if let SelectItem::Expr { expr, alias } = item {
2114 // An UNCORRELATED subquery is replaced by its value right
2115 // here, and the shape the column was named for goes with
2116 // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2117 // boolean literal, so SPG answered `?column?` where PG18
2118 // answers `exists`. Only a subquery at the TOP of the item
2119 // loses its name this way — one nested inside a call still
2120 // reports the call.
2121 if alias.is_none()
2122 && matches!(
2123 expr,
2124 Expr::ScalarSubquery(_)
2125 | Expr::Exists { .. }
2126 | Expr::InSubquery { .. }
2127 | Expr::RowInSubquery { .. }
2128 | Expr::RowCmpSubquery { .. }
2129 )
2130 {
2131 *alias = Some(default_output_name(expr, self.backslash_escapes));
2132 }
2133 self.resolve_expr_subqueries(expr, cancel)?;
2134 }
2135 }
2136 if let Some(w) = &mut stmt.where_ {
2137 self.resolve_expr_subqueries(w, cancel)?;
2138 }
2139 // v7.24.1 — JOIN ON conditions can carry subqueries too;
2140 // they were never walked, so even an UNCORRELATED subquery
2141 // in ON hit "subquery reached row eval".
2142 if let Some(from) = &mut stmt.from {
2143 for j in &mut from.joins {
2144 if let Some(on) = &mut j.on {
2145 self.resolve_expr_subqueries(on, cancel)?;
2146 }
2147 }
2148 }
2149 if let Some(gs) = &mut stmt.group_by {
2150 for g in gs {
2151 self.resolve_expr_subqueries(g, cancel)?;
2152 }
2153 }
2154 if let Some(h) = &mut stmt.having {
2155 self.resolve_expr_subqueries(h, cancel)?;
2156 }
2157 for o in &mut stmt.order_by {
2158 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2159 }
2160 for (_, peer) in &mut stmt.unions {
2161 self.resolve_select_subqueries(peer, cancel)?;
2162 }
2163 Ok(())
2164 }
2165
2166 #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2167 pub(crate) fn resolve_expr_subqueries(
2168 &self,
2169 e: &mut Expr,
2170 cancel: CancelToken<'_>,
2171 ) -> Result<(), EngineError> {
2172 // Replace-on-this-node cases first.
2173 if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2174 *e = replacement;
2175 return Ok(());
2176 }
2177 match e {
2178 Expr::NamedArg { expr, .. } => self.resolve_expr_subqueries(expr, cancel)?,
2179 Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2180 Expr::AggregateOrdered { call, order_by, .. } => {
2181 self.resolve_expr_subqueries(call, cancel)?;
2182 for o in order_by.iter_mut() {
2183 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2184 }
2185 }
2186 Expr::Binary { lhs, rhs, .. } => {
2187 self.resolve_expr_subqueries(lhs, cancel)?;
2188 self.resolve_expr_subqueries(rhs, cancel)?;
2189 }
2190 Expr::Unary { expr, .. }
2191 | Expr::Cast { expr, .. }
2192 | Expr::IsNull { expr, .. }
2193 | Expr::BoolTest { expr, .. }
2194 | Expr::FieldAccess { base: expr, .. } => {
2195 self.resolve_expr_subqueries(expr, cancel)?;
2196 }
2197 Expr::FunctionCall { args, .. } => {
2198 for a in args {
2199 self.resolve_expr_subqueries(a, cancel)?;
2200 }
2201 }
2202 Expr::Like { expr, pattern, .. } => {
2203 self.resolve_expr_subqueries(expr, cancel)?;
2204 self.resolve_expr_subqueries(pattern, cancel)?;
2205 }
2206 Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2207 // v4.12 window functions — recurse into args + ORDER BY
2208 // + PARTITION BY in case they carry inner subqueries.
2209 Expr::WindowFunction {
2210 args,
2211 partition_by,
2212 order_by,
2213 ..
2214 } => {
2215 for a in args {
2216 self.resolve_expr_subqueries(a, cancel)?;
2217 }
2218 for p in partition_by {
2219 self.resolve_expr_subqueries(p, cancel)?;
2220 }
2221 for (e, _, _) in order_by {
2222 self.resolve_expr_subqueries(e, cancel)?;
2223 }
2224 }
2225 // Subquery nodes are handled in subquery_replacement
2226 // (which returned None — defensive no-op); Literal /
2227 // Column are leaves.
2228 Expr::ScalarSubquery(_)
2229 | Expr::Exists { .. }
2230 | Expr::InSubquery { .. }
2231 | Expr::RowInSubquery { .. }
2232 | Expr::RowCmpSubquery { .. }
2233 | Expr::Literal(_)
2234 | Expr::Placeholder(_)
2235 | Expr::Column(_) => {}
2236 // v7.30.2 — list elements can carry scalar subqueries
2237 // (`x IN (1, (SELECT …))`).
2238 Expr::InList { expr, list, .. } => {
2239 self.resolve_expr_subqueries(expr, cancel)?;
2240 for item in list {
2241 self.resolve_expr_subqueries(item, cancel)?;
2242 }
2243 }
2244 // v7.10.10 — recurse children.
2245 Expr::Array(items) => {
2246 for elem in items {
2247 self.resolve_expr_subqueries(elem, cancel)?;
2248 }
2249 }
2250 Expr::ArraySubscript { target, index } => {
2251 self.resolve_expr_subqueries(target, cancel)?;
2252 self.resolve_expr_subqueries(index, cancel)?;
2253 }
2254 Expr::ArraySlice { target, lo, hi } => {
2255 self.resolve_expr_subqueries(target, cancel)?;
2256 if let Some(l) = lo {
2257 self.resolve_expr_subqueries(l, cancel)?;
2258 }
2259 if let Some(h) = hi {
2260 self.resolve_expr_subqueries(h, cancel)?;
2261 }
2262 }
2263 Expr::AnyAll { expr, array, .. } => {
2264 self.resolve_expr_subqueries(expr, cancel)?;
2265 // Quantified subquery — an uncorrelated one
2266 // materialises up front; a correlated one stays for
2267 // the per-row resolver.
2268 if let Expr::ScalarSubquery(inner) = array.as_mut() {
2269 if !crate::subquery::select_is_correlated(inner) {
2270 let s = (**inner).clone();
2271 **array = self.materialize_quantified_rows(&s, cancel)?;
2272 }
2273 } else {
2274 self.resolve_expr_subqueries(array, cancel)?;
2275 }
2276 }
2277 Expr::Case {
2278 operand,
2279 branches,
2280 else_branch,
2281 } => {
2282 if let Some(o) = operand {
2283 self.resolve_expr_subqueries(o, cancel)?;
2284 }
2285 for (w, t) in branches {
2286 self.resolve_expr_subqueries(w, cancel)?;
2287 self.resolve_expr_subqueries(t, cancel)?;
2288 }
2289 if let Some(e) = else_branch {
2290 self.resolve_expr_subqueries(e, cancel)?;
2291 }
2292 }
2293 }
2294 Ok(())
2295 }
2296}
2297
2298impl Engine {
2299 /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2300 /// `SelectItem::Wildcard` to all schema columns and
2301 /// `SelectItem::Expr` via the regular eval path.
2302 pub(crate) fn project_row_simple(
2303 &self,
2304 row: &Row<'static>,
2305 items: &[SelectItem],
2306 schema_cols: &[ColumnSchema],
2307 alias: &str,
2308 ) -> Result<Row<'static>, EngineError> {
2309 let ctx = self.ev_ctx(schema_cols, Some(alias));
2310 let cancel = CancelToken::none();
2311 let mut out_vals = Vec::new();
2312 for item in items {
2313 match item {
2314 // In a single-table projection (AS OF SEGMENT / RETURNING) a
2315 // qualified `t.*` covers exactly the same columns as a bare `*`.
2316 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2317 out_vals.extend(row.values.iter().cloned());
2318 }
2319 SelectItem::Expr { expr, .. } => {
2320 let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2321 out_vals.push(v);
2322 }
2323 }
2324 }
2325 Ok(Row::new(out_vals))
2326 }
2327
2328 /// v6.10.2 — derive the output `ColumnSchema` list for an
2329 /// AS OF SEGMENT projection. Wildcards take the full schema;
2330 /// expressions take the alias if present or a synthetic
2331 /// `?column?` (PG convention) otherwise.
2332 pub(crate) fn derive_output_columns(
2333 &self,
2334 items: &[SelectItem],
2335 schema_cols: &[ColumnSchema],
2336 table_alias: &str,
2337 ) -> Vec<ColumnSchema> {
2338 let mut out = Vec::new();
2339 for item in items {
2340 match item {
2341 // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2342 // a single-table projection.
2343 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2344 out.extend(schema_cols.iter().cloned());
2345 }
2346 SelectItem::Expr { expr, alias } => {
2347 // Bare column references inherit the schema
2348 // column's name + type — PG names `RETURNING id`
2349 // "id" and types it BIGINT, and the sqlx embed
2350 // path type-checks RowDescription against the
2351 // Rust target (mailrs embed round-12).
2352 if let Expr::Column(col) = expr
2353 && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2354 {
2355 let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2356 let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2357 // v7.39 (read01 round 54) — carry the enum identity:
2358 // it lives outside the DataType lattice, so a derived
2359 // table built from this schema otherwise forgets it and
2360 // the OUTER `ORDER BY <enum col>` silently sorts by the
2361 // label's TEXT instead of member order.
2362 c.user_enum_type = sc.user_enum_type.clone();
2363 out.push(c);
2364 continue;
2365 }
2366 let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2367 // v7.30.4 (mailrs round-27, P0) — type the
2368 // expression with the same inference the SELECT
2369 // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2370 // The old Text default broke every typed decode
2371 // of `RETURNING uidnext - 1 AS uid`: four days
2372 // of inbound mail indexed nowhere. Inference
2373 // failure keeps the old Text fallback rather
2374 // than inventing new error paths here.
2375 // v7.39 (round 258) — take the enum identity from the
2376 // same projection build, not just the type: a constant
2377 // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2378 // VALUES row lowers to) is an EXPRESSION, so it landed
2379 // here and the derived table forgot the enum.
2380 let (ty, nullable) = build_projection(
2381 core::slice::from_ref(item),
2382 schema_cols,
2383 table_alias,
2384 self.backslash_escapes,
2385 )
2386 .ok()
2387 .and_then(|p| p.into_iter().next())
2388 .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2389 out.push(ColumnSchema::new(name, ty, nullable));
2390 }
2391 }
2392 }
2393 out
2394 }
2395
2396 /// v4.5: SELECT with cooperative cancellation. The token is
2397 /// honoured between UNION peers and inside the bare-SELECT row
2398 /// loop; HNSW kNN graph walks and the aggregate executor don't
2399 /// honour it yet (deferred — those paths bound their work
2400 /// internally by `LIMIT k` and `GROUP BY` cardinality).
2401 /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2402 /// its (lowercased) name, or None if the name isn't a virtual view.
2403 /// Callers decide whether to return it directly (`SELECT *`) or stage
2404 /// it as a temp table for the full query pipeline.
2405 fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2406 Some(match name {
2407 "spg_statistic" => self.exec_spg_statistic(),
2408 "spg_stat_replication" => self.exec_spg_stat_replication(),
2409 "spg_stat_segment" => self.exec_spg_stat_segment(),
2410 "spg_memory_stats" => self.exec_spg_memory_stats(),
2411 "spg_stat_query" => self.exec_spg_stat_query(),
2412 "pg_stat_statements" => self.exec_pg_stat_statements(),
2413 "spg_stat_activity" => self.exec_spg_stat_activity(),
2414 "pg_stat_activity" => self.exec_pg_stat_activity(),
2415 "pg_locks" => self.exec_pg_locks(),
2416 "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2417 "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2418 "spg_partition_health" => self.exec_spg_partition_health(),
2419 "spg_audit_chain" => self.exec_spg_audit_chain(),
2420 "spg_audit_verify" => self.exec_spg_audit_verify(),
2421 "spg_table_ddl" => self.exec_spg_table_ddl(),
2422 "spg_role_ddl" => self.exec_spg_role_ddl(),
2423 "spg_database_ddl" => self.exec_spg_database_ddl(),
2424 _ => return None,
2425 })
2426 }
2427
2428 /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2429 /// describes against: this engine's catalog with the view staged as a
2430 /// table, exactly as `exec_select_cancel_as` stages it for a
2431 /// non-bare query.
2432 ///
2433 /// These views never reach the catalog — each is a fixed row set built
2434 /// inside its own `exec_*` — so Describe reported no columns for all
2435 /// seventeen of them. Rows are deliberately not inserted: Describe
2436 /// only needs the shape, and `infer_column_types` reads the rows we
2437 /// already have in hand.
2438 pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2439 let from = stmt.from.as_ref()?;
2440 if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2441 return None;
2442 }
2443 let lower = from.primary.name.to_ascii_lowercase();
2444 let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2445 return None;
2446 };
2447 let mut catalog = self.active_catalog().clone();
2448 let cols = infer_column_types(&columns, &rows);
2449 catalog
2450 .create_table(TableSchema::new(from.primary.name.clone(), cols))
2451 .ok()?;
2452 Some(catalog)
2453 }
2454
2455 pub(crate) fn exec_select_cancel(
2456 &self,
2457 stmt: &SelectStatement,
2458 cancel: CancelToken<'_>,
2459 ) -> Result<QueryResult, EngineError> {
2460 self.exec_select_cancel_as(stmt, cancel, None)
2461 }
2462
2463 /// v7.39 (round 334, V55) — the same read core, authorised as
2464 /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2465 /// function's OWNER: that is the entire point of the form, and without
2466 /// it every definer function failed with "permission denied" on the
2467 /// very table it exists to expose.
2468 /// v7.39 (round 559) — see the call site. `None` for anything but
2469 /// the bare shape, so every other query keeps its old path.
2470 fn try_bare_count_star(
2471 &self,
2472 stmt: &SelectStatement,
2473 as_role: Option<&str>,
2474 ) -> Result<Option<QueryResult>, EngineError> {
2475 use spg_sql::ast::SelectItem;
2476 if as_role.is_some()
2477 || !stmt.ctes.is_empty()
2478 || !stmt.unions.is_empty()
2479 || stmt.where_.is_some()
2480 || stmt.group_by.is_some()
2481 || stmt.having.is_some()
2482 || stmt.distinct
2483 || !stmt.order_by.is_empty()
2484 || stmt.limit.is_some()
2485 || stmt.offset.is_some()
2486 || stmt.items.len() != 1
2487 {
2488 return Ok(None);
2489 }
2490 let Some(from) = &stmt.from else {
2491 return Ok(None);
2492 };
2493 if !from.joins.is_empty()
2494 || stmt.locking.is_some()
2495 || from.primary.lateral_subquery.is_some()
2496 || from.primary.unnest_expr.is_some()
2497 || from.primary.generate_series_args.is_some()
2498 || from.primary.name.is_empty()
2499 || from.primary.name.starts_with("__spg_")
2500 {
2501 return Ok(None);
2502 }
2503 // A partition PARENT holds no rows of its own — they live in the
2504 // children — so its header count is 0 and the ordinary path has
2505 // to fan out. Caught by the partition conformance cases.
2506 //
2507 // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2508 // of them, which is worse: its header count is a real number,
2509 // just not the answer. `SELECT count(*) FROM par` returned 1
2510 // where PG returns 2, because this shortcut fired before the
2511 // fan-out could. The question is "does anything descend from
2512 // this", not "was it declared a partition parent".
2513 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2514 return Ok(None);
2515 }
2516 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2517 return Ok(None);
2518 };
2519 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2520 return Ok(None);
2521 };
2522 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2523 return Ok(None);
2524 }
2525 // A row-security policy filters rows, so the header count is not
2526 // the answer; the ordinary path applies the policy.
2527 let Some(table) = self.active_catalog().get(&from.primary.name) else {
2528 return Ok(None);
2529 };
2530 if table.schema().row_security {
2531 return Ok(None);
2532 }
2533 // Rows frozen to the cold tier are not in `headers`, so the
2534 // header count would miss them. Caught by the cold-tier e2e.
2535 if table.has_cold_rows_fast() {
2536 return Ok(None);
2537 }
2538 let n = table.count_visible(&self.current_snapshot());
2539 let col = alias.clone().unwrap_or_else(|| String::from("count"));
2540 Ok(Some(QueryResult::Rows {
2541 columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2542 rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2543 i64::try_from(n).unwrap_or(i64::MAX)
2544 )])],
2545 }))
2546 }
2547
2548 /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2549 /// that col>` served from the index, never reading a row.
2550 ///
2551 /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2552 /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2553 /// count (2x at 1k). PG needs its visibility map for this — a heap
2554 /// tuple carries its own visibility, so an index entry alone cannot
2555 /// say whether the row is live, and PG reads the heap for any page
2556 /// the map does not mark all-visible. SPG keeps a header array
2557 /// beside the rows, so the locator answers it directly and there is
2558 /// no map to be stale.
2559 /// v7.39 (round 564) — the shape test, once, for both the
2560 /// materialising scan and the streaming one.
2561 ///
2562 /// Two callers asking the same question in two places is how a fact
2563 /// starts drifting; the answer here is the single copy. Returns the
2564 /// table, the alias the predicate is written against, the projected
2565 /// column's position, and the name the single output column takes.
2566 pub(crate) fn index_only_shape<'s>(
2567 &'s self,
2568 stmt: &'s SelectStatement,
2569 ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2570 use spg_sql::ast::SelectItem;
2571 if !stmt.ctes.is_empty()
2572 || !stmt.unions.is_empty()
2573 || stmt.group_by.is_some()
2574 || stmt.having.is_some()
2575 || stmt.distinct
2576 || stmt.locking.is_some()
2577 || !stmt.order_by.is_empty()
2578 || stmt.limit.is_some()
2579 || stmt.offset.is_some()
2580 || stmt.items.len() != 1
2581 {
2582 return None;
2583 }
2584 let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2585 return None;
2586 };
2587 if !from.joins.is_empty()
2588 || from.primary.lateral_subquery.is_some()
2589 || from.primary.unnest_expr.is_some()
2590 || from.primary.generate_series_args.is_some()
2591 || from.primary.name.is_empty()
2592 || from.primary.name.starts_with("__spg_")
2593 {
2594 return None;
2595 }
2596 // v7.39 (round 645) — see the note on the sibling shortcut above:
2597 // an inheritance parent's own header count is not the answer.
2598 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2599 return None;
2600 }
2601 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2602 return None;
2603 };
2604 let spg_sql::ast::Expr::Column(c) = expr else {
2605 return None;
2606 };
2607 let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2608 if let Some(q) = c.qualifier.as_deref()
2609 && !q.eq_ignore_ascii_case(alias_name)
2610 {
2611 return None;
2612 }
2613 let table = self.active_catalog().get(&from.primary.name)?;
2614 if table.schema().row_security {
2615 return None;
2616 }
2617 let cols = &table.schema().columns;
2618 let pos = cols
2619 .iter()
2620 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2621 let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2622 Some((table, alias_name, pos, out))
2623 }
2624
2625 /// v7.39 (round 565) — would this statement be answered out of the
2626 /// index alone?
2627 ///
2628 /// EXPLAIN has to name the node the executor will actually run, and
2629 /// the only honest way to know is to ask the same two questions the
2630 /// executor asks: the statement's shape, and everything decidable
2631 /// about the scan before it walks. Neither is re-stated here.
2632 pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2633 let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2634 return false;
2635 };
2636 let Some(where_) = stmt.where_.as_ref() else {
2637 return false;
2638 };
2639 crate::index_access::index_only_precheck(
2640 where_,
2641 &table.schema().columns,
2642 table,
2643 alias_name,
2644 pos,
2645 )
2646 .is_some()
2647 }
2648
2649 fn try_index_only_scan(
2650 &self,
2651 stmt: &SelectStatement,
2652 ) -> Result<Option<QueryResult>, EngineError> {
2653 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2654 return Ok(None);
2655 };
2656 let where_ = stmt.where_.as_ref().expect("shape checked it");
2657 let cols = &table.schema().columns;
2658 let Some(values) = crate::index_access::try_index_only_range(
2659 where_,
2660 cols,
2661 table,
2662 alias_name,
2663 &self.current_snapshot(),
2664 pos,
2665 ) else {
2666 return Ok(None);
2667 };
2668 let schema = alloc::vec![ColumnSchema::new(
2669 out_name,
2670 cols[pos].ty,
2671 cols[pos].nullable
2672 )];
2673 Ok(Some(QueryResult::Rows {
2674 columns: schema,
2675 rows: values
2676 .into_iter()
2677 .map(|v| Row::new(alloc::vec![v]))
2678 .collect(),
2679 }))
2680 }
2681
2682 /// v7.39 (round 564) — the same scan, emitting each value instead of
2683 /// building a `Vec<Row>` for the encoder to walk once and drop.
2684 ///
2685 /// A profile of the server serving a 50k-row range put 10.2% of the
2686 /// connection thread's CPU on BUILDING that vector and another 9.7%
2687 /// on dropping it — a fifth of the query, spent allocating and
2688 /// freeing one single-element `Vec` per output row so that the wire
2689 /// encoder could borrow each value for a few nanoseconds. The
2690 /// streaming interface it then hands them to takes `&[Value]`
2691 /// already.
2692 ///
2693 /// Returns `None` when the shape does not apply, so the caller falls
2694 /// back before anything has been emitted.
2695 pub(crate) fn try_index_only_stream<F>(
2696 &self,
2697 stmt: &SelectStatement,
2698 emit: &mut F,
2699 ) -> Result<Option<usize>, EngineError>
2700 where
2701 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2702 {
2703 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2704 return Ok(None);
2705 };
2706 let where_ = stmt.where_.as_ref().expect("shape checked it");
2707 let cols = &table.schema().columns;
2708 let schema = alloc::vec![ColumnSchema::new(
2709 out_name,
2710 cols[pos].ty,
2711 cols[pos].nullable
2712 )];
2713 let snapshot = self.current_snapshot();
2714 // The header goes out only once the walk has agreed to run — a
2715 // shape rejection after it would leave the client with a
2716 // RowDescription for a result that never comes.
2717 let mut wrote_header = false;
2718 let counted = crate::index_access::index_only_range_each(
2719 where_,
2720 cols,
2721 table,
2722 alias_name,
2723 &snapshot,
2724 pos,
2725 &mut |v: spg_storage::Value<'_>| {
2726 if !wrote_header {
2727 emit(crate::StreamItem::Header(&schema))?;
2728 wrote_header = true;
2729 }
2730 emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2731 },
2732 );
2733 match counted {
2734 None => Ok(None),
2735 Some(Err(e)) => Err(e),
2736 Some(Ok(n)) => {
2737 if !wrote_header {
2738 emit(crate::StreamItem::Header(&schema))?;
2739 }
2740 Ok(Some(n))
2741 }
2742 }
2743 }
2744
2745 /// `DISTINCT ON`'s de-duplication, which runs after the inner
2746 /// SELECT has produced its rows.
2747 ///
2748 /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2749 /// reason round 848 established: a debug build gives every branch's
2750 /// locals a slot in the frame whichever branch runs, and this one is
2751 /// eighty lines of hashing, key slicing and survivor sorting that a
2752 /// statement without `DISTINCT ON` never touches. Round 867
2753 /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2754 /// reaches none of it — the segment that had been blamed on
2755 /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2756 #[inline(never)]
2757 fn apply_distinct_on(
2758 &self,
2759 result: QueryResult,
2760 don_hidden: usize,
2761 don_limit: &(
2762 Option<spg_sql::ast::LimitExpr>,
2763 Option<spg_sql::ast::LimitExpr>,
2764 ),
2765 don_top1: usize,
2766 orig_order_by: &[spg_sql::ast::OrderBy],
2767 ) -> Result<QueryResult, EngineError> {
2768 let QueryResult::Rows { columns, rows } = result else {
2769 return Ok(result);
2770 };
2771 // The keys are the hidden trailing columns appended above.
2772 // v7.39 (round 729) — top-1 mode: the trailing columns are the
2773 // DON keys plus the ORDER tail; keep each group's best in one
2774 // hash pass, then sort the SURVIVORS with the original spec.
2775 let mut kept: alloc::vec::Vec<Row<'static>>;
2776 let key_start;
2777 if don_top1 > 0 {
2778 let tail = don_top1 - 1;
2779 key_start = columns.len().saturating_sub(don_hidden + tail);
2780 let ord_start = key_start + don_hidden;
2781 let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2782 .iter()
2783 .map(|o| (o.desc, o.nulls_first))
2784 .collect();
2785 let mysql = self.backslash_escapes;
2786 let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2787 for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2788 let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2789 let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2790 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2791 core::cmp::Ordering::Less => return true,
2792 core::cmp::Ordering::Greater => return false,
2793 core::cmp::Ordering::Equal => {}
2794 }
2795 }
2796 false
2797 };
2798 let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2799 let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2800 let mut keybuf = String::new();
2801 for row in rows {
2802 keybuf.clear();
2803 for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2804 aggregate::push_canonical_key(&mut keybuf, v);
2805 }
2806 match slot.get(keybuf.as_str()) {
2807 Some(&i) => {
2808 if better(&row, &best[i]) {
2809 best[i] = row;
2810 }
2811 }
2812 None => {
2813 slot.insert(keybuf.clone(), best.len());
2814 best.push(row);
2815 }
2816 }
2817 }
2818 // Survivors sort with the FULL original spec (keys are still
2819 // aboard as hidden columns).
2820 let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2821 .iter()
2822 .map(|o| (o.desc, o.nulls_first))
2823 .collect();
2824 best.sort_by(|a, b| {
2825 for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2826 let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2827 let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2828 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2829 core::cmp::Ordering::Equal => {}
2830 o => return o,
2831 }
2832 }
2833 core::cmp::Ordering::Equal
2834 });
2835 for r in &mut best {
2836 r.values.truncate(key_start);
2837 }
2838 kept = best;
2839 } else {
2840 key_start = columns.len().saturating_sub(don_hidden);
2841 let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2842 kept = alloc::vec::Vec::new();
2843 for mut row in rows {
2844 let key: alloc::vec::Vec<Value<'static>> =
2845 row.values.get(key_start..).unwrap_or(&[]).to_vec();
2846 if seen.iter().any(|k| k == &key) {
2847 continue;
2848 }
2849 seen.push(key);
2850 row.values.truncate(key_start);
2851 kept.push(row);
2852 }
2853 }
2854 let mut columns = columns;
2855 columns.truncate(key_start);
2856 // PG limits what DISTINCT ON left, not what fed it.
2857 let kept = apply_deferred_limit(kept, don_limit);
2858 Ok(QueryResult::Rows {
2859 columns,
2860 rows: kept,
2861 })
2862 }
2863
2864 pub(crate) fn exec_select_cancel_as(
2865 &self,
2866 stmt: &SelectStatement,
2867 cancel: CancelToken<'_>,
2868 as_role: Option<&str>,
2869 ) -> Result<QueryResult, EngineError> {
2870 // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2871 // <all columns>` is legal PG (the wildcard expands to grouped
2872 // columns); SPG refused the whole shape. Expand the wildcard
2873 // into explicit column refs up front — the aggregate layer's
2874 // existing "must appear in the GROUP BY clause" validation
2875 // then answers PG's sentence for any non-grouped column.
2876 if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2877 return self.exec_select_cancel_as(&expanded, cancel, as_role);
2878 }
2879 // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2880 // a row.
2881 //
2882 // The aggregate layer already short-circuits this to
2883 // `rows.len()`, so the O(1) part was never the problem — the
2884 // cost is UPSTREAM, materialising every visible row so that
2885 // layer can take its length. Measured over pgwire on 500k rows:
2886 // PG18 8.2 ms with two parallel workers, 10.3 ms with
2887 // parallelism off, SPG 16.5 ms — 1.6x slower than a
2888 // single-threaded PG on the commonest aggregate there is, and no
2889 // ledger entry recorded it.
2890 //
2891 // Counting visible HEADERS needs no row at all. PG cannot do
2892 // this: its visibility lives in the heap tuples themselves, so
2893 // it has to read them (that is why its own count(*) is a full
2894 // scan, parallel or not).
2895 // v7.39 (read01 round 57) — the table-privilege gate on the common
2896 // read core. A superuser session returns from it immediately.
2897 // v7.39 (round 529) — resolve an ORDER BY that names an output
2898 // ALIAS. The statement-level pass never reached a SELECT nested in
2899 // a FROM clause, a CTE or a scalar subquery, so the same query
2900 // worked on its own and failed the moment anything wrapped it —
2901 // which is what generated SQL does constantly.
2902 let aliased;
2903 let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
2904 let mut s = stmt.clone();
2905 crate::orderby::resolve_order_by_position(&mut s);
2906 aliased = s;
2907 &aliased
2908 } else {
2909 stmt
2910 };
2911 // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
2912 //
2913 // Its keys were evaluated against the PROJECTED row, so a key that
2914 // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
2915 // ORDER BY g, v DESC`, the canonical "latest row per group" — could
2916 // not be read at all and the query failed. PG evaluates them on the
2917 // input. They are projected as hidden columns here and stripped
2918 // again below, the same way the grouping-set ordering columns
2919 // already travel.
2920 //
2921 // And the dedup ran AFTER the inner statement's LIMIT, so
2922 // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
2923 // PG answers two: the limit had already taken two rows of the same
2924 // group before anything deduplicated them. A paginated DISTINCT ON
2925 // returned short pages, with no error. The limit is deferred to
2926 // after the dedup, which is PG's order.
2927 let don_stmt;
2928 // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
2929 // order spec (the rewritten stmt's is emptied).
2930 let orig_order_by = stmt.order_by.clone();
2931 let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
2932 (stmt, 0, (None, None), 0usize)
2933 } else {
2934 let mut s = stmt.clone();
2935 let hidden = s.distinct_on.len();
2936 for (i, e) in stmt.distinct_on.iter().enumerate() {
2937 s.items.push(SelectItem::Expr {
2938 expr: e.clone(),
2939 alias: Some(alloc::format!("__distinct_on_{i}")),
2940 });
2941 }
2942 // v7.39 (round 729) — group-top-1 short circuit. When the
2943 // DISTINCT ON keys are exactly the ORDER BY's leading keys,
2944 // the answer is "per group, the row that wins the remaining
2945 // order" — a single O(n) hash pass. The old path sorted the
2946 // ENTIRE input first (500k rows, ~180 ms on the panel cell)
2947 // to keep 100. The inner query runs UNSORTED with every
2948 // order key appended as a hidden column; the dedup below
2949 // keeps each group's best, then sorts the SURVIVORS.
2950 // Declared-collation order keys stay on the sorting path
2951 // (the value comparator here is collation-blind).
2952 let prefix_matches = s.order_by.len() >= hidden
2953 && stmt
2954 .distinct_on
2955 .iter()
2956 .zip(s.order_by.iter())
2957 .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
2958 let colls_plain =
2959 crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
2960 .map(|cs| cs.iter().all(Option::is_none))
2961 .unwrap_or(false);
2962 let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
2963 let tail = s.order_by.len() - hidden;
2964 for (j, o) in s.order_by[hidden..].iter().enumerate() {
2965 s.items.push(SelectItem::Expr {
2966 expr: o.expr.clone(),
2967 alias: Some(alloc::format!("__don_ord_{j}")),
2968 });
2969 }
2970 // Carry the tail's direction flags through the aliases'
2971 // ORDER; the survivors re-sort below with the full spec.
2972 s.order_by = Vec::new();
2973 tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
2974 } else {
2975 0
2976 };
2977 // Only a folded literal is deferred; a placeholder or an
2978 // expression keeps the path it has today rather than being
2979 // resolved a second way here.
2980 let deferrable = matches!(
2981 (&s.limit, &s.offset),
2982 (
2983 None | Some(spg_sql::ast::LimitExpr::Literal(_)),
2984 None | Some(spg_sql::ast::LimitExpr::Literal(_))
2985 )
2986 );
2987 let deferred = if deferrable {
2988 (s.limit.take(), s.offset.take())
2989 } else {
2990 (None, None)
2991 };
2992 don_stmt = s;
2993 (&don_stmt, hidden, deferred, top1_tail)
2994 };
2995 self.acl_check_select_as(stmt, as_role)?;
2996 validate_aggregate_placement(stmt)?;
2997 // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
2998 // privilege gate above. Placed before it at first, and the
2999 // security-definer e2e caught it immediately: a SECURITY INVOKER
3000 // function whose body is `SELECT count(*) FROM t` answered
3001 // instead of being refused, because the fast path never reached
3002 // the check.
3003 if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3004 return Ok(r);
3005 }
3006 // v7.39 (round 560) — an index-only range scan. Same placement
3007 // reasoning as the count above: after the privilege gate.
3008 if let Some(r) = self.try_index_only_scan(stmt)? {
3009 return Ok(r);
3010 }
3011 validate_locking_clause(stmt)?;
3012 let result = self.exec_select_cancel_inner(stmt, cancel)?;
3013 // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3014 // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3015 // They carry the per-branch mask through the UNION-ALL sort and must not
3016 // appear in the output. Stripped per SELECT level (grouping-set queries
3017 // are often wrapped in a derived subquery), before DISTINCT ON.
3018 let result = strip_synthetic_order_cols(result);
3019 // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3020 // rows arrive here already ORDER BY'd; keep the FIRST row of
3021 // each group the expressions define (PG semantics). The
3022 // expressions evaluate against the projected schema — an
3023 // expression that isn't in the select list errors honestly.
3024 if stmt.distinct_on.is_empty() {
3025 return Ok(result);
3026 }
3027 self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3028 }
3029
3030 /// The UNION chain: execute the head as a bare block, then fold each
3031 /// peer in with left-associative dedup.
3032 ///
3033 /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3034 /// reason round 848 established. A statement with no unions returns
3035 /// one line above the call — and every nested subquery on a deep
3036 /// path is such a statement, so each level of the recursion carried
3037 /// 170 lines of locals it could not reach. Round 867 measured that
3038 /// frame at 34,800 bytes, the largest single one on the descent,
3039 /// after two earlier attributions had blamed its caller and then its
3040 /// callee: the gap between two marks is the frame of everything
3041 /// BETWEEN them, and this function had no mark of its own.
3042 #[inline(never)]
3043 fn exec_union_chain(
3044 &self,
3045 stmt_ref: &SelectStatement,
3046 stmt: &SelectStatement,
3047 cancel: CancelToken<'_>,
3048 ) -> Result<QueryResult, EngineError> {
3049 // UNION path: clone-strip the head into a bare block (its own
3050 // DISTINCT and any inner ORDER BY are dropped by parser rule —
3051 // the wrapper SelectStatement carries them), execute, then chain
3052 // peers with left-associative dedup semantics.
3053 // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3054 // output columns; a position past their count is PG's 42P10.
3055 crate::orderby::check_order_by_positions(stmt_ref)?;
3056 let mut head_unknown = branch_unknown_mask(stmt_ref);
3057 let mut head = stmt_ref.clone();
3058 head.unions = Vec::new();
3059 head.order_by = Vec::new();
3060 head.limit = None;
3061 let QueryResult::Rows {
3062 mut columns,
3063 mut rows,
3064 } = self.exec_bare_select_cancel(&head, cancel)?
3065 else {
3066 unreachable!("bare SELECT cannot return CommandOk")
3067 };
3068 for (kind, peer) in &stmt_ref.unions {
3069 // v7.37.17 (17.6 siblings) — a peer carrying its own
3070 // unions is a nested INTERSECT group (the parser's
3071 // precedence regrouping); recurse through the
3072 // union-aware wrapper for it.
3073 let peer_result = if peer.unions.is_empty() {
3074 self.exec_bare_select_cancel(peer, cancel)?
3075 } else {
3076 self.exec_select_cancel(peer, cancel)?
3077 };
3078 let QueryResult::Rows {
3079 columns: peer_cols,
3080 rows: mut peer_rows,
3081 } = peer_result
3082 else {
3083 unreachable!("bare SELECT cannot return CommandOk")
3084 };
3085 if peer_cols.len() != columns.len() {
3086 // v7.39 (round 232) — PG's wording, which clients match on.
3087 return Err(EngineError::Unsupported(alloc::format!(
3088 "each {} query must have the same number of columns",
3089 set_op_name(*kind)
3090 )));
3091 }
3092 // v7.39 (round 232+233) — PG resolves each result column to one
3093 // type before it merges anything, and refuses the query when the
3094 // two branches have no common type. SPG's unifier
3095 // (`unify_union_columns`) is value-driven and deliberately
3096 // conservative — "a column where any cell fails to coerce is left
3097 // exactly as it was" — so a mismatch produced a column holding
3098 // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3099 // back with integers and text interleaved) instead of an error.
3100 //
3101 // The check has to read the branch ASTs, not just their schemas:
3102 // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3103 // as TEXT and is indistinguishable from a real text column by
3104 // schema alone — yet PG treats the two completely differently
3105 // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3106 // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3107 let peer_unknown = branch_unknown_mask(peer);
3108 for i in 0..columns.len() {
3109 let hu = head_unknown.get(i).copied().unwrap_or(false);
3110 let pu = peer_unknown.get(i).copied().unwrap_or(false);
3111 let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3112 match (hu, pu) {
3113 // Both sides carry a real type: they must share a category.
3114 (false, false) => {
3115 if !crate::conversions::types_unify(ht, pt) {
3116 return Err(EngineError::Unsupported(alloc::format!(
3117 "{} types {} and {} cannot be matched",
3118 set_op_name(*kind),
3119 crate::conversions::pg_type_name_for_error(ht),
3120 crate::conversions::pg_type_name_for_error(pt),
3121 )));
3122 }
3123 }
3124 // One side is an untyped literal: it takes the other's
3125 // type, and failing to convert is the error PG reports.
3126 (true, false) => {
3127 coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3128 columns[i].ty = pt;
3129 head_unknown[i] = false;
3130 }
3131 (false, true) => {
3132 coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3133 }
3134 // Both untyped — nothing to resolve against yet.
3135 (true, true) => {}
3136 }
3137 }
3138 // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3139 // nullable (PG semantics). Previously the result kept only the head's
3140 // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3141 // non-null `1`) wrongly reported the column NOT NULL, which let
3142 // `count(col)`'s NOT-NULL fast-path count the NULL row.
3143 for (i, pc) in peer_cols.iter().enumerate() {
3144 if pc.nullable {
3145 columns[i].nullable = true;
3146 }
3147 }
3148 // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3149 // text by the session collation (CI + accent + PAD SPACE), like
3150 // GROUP BY. PG stays byte-exact.
3151 let mysql = self.backslash_escapes;
3152 match kind {
3153 UnionKind::All => rows.extend(peer_rows),
3154 UnionKind::Distinct => {
3155 rows.extend(peer_rows);
3156 rows = dedup_rows(rows, mysql);
3157 }
3158 // v7.37.17 (17.6 siblings) — PG set semantics.
3159 // v7.39 (round 591) — all four ask the same question of the
3160 // right side, and all four used to answer it by scanning it
3161 // once per left row. `PeerIndex` buckets it by the hash
3162 // DISTINCT already uses, so the answer is a lookup.
3163 // INTERSECT: distinct rows present on both sides.
3164 UnionKind::Intersect => {
3165 let idx = PeerIndex::build(&peer_rows, mysql);
3166 rows = dedup_rows(rows, mysql)
3167 .into_iter()
3168 .filter(|r| idx.contains(r))
3169 .collect();
3170 }
3171 // INTERSECT ALL: multiset intersection — each row
3172 // keeps min(left count, right count) occurrences.
3173 UnionKind::IntersectAll => {
3174 let mut idx = PeerIndex::build(&peer_rows, mysql);
3175 let mut kept: Vec<Row<'static>> = Vec::new();
3176 for r in rows {
3177 if idx.take_one(&r) {
3178 kept.push(r);
3179 }
3180 }
3181 rows = kept;
3182 }
3183 // EXCEPT: distinct left rows absent from the right.
3184 UnionKind::Except => {
3185 let idx = PeerIndex::build(&peer_rows, mysql);
3186 rows = dedup_rows(rows, mysql)
3187 .into_iter()
3188 .filter(|r| !idx.contains(r))
3189 .collect();
3190 }
3191 // EXCEPT ALL: multiset subtraction — each right
3192 // occurrence cancels one left occurrence.
3193 UnionKind::ExceptAll => {
3194 let mut idx = PeerIndex::build(&peer_rows, mysql);
3195 let mut kept: Vec<Row<'static>> = Vec::new();
3196 for r in rows {
3197 if !idx.take_one(&r) {
3198 kept.push(r);
3199 }
3200 }
3201 rows = kept;
3202 }
3203 }
3204 }
3205 // PG resolves a UNION / VALUES result column to one common type
3206 // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3207 // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3208 // built each branch independently, leaving mixed-type columns
3209 // that broke ORDER BY, comparisons, and value-based window
3210 // frames. Unify + coerce before the combined ORDER BY sees them.
3211 unify_union_columns(&mut columns, &mut rows);
3212 // ORDER BY at the top of a UNION applies to the combined result.
3213 // Eval against the projected schema (NOT the source table).
3214 if !stmt.order_by.is_empty() {
3215 // v7.39 (read01 round 54) — the combined-result ctx must carry the
3216 // catalog, and the projected columns must keep their enum identity
3217 // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3218 // by TEXT instead of member order — silently wrong rows, not an
3219 // error. (Same shape as the enum-order knife's GROUP BY fix.)
3220 let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3221 // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3222 // survive to here when the head projects a Wildcard (the
3223 // group-tail wrapper shape): map them onto the Nth
3224 // projected column so the combined sort works.
3225 let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3226 .order_by
3227 .iter()
3228 .map(|o| {
3229 let mut o = o.clone();
3230 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3231 && *n >= 1
3232 && let Ok(idx) = usize::try_from(*n - 1)
3233 && idx < columns.len()
3234 {
3235 o.expr = Expr::Column(spg_sql::ast::ColumnName {
3236 qualifier: None,
3237 name: columns[idx].name.clone(),
3238 });
3239 }
3240 o
3241 })
3242 .collect();
3243 let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3244 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3245 for r in rows {
3246 let keys = build_order_keys(&resolved_order, &r, &synth_ctx)?;
3247 tagged.push((keys, r));
3248 }
3249 sort_by_keys(&mut tagged, &descs);
3250 rows = tagged.into_iter().map(|(_, r)| r).collect();
3251 }
3252 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3253 Ok(QueryResult::Rows { columns, rows })
3254 }
3255
3256 fn exec_select_cancel_inner(
3257 &self,
3258 stmt: &SelectStatement,
3259 cancel: CancelToken<'_>,
3260 ) -> Result<QueryResult, EngineError> {
3261 cancel.check()?;
3262 // v7.38 P0 元机制 A — first observable point inside the
3263 // planner / executor. Tests use this to inject a delay or
3264 // a cancellation race before any row is produced. Release
3265 // build expands to `let _ = (...);` — zero cost.
3266 crate::injection_point!("planner_first_row_fetch", &stmt.from);
3267 // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3268 // PG analyses every definition, referenced or not, so `SELECT i FROM
3269 // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3270 // succeeded here (the parser used to drop the unreferenced defs
3271 // whole). The check is the CREATE VIEW check's shape (round 700): a
3272 // LIMIT-0 run of the same FROM with the definitions' key
3273 // expressions as the projection — it cannot disagree with what a
3274 // referencing window would have done, because it resolves the same
3275 // names the same way. Zero cost for the ordinary statement: the
3276 // list is empty unless a WINDOW clause left unreferenced defs.
3277 if !stmt.window_check_exprs.is_empty() {
3278 let mut probe = stmt.clone();
3279 probe.items = stmt
3280 .window_check_exprs
3281 .iter()
3282 .map(|e| spg_sql::ast::SelectItem::Expr {
3283 expr: e.clone(),
3284 alias: None,
3285 })
3286 .collect();
3287 probe.window_check_exprs = Vec::new();
3288 probe.distinct = false;
3289 probe.distinct_on = Vec::new();
3290 probe.group_by = None;
3291 probe.group_by_all = false;
3292 probe.having = None;
3293 probe.unions = Vec::new();
3294 probe.order_by = Vec::new();
3295 probe.locking = None;
3296 probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3297 probe.offset = None;
3298 probe.limit_with_ties = false;
3299 self.exec_select_cancel_inner(&probe, cancel)?;
3300 }
3301 // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3302 // takes the catalog, so the parser leaves a marker and the rewrite lands
3303 // here: the call moves into a LATERAL FROM item and the item becomes one
3304 // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3305 // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3306 // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3307 // second one.
3308 if let Some(lowered) = self.lower_record_expansion(stmt)? {
3309 return self.exec_select_cancel_inner(&lowered, cancel);
3310 }
3311 // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3312 // FROM / JOIN graph references any catalogued view name,
3313 // re-parse the view body and prepend it as a synthetic
3314 // CTE. Recurses on views-in-views via the regular CTE
3315 // dispatch below. Fast-path: skip the walker entirely when
3316 // the catalog has no views (the typical OLTP load).
3317 if !self.active_catalog().views_all().is_empty() {
3318 if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3319 return self.exec_select_cancel(&rewritten, cancel);
3320 }
3321 }
3322 // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3323 // gets rewritten to a UNION-ALL over the children that overlap
3324 // the WHERE-derived key range. Uses the same CTE-injection
3325 // trick as VIEW expansion above so downstream resolution
3326 // doesn't need a partition-aware code path.
3327 if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3328 return self.exec_select_cancel(&rewritten, cancel);
3329 }
3330 // v7.16.2 — information_schema / pg_catalog virtual
3331 // views (mailrs round-10 A.3). If the SELECT touches a
3332 // synthetic meta-table name (`__spg_info_*` /
3333 // `__spg_pg_*` — produced by the parser for
3334 // `information_schema.X` / `pg_catalog.X`), clone the
3335 // catalog, materialise the requested view as a real
3336 // temporary table, and re-execute against an enriched
3337 // engine. Same pattern as `exec_with_ctes` for CTEs.
3338 if !self.meta_views_materialised && select_references_meta_view(stmt) {
3339 return self.exec_select_with_meta_views(stmt, cancel);
3340 }
3341 // v6.10.2 — cold-tier time-travel short-circuit. When the
3342 // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3343 // dedicated cold-segment scan instead of the regular
3344 // hot+index path. The scope is intentionally narrow for
3345 // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3346 // optionally with a single-column-equality WHERE. JOINs /
3347 // aggregates / ORDER BY / subqueries on top of a time-
3348 // travelled scan are STABILITY § "Out of v6.10".
3349 if let Some(from) = &stmt.from
3350 && let Some(seg_id) = from.primary.as_of_segment
3351 {
3352 return self.exec_select_as_of_segment(stmt, from, seg_id);
3353 }
3354 // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3355 // pre-CTE because they don't read from the catalog and
3356 // shouldn't participate in regular FROM resolution.
3357 // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3358 // short-circuits. A meta-view FROM materialises to a fixed row
3359 // set. For a bare `SELECT *` we return it directly; otherwise we
3360 // stage it as a temp table and run the normal pipeline, so
3361 // projection / WHERE / ORDER BY / aggregates work over these views
3362 // (they were `SELECT *`-only before). A real table shadowing the
3363 // name wins (checked first), which also stops the staged re-run
3364 // from recursing back into meta-view detection.
3365 if let Some(from) = &stmt.from
3366 && from.joins.is_empty()
3367 && self.active_catalog().get(&from.primary.name).is_none()
3368 {
3369 let lower = from.primary.name.to_ascii_lowercase();
3370 if let Some(result) = self.meta_view_result(&lower) {
3371 let bare = stmt.where_.is_none()
3372 && stmt.group_by.is_none()
3373 && stmt.having.is_none()
3374 && stmt.unions.is_empty()
3375 && stmt.order_by.is_empty()
3376 && stmt.limit.is_none()
3377 && stmt.offset.is_none()
3378 && !stmt.distinct
3379 && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3380 if bare {
3381 return Ok(result);
3382 }
3383 if let QueryResult::Rows { columns, rows } = result {
3384 let mut catalog = self.active_catalog().clone();
3385 let cols = infer_column_types(&columns, &rows);
3386 let schema = TableSchema::new(from.primary.name.clone(), cols);
3387 catalog.create_table(schema).map_err(EngineError::Storage)?;
3388 let t = catalog
3389 .get_mut(&from.primary.name)
3390 .expect("just-created meta-view table must exist");
3391 for row in rows {
3392 t.insert(row).map_err(EngineError::Storage)?;
3393 }
3394 let mut eng = Engine::restore(catalog);
3395 if let Some(c) = self.clock {
3396 eng = eng.with_clock(c);
3397 }
3398 if let Some(f) = self.salt_fn {
3399 eng = eng.with_salt_fn(f);
3400 }
3401 // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3402 // connection identity so `WHERE pid = pg_backend_pid()`
3403 // matches inside the staged meta-view run.
3404 if let Some(f) = self.backend_pid_fn {
3405 eng.set_backend_pid_fn(f);
3406 }
3407 return eng.exec_select_cancel(stmt, cancel);
3408 }
3409 return Ok(result);
3410 }
3411 }
3412 // v4.11: CTEs materialise into a temporary enriched catalog
3413 // *before* anything else — the body SELECT can then refer
3414 // to CTE names via the regular FROM-clause resolution.
3415 // Uncorrelated only: each CTE body runs once against the
3416 // current catalog, not against later CTEs' results (left-
3417 // to-right materialisation would relax this, but we keep
3418 // it simple for v4.11 MVP).
3419 if !stmt.ctes.is_empty() {
3420 return self.exec_with_ctes(stmt, cancel);
3421 }
3422 // v4.10: subqueries (uncorrelated) are resolved here, before
3423 // the executor sees the row loop. We clone the statement so
3424 // we can mutate without disturbing the caller's AST — most
3425 // queries pass through with no subquery nodes and the clone
3426 // is cheap; with subqueries the materialisation cost
3427 // dominates anyway.
3428 let mut stmt_owned;
3429 let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3430 stmt_owned = stmt.clone();
3431 // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3432 // aggregate-wrapped correlated scalar subquery whose
3433 // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3434 // executor streams one join instead of splicing a per-row
3435 // subplan. Runs before the per-row/batch resolver, which then
3436 // only sees the subqueries the pull-up left behind.
3437 self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3438 // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3439 // the "per-key latest" scalar subquery shape (inbox / feed
3440 // / timeline applications) becomes a CTE + LEFT JOIN
3441 // against a GROUP BY pre-aggregation that reuses the v7.33
3442 // first_ordered argmax executor. Runs AFTER unique-key
3443 // pull-up (so the unique-key fast path still wins for
3444 // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3445 // Phase 1 (this commit) is skeleton only — no-op pass.
3446 self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3447 // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3448 // sublink pull-up to semi/anti-join, before the resolver gets
3449 // a chance to walk per-row.
3450 self.pull_up_exists_sublinks(&mut stmt_owned);
3451 // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3452 // exec_with_ctes so they materialise once before the body
3453 // SELECT runs. exec_with_ctes strips ctes from the body
3454 // clone, then re-enters select.
3455 if !stmt_owned.ctes.is_empty() {
3456 return self.exec_with_ctes(&stmt_owned, cancel);
3457 }
3458 // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3459 // SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3460 // BEFORE `resolve_select_subqueries` materialises the inner
3461 // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3462 // INSUBQ benchmark). Run the inner once, collect the result
3463 // values into a `HashSet<i64>` directly, then probe A.pk per
3464 // value and tally. Returns `Some` when the shape matches.
3465 if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3466 return Ok(out);
3467 }
3468 self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3469 &stmt_owned
3470 } else {
3471 stmt
3472 };
3473 if stmt_ref.unions.is_empty() {
3474 return self.exec_bare_select_cancel(stmt_ref, cancel);
3475 }
3476 self.exec_union_chain(stmt_ref, stmt, cancel)
3477 }
3478
3479 #[allow(clippy::too_many_lines)]
3480 #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3481 /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3482 /// Synthesises a single-column virtual table whose column type
3483 /// is TEXT and whose rows are the array elements. Routes
3484 /// through the regular projection / WHERE / ORDER BY / LIMIT
3485 /// machinery so set-returning UNNEST composes naturally with
3486 /// the rest of the SELECT surface.
3487 fn exec_select_unnest(
3488 &self,
3489 stmt: &SelectStatement,
3490 primary: &TableRef,
3491 cancel: CancelToken<'_>,
3492 ) -> Result<QueryResult, EngineError> {
3493 let expr = primary
3494 .unnest_expr
3495 .as_deref()
3496 .expect("caller guards unnest_expr.is_some()");
3497 // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3498 // N value columns instead of one; the shared builder does
3499 // the work and the tail below (WHERE / agg / projection)
3500 // runs against the wider schema.
3501 let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3502 match unnest_zip_args(expr) {
3503 Some(args) => Some(unnest_zip_rows(args)?),
3504 None => None,
3505 };
3506 // Evaluate the array expression once. Empty schema / empty
3507 // row — uncorrelated UNNEST cannot reference outer columns.
3508 // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3509 // introspection family (enum_range / enum_first / enum_last) resolves
3510 // its labels from the argument's STATIC enum type against the
3511 // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3512 // fell through to the generic arm, got NULL, and expanded to zero rows
3513 // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3514 // carry the catalog) worked.
3515 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3516 let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3517 let dummy_row = Row::new(alloc::vec::Vec::new());
3518 // v7.11.13 — unnest dispatches per array element type so
3519 // INT[] / BIGINT[] surface their PG types in projection.
3520 // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3521 // columns (PG: lexeme | positions | weights); everything else
3522 // keeps the alias / "unnest" defaults below.
3523 let mut composite_names: Option<&[&str]> = None;
3524 let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3525 if let Some(m) = multi {
3526 m
3527 } else {
3528 // v7.39 (round 236) — flatten a multidimensional array into
3529 // its row-major elements (PG) before the 1-D-only match.
3530 let unnest_src = {
3531 let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3532 crate::eval::values::flatten_2d(&v).unwrap_or(v)
3533 };
3534 let mut return_multi: Option<(
3535 alloc::vec::Vec<DataType>,
3536 alloc::vec::Vec<Row<'static>>,
3537 )> = None;
3538 let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3539 {
3540 Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3541 Value::TextArray(items) => {
3542 let rows = items
3543 .into_iter()
3544 .map(|item| {
3545 Row::new(alloc::vec![match item {
3546 Some(s) => Value::text(s),
3547 None => Value::Null,
3548 }])
3549 })
3550 .collect();
3551 (DataType::Text, rows)
3552 }
3553 Value::IntArray(items) => {
3554 let rows = items
3555 .into_iter()
3556 .map(|item| {
3557 Row::new(alloc::vec![match item {
3558 Some(n) => Value::Int(n),
3559 None => Value::Null,
3560 }])
3561 })
3562 .collect();
3563 (DataType::Int, rows)
3564 }
3565 Value::BigIntArray(items) => {
3566 let rows = items
3567 .into_iter()
3568 .map(|item| {
3569 Row::new(alloc::vec![match item {
3570 Some(n) => Value::BigInt(n),
3571 None => Value::Null,
3572 }])
3573 })
3574 .collect();
3575 (DataType::BigInt, rows)
3576 }
3577 Value::Multirange { kind, ranges } => {
3578 let rows = ranges
3579 .iter()
3580 .map(|sp| {
3581 Row::new(alloc::vec![Value::Range {
3582 kind,
3583 lower: sp.lower.clone(),
3584 upper: sp.upper.clone(),
3585 lower_inc: sp.lower_inc,
3586 upper_inc: sp.upper_inc,
3587 empty: false,
3588 }])
3589 })
3590 .collect();
3591 (DataType::Range(kind), rows)
3592 }
3593 // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3594 // one row per lexeme, PG18-measured columns
3595 // lexeme | positions | weights (`a | {1,3} |
3596 // {D,D}`); a position-less lexeme (a stripped
3597 // vector) reads NULL in both array columns.
3598 Value::TsVector(lexemes) => {
3599 composite_names = Some(&["lexeme", "positions", "weights"]);
3600 let rows = lexemes
3601 .iter()
3602 .map(|l| {
3603 let (pos, wts) = if l.positions.is_empty() {
3604 (Value::Null, Value::Null)
3605 } else {
3606 let letter = match l.weight {
3607 3 => "A",
3608 2 => "B",
3609 1 => "C",
3610 _ => "D",
3611 };
3612 (
3613 Value::SmallIntArray(
3614 l.positions
3615 .iter()
3616 .map(|p| {
3617 Some(i16::try_from(*p).unwrap_or(i16::MAX))
3618 })
3619 .collect(),
3620 ),
3621 Value::TextArray(
3622 l.positions
3623 .iter()
3624 .map(|_| Some(letter.into()))
3625 .collect(),
3626 ),
3627 )
3628 };
3629 Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3630 })
3631 .collect();
3632 return_multi = Some((
3633 alloc::vec![
3634 DataType::Text,
3635 DataType::SmallIntArray,
3636 DataType::TextArray
3637 ],
3638 rows,
3639 ));
3640 (DataType::Text, alloc::vec::Vec::new())
3641 }
3642 other => {
3643 // v7.39 (round 622, S05a) — see table_access.rs:
3644 // the same sentence, and it is a type mismatch.
3645 return Err(EngineError::Eval(EvalError::TypeMismatch {
3646 detail: alloc::format!(
3647 "unnest() expects an array argument, got {}",
3648 crate::conversions::pg_type_name_for_error_opt(other.data_type())
3649 ),
3650 }));
3651 }
3652 };
3653 if let Some(m) = return_multi {
3654 m
3655 } else {
3656 (alloc::vec![elem_dtype], rows)
3657 }
3658 };
3659 let alias = primary
3660 .alias
3661 .clone()
3662 .unwrap_or_else(|| "unnest".to_string());
3663 // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3664 // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3665 // entries map positionally over the value columns. Without
3666 // the column list, a single column falls back to the table
3667 // alias (pre-v7.13.2 behaviour); multi-arg columns default
3668 // to PG's `unnest`.
3669 let n_vals = dtypes.len();
3670 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3671 .iter()
3672 .enumerate()
3673 .map(|(i, dt)| {
3674 let name = primary
3675 .unnest_column_aliases
3676 .get(i)
3677 .cloned()
3678 .unwrap_or_else(|| {
3679 if let Some(names) = composite_names {
3680 names
3681 .get(i)
3682 .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3683 } else if n_vals == 1 {
3684 alias.clone()
3685 } else {
3686 "unnest".to_string()
3687 }
3688 });
3689 ColumnSchema::new(name, *dt, true)
3690 })
3691 .collect();
3692 // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3693 // parser desugared a base-type-returning function here (see
3694 // TableRef::scalar_fn_item); the marker rides the column so it survives
3695 // every EvalContext an inner stage rebuilds.
3696 if primary.scalar_fn_item && schema_cols.len() == 1 {
3697 schema_cols[0].scalar_row_source = true;
3698 }
3699 // WITH ORDINALITY — trailing BIGINT counting rows from 1
3700 // in element order. The alias entry after the value
3701 // columns renames it (PG default: `ordinality`).
3702 let rows = if primary.with_ordinality {
3703 let ord_name = primary
3704 .unnest_column_aliases
3705 .get(n_vals)
3706 .cloned()
3707 .unwrap_or_else(|| "ordinality".to_string());
3708 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3709 rows.into_iter()
3710 .enumerate()
3711 .map(|(i, row)| {
3712 let mut vals = row.values.clone();
3713 vals.push(Value::BigInt(i as i64 + 1));
3714 Row::new(vals)
3715 })
3716 .collect()
3717 } else {
3718 rows
3719 };
3720 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3721 // `EvalContext::new` drops it and every catalog-dependent cast
3722 // (regclass / enum / composite / domain) silently degrades.
3723 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3724 // Apply WHERE.
3725 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3726 let mut out = alloc::vec::Vec::with_capacity(rows.len());
3727 for row in rows {
3728 cancel.check()?;
3729 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3730 if matches!(v, Value::Bool(true)) {
3731 out.push(row);
3732 }
3733 }
3734 out
3735 } else {
3736 rows
3737 };
3738 // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3739 // unnest source. Same routing the relational scan path
3740 // already takes — without it `SELECT COUNT(*) FROM
3741 // unnest(ARRAY[…])` either errored at projection time or
3742 // returned the wrong shape.
3743 if aggregate::uses_aggregate(stmt) {
3744 // v7.29 — a per-query memo so correlated scalar
3745 // subqueries batch-evaluate once (group map) instead of
3746 // executing per group.
3747 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3748 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3749 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3750 .map_err(|err| match err {
3751 EngineError::Eval(ev) => ev,
3752 other => eval::EvalError::TypeMismatch {
3753 detail: alloc::format!("{other}"),
3754 },
3755 })
3756 };
3757 // v7.39 (round 656) — hand the rows over as they are rather than
3758 // collecting a second vector of `RowRef` wrappers. Note this is
3759 // a set-returning-function path, NOT the relational scan: the
3760 // measured O(rows) cost lived in `run_single_table_aggregate`,
3761 // and converting these four first was a miss that cost a full
3762 // round — every test stayed green and the number did not move.
3763 let agg = aggregate::run(
3764 stmt,
3765 crate::join::AggRows::Owned(&filtered),
3766 &schema_cols,
3767 Some(&alias),
3768 Some(&agg_correlated),
3769 self.parallel_runner.0.as_deref(),
3770 Some(self.active_catalog()),
3771 Some(self),
3772 )?;
3773 return self.finish_agg_result(agg, stmt, cancel);
3774 }
3775 // Projection.
3776 let projection =
3777 build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
3778 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3779 alloc::vec::Vec::with_capacity(filtered.len());
3780 // v7.19 P5 — Set-Returning-Function in projection
3781 // position (PG `SELECT unnest(arr) FROM t` shape). When a
3782 // SELECT item evaluates to a top-level unnest(arr) call,
3783 // expand it: for each input row, evaluate the array, emit
3784 // one output row per element, broadcasting non-SRF
3785 // projections from the same input row. Multi-SRF + LCM
3786 // padding stays a documented carve-out; mailrs uses
3787 // single-SRF for redirect_uris.
3788 // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3789 // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3790 let srf_idxs = self.srf_target_idxs(&projection);
3791 // v7.39 (round 621) — which input row each output row came from. An
3792 // SRF turns one input row into many, and the ORDER BY below used to
3793 // index the EXPANDED rows by the INPUT row's position: the result was
3794 // silently truncated to the input row count and left unsorted, so
3795 // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3796 // answered three of its six rows, in no order. Without the ORDER BY
3797 // the same query was already right.
3798 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3799 if !srf_idxs.is_empty() {
3800 let (rows, src) =
3801 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3802 projected_rows = rows;
3803 src_of_row = src;
3804 } else {
3805 // v7.24 (round-16 B) — select-list subqueries resolve
3806 // per row (correlated-aware; plain exprs take the fast
3807 // path inside).
3808 let mut proj_memo = memoize::MemoizeCache::default();
3809 for row in &filtered {
3810 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3811 for p in &projection {
3812 vals.push(self.eval_expr_with_correlated(
3813 &p.expr,
3814 row,
3815 &scan_ctx,
3816 cancel,
3817 Some(&mut proj_memo),
3818 )?);
3819 }
3820 projected_rows.push(Row::new(vals));
3821 }
3822 }
3823 // ORDER BY / LIMIT — apply on the projected rows (cheap;
3824 // unnest result sets are small by design).
3825 let columns: alloc::vec::Vec<ColumnSchema> = projection
3826 .iter()
3827 // v7.39 (read01 round 54) — keep the column's enum identity through
3828 // the projection (it lives outside the DataType lattice), or a
3829 // derived table / UNION / windowed result forgets it and any outer
3830 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
3831 .map(|p| {
3832 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
3833 c.user_enum_type = p.user_enum_type.clone();
3834 c.mysql_fsp = p.mysql_fsp;
3835 c
3836 })
3837 .collect();
3838 // Re-evaluate ORDER BY against the source schema (pre-projection
3839 // so col refs by name still resolve through `scan_ctx`).
3840 // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
3841 // column. Evaluated as an expression it is just the constant N: the same
3842 // key for every row, so the sort ran and changed nothing.
3843 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
3844 if !order_by.is_empty() {
3845 // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
3846 // A key that names a select-list item reads it out of the expanded
3847 // row (PG sorts AFTER the expansion); one that names a source
3848 // column the query does not project is evaluated on the input row
3849 // it came from, which is what `srf_order_output_cols` decides.
3850 let out_cols = if srf_idxs.is_empty() {
3851 alloc::vec![None; order_by.len()]
3852 } else {
3853 srf_order_output_cols(&order_by, &projection)
3854 };
3855 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
3856 .iter()
3857 .enumerate()
3858 .map(|(k, out)| -> Result<_, EngineError> {
3859 let src = src_of_row.get(k).copied().unwrap_or(k);
3860 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
3861 .iter()
3862 .zip(out_cols.iter())
3863 .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
3864 .collect();
3865 Ok((k, keys?))
3866 })
3867 .collect::<Result<_, _>>()?;
3868 indexed.sort_by(|a, b| {
3869 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
3870 let o = &order_by[idx];
3871 let cmp = order_by_value_cmp_in(
3872 o.desc,
3873 o.nulls_first,
3874 ka,
3875 kb,
3876 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
3877 );
3878 if cmp != core::cmp::Ordering::Equal {
3879 return cmp;
3880 }
3881 }
3882 core::cmp::Ordering::Equal
3883 });
3884 projected_rows = indexed
3885 .into_iter()
3886 .map(|(i, _)| projected_rows[i].clone())
3887 .collect();
3888 }
3889 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
3890 if stmt.distinct {
3891 projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
3892 }
3893 // LIMIT / OFFSET — apply at the tail.
3894 if let Some(offset) = stmt.offset_literal() {
3895 let off = (offset as usize).min(projected_rows.len());
3896 projected_rows.drain(..off);
3897 }
3898 if let Some(limit) = stmt.limit_literal() {
3899 projected_rows.truncate(limit as usize);
3900 }
3901 Ok(QueryResult::Rows {
3902 columns,
3903 rows: projected_rows,
3904 })
3905 }
3906
3907 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
3908 /// step])` set-returning source. Mirrors `exec_select_unnest`'s
3909 /// shape: evaluate the arg list once against an empty row,
3910 /// materialise the row stream by stepping start → stop, then
3911 /// route through the standard WHERE / projection / ORDER BY /
3912 /// LIMIT pipeline. Two arg-type combos in v7.17:
3913 /// * integer / integer [/ integer] — SmallInt, Int, BigInt
3914 /// (widened to BigInt internally; step defaults to 1)
3915 /// * timestamp / timestamp / interval — date-range
3916 /// iteration (mailrs's daily-report pattern)
3917 fn exec_select_generate_series(
3918 &self,
3919 stmt: &SelectStatement,
3920 primary: &TableRef,
3921 cancel: CancelToken<'_>,
3922 ) -> Result<QueryResult, EngineError> {
3923 let args = primary
3924 .generate_series_args
3925 .as_ref()
3926 .expect("caller guards generate_series_args.is_some()");
3927 let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
3928 let alias = primary
3929 .alias
3930 .clone()
3931 .unwrap_or_else(|| "generate_series".to_string());
3932 // `AS t(n)` — the first column-alias entry renames the
3933 // series column (PG semantics); bare alias keeps the
3934 // pre-existing behaviour of naming the column after it.
3935 let col_name = primary
3936 .unnest_column_aliases
3937 .first()
3938 .cloned()
3939 .unwrap_or_else(|| alias.clone());
3940 let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
3941 let mut schema_cols = alloc::vec![col_schema.clone()];
3942 // WITH ORDINALITY — trailing BIGINT counting rows from 1;
3943 // the second column-alias entry renames it.
3944 let rows = if primary.with_ordinality {
3945 let ord_name = primary
3946 .unnest_column_aliases
3947 .get(1)
3948 .cloned()
3949 .unwrap_or_else(|| "ordinality".to_string());
3950 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3951 rows.into_iter()
3952 .enumerate()
3953 .map(|(i, row)| {
3954 let mut vals = row.values.clone();
3955 vals.push(Value::BigInt(i as i64 + 1));
3956 Row::new(vals)
3957 })
3958 .collect()
3959 } else {
3960 rows
3961 };
3962 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3963 // `EvalContext::new` drops it and every catalog-dependent cast
3964 // (regclass / enum / composite / domain) silently degrades.
3965 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3966 // WHERE.
3967 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3968 let mut out = alloc::vec::Vec::with_capacity(rows.len());
3969 for row in rows {
3970 cancel.check()?;
3971 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3972 if matches!(v, Value::Bool(true)) {
3973 out.push(row);
3974 }
3975 }
3976 out
3977 } else {
3978 rows
3979 };
3980 // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
3981 // returning sources. When the SELECT projection contains
3982 // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
3983 // …) we route the filtered row stream through the same
3984 // aggregate executor the relational scan path uses, so
3985 // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
3986 // a single 100 row instead of erroring at projection
3987 // time. GROUP BY / HAVING / ORDER BY over the aggregate
3988 // output all ride through `aggregate::run`.
3989 if aggregate::uses_aggregate(stmt) {
3990 // v7.29 — a per-query memo so correlated scalar
3991 // subqueries batch-evaluate once (group map) instead of
3992 // executing per group.
3993 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3994 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3995 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3996 .map_err(|err| match err {
3997 EngineError::Eval(ev) => ev,
3998 other => eval::EvalError::TypeMismatch {
3999 detail: alloc::format!("{other}"),
4000 },
4001 })
4002 };
4003 // v7.39 (round 656) — hand the rows over as they are rather than
4004 // collecting a second vector of `RowRef` wrappers. Note this is
4005 // a set-returning-function path, NOT the relational scan: the
4006 // measured O(rows) cost lived in `run_single_table_aggregate`,
4007 // and converting these four first was a miss that cost a full
4008 // round — every test stayed green and the number did not move.
4009 let agg = aggregate::run(
4010 stmt,
4011 crate::join::AggRows::Owned(&filtered),
4012 &schema_cols,
4013 Some(&alias),
4014 Some(&agg_correlated),
4015 self.parallel_runner.0.as_deref(),
4016 Some(self.active_catalog()),
4017 Some(self),
4018 )?;
4019 return self.finish_agg_result(agg, stmt, cancel);
4020 }
4021 // Projection.
4022 let projection =
4023 build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
4024 // v7.39 (round 621) — and here, for the same reason.
4025 let srf_idxs = self.srf_target_idxs(&projection);
4026 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4027 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4028 alloc::vec::Vec::with_capacity(filtered.len());
4029 let mut proj_memo = memoize::MemoizeCache::default();
4030 if !srf_idxs.is_empty() {
4031 let (rows, src) =
4032 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4033 projected_rows = rows;
4034 src_of_row = src;
4035 } else {
4036 for row in &filtered {
4037 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4038 for p in &projection {
4039 // v7.24 (round-16 B) — correlated-aware.
4040 vals.push(self.eval_expr_with_correlated(
4041 &p.expr,
4042 row,
4043 &scan_ctx,
4044 cancel,
4045 Some(&mut proj_memo),
4046 )?);
4047 }
4048 projected_rows.push(Row::new(vals));
4049 }
4050 }
4051 let columns: alloc::vec::Vec<ColumnSchema> = projection
4052 .iter()
4053 // v7.39 (read01 round 54) — keep the column's enum identity through
4054 // the projection (it lives outside the DataType lattice), or a
4055 // derived table / UNION / windowed result forgets it and any outer
4056 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4057 .map(|p| {
4058 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
4059 c.user_enum_type = p.user_enum_type.clone();
4060 c.mysql_fsp = p.mysql_fsp;
4061 c
4062 })
4063 .collect();
4064 // ORDER BY against the source schema.
4065 // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4066 // more of them than there were inputs), and a positional key means the
4067 // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4068 // and what the other two synthetic-source tails already did.
4069 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4070 if !order_by.is_empty() {
4071 let out_cols = if srf_idxs.is_empty() {
4072 alloc::vec![None; order_by.len()]
4073 } else {
4074 srf_order_output_cols(&order_by, &projection)
4075 };
4076 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4077 .iter()
4078 .enumerate()
4079 .map(|(k, out)| -> Result<_, EngineError> {
4080 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4081 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4082 .iter()
4083 .zip(out_cols.iter())
4084 .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4085 .collect();
4086 Ok((k, keys?))
4087 })
4088 .collect::<Result<_, _>>()?;
4089 indexed.sort_by(|a, b| {
4090 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4091 let o = &stmt.order_by[idx];
4092 let cmp = order_by_value_cmp_in(
4093 o.desc,
4094 o.nulls_first,
4095 ka,
4096 kb,
4097 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4098 );
4099 if cmp != core::cmp::Ordering::Equal {
4100 return cmp;
4101 }
4102 }
4103 core::cmp::Ordering::Equal
4104 });
4105 projected_rows = indexed
4106 .into_iter()
4107 .map(|(i, _)| projected_rows[i].clone())
4108 .collect();
4109 }
4110 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4111 if stmt.distinct {
4112 projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
4113 }
4114 if let Some(offset) = stmt.offset_literal() {
4115 let off = (offset as usize).min(projected_rows.len());
4116 projected_rows.drain(..off);
4117 }
4118 if let Some(limit) = stmt.limit_literal() {
4119 projected_rows.truncate(limit as usize);
4120 }
4121 Ok(QueryResult::Rows {
4122 columns,
4123 rows: projected_rows,
4124 })
4125 }
4126
4127 /// The FROM shapes that are not an ordinary table scan — joins, the
4128 /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4129 ///
4130 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4131 /// reason round 848 established in the parser: a debug build gives
4132 /// EVERY branch's locals a slot in the frame, whichever branch runs.
4133 /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4134 /// stacks several of them; a plain scan reaches none of these
4135 /// branches. Moving them out took the frame to 52,336.
4136 ///
4137 /// `Ok(None)` means "not one of these shapes, carry on".
4138 #[inline(never)]
4139 fn try_from_shape_paths(
4140 &self,
4141 stmt: &SelectStatement,
4142 from: &spg_sql::ast::FromClause,
4143 cancel: CancelToken<'_>,
4144 ) -> Result<Option<QueryResult>, EngineError> {
4145 if !from.joins.is_empty() {
4146 // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4147 // elimination: when a LEFT JOIN's right side is referenced
4148 // ONLY in the ON equality and the right-side join key is
4149 // UNIQUE/PK, the join preserves outer cardinality exactly
4150 // and contributes no values used downstream. Drop the
4151 // entire join. PG does this on the
4152 // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4153 // — A's row count is what survives, B never has to be
4154 // touched.
4155 if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4156 return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4157 }
4158 // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4159 // the v7.32 joinfold rewrite that turns inner JOINs into a
4160 // single-table scan when the catalogue can prove key-only
4161 // dependency. Tests use this to assert "without joinfold,
4162 // the join still executes correctly" (joinfold is a
4163 // semantically-equivalent rewrite, not a correctness fix).
4164 if !self.env_cfg().disable_joinfold {
4165 if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4166 return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4167 }
4168 }
4169 return self.exec_joined_select(stmt, from, cancel).map(Some);
4170 }
4171 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4172 // single-column table at SELECT entry by evaluating the
4173 // expression once against the empty row (UNNEST is
4174 // uncorrelated in v7.11; correlated / LATERAL unnest is a
4175 // v7.12 carve-out). Build a virtual `Table` in a heap-only
4176 // catalog, then route to the regular scan path.
4177 if from.primary.unnest_expr.is_some() {
4178 return self
4179 .exec_select_unnest(stmt, &from.primary, cancel)
4180 .map(Some);
4181 }
4182 // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4183 // returning function. Same dispatch shape as unnest but
4184 // emits a two-column (key TEXT, value TEXT) row stream.
4185 if from.primary.jsonb_each_text_arg.is_some() {
4186 return self
4187 .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4188 .map(Some);
4189 }
4190 // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4191 // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4192 // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4193 // array form. Each function runs; the results zip in LOCKSTEP with the
4194 // shorter padded to NULL — the SAME rule the target-list SRFs follow
4195 // (round 67), which is why `srf_values` is what evaluates each entry.
4196 if from.primary.rows_from.is_some() {
4197 let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4198 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4199 if let Some(col) = schema_cols.get_mut(i) {
4200 col.name = new_name.clone();
4201 }
4202 }
4203 let alias = from
4204 .primary
4205 .alias
4206 .clone()
4207 .unwrap_or_else(|| from.primary.name.clone());
4208 return self
4209 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4210 .map(Some);
4211 }
4212 // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4213 // COLUMNS (...))`. Materialise the row stream + schema by
4214 // walking the row path, then run the regular pipeline over it.
4215 if let Some(jt) = &from.primary.json_table {
4216 let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4217 let alias = from
4218 .primary
4219 .alias
4220 .clone()
4221 .unwrap_or_else(|| from.primary.name.clone());
4222 return self
4223 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4224 .map(Some);
4225 }
4226 if from.primary.table_fn_call.is_some() {
4227 let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4228 // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4229 // (from 1, in output order) AFTER the function's own columns. The
4230 // alias list names it like any other, which is why it is appended
4231 // BEFORE the renaming pass below.
4232 let rows = if from.primary.with_ordinality {
4233 schema_cols.push(ColumnSchema::new(
4234 "ordinality".to_string(),
4235 DataType::BigInt,
4236 false,
4237 ));
4238 rows.into_iter()
4239 .enumerate()
4240 .map(|(i, r)| {
4241 let mut vals = r.values;
4242 vals.push(Value::BigInt(i as i64 + 1));
4243 Row::new(vals)
4244 })
4245 .collect()
4246 } else {
4247 rows
4248 };
4249 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4250 if let Some(col) = schema_cols.get_mut(i) {
4251 col.name = new_name.clone();
4252 }
4253 }
4254 let alias = from
4255 .primary
4256 .alias
4257 .clone()
4258 .unwrap_or_else(|| from.primary.name.clone());
4259 return self
4260 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4261 .map(Some);
4262 }
4263 // v7.37.17 (17.6 siblings) — plain derived table in primary
4264 // position: `FROM ( SELECT … ) alias` (no joins). The inner
4265 // SELECT materialises once (it is uncorrelated by
4266 // construction), then the outer projection / WHERE /
4267 // aggregate / ORDER BY pipeline runs over the synthetic
4268 // table. Joined derived tables keep riding the LATERAL
4269 // machinery in join.rs.
4270 if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4271 // v7.39 (round 727) — flatten first. A simple derived table
4272 // (bare-column projection over one stored table, nothing that
4273 // changes cardinality or order) used to force the inner
4274 // SELECT through the SERIAL row-at-a-time projection pipeline
4275 // just to materialise a synthetic table the outer query then
4276 // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4277 // measured 18.6 ms against PG's 5 — and bare count over the
4278 // same filter WITHOUT the wrapper is 2 ms here, because it
4279 // rides the fused parallel lane. Rewriting to the unwrapped
4280 // form is PG's subquery pull-up; the whole tree gets the
4281 // fast lanes back.
4282 if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4283 return self.exec_select_cancel(&flat, cancel).map(Some);
4284 }
4285 // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4286 // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4287 // ORDER BY never changes the row count, and OFFSET drops
4288 // exactly k. The materialising path sorted 500k rows to
4289 // count 10k (57 ms); PG runs its parallel sort anyway
4290 // (28 ms). The rewrite skips the sort entirely on both
4291 // counts — a plan PG itself does not have.
4292 if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4293 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4294 }
4295 // v7.39 (round 743) — `count(*) OVER a derived whose only
4296 // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4297 // a constant-length array unnests to exactly k rows per
4298 // input row, NULL elements included. PG expands the set to
4299 // count it (6.6 ms on the panel cell); the identity doesn't.
4300 if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4301 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4302 }
4303 return self
4304 .exec_select_derived(stmt, &from.primary, cancel)
4305 .map(Some);
4306 }
4307 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4308 // [, step])` set-returning source. Dispatch mirrors UNNEST:
4309 // materialise the row stream from a single eval pass, then
4310 // run the regular projection / WHERE / ORDER BY / LIMIT
4311 // pipeline over the synthetic single-column table.
4312 if from.primary.generate_series_args.is_some() {
4313 return self
4314 .exec_select_generate_series(stmt, &from.primary, cancel)
4315 .map(Some);
4316 }
4317 Ok(None)
4318 }
4319
4320 /// Pick an index seek for this WHERE, if any of the four apply:
4321 /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4322 ///
4323 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4324 /// frame reason on `try_from_shape_paths`: in a debug build a
4325 /// closure's locals belong to the enclosing frame, and this one is
4326 /// four seek attempts wide on a function that nests.
4327 #[inline(never)]
4328 fn pick_indexed_rows<'r>(
4329 &'r self,
4330 stmt: &SelectStatement,
4331 table: &'r spg_storage::Table,
4332 schema_cols: &[spg_storage::ColumnSchema],
4333 alias: &str,
4334 ctx: &crate::eval::EvalContext<'_>,
4335 seek_snapshot: &crate::Snapshot,
4336 ) -> Option<Vec<Cow<'r, Row<'static>>>> {
4337 stmt.where_.as_ref().and_then(|w| {
4338 // BTree / col=literal seek first — covers the v7.11.3 multi-
4339 // column AND case and the leading-column equality lookup.
4340 try_index_seek(
4341 w,
4342 schema_cols,
4343 self.active_catalog(),
4344 table,
4345 alias,
4346 seek_snapshot,
4347 )
4348 .or_else(|| {
4349 // v7.12.3 — GIN-accelerated `WHERE col @@
4350 // tsquery` when the column has a `USING gin`
4351 // index. Returns an over-approximate candidate
4352 // set; the WHERE re-eval loop below verifies
4353 // the full `@@` predicate per row.
4354 try_gin_seek(
4355 w,
4356 schema_cols,
4357 self.active_catalog(),
4358 table,
4359 alias,
4360 ctx,
4361 seek_snapshot,
4362 )
4363 })
4364 .or_else(|| {
4365 // v7.15.0 — trigram-GIN-accelerated
4366 // `WHERE col LIKE / ILIKE '<pat>'` when the
4367 // column has a `gin_trgm_ops` GIN index.
4368 // Over-approximate candidate set; the WHERE
4369 // re-eval verifies the LIKE per row.
4370 try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4371 })
4372 .or_else(|| {
4373 // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4374 // accelerated `WHERE col @> <jsonb_literal>`
4375 // when the column has a `USING gin` index. The
4376 // posting-list intersection returns an over-
4377 // approximate candidate set; the WHERE re-eval
4378 // verifies the full `@>` predicate per row.
4379 try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4380 })
4381 })
4382 }
4383
4384 /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4385 /// the two `count(*)` short-circuits. Out-of-line for the frame
4386 /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4387 /// of them, and in a debug build their locals sit in the frame
4388 /// regardless.
4389 #[inline(never)]
4390 fn try_seek_fast_paths(
4391 &self,
4392 stmt: &SelectStatement,
4393 table: &spg_storage::Table,
4394 schema_cols: &[spg_storage::ColumnSchema],
4395 alias: &str,
4396 seek_snapshot: &crate::Snapshot,
4397 cancel: CancelToken<'_>,
4398 ) -> Result<Option<QueryResult>, EngineError> {
4399 if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4400 // NSW kNN dispatches against the hot-tier vector index only
4401 // (vector cells aren't promoted to cold segments), so wrap
4402 // the returned row indices as `Cow::Borrowed` for the
4403 // unified `materialise_in_order` shape.
4404 let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4405 .into_iter()
4406 .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4407 .collect();
4408 return materialise_in_order(
4409 stmt,
4410 schema_cols,
4411 alias,
4412 &ordered,
4413 self.backslash_escapes,
4414 )
4415 .map(Some);
4416 }
4417
4418 // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4419 // the scan via the BTree iterator in the requested direction
4420 // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4421 // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4422 // the load-bearing consumer; this skips the materialise-every-
4423 // row + partial-sort tail entirely. Walker output is already
4424 // in ORDER BY order so `materialise_in_order` (no extra sort)
4425 // is the natural sink.
4426 if let Some(walked) = try_pk_walk_top_n(
4427 stmt,
4428 self.active_catalog(),
4429 table,
4430 schema_cols,
4431 alias,
4432 self,
4433 cancel,
4434 ) {
4435 return materialise_in_order(stmt, schema_cols, alias, &walked, self.backslash_escapes)
4436 .map(Some);
4437 }
4438
4439 // Index seek: if WHERE is `col = literal` (or commuted) and the
4440 // referenced column has an index, dispatch each locator through
4441 // the catalog (hot tier → borrow, cold tier → page-read +
4442 // decode) and iterate just those rows. Otherwise fall back to a
4443 // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4444 // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4445 // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4446 // we don't pay the row materialisation cost twice. Returns
4447 // a bare `Rows{count}` if the shape matches.
4448 if aggregate::uses_aggregate(stmt)
4449 && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4450 {
4451 return Ok(Some(out));
4452 }
4453 // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4454 // locators directly, skipping row materialisation + WHERE re-eval.
4455 if aggregate::uses_aggregate(stmt)
4456 && let Some(out) = self.try_count_star_indexed_range_fast(
4457 stmt,
4458 table,
4459 schema_cols,
4460 alias,
4461 seek_snapshot,
4462 )
4463 {
4464 return Ok(Some(out));
4465 }
4466 Ok(None)
4467 }
4468
4469 /// The two rewrites that must happen before the FROM clause is even
4470 /// looked at: a meta-view reference needs the catalog views
4471 /// materialised, and a windowed projection belongs to the window
4472 /// executor. Out-of-line for the frame reason on
4473 /// `try_from_shape_paths`.
4474 #[inline(never)]
4475 fn try_pre_from_paths(
4476 &self,
4477 stmt: &SelectStatement,
4478 cancel: CancelToken<'_>,
4479 ) -> Result<Option<QueryResult>, EngineError> {
4480 if !self.meta_views_materialised && select_references_meta_view(stmt) {
4481 return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4482 }
4483 // v4.12: window-function path. When the projection contains
4484 // any `name(args) OVER (...)` we route to the dedicated
4485 // executor — partition + sort + per-row window value before
4486 // the regular projection.
4487 if select_has_window(stmt) {
4488 // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4489 // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4490 // needs the aggregation done first, then windows over the grouped
4491 // rows. Rewrite to an aggregate derived subquery + outer window query
4492 // (which the window-over-derived path, D.13, executes). Only fires on
4493 // the currently-erroring agg+window+GROUP BY shape, so it can't
4494 // regress working window-only or aggregate-only queries.
4495 if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4496 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4497 }
4498 return self.exec_select_with_window(stmt, cancel).map(Some);
4499 }
4500 Ok(None)
4501 }
4502
4503 /// A projection naming `ctid` or another system column: the schema
4504 /// has to be widened with them before the scan. Out-of-line for the
4505 /// frame reason on `try_from_shape_paths`.
4506 #[inline(never)]
4507 fn try_ctid_projection(
4508 &self,
4509 stmt: &SelectStatement,
4510 primary: &spg_sql::ast::TableRef,
4511 table: &spg_storage::Table,
4512 schema_cols: &[spg_storage::ColumnSchema],
4513 alias: &str,
4514 cancel: CancelToken<'_>,
4515 ) -> Result<Option<QueryResult>, EngineError> {
4516 if references_ctid(stmt) {
4517 let snapshot = self.current_snapshot();
4518 let mut ext_cols = schema_cols.to_vec();
4519 for name in SYSTEM_COLUMNS {
4520 ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4521 }
4522 let table_oid =
4523 crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4524 .unwrap_or(0);
4525 let headers = table.headers();
4526 let rows: Vec<Row<'static>> = table
4527 .scan_visible(&snapshot)
4528 .map(|(i, r)| {
4529 let mut vals = r.values.clone();
4530 // One block, offsets from 1, as PG numbers them.
4531 vals.push(Value::Tid(0, i as u32 + 1));
4532 let h = headers.get(i);
4533 vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4534 vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4535 // SPG keeps no per-statement command ids; PG shows 0 for
4536 // every row a reader can see, which is every row here.
4537 vals.push(Value::Cid(0));
4538 vals.push(Value::Cid(0));
4539 vals.push(Value::BigInt(table_oid));
4540 Row::new(vals)
4541 })
4542 .collect();
4543 return self
4544 .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4545 .map(Some);
4546 }
4547 Ok(None)
4548 }
4549
4550 /// A sequence read as a one-row relation (`SELECT last_value FROM
4551 /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4552 /// the frame reason on `try_from_shape_paths`.
4553 #[inline(never)]
4554 fn try_sequence_relation(
4555 &self,
4556 stmt: &SelectStatement,
4557 primary: &spg_sql::ast::TableRef,
4558 cancel: CancelToken<'_>,
4559 ) -> Result<Option<QueryResult>, EngineError> {
4560 if self.active_catalog().get(&primary.name).is_none()
4561 && let Some(seq) = self.active_catalog().sequence(&primary.name)
4562 {
4563 let rows = alloc::vec![Row::new(alloc::vec![
4564 Value::BigInt(seq.last_value),
4565 Value::BigInt(0),
4566 Value::Bool(seq.is_called),
4567 ])];
4568 let schema_cols = alloc::vec![
4569 ColumnSchema::new("last_value", DataType::BigInt, false),
4570 ColumnSchema::new("log_cnt", DataType::BigInt, false),
4571 ColumnSchema::new("is_called", DataType::Bool, false),
4572 ];
4573 let alias = primary
4574 .alias
4575 .clone()
4576 .unwrap_or_else(|| primary.name.clone());
4577 return self
4578 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4579 .map(Some);
4580 }
4581 Ok(None)
4582 }
4583
4584 pub(crate) fn exec_bare_select_cancel(
4585 &self,
4586 stmt: &SelectStatement,
4587 cancel: CancelToken<'_>,
4588 ) -> Result<QueryResult, EngineError> {
4589 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4590 // is meaningless without an ORDER BY; PG raises a hard
4591 // error and SPG mirrors the surface so the same DDL/app
4592 // path behaves identically on cutover.
4593 check_with_ties_requires_order_by(stmt)?;
4594 // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4595 // PG rejects window calls there outright. Checked here rather than
4596 // on the window path: `HAVING row_number() OVER () = 1` has no
4597 // window in its projection at all.
4598 crate::window::reject_window_in_row_clauses(stmt)?;
4599 // v7.39 (round 232) — the ORDER BY legality rules (positional
4600 // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4601 // check: before anything scans.
4602 crate::orderby::check_order_by_legality(stmt)?;
4603 // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4604 // equivalent statement the regular executor handles (merged join
4605 // columns collapse to a single unqualified output column; NATURAL
4606 // gets its common-column ON synthesised). The rewrite clears the
4607 // flags, so this re-entrant call is a no-op on the second pass.
4608 if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4609 return self.exec_bare_select_cancel(&rewritten, cancel);
4610 }
4611 // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4612 // operand in a security-barrier subquery, then re-enter (the wrapped
4613 // operands are no longer bare RLS tables, so this is a no-op on the
4614 // second pass).
4615 if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4616 return self.exec_bare_select_cancel(&rewritten, cancel);
4617 }
4618 // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4619 // AND the RLS USING predicate into a single-table SELECT's WHERE.
4620 // Superuser sessions and non-RLS tables get `None` (no clone, no
4621 // change). Applied inline (shadowing `stmt`) rather than via re-entry
4622 // so it can't re-inject on a recursive pass.
4623 let rls_stmt;
4624 let stmt = match self.rls_select_predicate(stmt)? {
4625 Some(pred) => {
4626 let mut s = stmt.clone();
4627 s.where_ = Some(match s.where_.take() {
4628 Some(existing) => spg_sql::ast::Expr::Binary {
4629 lhs: alloc::boxed::Box::new(existing),
4630 op: spg_sql::ast::BinOp::And,
4631 rhs: alloc::boxed::Box::new(pred),
4632 },
4633 None => pred,
4634 });
4635 rls_stmt = s;
4636 &rls_stmt
4637 }
4638 None => stmt,
4639 };
4640 // v7.16.2 — same meta-view dispatch as
4641 // `exec_select_cancel`, applied here too because
4642 // `subquery_replacement` enters this function directly
4643 // for Exists / ScalarSubquery / InSubquery resolution
4644 // (bypassing the top-level entry to avoid double
4645 // subquery walking). Without this dispatch the subquery
4646 // hits `__spg_info_columns` and reports TableNotFound.
4647 if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4648 return Ok(done);
4649 }
4650 // Constant SELECT (no FROM) — evaluate each item once against an
4651 // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4652 // `SELECT '7'::INT`. Column references will surface as
4653 // ColumnNotFound on eval since the schema is empty.
4654 let Some(from) = &stmt.from else {
4655 return self.exec_constant_select(stmt);
4656 };
4657 // Multi-table FROM (one or more joined peers) goes through the
4658 // nested-loop join executor. Single-table FROM stays on the
4659 // existing scan + index-seek path.
4660 if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4661 return Ok(done);
4662 }
4663 // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4664 // tested — eight ORDER BY shapes byte-identical spilled against
4665 // in-memory, with 103 runs opened to prove the spill ran — and it
4666 // loses on wall clock, which is a hard stop whatever the memory
4667 // buys. Measured round 865, same psql client both sides, same
4668 // machine, row counts verified, and both sides confirmed to be
4669 // doing an external merge rather than an indexed walk:
4670 //
4671 // PG18 178.7 - 187.0 ms Sort Method: external merge, 85 MB
4672 // SPG spilled 269.7 - 299.6 ms 33 spill files at peak
4673 //
4674 // Non-overlapping, about 1.55x. Re-enable by restoring the call
4675 // below once that closes; nothing else has to change, which is
4676 // the point of it being a separate path.
4677 //
4678 // if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4679 // return Ok(done);
4680 // }
4681 //
4682 // v7.37 (round 882) — this walk stays unhooked, but its streaming
4683 // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4684 // bail in `try_exec_joined_streaming`. Collecting the answer was
4685 // most of what this one cost: handing rows over as the merge
4686 // produces them holds peak to the budget plus one row, and the
4687 // wall clock lands inside PG18's range rather than 1.55x outside
4688 // it. Numbers in `extsort.rs`'s header.
4689 let primary = &from.primary;
4690 // v7.39 (round 244) — a sequence is selectable as a one-row relation
4691 // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4692 // read it). Synthesize PG's three columns.
4693 if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4694 return Ok(done);
4695 }
4696 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4697 StorageError::TableNotFound {
4698 name: primary.name.clone(),
4699 }
4700 })?;
4701 let schema_cols = &table.schema().columns;
4702 // The qualifier accepted on column refs is the alias (if any) else the
4703 // bare table name.
4704 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4705 // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4706 // system columns at all: `SELECT ctid FROM t` answered "column
4707 // \"ctid\" does not exist", which takes out the dedup idiom every
4708 // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4709 // GROUP BY key)`.
4710 //
4711 // The value comes from the row's position, which the scan already
4712 // yields; the column is appended to the schema and the rows only
4713 // when the statement asks for it, so nothing else pays for it. That
4714 // also routes the query down the general path, past the index fast
4715 // paths below — they hand back rows without positions, and a ctid
4716 // that was sometimes right would be worse than none.
4717 if let Some(done) =
4718 self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4719 {
4720 return Ok(done);
4721 }
4722 let ctx = self.ev_ctx(schema_cols, Some(alias));
4723
4724 // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4725 // WHERE and an NSW index on `col` skips the full scan. The
4726 // walk returns rows already in ascending-distance order, so
4727 // ORDER BY / LIMIT are honoured implicitly.
4728 // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4729 // and thread it into every index-seek fast path below. No-op
4730 // today (every hot header is committed-alive).
4731 let seek_snapshot = self.current_snapshot();
4732 if let Some(done) =
4733 self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4734 {
4735 return Ok(done);
4736 }
4737 // full scan over the hot tier (cold-tier rows are only reached
4738 // via index seek in v5.1 — full table scans against cold-tier
4739 // data ship in v5.2 with the freezer's per-segment scan API).
4740 let indexed_rows =
4741 self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4742
4743 // Aggregate path: filter rows first, then hand off to the
4744 // aggregate executor which does its own projection + ORDER BY.
4745 if aggregate::uses_aggregate(stmt) {
4746 return self.run_single_table_aggregate(
4747 stmt,
4748 table,
4749 schema_cols,
4750 alias,
4751 indexed_rows,
4752 cancel,
4753 );
4754 }
4755 self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4756 }
4757
4758 /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4759 /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4760 /// uncorrelated FROM-primary case is the simpler shape, used by
4761 /// e2e pins. Materialises the (key, value) pair stream into a
4762 /// synthetic two-column TEXT table, then routes through the
4763 /// regular projection / WHERE / ORDER BY pipeline.
4764 /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4765 /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4766 /// item into (rows, schema). `outer_doc` is `Some` only when this
4767 /// is a NESTED level being expanded against a parent row item's
4768 /// already-parsed sub-document; the top-level call parses the doc
4769 /// expr itself. Row/column paths reuse the existing jsonpath
4770 /// evaluator (`json::json_table_path`); coercion reuses
4771 /// `coerce_value` on the JSON scalar text, so a json string
4772 /// coerces to DATE by its content, matching PG.
4773 #[allow(clippy::type_complexity)]
4774 pub(crate) fn json_table_rows(
4775 &self,
4776 jt: &spg_sql::ast::JsonTable,
4777 outer_doc: Option<&crate::json::JsonValue>,
4778 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4779 // Column schema is static (independent of data): flatten the
4780 // COLUMNS tree in declaration order (NESTED contributes its
4781 // children inline, the PG output shape).
4782 let schema = json_table_schema(&jt.columns);
4783
4784 // PASSING variables → a single JsonValue object the jsonpath
4785 // engine reads `$name` from.
4786 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4787 let ctx = EvalContext::new(&empty_schema, None);
4788 let dummy = Row::new(alloc::vec::Vec::new());
4789 let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4790 None
4791 } else {
4792 let mut entries = alloc::vec::Vec::new();
4793 for (name, e) in &jt.passing {
4794 let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
4795 entries.push((name.clone(), value_to_json_value(&v)));
4796 }
4797 Some(crate::json::JsonValue::Object(entries))
4798 };
4799
4800 // The document root: a NESTED level gets it from the parent;
4801 // the top level parses its doc expr.
4802 let root_owned;
4803 let root: &crate::json::JsonValue = match outer_doc {
4804 Some(d) => d,
4805 None => {
4806 let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
4807 let src = match &doc_val {
4808 Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
4809 Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
4810 other => {
4811 return Err(EngineError::Unsupported(alloc::format!(
4812 "JSON_TABLE document must be json/text, got {}",
4813 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4814 )));
4815 }
4816 };
4817 root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
4818 &root_owned
4819 }
4820 };
4821
4822 let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
4823 .map_err(EngineError::Eval)?;
4824 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
4825 for (idx, item) in items.iter().enumerate() {
4826 self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
4827 }
4828 Ok((rows, schema))
4829 }
4830
4831 /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
4832 /// Regular columns produce one value each; a NESTED column expands
4833 /// as an outer join (each nested match → one row sharing the
4834 /// parent cells; no nested match → one row with the nested cells
4835 /// NULL). Sibling NESTED at one level cross by concatenation of
4836 /// their independent expansions (PG's UNION-of-outer shape).
4837 fn json_table_emit_item(
4838 &self,
4839 jt: &spg_sql::ast::JsonTable,
4840 item: &crate::json::JsonValue,
4841 ordinality: usize,
4842 vars: Option<&crate::json::JsonValue>,
4843 out: &mut alloc::vec::Vec<Row<'static>>,
4844 ) -> Result<(), EngineError> {
4845 use spg_sql::ast::JsonTableColumn as C;
4846 // Parent cells (regular + ordinality), left-to-right; NESTED
4847 // columns contribute a run of child cells appended after.
4848 let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
4849 let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
4850 alloc::vec::Vec::new();
4851 let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4852 for col in &jt.columns {
4853 match col {
4854 C::Ordinality { .. } => {
4855 parent_cells.push(Value::BigInt(ordinality as i64 + 1));
4856 }
4857 C::Regular { .. } => {
4858 parent_cells.push(self.json_table_column_value(col, item, vars)?);
4859 }
4860 C::Nested { path, columns } => {
4861 // Recurse: a nested JSON_TABLE over `item` filtered
4862 // by `path`, with the same PASSING vars.
4863 let sub = spg_sql::ast::JsonTable {
4864 doc: jt.doc.clone(), // unused (outer_doc provided)
4865 row_path: path.clone(),
4866 columns: columns.clone(),
4867 passing: alloc::vec::Vec::new(),
4868 };
4869 let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
4870 nested_widths.push(nschema.len());
4871 nested_runs.push(nrows);
4872 }
4873 }
4874 }
4875 if nested_runs.is_empty() {
4876 out.push(Row::new(parent_cells));
4877 return Ok(());
4878 }
4879 // PG sibling-NESTED semantics: each sibling expands
4880 // INDEPENDENTLY and the results CONCATENATE — a row from
4881 // sibling s fills only s's cells, every other sibling's cells
4882 // NULL. An empty sibling contributes ZERO rows (not a NULL
4883 // row). Only when EVERY sibling is empty does the parent still
4884 // emit one all-NULL row (the outer-join guarantee that a parent
4885 // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
4886 // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
4887 let before = out.len();
4888 for (s_idx, run) in nested_runs.iter().enumerate() {
4889 for nrow in run {
4890 let mut cells = parent_cells.clone();
4891 for (o_idx, w) in nested_widths.iter().enumerate() {
4892 if o_idx == s_idx {
4893 cells.extend(nrow.values.iter().cloned());
4894 } else {
4895 for _ in 0..*w {
4896 cells.push(Value::Null);
4897 }
4898 }
4899 }
4900 out.push(Row::new(cells));
4901 }
4902 }
4903 if out.len() == before {
4904 // Every sibling empty → one all-NULL nested row.
4905 let mut cells = parent_cells.clone();
4906 for w in &nested_widths {
4907 for _ in 0..*w {
4908 cells.push(Value::Null);
4909 }
4910 }
4911 out.push(Row::new(cells));
4912 }
4913 Ok(())
4914 }
4915
4916 /// v7.39 (round 205) — evaluate one Regular column against a row
4917 /// item: EXISTS → bool; else path → at most one value, coerced to
4918 /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
4919 fn json_table_column_value(
4920 &self,
4921 col: &spg_sql::ast::JsonTableColumn,
4922 item: &crate::json::JsonValue,
4923 vars: Option<&crate::json::JsonValue>,
4924 ) -> Result<Value<'static>, EngineError> {
4925 use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
4926 let C::Regular {
4927 name,
4928 ty,
4929 path,
4930 exists,
4931 format_json,
4932 wrapper,
4933 on_empty,
4934 on_error,
4935 } = col
4936 else {
4937 unreachable!("caller guards Regular");
4938 };
4939 let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
4940 if *exists {
4941 return Ok(Value::Bool(!matches.is_empty()));
4942 }
4943 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4944 let ctx = EvalContext::new(&empty_schema, None);
4945 let dummy = Row::new(alloc::vec::Vec::new());
4946 let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
4947 match b {
4948 B::Null => Ok(Some(Value::Null)),
4949 B::Error => Ok(None),
4950 B::Default(e) => Ok(Some(
4951 eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
4952 )),
4953 }
4954 };
4955 // Empty match set → ON EMPTY.
4956 if matches.is_empty() {
4957 return match default_of(on_empty)? {
4958 Some(v) => coerce_json_table_default(v, *ty, name),
4959 None => Err(EngineError::Unsupported(alloc::format!(
4960 "no SQL/JSON item found for JSON_TABLE column {name:?}"
4961 ))),
4962 };
4963 }
4964 let first = &matches[0];
4965 // FORMAT JSON: return the PG-canonical json representation.
4966 // WITH WRAPPER wraps the whole match SET in an array (even a
4967 // single scalar → `[5]`); without it, the single match's json.
4968 if *format_json {
4969 let text = if *wrapper {
4970 crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
4971 } else {
4972 first.canonical_json_text()
4973 };
4974 return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
4975 }
4976 if first.is_json_null() {
4977 return Ok(Value::Null);
4978 }
4979 // Coerce the scalar text to the declared type; on failure → ON
4980 // ERROR (default NULL, DEFAULT expr, or raise).
4981 let dt = crate::conversions::column_type_to_data_type(*ty);
4982 let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
4983 match crate::conversions::coerce_value(scalar, dt, name, 0) {
4984 Ok(v) => Ok(v),
4985 Err(e) => match default_of(on_error)? {
4986 Some(v) => coerce_json_table_default(v, *ty, name),
4987 None => Err(e),
4988 },
4989 }
4990 }
4991
4992 /// table function into (rows, default schema). Dispatch by name.
4993 pub(crate) fn table_fn_rows(
4994 &self,
4995 primary: &TableRef,
4996 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4997 let (fn_name, args) = primary
4998 .table_fn_call
4999 .as_deref()
5000 .expect("caller guards table_fn_call.is_some()");
5001 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5002 let ctx = EvalContext::new(&empty_schema, None);
5003 let dummy_row = Row::new(alloc::vec::Vec::new());
5004 let arg0: Option<Value<'static>> = match args.first() {
5005 Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5006 None => None,
5007 };
5008 match fn_name.as_str() {
5009 // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5010 // `…_recordset` (+ json_ variants). The row shape is the BASE
5011 // argument's declared type — a table's or a composite type's
5012 // column list — which only the catalog knows, so the parser hands
5013 // the raw arguments here rather than desugaring blind.
5014 "jsonb_populate_record"
5015 | "json_populate_record"
5016 | "jsonb_populate_recordset"
5017 | "json_populate_recordset" => {
5018 let type_name = match args.first() {
5019 Some(Expr::Cast {
5020 target: spg_sql::ast::CastTarget::Named(n),
5021 ..
5022 }) => n.clone(),
5023 _ => {
5024 return Err(EngineError::Unsupported(alloc::format!(
5025 "{fn_name}(): first argument must name a row type, \
5026 e.g. NULL::mytable"
5027 )));
5028 }
5029 };
5030 let cat = self.active_catalog();
5031 let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5032 t.schema().columns.clone()
5033 } else if let Some(c) = cat.composite_types().get(&type_name) {
5034 c.fields
5035 .iter()
5036 .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5037 .collect()
5038 } else {
5039 return Err(EngineError::Unsupported(alloc::format!(
5040 "type \"{type_name}\" does not exist"
5041 )));
5042 };
5043 let json_arg = match args.get(1) {
5044 Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5045 None => Value::Null,
5046 };
5047 // The set form iterates the JSON array; the scalar form is
5048 // the one-element case of the same walk.
5049 let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5050 crate::json::array_element_rows(&json_arg, false, fn_name)
5051 .map_err(EngineError::Eval)?
5052 .into_iter()
5053 .map(|s| s.map_or(Value::Null, Value::json))
5054 .collect()
5055 } else if matches!(json_arg, Value::Null) {
5056 alloc::vec::Vec::new()
5057 } else {
5058 alloc::vec![json_arg]
5059 };
5060 let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5061 for doc in &docs {
5062 let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5063 for c in &cols {
5064 // `->>` semantics: a missing key is NULL, present keys
5065 // arrive as text and cast to the declared column type.
5066 let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5067 .map_err(EngineError::Eval)?;
5068 let v = if matches!(raw, Value::Null) {
5069 Value::Null
5070 } else {
5071 crate::conversions::coerce_value(raw, c.ty, "", 0)
5072 .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5073 };
5074 vals.push(v);
5075 }
5076 rows.push(Row::new(vals));
5077 }
5078 Ok((rows, cols))
5079 }
5080 "pg_partition_tree" => {
5081 let cols = alloc::vec![
5082 ColumnSchema::new("relid".to_string(), DataType::Text, true),
5083 ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5084 ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5085 ColumnSchema::new("level".to_string(), DataType::Int, true),
5086 ];
5087 let Some(Value::Text(name)) = &arg0 else {
5088 // NULL (or missing) argument → zero rows (PG).
5089 return Ok((alloc::vec::Vec::new(), cols));
5090 };
5091 let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5092 if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5093 return Err(EngineError::Unsupported(alloc::format!(
5094 "relation \"{name}\" does not exist"
5095 )));
5096 }
5097 let rows = entries
5098 .into_iter()
5099 .map(|(relid, parent, isleaf, level)| {
5100 Row::new(alloc::vec![
5101 Value::text(relid),
5102 parent.map_or(Value::Null, Value::text),
5103 Value::Bool(isleaf),
5104 #[allow(clippy::cast_possible_truncation)]
5105 Value::Int(level as i32),
5106 ])
5107 })
5108 .collect();
5109 Ok((rows, cols))
5110 }
5111 "pg_partition_ancestors" => {
5112 let cols =
5113 alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5114 let Some(Value::Text(name)) = &arg0 else {
5115 return Ok((alloc::vec::Vec::new(), cols));
5116 };
5117 let cat = self.active_catalog();
5118 if cat.get(name.as_ref()).is_none() {
5119 return Err(EngineError::Unsupported(alloc::format!(
5120 "relation \"{name}\" does not exist"
5121 )));
5122 }
5123 // A relation outside any partition tree yields no rows (PG).
5124 let in_tree = cat
5125 .get(name.as_ref())
5126 .is_some_and(|t| t.schema().partition_role.is_some());
5127 let rows = if in_tree {
5128 crate::partition_walks::ancestors_of(cat, name.as_ref())
5129 .into_iter()
5130 .map(|n| Row::new(alloc::vec![Value::text(n)]))
5131 .collect()
5132 } else {
5133 alloc::vec::Vec::new()
5134 };
5135 Ok((rows, cols))
5136 }
5137 // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5138 // saw, what each token was called, which dictionary took it
5139 // and what came out. It is a projection of the same tokenizer
5140 // and the same map the indexer uses, so it cannot describe a
5141 // pipeline other than the one that runs.
5142 "ts_debug" => {
5143 use crate::fts::{TokenType, TsDict};
5144 let cols = alloc::vec![
5145 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5146 ColumnSchema::new("description".to_string(), DataType::Text, false),
5147 ColumnSchema::new("token".to_string(), DataType::Text, false),
5148 ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5149 ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5150 ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5151 ];
5152 // PG's one-arg form uses the session configuration; the
5153 // two-arg form names one.
5154 let (cfg_name, text) = match (&arg0, args.get(1)) {
5155 (Some(Value::Text(c)), Some(t)) => {
5156 let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5157 (c.to_string(), crate::eval::value_to_text(&v))
5158 }
5159 (Some(v), None) => (
5160 alloc::string::String::from("english"),
5161 crate::eval::value_to_text(v),
5162 ),
5163 _ => return Ok((alloc::vec::Vec::new(), cols)),
5164 };
5165 let english = match cfg_name
5166 .trim()
5167 .trim_start_matches("pg_catalog.")
5168 .to_ascii_lowercase()
5169 .as_str()
5170 {
5171 "english" => true,
5172 "simple" => false,
5173 other => {
5174 return Err(EngineError::Unsupported(alloc::format!(
5175 "text search configuration \"{other}\" does not exist"
5176 )));
5177 }
5178 };
5179 let rows = crate::fts::tokenize_typed(&text)
5180 .into_iter()
5181 .map(|tok| {
5182 let dict = tok.ty.dictionary(english);
5183 let dname = dict.map(|d| match d {
5184 TsDict::Simple => "simple",
5185 TsDict::EnglishStem => "english_stem",
5186 });
5187 let folded = tok.text.to_lowercase();
5188 let lexemes = dict.map(|d| match d {
5189 TsDict::Simple => alloc::vec![Some(folded.clone())],
5190 TsDict::EnglishStem => {
5191 if crate::fts::is_english_stopword(&folded) {
5192 alloc::vec::Vec::new()
5193 } else {
5194 alloc::vec![Some(crate::fts::porter_stem(&folded))]
5195 }
5196 }
5197 });
5198 Row::new(alloc::vec![
5199 Value::text(tok.ty.alias()),
5200 Value::text(tok.ty.description()),
5201 Value::text(tok.text),
5202 Value::TextArray(
5203 dname
5204 .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5205 .unwrap_or_default(),
5206 ),
5207 dname.map_or(Value::Null, Value::text),
5208 lexemes.map_or(Value::Null, Value::TextArray),
5209 ])
5210 })
5211 .collect();
5212 let _ = TokenType::AsciiWord;
5213 Ok((rows, cols))
5214 }
5215 // v7.39 (round 651) — `ts_token_type('default')`, the list the
5216 // parser actually produces. It is a projection of the
5217 // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5218 // read, so the three cannot disagree about what a token is.
5219 "ts_token_type" => {
5220 use crate::fts::TokenType as T;
5221 let cols = alloc::vec![
5222 ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5223 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5224 ColumnSchema::new("description".to_string(), DataType::Text, false),
5225 ];
5226 // PG takes the parser by name or oid; SPG has the one.
5227 if let Some(Value::Text(p)) = &arg0
5228 && !p.eq_ignore_ascii_case("default")
5229 && !p.eq_ignore_ascii_case("pg_catalog.default")
5230 {
5231 return Err(EngineError::Unsupported(alloc::format!(
5232 "text search parser \"{p}\" does not exist"
5233 )));
5234 }
5235 const TYPES: &[T] = &[
5236 T::AsciiWord,
5237 T::Word,
5238 T::NumWord,
5239 T::Email,
5240 T::Url,
5241 T::Host,
5242 T::SFloat,
5243 T::Version,
5244 T::HwordNumPart,
5245 T::HwordPart,
5246 T::HwordAsciiPart,
5247 T::Blank,
5248 T::Tag,
5249 T::Protocol,
5250 T::NumHword,
5251 T::AsciiHword,
5252 T::Hword,
5253 T::UrlPath,
5254 T::File,
5255 T::Float,
5256 T::Int,
5257 T::Uint,
5258 T::Entity,
5259 ];
5260 let rows = TYPES
5261 .iter()
5262 .map(|t| {
5263 Row::new(alloc::vec![
5264 Value::Int(*t as i32),
5265 Value::text(t.alias()),
5266 Value::text(t.description()),
5267 ])
5268 })
5269 .collect();
5270 Ok((rows, cols))
5271 }
5272 // v7.39 (read01 round 65) — a set-returning USER function in FROM
5273 // (`FROM rows_of(2)`). Its body runs through the real executor, like
5274 // every other function body since round 63.
5275 other => {
5276 if !self.active_catalog().functions_named(other).is_empty() {
5277 return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5278 }
5279 Err(EngineError::Unsupported(alloc::format!(
5280 "table function {other}() is not supported in FROM"
5281 )))
5282 }
5283 }
5284 }
5285
5286 /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5287 /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5288 /// are bound into it as literals and it goes through the read path, so the
5289 /// rows it yields are exactly the rows a hand-written query would see.
5290 ///
5291 /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5292 /// text)` names them, and a `SETOF <scalar>` yields a single column named
5293 /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5294 /// shows.
5295 fn exec_setof_user_function(
5296 &self,
5297 name: &str,
5298 args: &[spg_sql::ast::Expr],
5299 // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5300 // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5301 alias: Option<&str>,
5302 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5303 // The call's arguments belong to the ENCLOSING query, so they are
5304 // evaluated here and the body sees values.
5305 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5306 let arg_ctx = self.ev_ctx(&empty, None);
5307 let dummy = Row::new(alloc::vec::Vec::new());
5308 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5309 for a in args {
5310 vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5311 }
5312 self.setof_rows_of(name, &vals, alias)
5313 }
5314
5315 /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5316 /// arguments. Shared by the FROM position and the target-list expansion, so
5317 /// a function cannot behave differently depending on where it is called.
5318 pub(crate) fn setof_rows_of(
5319 &self,
5320 name: &str,
5321 arg_values: &[Value<'static>],
5322 alias: Option<&str>,
5323 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5324 let cat = self.active_catalog();
5325 let overloads = cat.functions_named(name);
5326 let def = overloads
5327 .iter()
5328 .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5329 .ok_or_else(|| {
5330 EngineError::Unsupported(alloc::format!(
5331 "function {name} does not exist with {} argument(s)",
5332 arg_values.len()
5333 ))
5334 })?;
5335 let declared = def.returns.trim().to_string();
5336 let upper = declared.to_ascii_uppercase();
5337 if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5338 return Err(EngineError::Unsupported(alloc::format!(
5339 "function {name}() does not return a set — it cannot be used in FROM"
5340 )));
5341 }
5342
5343 let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5344 // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5345 // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5346 if def.language.eq_ignore_ascii_case("plpgsql") {
5347 let out_rows = self
5348 .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5349 .map_err(EngineError::Eval)?;
5350 let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5351 let rows = out_rows.into_iter().map(Row::new).collect();
5352 return Ok((rows, cols));
5353 }
5354 let body = def.body.trim().trim_end_matches(';');
5355 let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5356 EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5357 })?;
5358 let spg_sql::ast::Statement::Select(body_select) = stmt else {
5359 return Err(EngineError::Unsupported(alloc::format!(
5360 "function {name}(): a set-returning body must be a SELECT"
5361 )));
5362 };
5363 let arg_names = spg_storage::function_arg_names(&def.args_repr);
5364 let bound = crate::eval::bind_user_fn_args(
5365 self.active_catalog(),
5366 &body_select,
5367 &arg_names,
5368 arg_values,
5369 )
5370 .map_err(EngineError::Eval)?;
5371 let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5372 let QueryResult::Rows { columns, rows } = out else {
5373 return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5374 };
5375 // Name the columns from the DECLARED shape — the same rule the plpgsql
5376 // path above uses, so a body's language cannot change the row shape.
5377 let cols = setof_column_shape_from(&declared, name, alias, &columns);
5378 Ok((rows, cols))
5379 }
5380
5381 fn exec_select_jsonb_each_text(
5382 &self,
5383 stmt: &SelectStatement,
5384 primary: &TableRef,
5385 cancel: CancelToken<'_>,
5386 ) -> Result<QueryResult, EngineError> {
5387 let (each_fn, arg_expr) = primary
5388 .jsonb_each_text_arg
5389 .as_ref()
5390 .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5391 .expect("caller guards jsonb_each_text_arg.is_some()");
5392 // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5393 // forms keep JSON rendering in the value column (JSON null
5394 // stays jsonb 'null', strings keep their quotes).
5395 let as_text = each_fn.ends_with("_text");
5396 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5397 let ctx = EvalContext::new(&empty_schema, None);
5398 let dummy_row = Row::new(alloc::vec::Vec::new());
5399 let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5400 let pairs =
5401 crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5402 let rows: alloc::vec::Vec<Row<'static>> = pairs
5403 .into_iter()
5404 .map(|(k, v)| {
5405 let key_val = Value::text(k);
5406 let value_val = match v {
5407 Some(s) if as_text => Value::text(s),
5408 Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5409 None => Value::Null,
5410 };
5411 Row::new(alloc::vec![key_val, value_val])
5412 })
5413 .collect();
5414 let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5415 let value_dtype = if as_text {
5416 spg_storage::DataType::Text
5417 } else {
5418 spg_storage::DataType::Json
5419 };
5420 let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5421 let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5422 let mut schema_cols = alloc::vec![key_col, value_col];
5423 // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5424 // LATERAL-position form of the same call already honours it.
5425 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5426 if let Some(col) = schema_cols.get_mut(i) {
5427 col.name = new_name.clone();
5428 }
5429 }
5430 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5431 // `EvalContext::new` drops it and every catalog-dependent cast
5432 // (regclass / enum / composite / domain) silently degrades.
5433 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5434 // WHERE.
5435 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5436 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5437 for row in rows {
5438 cancel.check()?;
5439 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5440 if matches!(v, Value::Bool(true)) {
5441 out.push(row);
5442 }
5443 }
5444 out
5445 } else {
5446 rows
5447 };
5448 // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5449 if aggregate::uses_aggregate(stmt) {
5450 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5451 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5452 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5453 .map_err(|err| match err {
5454 EngineError::Eval(ev) => ev,
5455 other => eval::EvalError::TypeMismatch {
5456 detail: alloc::format!("{other}"),
5457 },
5458 })
5459 };
5460 // v7.39 (round 656) — hand the rows over as they are rather than
5461 // collecting a second vector of `RowRef` wrappers. Note this is
5462 // a set-returning-function path, NOT the relational scan: the
5463 // measured O(rows) cost lived in `run_single_table_aggregate`,
5464 // and converting these four first was a miss that cost a full
5465 // round — every test stayed green and the number did not move.
5466 let agg = aggregate::run(
5467 stmt,
5468 crate::join::AggRows::Owned(&filtered),
5469 &schema_cols,
5470 Some(&alias),
5471 Some(&agg_correlated),
5472 self.parallel_runner.0.as_deref(),
5473 Some(self.active_catalog()),
5474 Some(self),
5475 )?;
5476 return self.finish_agg_result(agg, stmt, cancel);
5477 }
5478 // Projection.
5479 let projection =
5480 build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
5481 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5482 alloc::vec::Vec::with_capacity(filtered.len());
5483 for row in &filtered {
5484 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5485 for p in &projection {
5486 let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5487 vals.push(v);
5488 }
5489 projected_rows.push(Row::new(vals));
5490 }
5491 let columns: alloc::vec::Vec<ColumnSchema> = projection
5492 .iter()
5493 // v7.39 (read01 round 54) — keep the column's enum identity through
5494 // the projection (it lives outside the DataType lattice), or a
5495 // derived table / UNION / windowed result forgets it and any outer
5496 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5497 .map(|p| {
5498 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5499 c.user_enum_type = p.user_enum_type.clone();
5500 c.mysql_fsp = p.mysql_fsp;
5501 c
5502 })
5503 .collect();
5504 // ORDER BY.
5505 if !stmt.order_by.is_empty() {
5506 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5507 .iter()
5508 .enumerate()
5509 .map(|(i, r)| -> Result<_, EngineError> {
5510 let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5511 .order_by
5512 .iter()
5513 .map(|ob| {
5514 eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5515 })
5516 .collect();
5517 Ok((i, keys?))
5518 })
5519 .collect::<Result<_, _>>()?;
5520 indexed.sort_by(|a, b| {
5521 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5522 let o = &stmt.order_by[idx];
5523 let cmp = order_by_value_cmp_in(
5524 o.desc,
5525 o.nulls_first,
5526 ka,
5527 kb,
5528 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5529 );
5530 if cmp != core::cmp::Ordering::Equal {
5531 return cmp;
5532 }
5533 }
5534 core::cmp::Ordering::Equal
5535 });
5536 projected_rows = indexed
5537 .into_iter()
5538 .map(|(i, _)| projected_rows[i].clone())
5539 .collect();
5540 }
5541 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5542 if stmt.distinct {
5543 projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
5544 }
5545 if let Some(offset) = stmt.offset_literal() {
5546 let off = (offset as usize).min(projected_rows.len());
5547 projected_rows.drain(..off);
5548 }
5549 if let Some(limit) = stmt.limit_literal() {
5550 projected_rows.truncate(limit as usize);
5551 }
5552 Ok(QueryResult::Rows {
5553 columns,
5554 rows: projected_rows,
5555 })
5556 }
5557
5558 /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5559 /// ( SELECT … ) alias` in primary position. The inner SELECT
5560 /// materialises once through the regular bare-select executor
5561 /// (UNION tails included), then the outer WHERE / aggregate /
5562 /// projection / ORDER BY / LIMIT pipeline runs over the
5563 /// synthetic table — the same post-materialisation shape as
5564 /// exec_select_jsonb_each_text, generalised to N columns.
5565 fn exec_select_derived(
5566 &self,
5567 stmt: &SelectStatement,
5568 primary: &TableRef,
5569 cancel: CancelToken<'_>,
5570 ) -> Result<QueryResult, EngineError> {
5571 let inner = primary
5572 .lateral_subquery
5573 .as_deref()
5574 .expect("caller guards lateral_subquery.is_some()");
5575 // exec_select_cancel is the union-aware wrapper — the inner
5576 // SELECT may carry UNION tails on stmt.unions.
5577 let QueryResult::Rows {
5578 columns: inner_cols,
5579 rows,
5580 } = self.exec_select_cancel(inner, cancel)?
5581 else {
5582 return Err(EngineError::Unsupported(
5583 "derived table subquery must return rows".into(),
5584 ));
5585 };
5586 let alias = primary
5587 .alias
5588 .clone()
5589 .unwrap_or_else(|| primary.name.clone());
5590 // `AS t(a, b)` renames the materialised columns positionally
5591 // (extra inner columns keep their own names, PG behaviour).
5592 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5593 // v7.39 (read01 round 78) — a column-alias list longer than the item is
5594 // the error PG reports; SPG used to let the extra names through and then
5595 // fail two layers downstream with "column not found: <the extra name>".
5596 let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5597 if primary.unnest_column_aliases.len() > n_out {
5598 return Err(EngineError::Unsupported(alloc::format!(
5599 "table \"{alias}\" has {n_out} columns available but {} columns specified",
5600 primary.unnest_column_aliases.len()
5601 )));
5602 }
5603 if primary.scalar_fn_item && schema_cols.len() == 1 {
5604 schema_cols[0].scalar_row_source = true;
5605 }
5606 // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5607 // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5608 // The column-alias list, if given, names it like any other column.
5609 let mut rows = rows;
5610 if primary.with_ordinality {
5611 schema_cols.push(ColumnSchema::new(
5612 "ordinality".to_string(),
5613 DataType::BigInt,
5614 false,
5615 ));
5616 rows = rows
5617 .into_iter()
5618 .enumerate()
5619 .map(|(i, r)| {
5620 let mut v = r.values;
5621 #[allow(clippy::cast_possible_wrap)]
5622 v.push(Value::BigInt(i as i64 + 1));
5623 Row::new(v)
5624 })
5625 .collect();
5626 }
5627 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5628 if let Some(col) = schema_cols.get_mut(i) {
5629 col.name = new_name.clone();
5630 }
5631 }
5632 self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5633 }
5634
5635 /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5636 /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5637 /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5638 /// derived-table executor and the FROM-position table functions.
5639 fn exec_select_over_rows(
5640 &self,
5641 stmt: &SelectStatement,
5642 rows: alloc::vec::Vec<Row<'static>>,
5643 schema_cols: alloc::vec::Vec<ColumnSchema>,
5644 alias: &str,
5645 cancel: CancelToken<'_>,
5646 ) -> Result<QueryResult, EngineError> {
5647 let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5648 // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5649 // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5650 // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5651 // (the same path the aggregate branch uses); the old plain eval_expr let
5652 // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5653 let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5654 // WHERE.
5655 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5656 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5657 for row in rows {
5658 cancel.check()?;
5659 let v = self.eval_expr_with_correlated(
5660 w,
5661 &row,
5662 &scan_ctx,
5663 cancel,
5664 Some(&mut corr_memo.borrow_mut()),
5665 )?;
5666 if matches!(v, Value::Bool(true)) {
5667 out.push(row);
5668 }
5669 }
5670 out
5671 } else {
5672 rows
5673 };
5674 // Aggregate dispatch.
5675 if aggregate::uses_aggregate(stmt) {
5676 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5677 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5678 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5679 .map_err(|err| match err {
5680 EngineError::Eval(ev) => ev,
5681 other => eval::EvalError::TypeMismatch {
5682 detail: alloc::format!("{other}"),
5683 },
5684 })
5685 };
5686 // v7.39 (round 656) — hand the rows over as they are rather than
5687 // collecting a second vector of `RowRef` wrappers. Note this is
5688 // a set-returning-function path, NOT the relational scan: the
5689 // measured O(rows) cost lived in `run_single_table_aggregate`,
5690 // and converting these four first was a miss that cost a full
5691 // round — every test stayed green and the number did not move.
5692 let agg = aggregate::run(
5693 stmt,
5694 crate::join::AggRows::Owned(&filtered),
5695 &schema_cols,
5696 Some(alias),
5697 Some(&agg_correlated),
5698 self.parallel_runner.0.as_deref(),
5699 Some(self.active_catalog()),
5700 Some(self),
5701 )?;
5702 return self.finish_agg_result(agg, stmt, cancel);
5703 }
5704 // Projection.
5705 let projection =
5706 build_projection(&stmt.items, &schema_cols, alias, self.backslash_escapes)?;
5707 // v7.39 (round 621) — a target-list SRF expands here too. This tail
5708 // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5709 // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5710 // answered `function unnest(integer[]) does not exist` for a query PG
5711 // answers.
5712 let srf_idxs = self.srf_target_idxs(&projection);
5713 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5714 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5715 alloc::vec::Vec::with_capacity(filtered.len());
5716 if !srf_idxs.is_empty() {
5717 let (rows, src) =
5718 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5719 projected_rows = rows;
5720 src_of_row = src;
5721 } else {
5722 for row in &filtered {
5723 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5724 for p in &projection {
5725 let v = self.eval_expr_with_correlated(
5726 &p.expr,
5727 row,
5728 &scan_ctx,
5729 cancel,
5730 Some(&mut corr_memo.borrow_mut()),
5731 )?;
5732 vals.push(v);
5733 }
5734 projected_rows.push(Row::new(vals));
5735 }
5736 }
5737 let columns: alloc::vec::Vec<ColumnSchema> = projection
5738 .iter()
5739 // v7.39 (read01 round 54) — keep the column's enum identity through
5740 // the projection (it lives outside the DataType lattice), or a
5741 // derived table / UNION / windowed result forgets it and any outer
5742 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5743 .map(|p| {
5744 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5745 c.user_enum_type = p.user_enum_type.clone();
5746 c.mysql_fsp = p.mysql_fsp;
5747 c
5748 })
5749 .collect();
5750 // ORDER BY over the source rows (same shape as the other
5751 // synthetic-table executors).
5752 // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
5753 // OUTPUT column. Evaluated as an expression, as it was here, the literal
5754 // `1` is just the constant 1: the same sort key for every row, so the
5755 // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
5756 // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
5757 // landing on this executor) came back in input order.
5758 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
5759 if !order_by.is_empty() {
5760 // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
5761 // SRF makes more of them than there were inputs.
5762 let out_cols = if srf_idxs.is_empty() {
5763 alloc::vec![None; order_by.len()]
5764 } else {
5765 srf_order_output_cols(&order_by, &projection)
5766 };
5767 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
5768 .iter()
5769 .enumerate()
5770 .map(|(k, out)| -> Result<_, EngineError> {
5771 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
5772 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
5773 .iter()
5774 .zip(out_cols.iter())
5775 .map(|(ob, oc)| {
5776 // v7.39 (read01 round 54) — this path builds its
5777 // sort keys itself instead of going through
5778 // `build_order_keys`, so it skipped the enum-ordinal
5779 // substitution: an OUTER `ORDER BY <enum col>` over
5780 // a DERIVED TABLE sorted by the label TEXT, not by
5781 // member order. Silently wrong rows, not an error.
5782 let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
5783 Ok(
5784 match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
5785 Some(ord) => Value::Float(ord),
5786 None => v,
5787 },
5788 )
5789 })
5790 .collect();
5791 Ok((k, keys?))
5792 })
5793 .collect::<Result<_, _>>()?;
5794 indexed.sort_by(|a, b| {
5795 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5796 let o = &stmt.order_by[idx];
5797 let cmp = order_by_value_cmp_in(
5798 o.desc,
5799 o.nulls_first,
5800 ka,
5801 kb,
5802 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5803 );
5804 if cmp != core::cmp::Ordering::Equal {
5805 return cmp;
5806 }
5807 }
5808 core::cmp::Ordering::Equal
5809 });
5810 projected_rows = indexed
5811 .into_iter()
5812 .map(|(i, _)| projected_rows[i].clone())
5813 .collect();
5814 }
5815 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5816 if stmt.distinct {
5817 projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
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 /// Constant `SELECT` with no FROM: evaluate each projection item
5833 /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
5834 fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
5835 let empty_schema: Vec<ColumnSchema> = Vec::new();
5836 let ctx = self.ev_ctx(&empty_schema, None);
5837 // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
5838 // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
5839 // `SELECT string_agg('x',',')` → x). Before this it fell through to the
5840 // scalar projection, where the aggregate name looked like an unknown
5841 // function. The WHERE filters that one row, so `… WHERE false` leaves
5842 // the aggregate zero input rows (`count(*)` → 0).
5843 if aggregate::uses_aggregate(stmt) {
5844 let dummy = Row::new(Vec::new());
5845 let passes = match &stmt.where_ {
5846 Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
5847 None => true,
5848 };
5849 let rows: Vec<RowRef<'_>> = if passes {
5850 alloc::vec![RowRef::Owned(&dummy)]
5851 } else {
5852 Vec::new()
5853 };
5854 let agg = aggregate::run(
5855 stmt,
5856 crate::join::AggRows::Refs(&rows),
5857 &empty_schema,
5858 None,
5859 None,
5860 self.parallel_runner.0.as_deref(),
5861 Some(self.active_catalog()),
5862 Some(self),
5863 )?;
5864 return self.finish_agg_result(agg, stmt, CancelToken::none());
5865 }
5866 let projection = build_projection(&stmt.items, &empty_schema, "", self.backslash_escapes)?;
5867 // `SELECT … WHERE cond` with no FROM — the one conceptual
5868 // row survives only when the condition is true (previously
5869 // the WHERE was silently ignored: `SELECT 1 WHERE false`
5870 // returned a row).
5871 let dummy_row = Row::new(Vec::new());
5872 if let Some(w) = &stmt.where_ {
5873 let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
5874 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
5875 let columns: Vec<ColumnSchema> = projection
5876 .into_iter()
5877 .map(|p| {
5878 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5879 c.user_enum_type = p.user_enum_type;
5880 c.collation_name = p.collation_name;
5881 c.mysql_fsp = p.mysql_fsp;
5882 c
5883 })
5884 .collect();
5885 return Ok(QueryResult::Rows {
5886 columns,
5887 rows: Vec::new(),
5888 });
5889 }
5890 }
5891 // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
5892 // into a FROM item (regexp_matches, whose rows are arrays and so cannot
5893 // desugar to unnest) expands here: one output row per SRF row, sibling
5894 // scalar columns repeated. unnest / array_elements / path_query reach a
5895 // real FROM via the parser rewrite and never land here.
5896 // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
5897 let srf_idxs = self.srf_target_idxs(&projection);
5898 if !srf_idxs.is_empty() {
5899 let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
5900 let columns: Vec<ColumnSchema> = projection
5901 .into_iter()
5902 .map(|p| {
5903 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5904 c.user_enum_type = p.user_enum_type;
5905 c.collation_name = p.collation_name;
5906 c.mysql_fsp = p.mysql_fsp;
5907 c
5908 })
5909 .collect();
5910 // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
5911 // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
5912 // to. This returned straight out of the expansion, so
5913 // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
5914 // input order — the sort was not wrong, it never ran. (There is
5915 // exactly one conceptual input row here, which is why the ordinary
5916 // scan pipeline is not on this path at all.)
5917 if !stmt.order_by.is_empty() {
5918 let synth_ctx =
5919 EvalContext::new(&columns, None).with_catalog(self.active_catalog());
5920 let resolved: Vec<spg_sql::ast::OrderBy> = stmt
5921 .order_by
5922 .iter()
5923 .map(|o| {
5924 let mut o = o.clone();
5925 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
5926 && *n >= 1
5927 && let Ok(idx) = usize::try_from(*n - 1)
5928 && idx < columns.len()
5929 {
5930 o.expr = Expr::Column(spg_sql::ast::ColumnName {
5931 qualifier: None,
5932 name: columns[idx].name.clone(),
5933 });
5934 }
5935 o
5936 })
5937 .collect();
5938 let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
5939 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
5940 for r in rows {
5941 let keys = build_order_keys(&resolved, &r, &synth_ctx)?;
5942 tagged.push((keys, r));
5943 }
5944 sort_by_keys(&mut tagged, &descs);
5945 rows = tagged.into_iter().map(|(_, r)| r).collect();
5946 }
5947 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
5948 return Ok(QueryResult::Rows { columns, rows });
5949 }
5950 let mut values = Vec::with_capacity(projection.len());
5951 for p in &projection {
5952 values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
5953 }
5954 let columns: Vec<ColumnSchema> = projection
5955 .into_iter()
5956 .map(|p| {
5957 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5958 c.user_enum_type = p.user_enum_type;
5959 c.collation_name = p.collation_name;
5960 c.mysql_fsp = p.mysql_fsp;
5961 c
5962 })
5963 .collect();
5964 // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
5965 // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
5966 // returns none. (The SRF and aggregate arms above already applied
5967 // them; this tail was the one that didn't.)
5968 let mut rows = alloc::vec![Row::new(values)];
5969 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
5970 Ok(QueryResult::Rows { columns, rows })
5971 }
5972
5973 /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
5974 /// circuit. Catches
5975 /// SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
5976 /// BEFORE `resolve_select_subqueries` materialises the inner result
5977 /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
5978 /// values into a `HashSet<i64>` directly, then probes A.pk per
5979 /// HashSet entry and tallies. Saves the Expr-literal roundtrip
5980 /// (~150 µs / query at INSUBQ benchmark scale).
5981 pub(crate) fn try_count_star_pk_in_subquery_fast(
5982 &self,
5983 stmt: &SelectStatement,
5984 cancel: CancelToken<'_>,
5985 ) -> Result<Option<QueryResult>, EngineError> {
5986 use spg_sql::ast::SelectItem;
5987 if stmt.distinct
5988 || stmt.limit_with_ties
5989 || stmt.group_by.is_some()
5990 || stmt.having.is_some()
5991 || !stmt.unions.is_empty()
5992 || !stmt.order_by.is_empty()
5993 || stmt.limit.is_some()
5994 || stmt.offset.is_some()
5995 || stmt.items.len() != 1
5996 {
5997 return Ok(None);
5998 }
5999 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6000 return Ok(None);
6001 };
6002 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6003 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6004 if !is_count_star {
6005 return Ok(None);
6006 }
6007 let Some(from) = stmt.from.as_ref() else {
6008 return Ok(None);
6009 };
6010 if !from.joins.is_empty()
6011 || from.primary.lateral_subquery.is_some()
6012 || from.primary.unnest_expr.is_some()
6013 || from.primary.generate_series_args.is_some()
6014 || from.primary.table_fn_call.is_some()
6015 || from.primary.as_of_segment.is_some()
6016 {
6017 return Ok(None);
6018 }
6019 let Some(where_expr) = stmt.where_.as_ref() else {
6020 return Ok(None);
6021 };
6022 // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6023 // negated=false; no other predicates.
6024 let Expr::InSubquery {
6025 expr: col_expr,
6026 subquery,
6027 negated: false,
6028 } = where_expr
6029 else {
6030 return Ok(None);
6031 };
6032 let Expr::Column(c) = col_expr.as_ref() else {
6033 return Ok(None);
6034 };
6035 let outer_alias = from
6036 .primary
6037 .alias
6038 .as_deref()
6039 .unwrap_or(from.primary.name.as_str());
6040 if let Some(q) = c.qualifier.as_deref()
6041 && !q.eq_ignore_ascii_case(outer_alias)
6042 {
6043 return Ok(None);
6044 }
6045 // Outer column must be a single-column PK on integer family.
6046 let catalog = self.active_catalog();
6047 let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6048 return Ok(None);
6049 };
6050 let outer_schema = outer_table.schema();
6051 let Some(outer_pos) = outer_schema
6052 .columns
6053 .iter()
6054 .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6055 else {
6056 return Ok(None);
6057 };
6058 if !matches!(
6059 outer_schema.columns[outer_pos].ty,
6060 spg_storage::DataType::BigInt
6061 | spg_storage::DataType::Int
6062 | spg_storage::DataType::SmallInt
6063 ) {
6064 return Ok(None);
6065 }
6066 if !outer_schema
6067 .uniqueness_constraints
6068 .iter()
6069 .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6070 {
6071 return Ok(None);
6072 }
6073 let Some(idx) = outer_table.index_on(outer_pos) else {
6074 return Ok(None);
6075 };
6076 // Inner must be uncorrelated. The cheap-correlation pre-check
6077 // exists upstream; here we just attempt the bare exec.
6078 if crate::subquery::select_is_correlated(subquery) {
6079 return Ok(None);
6080 }
6081 let mut inner = (**subquery).clone();
6082 self.resolve_select_subqueries(&mut inner, cancel)?;
6083 let r = match self.exec_bare_select_cancel(&inner, cancel) {
6084 Ok(r) => r,
6085 Err(_) => return Ok(None),
6086 };
6087 let QueryResult::Rows { columns, rows, .. } = r else {
6088 return Ok(None);
6089 };
6090 if columns.len() != 1 {
6091 return Ok(None);
6092 }
6093 // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6094 // subquery projects a column known to be UNIQUE/PK on its table
6095 // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6096 // in `tbl.uniqueness_constraints`), survivor values are
6097 // guaranteed distinct and the per-survivor `HashSet::insert`
6098 // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6099 //
6100 // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6101 // projection that is a bare Column ref, table-column lookup in
6102 // catalog confirms the column appears as a unique constraint's
6103 // sole member. UNIQUE NOT NULL is required — a nullable unique
6104 // column may have multiple NULLs, but NULLs are already skipped
6105 // above (`Value::Null => continue`), so a UNIQUE-only column is
6106 // still safe to dedup-skip.
6107 let inner_unique = (|| -> bool {
6108 if inner.distinct
6109 || inner.group_by.is_some()
6110 || !inner.unions.is_empty()
6111 || inner.having.is_some()
6112 || inner.items.len() != 1
6113 {
6114 return false;
6115 }
6116 let Some(inner_from) = inner.from.as_ref() else {
6117 return false;
6118 };
6119 if !inner_from.joins.is_empty()
6120 || inner_from.primary.lateral_subquery.is_some()
6121 || inner_from.primary.unnest_expr.is_some()
6122 || inner_from.primary.generate_series_args.is_some()
6123 || inner_from.primary.table_fn_call.is_some()
6124 {
6125 return false;
6126 }
6127 let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6128 return false;
6129 };
6130 let Expr::Column(pc) = proj else {
6131 return false;
6132 };
6133 let inner_alias = inner_from
6134 .primary
6135 .alias
6136 .as_deref()
6137 .unwrap_or(inner_from.primary.name.as_str());
6138 if let Some(q) = pc.qualifier.as_deref()
6139 && !q.eq_ignore_ascii_case(inner_alias)
6140 {
6141 return false;
6142 }
6143 let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6144 return false;
6145 };
6146 let isch = inner_table.schema();
6147 let Some(ipos) = isch
6148 .columns
6149 .iter()
6150 .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6151 else {
6152 return false;
6153 };
6154 isch.uniqueness_constraints
6155 .iter()
6156 .any(|u| u.columns.as_slice() == [ipos])
6157 })();
6158 // Collect inner i64 values directly into a HashSet, then probe.
6159 let mut count: i64 = 0;
6160 let mut probed = if inner_unique {
6161 hashbrown::HashSet::<i64>::new()
6162 } else {
6163 hashbrown::HashSet::<i64>::with_capacity(rows.len())
6164 };
6165 for row in &rows {
6166 let v = row.values.first().cloned().unwrap_or(Value::Null);
6167 let n = match v {
6168 Value::BigInt(n) => n,
6169 Value::Int(n) => i64::from(n),
6170 Value::SmallInt(n) => i64::from(n),
6171 Value::Null => continue,
6172 _ => return Ok(None),
6173 };
6174 // De-duplicate inner key set so a duplicate inner value
6175 // doesn't double-count the same outer row. Skipped when
6176 // the inner projection is statically unique.
6177 if !inner_unique && !probed.insert(n) {
6178 continue;
6179 }
6180 // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6181 // the `IndexKey::from_value` enum-dispatch and the per-call
6182 // `IndexKey` wrapper construction. The outer column is
6183 // already gated to integer-family above, so an i64 key
6184 // always corresponds to a valid PK lookup.
6185 if !idx.lookup_eq_i64(n).is_empty() {
6186 count += 1;
6187 }
6188 }
6189 let columns_out = alloc::vec![ColumnSchema::new(
6190 "count".to_string(),
6191 spg_storage::DataType::BigInt,
6192 false,
6193 )];
6194 let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6195 Ok(Some(QueryResult::Rows {
6196 columns: columns_out,
6197 rows: rows_out,
6198 }))
6199 }
6200
6201 /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6202 /// SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6203 /// (the post-subquery-replacement shape of the INSUBQ probe
6204 /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6205 /// The general aggregate path materialises every seeked row into
6206 /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6207 /// For COUNT(*) we only care how many keys hit; iterate the list
6208 /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6209 /// row materialisation, the aggregate state machine, and the per-
6210 /// row WHERE re-eval (the seek already filtered by the same list).
6211 /// Returns `None` when the shape doesn't match.
6212 fn try_count_star_pk_in_list_fast(
6213 &self,
6214 stmt: &SelectStatement,
6215 table: &spg_storage::Table,
6216 schema_cols: &[ColumnSchema],
6217 alias: &str,
6218 ) -> Option<QueryResult> {
6219 use spg_sql::ast::{ColumnName, SelectItem};
6220 // Gates on the SELECT shape.
6221 if stmt.distinct
6222 || stmt.limit_with_ties
6223 || stmt.group_by.is_some()
6224 || stmt.having.is_some()
6225 || !stmt.unions.is_empty()
6226 || !stmt.order_by.is_empty()
6227 || stmt.limit.is_some()
6228 || stmt.offset.is_some()
6229 || stmt.items.len() != 1
6230 {
6231 return None;
6232 }
6233 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6234 return None;
6235 };
6236 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6237 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6238 if !is_count_star {
6239 return None;
6240 }
6241 // WHERE must be `<col> IN (literal list)` with no other
6242 // conjuncts (the seek result is a true subset of the row
6243 // population for this predicate).
6244 let where_expr = stmt.where_.as_ref()?;
6245 let Expr::InList {
6246 expr: col_expr,
6247 list,
6248 negated: false,
6249 } = where_expr
6250 else {
6251 return None;
6252 };
6253 let Expr::Column(c) = col_expr.as_ref() else {
6254 return None;
6255 };
6256 if let Some(q) = c.qualifier.as_deref()
6257 && !q.eq_ignore_ascii_case(alias)
6258 {
6259 return None;
6260 }
6261 let col_pos = schema_cols
6262 .iter()
6263 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6264 // The column must be a single-column PK on an integer family
6265 // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6266 // so the antiset stays collision-free under `HashSet<i64>`.
6267 let schema = table.schema();
6268 if !matches!(
6269 schema.columns[col_pos].ty,
6270 spg_storage::DataType::BigInt
6271 | spg_storage::DataType::Int
6272 | spg_storage::DataType::SmallInt
6273 ) {
6274 return None;
6275 }
6276 if !schema
6277 .uniqueness_constraints
6278 .iter()
6279 .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6280 {
6281 return None;
6282 }
6283 let idx = table.index_on(col_pos)?;
6284 // Tally non-empty seek results across all literal values.
6285 let mut count: i64 = 0;
6286 for lit in list {
6287 let Expr::Literal(l) = lit else {
6288 return None;
6289 };
6290 let v = eval::literal_to_value(l);
6291 let key = spg_storage::IndexKey::from_value(&v)?;
6292 if !idx.lookup_eq(&key).is_empty() {
6293 count += 1;
6294 }
6295 }
6296 let columns = alloc::vec![ColumnSchema::new(
6297 "count".to_string(),
6298 spg_storage::DataType::BigInt,
6299 false,
6300 )];
6301 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6302 let _ = ColumnName {
6303 qualifier: None,
6304 name: String::new(),
6305 };
6306 Some(QueryResult::Rows { columns, rows })
6307 }
6308
6309 /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6310 /// BETWEEN a AND b` on an indexed column. The index range walk yields
6311 /// exactly the matching (visible) rows, so we count locators directly —
6312 /// skipping the row materialisation, the aggregate state machine, and the
6313 /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6314 /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6315 /// when the shape doesn't match.
6316 fn try_count_star_indexed_range_fast(
6317 &self,
6318 stmt: &SelectStatement,
6319 table: &spg_storage::Table,
6320 schema_cols: &[ColumnSchema],
6321 alias: &str,
6322 snapshot: &spg_storage::snapshot::Snapshot,
6323 ) -> Option<QueryResult> {
6324 use spg_sql::ast::SelectItem;
6325 if stmt.distinct
6326 || stmt.limit_with_ties
6327 || stmt.group_by.is_some()
6328 || stmt.having.is_some()
6329 || !stmt.unions.is_empty()
6330 || !stmt.order_by.is_empty()
6331 || stmt.limit.is_some()
6332 || stmt.offset.is_some()
6333 || stmt.items.len() != 1
6334 {
6335 return None;
6336 }
6337 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6338 return None;
6339 };
6340 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6341 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6342 if !is_count_star {
6343 return None;
6344 }
6345 let where_expr = stmt.where_.as_ref()?;
6346 let count =
6347 crate::index_access::try_range_count(where_expr, schema_cols, table, alias, snapshot)?;
6348 let columns = alloc::vec![ColumnSchema::new(
6349 "count".to_string(),
6350 spg_storage::DataType::BigInt,
6351 false,
6352 )];
6353 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6354 Some(QueryResult::Rows { columns, rows })
6355 }
6356
6357 /// Single-table aggregate path: filter the (optionally index-seeked)
6358 /// rows, then hand off to the aggregate executor which does its own
6359 /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6360 fn run_single_table_aggregate<'a>(
6361 &self,
6362 stmt: &SelectStatement,
6363 table: &'a spg_storage::Table,
6364 schema_cols: &'a [ColumnSchema],
6365 alias: &str,
6366 indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6367 cancel: CancelToken<'_>,
6368 ) -> Result<QueryResult, EngineError> {
6369 // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6370 // REPEATABLE (see run_single_table_scan). Aggregates
6371 // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6372 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6373 let ctx = self
6374 .ev_ctx(schema_cols, Some(alias))
6375 .with_sample_rng(&sample_cell);
6376 // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6377 // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6378 // and every abandoned buffer on the way stays resident: RSS is a
6379 // high-water mark, so the intermediates are paid for even though
6380 // they are freed. Round 656 measured the scan at 17 bytes/row
6381 // where the survivor list itself only needs 8.
6382 let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6383 Vec::with_capacity(table.rows().len())
6384 } else {
6385 // With a WHERE, the row count is an UPPER bound and reserving it
6386 // is the worse trade: `… WHERE id = 5` over 50M rows would take
6387 // 400 MB of pointers to hold one survivor. Let it grow.
6388 Vec::new()
6389 };
6390 // v6.2.6 — Memoize: per-query LRU cache for correlated
6391 // scalar subqueries. Fresh per row-loop entry so each
6392 // SELECT execution gets an isolated cache.
6393 let mut memo = memoize::MemoizeCache::new();
6394 // v7.37 (perf) — single-table aggregate's WHERE filter
6395 // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6396 // correlated`) per row, even for subquery-free WHEREs that
6397 // the single-table SCAN path has compiled since v7.32
6398 // (perf knife D). The asymmetry meant a fold-to-filter
6399 // rewrite (joinfold) that swapped a JOIN for a single-table
6400 // aggregate over a compiled WHERE saw the tree-walker
6401 // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6402 // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6403 // step. Compile once if eligible; fall back to the walker
6404 // for subquery-bearing or non-compilable WHEREs.
6405 let compiled_where: Option<eval::CompiledExpr> = stmt
6406 .where_
6407 .as_ref()
6408 .filter(|w| eval::fully_compilable(w))
6409 .map(|w| eval::compile_expr(w, &ctx));
6410 let mut eval_stack: Vec<Value<'static>> = Vec::new();
6411 let mut row_passes_where = |row: &Row<'static>,
6412 eval_stack: &mut Vec<Value<'static>>,
6413 memo: &mut memoize::MemoizeCache|
6414 -> Result<bool, EngineError> {
6415 match (&compiled_where, &stmt.where_) {
6416 (Some(cw), _) => {
6417 // v7.39 (round 479) — the predicate wants a bool, not a
6418 // Value. The owned entry ended in `Value::into_owned`
6419 // and the caller then dropped it, once per row; round
6420 // 478's profile put that pair above the comparison
6421 // itself.
6422 Ok(eval::compiled::eval_compiled_pred(
6423 cw,
6424 row,
6425 &ctx,
6426 eval_stack,
6427 ctx.mysql_dialect,
6428 )
6429 .map_err(EngineError::Eval)?)
6430 }
6431 (None, Some(w)) => {
6432 let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6433 Ok(crate::eval::predicate_is_true(
6434 &cond,
6435 "WHERE",
6436 ctx.mysql_dialect,
6437 )?)
6438 }
6439 (None, None) => Ok(true),
6440 }
6441 };
6442 if let Some(rows) = &indexed_rows {
6443 for cow in rows {
6444 let row = cow.as_ref();
6445 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6446 continue;
6447 }
6448 filtered.push(row);
6449 }
6450 }
6451 // v7.36 (cold-tier coverage) — single-table aggregate's
6452 // non-indexed full scan was hot-only and silently lost cold
6453 // rows on COUNT/SUM/etc. Materialise cold rows once into
6454 // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6455 // shape stays unchanged; the cold rows live until the end of
6456 // the aggregate run.
6457 let cold_rows_storage = if indexed_rows.is_none() {
6458 self.iter_cold_rows_of_table(table)
6459 } else {
6460 Vec::new()
6461 };
6462 if indexed_rows.is_none() {
6463 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6464 // single-table aggregate full-scan path. Mirrors the gate on
6465 // `run_single_table_scan`: this is a user-query result path,
6466 // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6467 // reader's snapshot cannot see (e.g. tombstoned versions),
6468 // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6469 // under the default gate-off: every hot row is frozen or
6470 // committed-and-alive, so `is_row_visible` returns true.
6471 // Cold-tier rows are frozen (visible) by definition — left
6472 // ungated, matching the plain-scan path.
6473 let scan_snapshot = self.current_snapshot();
6474 // v7.39 (pg_stat knife B) — this full-scan branch walks
6475 // headers directly (serial and sharded alike); count the
6476 // sequential scan here.
6477 table.note_seq_scan();
6478 // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6479 // filter dominate the pre-aggregate wall time on big
6480 // scans (P1's ground truth: accumulation is only ~17%).
6481 // Shard THAT work when the host injected an executor and
6482 // the WHERE is compiled (the compiled evaluator is pure
6483 // over &row; the tree-walker fallback can hit correlated
6484 // subqueries and stays serial). Shards return surviving
6485 // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6486 // 'static bound — and the main thread only dereferences.
6487 let n = table.row_count();
6488 let par = self.parallel_runner.0.as_deref().filter(|_| {
6489 n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6490 });
6491 if let Some(r) = par {
6492 let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6493 let chunk = n.div_ceil(n_shards);
6494 type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6495 let cw = &compiled_where;
6496 let snap_ref = &scan_snapshot;
6497 let results = r.run_shards(n_shards, &|s| {
6498 let lo = s * chunk;
6499 let hi = ((s + 1) * chunk).min(n);
6500 let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6501 // EvalContext carries Cells (sampler / row counters)
6502 // and is !Sync — each shard builds its own from the
6503 // same Sync inputs. The compiled WHERE is gated to
6504 // the pure-scalar whitelist, which reads none of the
6505 // session state the engine-built ctx would add
6506 // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6507 // sampled scans never take this branch).
6508 let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6509 let mut stack: Vec<Value<'static>> = Vec::new();
6510 let out: ShardOut = (|| {
6511 for i in lo..hi {
6512 if !table.is_row_visible(i, snap_ref) {
6513 continue;
6514 }
6515 let row = &table.rows()[i];
6516 // v7.39 (round 480) — the parallel full-scan
6517 // shard is the path the aggregate benchmark
6518 // actually takes, and it was still on the OWNED
6519 // entry: round 480's profile attributed 68.7 %
6520 // of `drop_glue<Value>` to this closure, which
6521 // is why round 479's fix to the indexed path
6522 // barely moved the total.
6523 //
6524 // The `matches!(…, Value::Bool(true))` form was
6525 // also a narrower reading than the rest of the
6526 // engine uses — `predicate_is_true` is what
6527 // handles NULL and MySQL truthiness — so the
6528 // bool entry fixes the shape as well as the cost.
6529 let pass = match cw {
6530 Some(c) => eval::compiled::eval_compiled_pred(
6531 c,
6532 row,
6533 &shard_ctx,
6534 &mut stack,
6535 shard_ctx.mysql_dialect,
6536 )
6537 .map_err(EngineError::Eval)?,
6538 None => true,
6539 };
6540 if pass {
6541 keep.push(i);
6542 }
6543 }
6544 Ok(keep)
6545 })();
6546 alloc::boxed::Box::new(out)
6547 });
6548 // v7.39 (round 567) — `rows()` is a 32-way trie, so
6549 // indexing it is four dependent loads and a scan that
6550 // reads every row paid them every row. A profile of
6551 // `SELECT sum(id)` over 500k rows put 37.8% of the
6552 // connection thread's CPU on THIS ONE LINE. The cursor
6553 // holds the leaf, making that one descent per 32.
6554 let mut rows_cur = table.rows().run_cursor();
6555 for boxed in results {
6556 let shard = boxed
6557 .downcast::<ShardOut>()
6558 .expect("runner echoes the closure's box");
6559 for i in (*shard)? {
6560 if let Some(row) = rows_cur.get(i) {
6561 filtered.push(row);
6562 }
6563 }
6564 }
6565 } else {
6566 let mut rows_cur = table.rows().run_cursor();
6567 for i in 0..n {
6568 if !table.is_row_visible(i, &scan_snapshot) {
6569 continue;
6570 }
6571 let Some(row) = rows_cur.get(i) else { continue };
6572 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6573 continue;
6574 }
6575 filtered.push(row);
6576 }
6577 }
6578 for row in &cold_rows_storage {
6579 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6580 continue;
6581 }
6582 filtered.push(row);
6583 }
6584 }
6585 // v7.29 — a per-query memo so correlated scalar
6586 // subqueries batch-evaluate once (group map) instead of
6587 // executing per group.
6588 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6589 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6590 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6591 .map_err(|err| match err {
6592 EngineError::Eval(ev) => ev,
6593 other => eval::EvalError::TypeMismatch {
6594 detail: alloc::format!("{other}"),
6595 },
6596 })
6597 };
6598 // v7.39 (round 656) — the plain relational scan. This collect() was
6599 // the measured defect: one 64-byte `RowRef` per surviving row to
6600 // wrap an 8-byte pointer `filtered` already holds. Scalar
6601 // aggregates measured ~81 bytes/row of working memory because of
6602 // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6603 // one number. `AggRows::Ptrs` reads the pointers directly.
6604 let agg = aggregate::run(
6605 stmt,
6606 crate::join::AggRows::Ptrs(&filtered),
6607 schema_cols,
6608 Some(alias),
6609 Some(&agg_correlated),
6610 self.parallel_runner.0.as_deref(),
6611 Some(self.active_catalog()),
6612 Some(self),
6613 )?;
6614 self.finish_agg_result(agg, stmt, cancel)
6615 }
6616
6617 /// Single-table scan + projection path: WHERE filter (compiled when
6618 /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6619 /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6620 fn run_single_table_scan<'a>(
6621 &self,
6622 stmt: &SelectStatement,
6623 table: &'a spg_storage::Table,
6624 schema_cols: &'a [ColumnSchema],
6625 alias: &str,
6626 indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6627 cancel: CancelToken<'_>,
6628 ) -> Result<QueryResult, EngineError> {
6629 // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6630 // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6631 // deterministic `__tsm_fract(seed)` draws share one scan-local
6632 // state (isolated from the global random() PRNG); a fresh cell per
6633 // scan makes a repeat / rescan reproduce the same sample. Unused
6634 // and cheap when the query carries no sample.
6635 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6636 let ctx = self
6637 .ev_ctx(schema_cols, Some(alias))
6638 .with_sample_rng(&sample_cell);
6639 let projection = build_projection(&stmt.items, schema_cols, alias, self.backslash_escapes)?;
6640 // v7.19 P5 — single-table SELECT path for SRF
6641 // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6642 // unnest in the projection list. When present, the
6643 // per-row processor emits one output row per array
6644 // element (broadcasting non-SRF projections from the
6645 // same input row). Empty / NULL arrays emit zero rows
6646 // for that input — PG semantics.
6647 // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
6648 let srf_idxs = self.srf_target_idxs(&projection);
6649 let srf_position = srf_idxs.first().copied();
6650 // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
6651 let mut srf_plan = if srf_position.is_some() {
6652 Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
6653 } else {
6654 None
6655 };
6656
6657 // Materialise the filter pass into `(order_key, projected_row)`
6658 // tuples. The order key is `None` when there's no ORDER BY clause.
6659 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
6660 // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
6661 // output row to the per-query byte budget as it is built, so a
6662 // fat single-table scan / sort REJECTS with QueryBytesExceeded
6663 // at ~the ceiling instead of materialising the whole table and
6664 // only noticing at the final enforce_row_limit check. Without
6665 // this, N concurrent fat scans peak at N×table and OOM the host.
6666 // `max_query_bytes = None` (the embedded default) = no ceiling,
6667 // so existing unbudgeted behaviour is byte-identical.
6668 let mut budget = ByteBudget::new(self.max_query_bytes);
6669 // v6.2.6 — Memoize per-row WHERE eval shares one cache.
6670 let mut memo = memoize::MemoizeCache::new();
6671 // v7.32 (perf knife D) — subquery-free WHERE compiles once;
6672 // the row loop then runs a flat step program instead of a
6673 // tree interpretation per row.
6674 let compiled_where: Option<eval::CompiledExpr> = stmt
6675 .where_
6676 .as_ref()
6677 .filter(|w| eval::fully_compilable(w))
6678 .map(|w| eval::compile_expr(w, &ctx));
6679 let mut eval_stack: Vec<Value<'static>> = Vec::new();
6680 // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
6681 // SELECT-item scalar subquery for the PK-probe fast path. The
6682 // analysis (gate checks + catalog lookups) takes ~500 ns; doing
6683 // it once per query instead of once per row × 100 rows saves
6684 // ~50 µs and lets the per-row evaluation reduce to a single
6685 // index probe + outer-column read.
6686 let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
6687 .iter()
6688 .map(|p| {
6689 if let Expr::ScalarSubquery(inner) = &p.expr {
6690 self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
6691 } else {
6692 None
6693 }
6694 })
6695 .collect();
6696 let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
6697 // v7.39 (round 487) — a projection item that is a bare column
6698 // reference binds its position ONCE per query.
6699 //
6700 // Per row it used to walk `eval_expr_with_correlated` (a memo
6701 // lookup for "does this have a subquery", then an un-memoised
6702 // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
6703 // then `resolve_column`, which finds the column by scanning the
6704 // schema and comparing NAMES. On `SELECT g FROM h` that chain was
6705 // 19 % of self time for what is ultimately one cell read.
6706 //
6707 // `compile_column_pos` is the Step VM's resolver, already
6708 // `pub(crate)` and already reused by the aggregate's bind-once
6709 // path: it mirrors `resolve_column`'s happy layers and returns
6710 // None for anything that would reach an error, an ambiguity, or a
6711 // miss, so those still go the interpreter's way and keep its
6712 // exact message. A composite column is excluded for the same
6713 // reason `compile_into` excludes it — it must be rehydrated from
6714 // stored JSON, which is not a cell read.
6715 let proj_direct = bind_direct_columns(&projection, &ctx);
6716 let any_proj_direct = proj_direct.iter().any(Option::is_some);
6717 // v7.39 (round 605) — a projection item that cannot depend on the row
6718 // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
6719 // allocations a row against one for a plain column, `'abc' || 'def'`
6720 // six and `upper('abc')` five, all of them producing the same value
6721 // 50,000 times. An item that fails to evaluate is left alone, so its
6722 // error still comes from the row loop in the interpreter's wording.
6723 let proj_const: Vec<Option<Value<'static>>> = projection
6724 .iter()
6725 .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
6726 .collect();
6727 let any_proj_const = proj_const.iter().any(Option::is_some);
6728 crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
6729 // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
6730 // projection. Statement prep (`resolve_order_by_position`) can only map
6731 // `ORDER BY 1` onto the first SELECT item when that item is an
6732 // expression; a `*` is not one, so the literal survived to here and was
6733 // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
6734 // at all. The parser rewrites `SELECT unnest(a) x` into
6735 // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
6736 // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
6737 // back in input order. The projection is built by now, so the Nth output
6738 // column is known — resolve against it.
6739 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6740 // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
6741 // EXPANDED rows, so a key naming a select-list item reads that item.
6742 let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
6743 srf_order_output_cols(&order_by, &projection)
6744 } else {
6745 Vec::new()
6746 };
6747 let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
6748 // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
6749 // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
6750 // Hoisted above the closure so the projection-eval path can
6751 // gate `memo` passing on it: the SELECT-item correlated-scalar
6752 // batch path scans the FULL inner table once (~5 ms for 12.5 k
6753 // rows) and is only a win when N outer rows is large; for small
6754 // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
6755 let early_cap: Option<usize> = if order_by.is_empty()
6756 && !stmt.distinct
6757 && !stmt.limit_with_ties
6758 && srf_position.is_none()
6759 && stmt.where_.is_none()
6760 {
6761 stmt.limit_literal()
6762 .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
6763 } else {
6764 None
6765 };
6766 // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
6767 // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
6768 // full-sort by the test gate) keep only the running top-`keep`
6769 // rows in memory instead of materialising every projected row,
6770 // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
6771 // space, not O(rows). `None` = accumulate everything (the prior
6772 // behaviour). The final `partial_sort_tagged(keep)` below still
6773 // runs and produces the identical rows.
6774 // v7.39 (round 683) — the declared collation for each ORDER BY
6775 // position, resolved once and carried beside `descs` for the same
6776 // reason `descs` is carried: it is per key position, not per row.
6777 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
6778 let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
6779 && !stmt.distinct
6780 && !stmt.limit_with_ties
6781 && srf_position.is_none()
6782 && !self.env_cfg().disable_topk
6783 {
6784 stmt.limit_literal().and_then(|l| {
6785 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
6786 (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
6787 })
6788 } else {
6789 None
6790 };
6791 // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
6792 // kept rows in `tagged`. Probing on the PROJECTED row as soon as
6793 // it is built means a duplicate costs neither a build_order_keys
6794 // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
6795 // a tagged slot, and the sort below runs over u survivors, not
6796 // n input rows — PG's hash-distinct-then-sort plan shape.
6797 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
6798 hashbrown::HashMap::new();
6799 let distinct_hb = hashbrown::DefaultHashBuilder::default();
6800 // v7.39 (round 485) — one projection buffer for the whole scan
6801 // rather than a fresh `Vec` per input row. A row that survives
6802 // the DISTINCT probe takes the buffer with it (`mem::take`) and
6803 // the next row allocates a new one; a row that duplicates an
6804 // earlier one leaves the buffer — and its capacity — in place.
6805 // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
6806 // projected rows are duplicates, so that is 49 900 allocate /
6807 // free pairs the scan no longer performs. Shapes where every row
6808 // survives (plain projection, `DISTINCT` over a unique column)
6809 // allocate exactly as often as before.
6810 let mut proj_buf: Vec<Value<'static>> = Vec::new();
6811 // v7.39 (round 571) — buffers handed back by the top-N trim.
6812 // Round 485 made the scan share ONE projection buffer, but a
6813 // surviving row takes it (`mem::take`) and without DISTINCT
6814 // almost every row survives, so the next one starts from zero
6815 // capacity and allocates. The trim drops `keep` rows at a time
6816 // and their buffers come back here instead of being freed.
6817 let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
6818 let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
6819 // v7.39 (round 581) — the worst row the accumulator is currently
6820 // keeping. Anything that loses to it cannot reach the answer, so
6821 // it is dropped before its projection is ever built.
6822 let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
6823 // v7.39 (round 582) — resolve each ORDER BY column once, not
6824 // once per row. See `order_by_bound_positions`.
6825 let order_bound =
6826 crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
6827 // v7.39 (round 581) — and it stops asking when the answer is
6828 // always "keep".
6829 //
6830 // The check earns its place only on rows it rejects. Over
6831 // ascending ids, `ORDER BY id DESC` never rejects one — every
6832 // row beats the current worst — so the comparison is pure
6833 // overhead there, measured at +5.5% in three batches out of
6834 // three. After a window of rows it looks at what it has
6835 // actually rejected and switches itself off if the shape is not
6836 // paying. The answers do not depend on it either way.
6837 const BOUNDARY_WINDOW: u32 = 8192;
6838 let mut boundary_checks: u32 = 0;
6839 let mut boundary_rejects: u32 = 0;
6840 let mut boundary_check_on = true;
6841 // Inline the per-row work in a closure so the indexed and full-
6842 // scan branches share the body.
6843 let mut process_row = |row: &Row<'static>, loop_idx: usize| -> Result<(), EngineError> {
6844 if loop_idx.is_multiple_of(256) {
6845 cancel.check()?;
6846 }
6847 if let Some(cw) = &compiled_where {
6848 let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
6849 .map_err(EngineError::Eval)?;
6850 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6851 return Ok(());
6852 }
6853 } else if let Some(where_expr) = &stmt.where_ {
6854 let cond =
6855 self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
6856 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6857 return Ok(());
6858 }
6859 }
6860 // Under DISTINCT the keys are built AFTER the dup probe
6861 // (survivors only); the non-distinct order is unchanged.
6862 // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
6863 // row further down, and building them here would evaluate the
6864 // ORDER BY against the INPUT row: a key naming the SRF's own
6865 // output became a scalar call to it, which is where
6866 // "function unnest(integer[]) does not exist" came from.
6867 let order_keys = if order_by.is_empty() || stmt.distinct || srf_position.is_some() {
6868 Vec::new()
6869 } else {
6870 let mut buf = key_pool.pop().unwrap_or_default();
6871 crate::orderby::build_order_keys_bound(
6872 &order_by,
6873 &order_bound,
6874 row,
6875 &ctx,
6876 &mut buf,
6877 )?;
6878 // v7.39 (round 581) — reject before projecting.
6879 //
6880 // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
6881 // 50 distinct `g` decides nearly every row on the FIRST
6882 // key, and PG answers it FASTER than the single-key form
6883 // (7.4 ms against 10.4) because a rejected row costs it
6884 // one comparison. SPG built both keys AND the projected
6885 // row for all 500k before throwing them away. The keys
6886 // are needed to compare; the projection is not.
6887 if boundary_check_on
6888 && let Some((_, descs)) = &topk_stream
6889 && let Some(b) = &topk_boundary
6890 {
6891 boundary_checks += 1;
6892 let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
6893 == core::cmp::Ordering::Greater;
6894 if loses {
6895 boundary_rejects += 1;
6896 }
6897 if boundary_checks == BOUNDARY_WINDOW {
6898 // Keep asking only if it has been rejecting at
6899 // least a quarter of what it saw.
6900 boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
6901 }
6902 if loses {
6903 buf.clear();
6904 key_pool.push(buf);
6905 return Ok(());
6906 }
6907 }
6908 buf
6909 };
6910 if srf_position.is_some() {
6911 let plan = srf_plan.as_mut().expect("srf_position implies a plan");
6912 for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
6913 if stmt.distinct {
6914 let bucket = seen_distinct
6915 .entry(norm_hash_row(&out, &distinct_hb, ctx.mysql_dialect))
6916 .or_default();
6917 if bucket
6918 .iter()
6919 .any(|i| row_eq_norm(&tagged[i].1, &out, ctx.mysql_dialect))
6920 {
6921 continue;
6922 }
6923 bucket.push(tagged.len());
6924 }
6925 budget.charge(approx_row_bytes(&out))?;
6926 // The keys come from THIS expanded row: a key naming a
6927 // select-list item reads its value, anything else is
6928 // still evaluated against the input row.
6929 let keys = if order_by.is_empty() {
6930 Vec::new()
6931 } else {
6932 let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
6933 for (k, ob) in order_by.iter().enumerate() {
6934 kv.push(match srf_order_cols.get(k).copied().flatten() {
6935 Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
6936 None => eval::eval_expr(&ob.expr, row, &ctx)
6937 .map_err(EngineError::Eval)?,
6938 });
6939 }
6940 // Packed by the same code every other ORDER BY uses,
6941 // so DESC / NULLS FIRST / the MySQL rule are not
6942 // restated here.
6943 let key_row = Row::new(kv);
6944 let mut buf = Vec::new();
6945 crate::orderby::build_order_keys_bound(
6946 &order_by,
6947 &srf_key_bound,
6948 &key_row,
6949 &ctx,
6950 &mut buf,
6951 )?;
6952 buf
6953 };
6954 tagged.push((keys, out));
6955 }
6956 } else {
6957 let values = &mut proj_buf;
6958 values.clear();
6959 values.reserve(projection.len());
6960 for (i, p) in projection.iter().enumerate() {
6961 // v7.37.x (docker-fair SCALARSQ attack) — pre-
6962 // analysed PK-probe fast path. The per-row work is
6963 // a read of outer.col from the row plus an index
6964 // probe — no Expr clone, no walker, no
6965 // `eval_expr_with_correlated` framework.
6966 if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
6967 values.push(self.probe_with_pk_fast_path(fp, row));
6968 continue;
6969 }
6970 // v7.39 (round 605) — the same value every row.
6971 if any_proj_const && let Some(v) = &proj_const[i] {
6972 values.push(v.clone());
6973 continue;
6974 }
6975 // v7.39 (round 487) — bound column: read the cell.
6976 // This is `rehydrate_cell`'s body for a non-composite
6977 // column, which is what the whole chain below reduces
6978 // to once the name has been resolved.
6979 if any_proj_direct && let Some(pos) = proj_direct[i] {
6980 crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
6981 values.push(row.values[pos].clone().into_owned());
6982 continue;
6983 }
6984 // v7.24 (round-16 B) — correlated-aware.
6985 // v7.37.x (docker-fair SCALARSQ attack) — share the
6986 // per-row memo with projection. Required for the
6987 // batch-evaluated correlated-scalar path to fire on
6988 // SELECT-item scalar subqueries; otherwise each row
6989 // re-executes the inner.
6990 //
6991 // Skip the memo when the outer row count is small
6992 // (early-limited): the batch path scans the FULL
6993 // inner table to build a GroupMap (~5 ms for a
6994 // 12.5 k-row inner), while per-row execution with a
6995 // PK index seek is ~5 µs per call — much cheaper for
6996 // N ≤ ~1000 outer rows.
6997 let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
6998 let memo_arg = if pass_memo { Some(&mut memo) } else { None };
6999 values.push(
7000 self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7001 );
7002 }
7003 crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7004 if stmt.distinct {
7005 let bucket = seen_distinct
7006 .entry(norm_hash_values(&proj_buf, &distinct_hb, ctx.mysql_dialect))
7007 .or_default();
7008 if bucket
7009 .iter()
7010 .any(|i| values_eq_norm(&tagged[i].1.values, &proj_buf, ctx.mysql_dialect))
7011 {
7012 crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7013 return Ok(());
7014 }
7015 bucket.push(tagged.len());
7016 }
7017 let out = Row::new(core::mem::replace(
7018 &mut proj_buf,
7019 proj_pool.pop().unwrap_or_default(),
7020 ));
7021 let order_keys = if stmt.distinct && !order_by.is_empty() {
7022 build_order_keys(&order_by, row, &ctx)?
7023 } else {
7024 order_keys
7025 };
7026 budget.charge(approx_row_bytes(&out))?;
7027 tagged.push((order_keys, out));
7028 }
7029 // Streaming top-N: bound the accumulator to O(keep) rows.
7030 if let Some((k, descs)) = &topk_stream {
7031 crate::orderby::topk_trim_recycling(
7032 &mut tagged,
7033 *k,
7034 descs,
7035 &mut proj_pool,
7036 &mut key_pool,
7037 &mut topk_boundary,
7038 );
7039 }
7040 Ok(())
7041 };
7042 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7043 // load-bearing full-scan path. This is the primary single-table
7044 // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7045 // in-place writers retain dead/old versions, an ungated scan
7046 // here would return them, so the gate must land BEFORE the
7047 // writers flip (see the plan's activation-order rule). A no-op
7048 // today: every hot row is frozen or committed-and-alive under
7049 // the reader's snapshot, so `is_row_visible` returns true for
7050 // all of them (verified by the full e2e suite staying green).
7051 let scan_snapshot = self.current_snapshot();
7052 let mut emitted: usize = 0;
7053 if let Some(rows) = &indexed_rows {
7054 for (loop_idx, cow) in rows.iter().enumerate() {
7055 if let Some(cap) = early_cap
7056 && emitted >= cap
7057 {
7058 break;
7059 }
7060 process_row(cow.as_ref(), loop_idx)?;
7061 emitted = emitted.saturating_add(1);
7062 }
7063 } else {
7064 // v7.39 (round 570) — the row store is a 32-way trie, so
7065 // indexing it is four dependent loads. Round 567 measured
7066 // -18% on the aggregate scan from holding the leaf between
7067 // rows; this is the same loop for the projecting scan.
7068 let mut rows_cur = table.rows().run_cursor();
7069 for i in 0..table.row_count() {
7070 if let Some(cap) = early_cap
7071 && emitted >= cap
7072 {
7073 break;
7074 }
7075 // Skip rows this snapshot cannot see (invisible rows do
7076 // not count toward the LIMIT).
7077 if !table.is_row_visible(i, &scan_snapshot) {
7078 continue;
7079 }
7080 let Some(row) = rows_cur.get(i) else { continue };
7081 process_row(row, i)?;
7082 emitted = emitted.saturating_add(1);
7083 }
7084 // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7085 // rows into the same loop. The full-scan path here is the
7086 // load-bearing single-table SELECT executor, and pre-
7087 // 7.35.1 it only walked `table.rows()` (hot), so any
7088 // `SELECT … FROM t` against a table with cold segments
7089 // silently returned a subset.
7090 let cold_rows = self.iter_cold_rows_of_table(table);
7091 for (offset, row) in cold_rows.iter().enumerate() {
7092 if let Some(cap) = early_cap
7093 && emitted >= cap
7094 {
7095 break;
7096 }
7097 process_row(row, table.row_count() + offset)?;
7098 emitted = emitted.saturating_add(1);
7099 }
7100 }
7101
7102 // (DISTINCT already de-duped STREAMING inside process_row, so the
7103 // sort below only sees the u survivors and the partial-sort
7104 // budget applies to DISTINCT too.)
7105 if !order_by.is_empty() {
7106 // Partial-sort fast path: when LIMIT is small relative to
7107 // the row count, select_nth_unstable + sort just the
7108 // prefix is O(n + k log k) instead of O(n log n).
7109 // WITH TIES needs the full sort so the tie extension can
7110 // scan past `limit` to find rows that share the last-kept
7111 // row's key.
7112 let keep = if stmt.limit_with_ties
7113 // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7114 // forces the full-sort fallback by suppressing the
7115 // partial-sort `keep` budget. See
7116 // `xtests/sigil/test-mode-gucs.md`.
7117 || self.env_cfg().disable_topk
7118 {
7119 None
7120 } else {
7121 stmt.limit_literal()
7122 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7123 };
7124 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7125 crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7126 }
7127
7128 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7129 // past the truncated tail through every row that shares the
7130 // last-kept row's ORDER BY key. The tie check uses the
7131 // already-computed `(order_keys, row)` pairs so it matches
7132 // the sort comparator exactly. DISTINCT + WITH TIES falls
7133 // through to the no-ties path (PG also disallows their
7134 // combination; SPG silently drops the tie extension here so
7135 // the customer doesn't see a hard error mid-query — the
7136 // user-visible result is still correct, just narrower).
7137 let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7138 apply_offset_and_limit_tagged(
7139 &mut tagged,
7140 stmt.offset_literal(),
7141 stmt.limit_literal(),
7142 true,
7143 );
7144 tagged.into_iter().map(|(_, r)| r).collect()
7145 } else {
7146 // DISTINCT already de-duped pre-sort above.
7147 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7148 apply_offset_and_limit(
7149 &mut output_rows,
7150 stmt.offset_literal(),
7151 stmt.limit_literal(),
7152 );
7153 output_rows
7154 };
7155
7156 let columns: Vec<ColumnSchema> = projection
7157 .into_iter()
7158 .map(|p| {
7159 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
7160 c.user_enum_type = p.user_enum_type;
7161 c.collation_name = p.collation_name;
7162 c.mysql_fsp = p.mysql_fsp;
7163 c
7164 })
7165 .collect();
7166
7167 Ok(QueryResult::Rows {
7168 columns,
7169 rows: output_rows,
7170 })
7171 }
7172
7173 /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7174 /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7175 /// select items for the surviving rows only — PG's Result-above-
7176 /// Limit shape, where SubPlan loops equal the OUTPUT row count
7177 /// (50) instead of the group count (24k).
7178 fn finish_agg_result(
7179 &self,
7180 mut agg: aggregate::AggResult,
7181 stmt: &SelectStatement,
7182 cancel: CancelToken<'_>,
7183 ) -> Result<QueryResult, EngineError> {
7184 apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7185 if !agg.deferred.is_empty() {
7186 apply_offset_and_limit(
7187 &mut agg.synth_rows,
7188 stmt.offset_literal(),
7189 stmt.limit_literal(),
7190 );
7191 let ctx = EvalContext::new(&agg.synth_schema, None);
7192 let mut memo = memoize::MemoizeCache::default();
7193 // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7194 // Deferred subqueries are referenced only by surviving
7195 // select-list rows (≤ LIMIT), so their correlation keys are
7196 // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7197 // each batchable subquery's group map over just those keys
7198 // via per-key index seek; the per-row splice loop below then
7199 // reuses the seeded map. A join-shaped or un-indexed inner
7200 // falls through to the all-keys batch inside the call (built
7201 // eagerly here instead of lazily on row 0 — same cost), so
7202 // it still pays the full scan, never the 715 ms per-row
7203 // direct eval; its index-nested-loop probe is the next
7204 // knife. Genuinely non-batchable shapes return None and are
7205 // left unseeded for the loop's per-row resolver, as before.
7206 for (_, expr) in &agg.deferred {
7207 let mut subs: Vec<&SelectStatement> = Vec::new();
7208 collect_scalar_subqueries(expr, &mut subs);
7209 for sub in subs {
7210 let repr = alloc::format!("{sub}");
7211 if memo.group_maps.contains_key(&repr) {
7212 continue;
7213 }
7214 if let Some(gm) = self.try_batch_correlated_scalar(
7215 sub,
7216 Some((&agg.synth_rows, &ctx)),
7217 cancel,
7218 )? {
7219 memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7220 }
7221 }
7222 }
7223 for (ri, srow) in agg.synth_rows.iter().enumerate() {
7224 cancel.check()?;
7225 for (col, expr) in &agg.deferred {
7226 let v =
7227 self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7228 if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7229 *cell = v;
7230 }
7231 }
7232 }
7233 }
7234 Ok(QueryResult::Rows {
7235 columns: agg.columns,
7236 rows: agg.rows,
7237 })
7238 }
7239
7240 /// v7.37 — streaming projection for the joined-non-aggregate
7241 /// shape (multi-table FROM, all projection items bound, no
7242 /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
7243 /// UNION). Walks the deferred join survivors and emits
7244 /// `&[&Value]` borrowed straight out of the source tables — no
7245 /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
7246 /// on the mailrs `PROJ` shape (about 4 ms saved).
7247 ///
7248 /// Returns `Ok(None)` when the shape doesn't qualify; the caller
7249 /// then falls back to the materialising path.
7250 /// v7.37 (round 831) — stream a joinless SELECT straight off the
7251 /// stored table, one row at a time, without ever building a row set.
7252 ///
7253 /// Returns `Ok(None)` for anything this cannot serve, and the caller
7254 /// falls through to the deferred-join path exactly as before: a
7255 /// missing table, or a cold tier whose hydration the fallback handles.
7256 /// Sort a single-table scan through the external sorter, so the
7257 /// answer's size is bounded by `work_mem` and not by the input.
7258 ///
7259 /// Sorting held every row twice — the scan's `Vec<Row>` and the
7260 /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
7261 /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
7262 /// enough ORDER BY took the server down, which is a liveness
7263 /// problem before it is a performance one.
7264 ///
7265 /// A SEPARATE walk rather than a change to `run_single_table_scan`,
7266 /// following what round 831 did for the joinless shape. That
7267 /// function is 552 lines whose projection loop is entangled with
7268 /// DISTINCT (which indexes back into the tagged vector) and with
7269 /// streaming top-N (whose boundary moves as the scan runs); both
7270 /// assume the projection has already happened when a row is
7271 /// pushed, which is exactly what spilling has to defer. Two earlier
7272 /// attempts tried to rework that loop and were reverted. Here the
7273 /// existing path is untouched and this one only claims shapes it
7274 /// can serve, so a decline costs nothing.
7275 ///
7276 /// Records are SOURCE rows, not projected ones: `finish` re-derives
7277 /// keys from what it decodes, and an ORDER BY key need not be in
7278 /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
7279 fn try_spill_sorted_scan(
7280 &self,
7281 stmt: &SelectStatement,
7282 from: &FromClause,
7283 cancel: CancelToken<'_>,
7284 ) -> Result<Option<QueryResult>, EngineError> {
7285 // Shapes this walk does not serve. Each one either needs the
7286 // whole tagged vector addressable (DISTINCT probes back into
7287 // it, WITH TIES re-reads its tail) or is already bounded
7288 // without spilling (a LIMIT makes the partial sort O(keep)).
7289 if !self.can_spill()
7290 || stmt.order_by.is_empty()
7291 || stmt.distinct
7292 || stmt.limit_with_ties
7293 || stmt.limit_literal().is_some()
7294 || !from.joins.is_empty()
7295 || from.primary.lateral_subquery.is_some()
7296 || from.primary.unnest_expr.is_some()
7297 || from.primary.generate_series_args.is_some()
7298 || select_has_window(stmt)
7299 {
7300 return Ok(None);
7301 }
7302 // A parent's rows are its children's. These walks scan the named
7303 // relation alone, so a partitioned or inherited parent comes back
7304 // short — and silently: the corpus caught `SELECT id FROM pr
7305 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
7306 // parent's own rows instead of the partitions'. `ONLY` is exactly
7307 // the case that does not fan out, so it stays, which is the test
7308 // the FROM-clause fan-out itself makes.
7309 if !from.primary.only
7310 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7311 {
7312 return Ok(None);
7313 }
7314 let Some(table) = self.active_catalog().get(&from.primary.name) else {
7315 return Ok(None);
7316 };
7317 // Cold-tier rows live outside `rows()`; this walk would drop
7318 // them silently, the same reason round 831's walk declines.
7319 if table.has_cold_rows_fast() {
7320 return Ok(None);
7321 }
7322
7323 let alias = from
7324 .primary
7325 .alias
7326 .as_deref()
7327 .unwrap_or(from.primary.name.as_str());
7328 let cols = table.schema().columns.clone();
7329 let sess = self.dml_session();
7330 let ctx = EvalContext::new(&cols, Some(alias))
7331 .with_catalog(self.active_catalog())
7332 .with_session(&sess);
7333 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7334 let order_by = stmt.order_by.clone();
7335 // The same one-shot resolution the general path does (round
7336 // 582): each ORDER BY column is bound once, not once per row.
7337 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
7338 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7339 // Resolved BEFORE the scan, because it now decides what the sort
7340 // STORES and not just what it decodes (round 995).
7341 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
7342
7343 let mut sorter = crate::extsort::ExternalSorter::new(
7344 self.temp_run_factory,
7345 self.session_work_mem_bytes(),
7346 cols.clone(),
7347 &descs,
7348 )
7349 .with_stats(&self.spill_stats)
7350 .with_pruned(&needed);
7351 let snapshot = self.current_snapshot();
7352 // One key buffer for the whole scan: `push` drains it and leaves
7353 // the capacity behind.
7354 let mut keys: Vec<OrderKey> = Vec::new();
7355 // r1024 — compile the predicate once for the scan.
7356 //
7357 // These two sorted-spill scans are the paths a single-table SELECT
7358 // with an ORDER BY takes, and they were the last row-returning ones
7359 // still walking the expression tree per row. r1023 did the
7360 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
7361 // exactly this shape.
7362 //
7363 // Found from the profile's CALL TREE rather than its leaves. The
7364 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
7365 // 261, `mod_op` 178 — and two attempts at reasoning out which
7366 // function asked for it were both wrong. The tree names the caller
7367 // chain, and it named this one.
7368 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7369 .where_
7370 .as_ref()
7371 .filter(|w| crate::eval::fully_compilable(w))
7372 .map(|w| crate::eval::compile_expr(w, &ctx));
7373 let mut eval_stack: Vec<Value<'static>> = Vec::new();
7374 for (i, row) in table.scan_visible_from(0, &snapshot) {
7375 if i.is_multiple_of(256) {
7376 cancel.check()?;
7377 }
7378 if let Some(c) = &compiled_where {
7379 if !crate::eval::compiled::eval_compiled_pred(
7380 c,
7381 row,
7382 &ctx,
7383 &mut eval_stack,
7384 ctx.mysql_dialect,
7385 )? {
7386 continue;
7387 }
7388 } else if let Some(w) = &stmt.where_ {
7389 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
7390 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7391 continue;
7392 }
7393 }
7394 keys.clear();
7395 crate::orderby::build_order_keys_bound(&order_by, &order_bound, row, &ctx, &mut keys)?;
7396 sorter.push(&mut keys, row)?;
7397 }
7398
7399 let key_ctx = &ctx;
7400 let rows = sorter.finish(
7401 |src, buf| {
7402 crate::orderby::build_order_keys_bound(&order_by, &order_bound, src, key_ctx, buf)
7403 },
7404 |src| {
7405 let mut values = Vec::with_capacity(projection.len());
7406 for p in &projection {
7407 values.push(
7408 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
7409 );
7410 }
7411 Ok(Row::new(values))
7412 },
7413 )?;
7414
7415 let columns: Vec<ColumnSchema> = projection
7416 .iter()
7417 .map(|p| {
7418 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7419 c.user_enum_type = p.user_enum_type.clone();
7420 c.mysql_fsp = p.mysql_fsp;
7421 c
7422 })
7423 .collect();
7424 Ok(Some(QueryResult::Rows { columns, rows }))
7425 }
7426
7427 /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
7428 /// handing each row to the consumer instead of collecting the answer.
7429 ///
7430 /// That walk bounds the SORT and then returns `QueryResult::Rows`,
7431 /// which holds every output row. Measured at `work_mem = 4 MB` over
7432 /// 200-byte rows, RSS above the server's own baseline while the
7433 /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
7434 /// at 400k — linear — while the spill underneath worked correctly
7435 /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
7436 /// removes each file, so a count taken afterwards reads 0 whatever
7437 /// happened, and an earlier reading of "no spill at all" was that
7438 /// blind witness). The growth is the collected result, not the sort.
7439 ///
7440 /// Emitting makes peak the budget, one buffer per run and a single
7441 /// row — the state a merge already holds at every step. It also
7442 /// frees each projected row as the next is built rather than
7443 /// accumulating them, which is where the time is: a profile of the
7444 /// collecting walk put the allocator at 586 samples, more than every
7445 /// sort comparison combined (420), against 19 for `push` itself.
7446 /// v7.37 (round 923) — which of a sort record's columns the output half
7447 /// reads. The record is the SOURCE row (round 836), so a narrow projection
7448 /// decoded every column: skipping one 200-byte text halves a decode
7449 /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
7450 ///
7451 /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
7452 /// column reads NULL. Answers only when every projection item is a bare
7453 /// column reference AND every ORDER BY key is a bound column; anything
7454 /// else returns empty, decoding everything as before.
7455 /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
7456 /// drops references from expression kinds it does not enumerate.
7457 ///
7458 /// ORDER BY columns are included — the merge re-derives keys from the
7459 /// decoded row on the spilled path, so pruning one would sort NULLs.
7460 pub(crate) fn sort_record_columns_needed(
7461 items: &[SelectItem],
7462 order_bound: &[Option<usize>],
7463 arity: usize,
7464 ctx: &EvalContext,
7465 ) -> Vec<bool> {
7466 let all_bare = items.iter().all(|i| {
7467 matches!(
7468 i,
7469 SelectItem::Expr {
7470 expr: Expr::Column(_),
7471 ..
7472 }
7473 )
7474 });
7475 if !all_bare || order_bound.iter().any(Option::is_none) {
7476 return Vec::new();
7477 }
7478 let mut mask = alloc::vec![false; arity];
7479 for item in items {
7480 if let SelectItem::Expr {
7481 expr: Expr::Column(c),
7482 ..
7483 } = item
7484 {
7485 match crate::eval::find_column_pos(c, ctx) {
7486 Some(p) if p < arity => mask[p] = true,
7487 _ => return Vec::new(),
7488 }
7489 }
7490 }
7491 for p in order_bound.iter().flatten() {
7492 if *p < arity {
7493 mask[*p] = true;
7494 } else {
7495 return Vec::new();
7496 }
7497 }
7498 mask
7499 }
7500
7501 /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
7502 /// of sorting.
7503 ///
7504 /// PG serves such an ordering from the index and never sorts. We sorted:
7505 /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
7506 /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
7507 /// the sorter's own round trip — `ExternalSorter::finish_each` →
7508 /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
7509 /// Every row is encoded into the sorter's arena and decoded back out,
7510 /// for an order the index already holds.
7511 ///
7512 /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
7513 /// because it was built for top-N. This is the unbounded sibling.
7514 ///
7515 /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
7516 /// from a btree, so walking one would silently drop those rows. That is
7517 /// exactly the defect r1020 fixed on the top-N path, where it had
7518 /// shipped.
7519 fn try_index_order_stream<F>(
7520 &self,
7521 stmt: &SelectStatement,
7522 from: &FromClause,
7523 cancel: CancelToken<'_>,
7524 emit: &mut F,
7525 ) -> Result<Option<usize>, EngineError>
7526 where
7527 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7528 {
7529 // The same shape gates the spill sort applies, minus `can_spill`:
7530 // this path never spills.
7531 if stmt.order_by.len() != 1
7532 || stmt.distinct
7533 || stmt.limit_with_ties
7534 || stmt.limit.is_some()
7535 || stmt.offset.is_some()
7536 || stmt.having.is_some()
7537 || stmt.group_by.is_some()
7538 || !stmt.unions.is_empty()
7539 || !from.joins.is_empty()
7540 || from.primary.lateral_subquery.is_some()
7541 || from.primary.unnest_expr.is_some()
7542 || from.primary.as_of_segment.is_some()
7543 || from.primary.generate_series_args.is_some()
7544 || select_has_window(stmt)
7545 || aggregate::uses_aggregate(stmt)
7546 {
7547 return Ok(None);
7548 }
7549 if stmt
7550 .items
7551 .iter()
7552 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7553 {
7554 return Ok(None);
7555 }
7556 crate::orderby::check_order_by_legality(stmt)?;
7557 crate::orderby::check_order_by_positions(stmt)?;
7558 crate::window::reject_window_in_row_clauses(stmt)?;
7559 let Some(table) = self.active_catalog().get(&from.primary.name) else {
7560 return Ok(None);
7561 };
7562 // Cold rows are reachable through locators, but the walk would have
7563 // to resolve them per key; the ordinary path already covers that.
7564 if table.has_cold_rows_fast() {
7565 return Ok(None);
7566 }
7567 if !from.primary.only
7568 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7569 {
7570 return Ok(None);
7571 }
7572 let alias = from
7573 .primary
7574 .alias
7575 .as_deref()
7576 .unwrap_or(from.primary.name.as_str());
7577 let cols = table.schema().columns.clone();
7578
7579 let order = &stmt.order_by[0];
7580 let Expr::Column(oc) = &order.expr else {
7581 return Ok(None);
7582 };
7583 if let Some(q) = &oc.qualifier
7584 && !q.eq_ignore_ascii_case(alias)
7585 {
7586 return Ok(None);
7587 }
7588 let Some(order_pos) = cols
7589 .iter()
7590 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
7591 else {
7592 return Ok(None);
7593 };
7594 // See the NOT NULL note above: this is the r1020 defect's gate.
7595 if cols[order_pos].nullable {
7596 return Ok(None);
7597 }
7598 let Some(index) = table.index_on(order_pos) else {
7599 return Ok(None);
7600 };
7601 if !matches!(index.kind, spg_storage::IndexKind::BTree(_))
7602 || index.expression.is_some()
7603 || index.partial_predicate.is_some()
7604 {
7605 return Ok(None);
7606 }
7607
7608 let sess = self.dml_session();
7609 let ctx = EvalContext::new(&cols, Some(alias))
7610 .with_catalog(self.active_catalog())
7611 .with_session(&sess);
7612 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7613 let columns: Vec<ColumnSchema> = projection
7614 .iter()
7615 .map(|p| {
7616 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7617 c.user_enum_type = p.user_enum_type.clone();
7618 c.mysql_fsp = p.mysql_fsp;
7619 c
7620 })
7621 .collect();
7622 emit(crate::StreamItem::Header(&columns))?;
7623 let bound_pos: Vec<Option<usize>> = projection
7624 .iter()
7625 .map(|p| match &p.expr {
7626 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
7627 Ok(Some(pos)) => Some(pos),
7628 _ => None,
7629 },
7630 _ => None,
7631 })
7632 .collect();
7633
7634 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7635 .where_
7636 .as_ref()
7637 .filter(|w| crate::eval::fully_compilable(w))
7638 .map(|w| crate::eval::compile_expr(w, &ctx));
7639 let mut eval_stack: Vec<Value<'static>> = Vec::new();
7640 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
7641 let snapshot = self.current_snapshot();
7642
7643 // A btree holds one locator per row VERSION, so a row whose key was
7644 // updated can sit under two keys and a dead one can sit beside its
7645 // replacement. The visibility gate drops the dead; `seen` drops a
7646 // live row that the walk reaches twice, which would otherwise be a
7647 // duplicated output row rather than a slow one.
7648 let mut emitted_rows = alloc::vec![false; table.rows().len()];
7649 let walker: alloc::boxed::Box<
7650 dyn Iterator<Item = (&spg_storage::IndexKey, &spg_storage::PostingList)>,
7651 > = if order.desc {
7652 alloc::boxed::Box::new(index.iter_desc())
7653 } else {
7654 alloc::boxed::Box::new(index.iter_asc())
7655 };
7656 let mut count = 0usize;
7657 let mut visited = 0usize;
7658 for (_key, locators) in walker {
7659 for loc in locators {
7660 let spg_storage::RowLocator::Hot(ri) = *loc else {
7661 continue;
7662 };
7663 if emitted_rows.get(ri).copied().unwrap_or(true) {
7664 continue;
7665 }
7666 if !table.is_row_visible(ri, &snapshot) {
7667 continue;
7668 }
7669 let Some(row) = table.rows().get(ri) else {
7670 continue;
7671 };
7672 visited += 1;
7673 if visited.is_multiple_of(256) {
7674 cancel.check()?;
7675 }
7676 emitted_rows[ri] = true;
7677 if Self::stream_project_row(
7678 row,
7679 stmt.where_.as_ref(),
7680 compiled_where.as_ref(),
7681 &mut eval_stack,
7682 &projection,
7683 &bound_pos,
7684 &ctx,
7685 &mut values,
7686 emit,
7687 )? {
7688 count += 1;
7689 }
7690 }
7691 }
7692 Ok(Some(count))
7693 }
7694
7695 /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
7696 /// building an `OrderKey` vector per row.
7697 ///
7698 /// The row-returning sorted scan allocates twice per row: one
7699 /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
7700 /// projection. Counted over 400 k rows (r1030,
7701 /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
7702 /// allocations and 208 MB of traffic for an answer of four hundred
7703 /// thousand integers.
7704 ///
7705 /// The key half is pure ceremony on this shape.
7706 /// `sort_tagged_by_inline_int_key` already sorts indices rather than
7707 /// rows, so the per-row vector is built, has one integer taken out of
7708 /// it, and is then dragged through the permutation — it exists to carry
7709 /// a number the row's column already held. This lane carries the number
7710 /// instead, in a fixed-size array that lives inside the buffer element
7711 /// and allocates nothing. Same idea as the predicate VM's integer lane.
7712 ///
7713 /// Declines to `None` for anything it does not cover, and every caller
7714 /// falls through to the general path, so the gate list is the
7715 /// specification.
7716 ///
7717 /// Ties: equal keys keep scan order, as the stable sort on the general
7718 /// path does. Rows that tie on every ORDER BY term are entitled to any
7719 /// order among themselves either way — see `STABILITY.md`.
7720 fn try_int_key_sorted_stream<F>(
7721 &self,
7722 stmt: &SelectStatement,
7723 from: &FromClause,
7724 cancel: CancelToken<'_>,
7725 emit: &mut F,
7726 ) -> Result<Option<usize>, EngineError>
7727 where
7728 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7729 {
7730 /// Sort terms this lane carries inline. Four covers every ORDER BY
7731 /// in the endpoint sweep and in the dogfood corpus; wider ones fall
7732 /// through rather than growing the buffer element for everybody.
7733 const MAX_KEYS: usize = 4;
7734
7735 if stmt.order_by.is_empty()
7736 || stmt.order_by.len() > MAX_KEYS
7737 || stmt.distinct
7738 || stmt.limit_with_ties
7739 || stmt.limit.is_some()
7740 || stmt.offset.is_some()
7741 || stmt.having.is_some()
7742 || stmt.group_by.is_some()
7743 || !stmt.unions.is_empty()
7744 || !from.joins.is_empty()
7745 || from.primary.lateral_subquery.is_some()
7746 || from.primary.unnest_expr.is_some()
7747 || from.primary.as_of_segment.is_some()
7748 || from.primary.generate_series_args.is_some()
7749 || select_has_window(stmt)
7750 || aggregate::uses_aggregate(stmt)
7751 {
7752 return Ok(None);
7753 }
7754 if stmt
7755 .items
7756 .iter()
7757 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7758 {
7759 return Ok(None);
7760 }
7761 crate::orderby::check_order_by_legality(stmt)?;
7762 crate::orderby::check_order_by_positions(stmt)?;
7763 crate::window::reject_window_in_row_clauses(stmt)?;
7764 let Some(table) = self.active_catalog().get(&from.primary.name) else {
7765 return Ok(None);
7766 };
7767 if table.has_cold_rows_fast() {
7768 return Ok(None);
7769 }
7770 if !from.primary.only
7771 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7772 {
7773 return Ok(None);
7774 }
7775 let alias = from
7776 .primary
7777 .alias
7778 .as_deref()
7779 .unwrap_or(from.primary.name.as_str());
7780 let cols = table.schema().columns.clone();
7781
7782 // Every ORDER BY term must be a NOT NULL integer column of this
7783 // table. NOT NULL is what lets the key be a bare integer: with
7784 // NULLs the lane would have to carry their ordering too, and
7785 // getting that subtly wrong is the r1020 defect.
7786 let mut key_pos = [0usize; MAX_KEYS];
7787 let mut descs = [false; MAX_KEYS];
7788 // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
7789 // which the AST records as `None`; `unwrap_or(desc)` is how the
7790 // rest of the engine resolves it.
7791 let mut nulls_first = [false; MAX_KEYS];
7792 let n_keys = stmt.order_by.len();
7793 for (slot, order) in stmt.order_by.iter().enumerate() {
7794 let Expr::Column(oc) = &order.expr else {
7795 return Ok(None);
7796 };
7797 if let Some(q) = &oc.qualifier
7798 && !q.eq_ignore_ascii_case(alias)
7799 {
7800 return Ok(None);
7801 }
7802 let Some(pos) = cols
7803 .iter()
7804 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
7805 else {
7806 return Ok(None);
7807 };
7808 if !matches!(
7809 cols[pos].ty,
7810 spg_storage::DataType::SmallInt
7811 | spg_storage::DataType::Int
7812 | spg_storage::DataType::BigInt
7813 ) {
7814 return Ok(None);
7815 }
7816 key_pos[slot] = pos;
7817 descs[slot] = order.desc;
7818 nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
7819 }
7820
7821 let sess = self.dml_session();
7822 let ctx = EvalContext::new(&cols, Some(alias))
7823 .with_catalog(self.active_catalog())
7824 .with_session(&sess);
7825 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7826 let columns: Vec<ColumnSchema> = projection
7827 .iter()
7828 .map(|p| {
7829 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7830 c.user_enum_type = p.user_enum_type.clone();
7831 c.mysql_fsp = p.mysql_fsp;
7832 c
7833 })
7834 .collect();
7835 let bound_pos: Vec<Option<usize>> = projection
7836 .iter()
7837 .map(|p| match &p.expr {
7838 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
7839 Ok(Some(pos)) => Some(pos),
7840 _ => None,
7841 },
7842 _ => None,
7843 })
7844 .collect();
7845 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7846 .where_
7847 .as_ref()
7848 .filter(|w| crate::eval::fully_compilable(w))
7849 .map(|w| crate::eval::compile_expr(w, &ctx));
7850
7851 // The same first-observable point the materialising planner fires,
7852 // placed after the gates so it fires exactly once: this lane runs
7853 // BEFORE that planner and would otherwise be a hole in the
7854 // panic-isolation and cancellation-race coverage rather than a
7855 // faster path through it.
7856 crate::injection_point!("planner_first_row_fetch", &stmt.from);
7857
7858 let mut eval_stack: Vec<Value<'static>> = Vec::new();
7859 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
7860 let mut budget = ByteBudget::new(self.max_query_bytes);
7861 let snapshot = self.current_snapshot();
7862 // Keys, a NULL bit per key slot, and the row. The bitmask keeps
7863 // the element small: a nullable key still costs one bit rather
7864 // than a second array.
7865 let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
7866
7867 for (ri, row) in table.rows().iter().enumerate() {
7868 if ri.is_multiple_of(256) {
7869 cancel.check()?;
7870 }
7871 if !table.is_row_visible(ri, &snapshot) {
7872 continue;
7873 }
7874 // The key comes from the STORED row, before projection: an
7875 // ORDER BY column need not appear in the select list.
7876 let mut keys = [0i64; MAX_KEYS];
7877 let mut nulls = 0u8;
7878 let mut keyed = true;
7879 for slot in 0..n_keys {
7880 match row.values.get(key_pos[slot]) {
7881 Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
7882 Some(Value::Int(v)) => keys[slot] = i64::from(*v),
7883 Some(Value::BigInt(v)) => keys[slot] = *v,
7884 Some(Value::Null) | None => nulls |= 1 << slot,
7885 // An integer column holding something else is a row
7886 // this lane cannot order; hand the whole query back
7887 // rather than guess at it.
7888 _ => {
7889 keyed = false;
7890 break;
7891 }
7892 }
7893 }
7894 if !keyed {
7895 return Ok(None);
7896 }
7897 if !Self::stream_filter_project(
7898 row,
7899 stmt.where_.as_ref(),
7900 compiled_where.as_ref(),
7901 &mut eval_stack,
7902 &projection,
7903 &bound_pos,
7904 &ctx,
7905 &mut values,
7906 )? {
7907 continue;
7908 }
7909 budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
7910 sorted.push((keys, nulls, core::mem::take(&mut values)));
7911 values.reserve(projection.len());
7912 }
7913
7914 sorted.sort_by(|a, b| {
7915 use core::cmp::Ordering;
7916 for slot in 0..n_keys {
7917 let bit = 1u8 << slot;
7918 let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
7919 (true, true) => Ordering::Equal,
7920 // Where the NULLs go is already decided — `nulls_first`
7921 // resolved DESC's default when it was read. Reversing
7922 // this for DESC as well would apply the direction
7923 // twice and put them at the wrong end.
7924 (true, false) => {
7925 if nulls_first[slot] {
7926 Ordering::Less
7927 } else {
7928 Ordering::Greater
7929 }
7930 }
7931 (false, true) => {
7932 if nulls_first[slot] {
7933 Ordering::Greater
7934 } else {
7935 Ordering::Less
7936 }
7937 }
7938 (false, false) => {
7939 let o = a.0[slot].cmp(&b.0[slot]);
7940 if descs[slot] { o.reverse() } else { o }
7941 }
7942 };
7943 if ord != Ordering::Equal {
7944 return ord;
7945 }
7946 }
7947 Ordering::Equal
7948 });
7949
7950 emit(crate::StreamItem::Header(&columns))?;
7951 let count = sorted.len();
7952 for (_, _, vals) in &sorted {
7953 emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
7954 }
7955 Ok(Some(count))
7956 }
7957
7958 fn try_spill_sorted_stream<F>(
7959 &self,
7960 stmt: &SelectStatement,
7961 from: &FromClause,
7962 cancel: CancelToken<'_>,
7963 emit: &mut F,
7964 ) -> Result<Option<usize>, EngineError>
7965 where
7966 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7967 {
7968 // The shapes `try_spill_sorted_scan` declines, plus the ones the
7969 // streaming executor does not carry (a LIMIT is already bounded
7970 // by a partial sort; the rest need the answer addressable).
7971 if !self.can_spill()
7972 || stmt.order_by.is_empty()
7973 || stmt.distinct
7974 || stmt.limit_with_ties
7975 || stmt.limit.is_some()
7976 || stmt.offset.is_some()
7977 || stmt.having.is_some()
7978 || stmt.group_by.is_some()
7979 || !stmt.unions.is_empty()
7980 || !from.joins.is_empty()
7981 || from.primary.lateral_subquery.is_some()
7982 || from.primary.unnest_expr.is_some()
7983 || from.primary.as_of_segment.is_some()
7984 || from.primary.generate_series_args.is_some()
7985 || select_has_window(stmt)
7986 || aggregate::uses_aggregate(stmt)
7987 {
7988 return Ok(None);
7989 }
7990 if stmt
7991 .items
7992 .iter()
7993 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7994 {
7995 return Ok(None);
7996 }
7997 // Everything `exec_bare_select_cancel` does before it scans runs
7998 // BELOW this path, so a statement claimed here skips it. Three of
7999 // those were missed on the way in and each was caught by a
8000 // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
8001 // ORDER BY 2` sorted happily instead of raising 42P10), the
8002 // cancellation check by another, the partition fan-out by the
8003 // differential corpus. What is reconciled, item by item: with-ties
8004 // needs ORDER BY (gated above), USING/NATURAL and RLS join
8005 // rewrites (joins gated above), the single-table RLS predicate
8006 // (the dispatcher declines a policy-subject table before this is
8007 // reached), the meta-view dispatch (those names are not in the
8008 // catalog, so the lookup below declines). These three are calls,
8009 // so the message and SQLSTATE are the ones the fall-back gives —
8010 // `select_has_window` above reads the select list and ORDER BY but
8011 // not WHERE, which is the case the third one covers.
8012 crate::orderby::check_order_by_legality(stmt)?;
8013 crate::orderby::check_order_by_positions(stmt)?;
8014 crate::window::reject_window_in_row_clauses(stmt)?;
8015 // A parent's rows are its children's. These walks scan the named
8016 // relation alone, so a partitioned or inherited parent comes back
8017 // short — and silently: the corpus caught `SELECT id FROM pr
8018 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8019 // parent's own rows instead of the partitions'. `ONLY` is exactly
8020 // the case that does not fan out, so it stays, which is the test
8021 // the FROM-clause fan-out itself makes.
8022 if !from.primary.only
8023 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8024 {
8025 return Ok(None);
8026 }
8027 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8028 return Ok(None);
8029 };
8030 // Cold-tier rows live outside `rows()`; this walk would drop
8031 // them silently, the same reason round 831's walk declines.
8032 if table.has_cold_rows_fast() {
8033 return Ok(None);
8034 }
8035
8036 let alias = from
8037 .primary
8038 .alias
8039 .as_deref()
8040 .unwrap_or(from.primary.name.as_str());
8041 let cols = table.schema().columns.clone();
8042 let sess = self.dml_session();
8043 let ctx = EvalContext::new(&cols, Some(alias))
8044 .with_catalog(self.active_catalog())
8045 .with_session(&sess);
8046 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8047 let order_by = stmt.order_by.clone();
8048 // The same one-shot resolution the general path does (round
8049 // 582): each ORDER BY column is bound once, not once per row.
8050 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8051 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8052 // Resolved BEFORE the scan, because it now decides what the sort
8053 // STORES and not just what it decodes (round 995).
8054 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8055
8056 let mut sorter = crate::extsort::ExternalSorter::new(
8057 self.temp_run_factory,
8058 self.session_work_mem_bytes(),
8059 cols.clone(),
8060 &descs,
8061 )
8062 .with_stats(&self.spill_stats)
8063 .with_pruned(&needed);
8064 let snapshot = self.current_snapshot();
8065 // One key buffer for the whole scan: `push` drains it and leaves
8066 // the capacity behind.
8067 let mut keys: Vec<OrderKey> = Vec::new();
8068 // r1024 — compile the predicate once for the scan.
8069 //
8070 // These two sorted-spill scans are the paths a single-table SELECT
8071 // with an ORDER BY takes, and they were the last row-returning ones
8072 // still walking the expression tree per row. r1023 did the
8073 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8074 // exactly this shape.
8075 //
8076 // Found from the profile's CALL TREE rather than its leaves. The
8077 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8078 // 261, `mod_op` 178 — and two attempts at reasoning out which
8079 // function asked for it were both wrong. The tree names the caller
8080 // chain, and it named this one.
8081 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8082 .where_
8083 .as_ref()
8084 .filter(|w| crate::eval::fully_compilable(w))
8085 .map(|w| crate::eval::compile_expr(w, &ctx));
8086 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8087 for (i, row) in table.scan_visible_from(0, &snapshot) {
8088 if i.is_multiple_of(256) {
8089 cancel.check()?;
8090 }
8091 if let Some(c) = &compiled_where {
8092 if !crate::eval::compiled::eval_compiled_pred(
8093 c,
8094 row,
8095 &ctx,
8096 &mut eval_stack,
8097 ctx.mysql_dialect,
8098 )? {
8099 continue;
8100 }
8101 } else if let Some(w) = &stmt.where_ {
8102 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8103 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8104 continue;
8105 }
8106 }
8107 keys.clear();
8108 crate::orderby::build_order_keys_bound(&order_by, &order_bound, row, &ctx, &mut keys)?;
8109 sorter.push(&mut keys, row)?;
8110 }
8111
8112 let columns: Vec<ColumnSchema> = projection
8113 .iter()
8114 .map(|p| {
8115 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8116 c.user_enum_type = p.user_enum_type.clone();
8117 c.mysql_fsp = p.mysql_fsp;
8118 c
8119 })
8120 .collect();
8121 emit(crate::StreamItem::Header(&columns))?;
8122
8123 let key_ctx = &ctx;
8124 let mut emitted_since_check = 0usize;
8125 let n = sorter.finish_each(
8126 |src, buf| {
8127 crate::orderby::build_order_keys_bound(&order_by, &order_bound, src, key_ctx, buf)
8128 },
8129 |src, values| {
8130 for p in &projection {
8131 values.push(
8132 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8133 );
8134 }
8135 Ok(())
8136 },
8137 |cells| {
8138 // The merge is the long half of a big sort, and the scan's
8139 // check above stops running once it ends: a cancelled
8140 // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
8141 // anyway. Same stride as the scan.
8142 emitted_since_check += 1;
8143 if emitted_since_check >= 256 {
8144 emitted_since_check = 0;
8145 cancel.check()?;
8146 }
8147 emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
8148 },
8149 )?;
8150 Ok(Some(n))
8151 }
8152
8153 /// One row of the single-table streaming walk: the WHERE test, the
8154 /// projection, the emit. Returns whether a row was emitted.
8155 ///
8156 /// v7.39 (round 970) — factored out because the walk now has two ways
8157 /// to reach a row, the sequential scan and an index seek's candidate
8158 /// positions, and both must do IDENTICALLY this. A copy in each is how
8159 /// two paths for one job drift; this file already carries the cost of
8160 /// that lesson twice (rounds 823 and 961, both resolvers).
8161 ///
8162 /// `#[inline]` so the scan loop keeps the shape round 957 measured it
8163 /// in — a shared hot path pays for a new abstraction whether or not it
8164 /// uses it, and this one is on the scan.
8165 #[inline]
8166 #[allow(clippy::too_many_arguments)]
8167 fn stream_filter_project(
8168 row: &spg_storage::Row<'static>,
8169 where_: Option<&Expr>,
8170 // r1023 — the same WHERE, compiled once by the caller. `None` means
8171 // the expression did not qualify and `where_` is evaluated as before.
8172 compiled_where: Option<&crate::eval::CompiledExpr>,
8173 eval_stack: &mut Vec<Value<'static>>,
8174 projection: &[ProjectedItem],
8175 bound_pos: &[Option<usize>],
8176 ctx: &crate::eval::EvalContext<'_>,
8177 values: &mut Vec<Value<'static>>,
8178 ) -> Result<bool, EngineError> {
8179 // r1023 — this scan ran its predicate through the TREE INTERPRETER,
8180 // once per row, and it was the only row-returning path that did.
8181 // The aggregate path, `table_access`, and the PK walker all compile
8182 // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
8183 // server's live samples were `eval_expr` 99, `apply_binary` 81,
8184 // `mod_op` 29 — the interpreter, not delivery.
8185 //
8186 // The arithmetic accounted for it exactly. Over the wire, the same
8187 // filter costs 6.375 ms returning rows and 0.679 ms counting them;
8188 // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
8189 // which is what an interpreted predicate costs against the compiled
8190 // lane's 11.7. It was named "delivery after a filter" before this
8191 // profile, and it was never delivery.
8192 if let Some(c) = compiled_where {
8193 if !crate::eval::compiled::eval_compiled_pred(
8194 c,
8195 row,
8196 ctx,
8197 eval_stack,
8198 ctx.mysql_dialect,
8199 )? {
8200 return Ok(false);
8201 }
8202 } else if let Some(w) = where_ {
8203 let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
8204 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8205 return Ok(false);
8206 }
8207 }
8208 values.clear();
8209 for (p, bound) in projection.iter().zip(bound_pos) {
8210 values.push(match bound {
8211 Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
8212 None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
8213 });
8214 }
8215 Ok(true)
8216 }
8217
8218 /// The same filter and projection, then emit. Split from
8219 /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
8220 /// before it can emit them — a sort — runs the identical predicate and
8221 /// projection rather than a second copy of them.
8222 #[allow(clippy::too_many_arguments)]
8223 fn stream_project_row<F>(
8224 row: &spg_storage::Row<'static>,
8225 where_: Option<&Expr>,
8226 compiled_where: Option<&crate::eval::CompiledExpr>,
8227 eval_stack: &mut Vec<Value<'static>>,
8228 projection: &[ProjectedItem],
8229 bound_pos: &[Option<usize>],
8230 ctx: &crate::eval::EvalContext<'_>,
8231 values: &mut Vec<Value<'static>>,
8232 emit: &mut F,
8233 ) -> Result<bool, EngineError>
8234 where
8235 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8236 {
8237 if !Self::stream_filter_project(
8238 row,
8239 where_,
8240 compiled_where,
8241 eval_stack,
8242 projection,
8243 bound_pos,
8244 ctx,
8245 values,
8246 )? {
8247 return Ok(false);
8248 }
8249 emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
8250 Ok(true)
8251 }
8252
8253 fn try_stream_single_table<F>(
8254 &self,
8255 stmt: &SelectStatement,
8256 from: &FromClause,
8257 cancel: CancelToken<'_>,
8258 emit: &mut F,
8259 ) -> Result<Option<usize>, EngineError>
8260 where
8261 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8262 {
8263 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8264 return Ok(None);
8265 };
8266 // Cold-tier rows live outside `rows()`; the materialising fallback
8267 // covers both tiers and this walk would silently drop them.
8268 if table.has_cold_rows_fast() {
8269 return Ok(None);
8270 }
8271 let alias = from
8272 .primary
8273 .alias
8274 .as_deref()
8275 .unwrap_or(from.primary.name.as_str());
8276 let cols = table.schema().columns.clone();
8277 let sess = self.dml_session();
8278 let ctx = EvalContext::new(&cols, Some(alias))
8279 .with_catalog(self.active_catalog())
8280 .with_session(&sess);
8281 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8282
8283 let columns: Vec<ColumnSchema> = projection
8284 .iter()
8285 .map(|p| {
8286 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8287 c.user_enum_type = p.user_enum_type.clone();
8288 c.mysql_fsp = p.mysql_fsp;
8289 c
8290 })
8291 .collect();
8292 emit(crate::StreamItem::Header(&columns))?;
8293
8294 // v7.37 (round 957) — resolve each bare-column projection ONCE
8295 // instead of once per row. `find_column_pos`-style resolution is a
8296 // linear walk of the schema comparing column-name strings, and the
8297 // row loop below ran it for every cell of every row: measured at
8298 // 400k rows, binding it out of the loop took `SELECT pad` from
8299 // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
8300 //
8301 // ORDER BY has bound its keys this way since round 582
8302 // (`order_by_bound_positions`); the projection never did.
8303 //
8304 // `locate_column` is the same resolution `resolve_column` performs,
8305 // returning the site instead of the value, so the two cannot drift
8306 // apart the way a second hand-written resolver would. Anything it
8307 // declines — an expression, a whole-row reference, a name that does
8308 // not resolve — binds to `None` and takes the general path below,
8309 // errors included, so an empty table still reports nothing rather
8310 // than raising at bind time.
8311 let bound_pos: Vec<Option<usize>> = projection
8312 .iter()
8313 .map(|p| match &p.expr {
8314 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8315 Ok(Some(pos)) => Some(pos),
8316 _ => None,
8317 },
8318 _ => None,
8319 })
8320 .collect();
8321
8322 // One snapshot for the whole scan, as the materialising path takes.
8323 let snapshot = self.current_snapshot();
8324
8325 // v7.39 (round 970) — ask the indices BEFORE walking the table.
8326 //
8327 // This walk had no index step at all, and it is preferred over the
8328 // materialising path, which does have one (`pick_indexed_rows` ->
8329 // `try_index_seek`). So a primary-key point lookup — the commonest
8330 // statement there is — read every row: measured on 500k rows,
8331 // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
8332 // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
8333 // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
8334 //
8335 // The control that named it: `... OFFSET 0` — semantically the same
8336 // query — answered in 0.159 ms, because OFFSET is one of the shape
8337 // gates that declines this walk and sends the statement to the path
8338 // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
8339 // no semantics in common; what they share is making this function
8340 // stand down.
8341 //
8342 // The seek only NARROWS: every candidate still goes through the
8343 // full WHERE below, exactly as the mutation paths use it, so a
8344 // partial index match cannot change an answer. Positions come back
8345 // already visibility-filtered and already capped at a quarter of the
8346 // table (round 490), so a seek can never cost more than the scan it
8347 // replaces, and `None` means "walk the table" as before.
8348 //
8349 // Sorted because the scan would have produced table order and the
8350 // index produces key order. Without an ORDER BY neither is promised,
8351 // but a walk that silently reorders its answer when an index happens
8352 // to exist is a difference nobody asked for.
8353 let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
8354 crate::index_access::try_index_seek_positions(w, &cols, table, alias, &snapshot)
8355 });
8356
8357 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8358 // r1023 — compile the predicate once for the whole scan. Same gate
8359 // every other path uses: `fully_compilable` or keep the interpreter,
8360 // so a shape the VM cannot take answers exactly as it did before.
8361 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8362 .where_
8363 .as_ref()
8364 .filter(|w| crate::eval::fully_compilable(w))
8365 .map(|w| crate::eval::compile_expr(w, &ctx));
8366 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8367 let mut count: usize = 0;
8368 match seek_positions {
8369 Some(mut positions) => {
8370 positions.sort_unstable();
8371 for (n, pos) in positions.into_iter().enumerate() {
8372 if n.is_multiple_of(256) {
8373 cancel.check()?;
8374 }
8375 let Some(row) = table.rows().get(pos) else {
8376 continue;
8377 };
8378 if Self::stream_project_row(
8379 row,
8380 stmt.where_.as_ref(),
8381 compiled_where.as_ref(),
8382 &mut eval_stack,
8383 &projection,
8384 &bound_pos,
8385 &ctx,
8386 &mut values,
8387 emit,
8388 )? {
8389 count += 1;
8390 }
8391 }
8392 }
8393 None => {
8394 for (i, row) in table.scan_visible_from(0, &snapshot) {
8395 if i.is_multiple_of(256) {
8396 cancel.check()?;
8397 }
8398 if Self::stream_project_row(
8399 row,
8400 stmt.where_.as_ref(),
8401 compiled_where.as_ref(),
8402 &mut eval_stack,
8403 &projection,
8404 &bound_pos,
8405 &ctx,
8406 &mut values,
8407 emit,
8408 )? {
8409 count += 1;
8410 }
8411 }
8412 }
8413 }
8414 Ok(Some(count))
8415 }
8416
8417 pub(crate) fn try_exec_joined_streaming<F>(
8418 &self,
8419 stmt: &SelectStatement,
8420 cancel: CancelToken<'_>,
8421 emit: &mut F,
8422 ) -> Result<Option<usize>, EngineError>
8423 where
8424 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8425 {
8426 // Shape gates — keep the streamable surface narrow on
8427 // purpose. The fall-back path still handles everything else.
8428 let Some(from) = &stmt.from else {
8429 return Ok(None);
8430 };
8431 // v7.37 (round 830) — decline anything a row-security policy binds
8432 // for this session. Policies are injected in
8433 // `exec_bare_select_cancel`, below this path, so a statement claimed
8434 // here would read the table unfiltered: measured, `SELECT val FROM
8435 // sec` returned all three rows to a session whose policy allows two,
8436 // while `SELECT upper(val) FROM sec` — declined by the shape gates
8437 // and so materialised — returned the correct two.
8438 //
8439 // Declining sends it to the path that enforces. Teaching this one to
8440 // inject the predicate itself would keep the streaming benefit for
8441 // RLS tables and is the better end state; it is not what a
8442 // correctness fix should carry, and the fall-back is exactly as
8443 // correct, only slower.
8444 if self.select_reads_policy_subject_table(stmt) {
8445 return Ok(None);
8446 }
8447 // v7.39 (round 790) — single-table SELECTs stream too. This
8448 // gate said "joins only" because the path was written for
8449 // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
8450 // fell to the materialising fallback, which builds the whole
8451 // `Vec<Row<'static>>` and only then iterates it. Measured on
8452 // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
8453 // reached through a one-row JOIN — 2.6x, purely for lacking a
8454 // join. The deferred-join structure handles one source as the
8455 // degenerate stride-1 case, so the walk below is unchanged.
8456 let _single_table = from.joins.is_empty();
8457 // An ORDER BY that the bounded sort can serve streams; everything
8458 // else still falls to the materialising fallback below.
8459 // r1025 — an ordering the index already holds needs no sort at all.
8460 // Tried before the spill sort, which is the path it replaces.
8461 if !stmt.order_by.is_empty()
8462 && from.joins.is_empty()
8463 && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
8464 {
8465 return Ok(Some(n));
8466 }
8467 if !stmt.order_by.is_empty()
8468 && from.joins.is_empty()
8469 && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
8470 {
8471 return Ok(Some(n));
8472 }
8473 // r1031 — integer keys carried inline instead of an `OrderKey`
8474 // vector per row. Tried AFTER the spill sort on purpose: this lane
8475 // buffers the whole answer, so anything the spill path would take
8476 // must keep taking it rather than be turned back into an in-memory
8477 // sort that answers with a budget error.
8478 if !stmt.order_by.is_empty()
8479 && from.joins.is_empty()
8480 && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
8481 {
8482 return Ok(Some(n));
8483 }
8484 if !stmt.order_by.is_empty()
8485 || stmt.limit.is_some()
8486 || stmt.offset.is_some()
8487 || stmt.having.is_some()
8488 || stmt.group_by.is_some()
8489 || stmt.distinct
8490 || !stmt.unions.is_empty()
8491 || stmt.limit_with_ties
8492 {
8493 return Ok(None);
8494 }
8495 if aggregate::uses_aggregate(stmt) {
8496 return Ok(None);
8497 }
8498 // No window / SRF on the streaming path.
8499 if select_has_window(stmt) {
8500 return Ok(None);
8501 }
8502 if stmt
8503 .items
8504 .iter()
8505 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8506 {
8507 return Ok(None);
8508 }
8509 // v7.37 (round 831) — a joinless FROM over a plain stored table
8510 // never needs the deferred structure, and building one costs the
8511 // whole table. `materialise_table_ref_filtered` clones every row
8512 // into a `Vec<Row<'static>>` before anything is filtered or
8513 // projected, so peak cost tracks the TABLE, not the result:
8514 // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
8515 // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
8516 // projection saving nothing, while an arithmetic projection — which
8517 // the shape gates decline, so it materialises through the ordinary
8518 // executor — cost +21 MB.
8519 //
8520 // Scanning in batches and releasing each one is what `cursor_fill`
8521 // already does for a lazy cursor, and it is the same walk: resume
8522 // from a slot, take visible rows, evaluate, hand them over, drop
8523 // them. Round 800's finding stands and is why this reads rows OUT
8524 // rather than seeding the join by index — touching the stored
8525 // `PersistentVec` in place makes the whole table resident, which is
8526 // worse than the copy. Each batch is copied, then freed.
8527 if from.joins.is_empty()
8528 && from.primary.unnest_expr.is_none()
8529 && from.primary.lateral_subquery.is_none()
8530 && from.primary.as_of_segment.is_none()
8531 && from.primary.generate_series_args.is_none()
8532 && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
8533 {
8534 return Ok(Some(n));
8535 }
8536 // Build the deferred join under the regular byte budget.
8537 let mut budget = ByteBudget::new(self.max_query_bytes);
8538 let deferred = {
8539 let mut needed = alloc::collections::BTreeSet::new();
8540 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
8541 self.build_joined_filtered_rows(
8542 from,
8543 stmt.where_.as_ref(),
8544 cancel,
8545 if prunable { Some(&needed) } else { None },
8546 &mut budget,
8547 )?
8548 };
8549 let combined_schema = &deferred.combined_schema;
8550 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
8551 // `::regclass` / enum cast in a joined projection or HAVING needs it.
8552 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
8553 // the same predicate the unjoined shape carries.
8554 let joined_sess = self.dml_session();
8555 let ctx = EvalContext::new(combined_schema, None)
8556 .with_catalog(self.active_catalog())
8557 .with_session(&joined_sess);
8558 let projection =
8559 build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
8560 // Every projection item must be a bound qualified column —
8561 // anything that needs `eval_expr_with_correlated` keeps the
8562 // materialising path.
8563 let bound_pos = |e: &Expr| -> Option<usize> {
8564 match e {
8565 // v7.39 (round 822) — an UNQUALIFIED column resolves here
8566 // too. The `qualifier.is_some()` guard this replaces meant
8567 // `SELECT pad FROM big` — the commonest projection there is
8568 // — never reached the streaming walk: it fell out at this
8569 // gate and re-ran on the materialising path, after the
8570 // deferred join structure had already been built and paid
8571 // for. Measured (round 821, statement_timeout=120 over 400k
8572 // rows): `big.pad` and `b.pad` streamed and cancelled at
8573 // ~65k rows in 0.14 s, while bare `pad` ran to completion in
8574 // 0.80 s with the timeout never consulted. `find_column_pos`
8575 // has always handled the unqualified case (it falls through
8576 // to a by-name match), so the guard narrowed the gate for no
8577 // reason it recorded.
8578 Expr::Column(c) => eval::find_column_pos(c, &ctx),
8579 _ => None,
8580 }
8581 };
8582 let proj_decomposed: Vec<(usize, usize)> = {
8583 let mut out = Vec::with_capacity(projection.len());
8584 for p in &projection {
8585 let Some(abs) = bound_pos(&p.expr) else {
8586 return Ok(None);
8587 };
8588 let Some(k) = deferred
8589 .offsets
8590 .partition_point(|&o| o <= abs)
8591 .checked_sub(1)
8592 else {
8593 return Ok(None);
8594 };
8595 out.push((k, abs - deferred.offsets[k]));
8596 }
8597 out
8598 };
8599 // Emit columns once.
8600 let columns: Vec<ColumnSchema> = projection
8601 .iter()
8602 // v7.39 (read01 round 54) — keep the column's enum identity through
8603 // the projection (it lives outside the DataType lattice), or a
8604 // derived table / UNION / windowed result forgets it and any outer
8605 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
8606 .map(|p| {
8607 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8608 c.user_enum_type = p.user_enum_type.clone();
8609 c.mysql_fsp = p.mysql_fsp;
8610 c
8611 })
8612 .collect();
8613 emit(crate::StreamItem::Header(&columns))?;
8614 let sources_ref = &deferred.sources;
8615 let stride = deferred.stride;
8616 let survivors_ref = &deferred.survivors;
8617 let n_surv = if stride == 0 {
8618 0
8619 } else {
8620 survivors_ref.len() / stride
8621 };
8622 // Reused per-row cell-ref scratch — pushes are zero-alloc
8623 // after the first row.
8624 let null_value = Value::Null;
8625 let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
8626 let mut count: usize = 0;
8627 for surv_i in 0..n_surv {
8628 if surv_i.is_multiple_of(256) {
8629 cancel.check()?;
8630 }
8631 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
8632 cell_refs.clear();
8633 for &(k, col_in_src) in &proj_decomposed {
8634 let ri = tuple[k];
8635 let v: &Value = if ri == usize::MAX {
8636 &null_value
8637 } else {
8638 sources_ref[k]
8639 .get(ri)
8640 .and_then(|r| r.values.get(col_in_src))
8641 .unwrap_or(&null_value)
8642 };
8643 cell_refs.push(v);
8644 }
8645 emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
8646 count += 1;
8647 }
8648 Ok(Some(count))
8649 }
8650
8651 fn exec_joined_select(
8652 &self,
8653 stmt: &SelectStatement,
8654 from: &FromClause,
8655 cancel: CancelToken<'_>,
8656 ) -> Result<QueryResult, EngineError> {
8657 // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
8658 // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
8659 // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
8660 // FROM B WHERE B.k = A.k)` into
8661 // SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
8662 // WHERE B.k IS NULL
8663 // The general join executor builds a hash, probes every outer
8664 // tuple, materialises (left_padded_with_null) for every miss,
8665 // then runs the aggregate over the result set. For COUNT(*) we
8666 // only need the count — skip the tuple materialisation. Build
8667 // a HashSet of B's unique join values, scan A's PK index, and
8668 // increment the counter on each miss. PG's Merge Anti-Join
8669 // does roughly this; ours becomes a simple HashSet probe.
8670 if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
8671 return Ok(out);
8672 }
8673 // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
8674 // When ORDER BY is on an indexed primary column, walking the
8675 // btree in the requested direction lets the streamer break
8676 // after `LIMIT + OFFSET` survivors without ever materialising
8677 // the rest of the join — the 80 ms `mailrs_prod_not_exists`
8678 // plateau is exactly this shape.
8679 if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
8680 return Ok(out);
8681 }
8682 // v7.30.3 (mailrs round-26) — the bounded single-join path
8683 // first; peak memory scales with LIMIT instead of the table.
8684 if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
8685 return Ok(out);
8686 }
8687 // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
8688 // WHERE materialisation to the shared helper so the LATERAL
8689 // / UNNEST / regular-catalog paths route through one place.
8690 // (`build_joined_filtered_rows` carries LATERAL support as
8691 // of Phase 3.P0-41.) Downstream we still handle aggregate /
8692 // projection / ORDER BY / DISTINCT / LIMIT inline because
8693 // those depend on the SelectStatement's items list.
8694 let mut budget = ByteBudget::new(self.max_query_bytes);
8695 let deferred = {
8696 let mut needed = alloc::collections::BTreeSet::new();
8697 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
8698 self.build_joined_filtered_rows(
8699 from,
8700 stmt.where_.as_ref(),
8701 cancel,
8702 if prunable { Some(&needed) } else { None },
8703 &mut budget,
8704 )?
8705 };
8706 let combined_schema = &deferred.combined_schema;
8707 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
8708 // `::regclass` / enum cast in a joined projection or HAVING needs it.
8709 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
8710 // the same predicate the unjoined shape carries.
8711 let joined_sess = self.dml_session();
8712 let ctx = EvalContext::new(combined_schema, None)
8713 .with_catalog(self.active_catalog())
8714 .with_session(&joined_sess);
8715 // Aggregate path: handle GROUP BY / aggregate calls over the
8716 // joined+filtered rows.
8717 if aggregate::uses_aggregate(stmt) {
8718 // v7.32 (P4 borrow channel, increment 2) — borrow each
8719 // surviving join tuple as a RowRef::Tuple; the aggregate
8720 // engine reads source cells by reference (bound fast path =
8721 // zero clone) instead of consuming materialised combined
8722 // Rows. This is where the +211k materialise_tuple_vals
8723 // clones disappear for the join+aggregate shape.
8724 let refs = deferred.row_refs();
8725 // v7.29 — a per-query memo so correlated scalar
8726 // subqueries batch-evaluate once (group map) instead of
8727 // executing per group.
8728 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
8729 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
8730 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
8731 .map_err(|err| match err {
8732 EngineError::Eval(ev) => ev,
8733 other => eval::EvalError::TypeMismatch {
8734 detail: alloc::format!("{other}"),
8735 },
8736 })
8737 };
8738 let agg = aggregate::run(
8739 stmt,
8740 crate::join::AggRows::Refs(&refs),
8741 combined_schema,
8742 None,
8743 Some(&agg_correlated),
8744 self.parallel_runner.0.as_deref(),
8745 Some(self.active_catalog()),
8746 Some(self),
8747 )?;
8748 return self.finish_agg_result(agg, stmt, cancel);
8749 }
8750
8751 let projection =
8752 build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
8753 // v7.39 (round 734) — a set-returning projection over a JOIN.
8754 // This executor's projection loop treats every item as a scalar,
8755 // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
8756 // "function unnest(integer[]) does not exist" where PG expands
8757 // it. The row-set executor already carries the full SRF pipeline
8758 // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
8759 // sharding): materialise the joined survivors and hand over. The
8760 // WHERE is cleared — the join already applied it, and combined
8761 // columns resolve identically in both executors.
8762 if !self.srf_target_idxs(&projection).is_empty() {
8763 let refs = deferred.row_refs();
8764 let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
8765 let mut s2 = stmt.clone();
8766 s2.where_ = None;
8767 let schema = combined_schema.clone();
8768 return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
8769 }
8770 // v7.33 (P4 borrow channel, increment 3) — project directly off
8771 // the deferred row-index tuples instead of materialising an
8772 // intermediate combined Row per survivor. A bound qualified
8773 // column is read by reference (`RowRef::get` → `tuple_value`) and
8774 // cloned ONCE into the output row; the old `materialise()` (a full
8775 // combined Row plus a source→intermediate clone per referenced
8776 // cell, for every survivor) is gone. A row materialises on demand
8777 // only when a projection or ORDER BY expression needs the eval
8778 // path (subquery / function / arithmetic / unqualified column).
8779 // Same bind-once classification the aggregate input fast path uses
8780 // (`accumulate_groups`), reading the same `tuple_value` mapping the
8781 // differential gate already covers.
8782 let refs = deferred.row_refs();
8783 let bound_pos = |e: &Expr| -> Option<usize> {
8784 match e {
8785 Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
8786 _ => None,
8787 }
8788 };
8789 let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
8790 let all_proj_bound = proj_pos.iter().all(Option::is_some);
8791 // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
8792 // pre-decompose each bound projection position into
8793 // `(source_k, col_in_source)` so the per-row column read
8794 // skips the per-cell `tuple_value` partition_point + slice
8795 // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
8796 // calls) that walk dominated; this version reaches into
8797 // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
8798 let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
8799 .iter()
8800 .map(|p| {
8801 p.and_then(|abs| {
8802 let k = deferred
8803 .offsets
8804 .partition_point(|&o| o <= abs)
8805 .checked_sub(1)?;
8806 Some((k, abs - deferred.offsets[k]))
8807 })
8808 })
8809 .collect();
8810 // v7.39 (round 962) — which projection items are whole-row
8811 // references, and to which join source. The test is
8812 // `locate_column` declining the name, which is the SAME resolver
8813 // the evaluation path uses, so this cannot drift from it: a real
8814 // column carrying an alias's name resolves to a position and is
8815 // not reported here. The source index comes from the alias
8816 // prefix, the way the combined schema names its columns.
8817 let whole_row_src: Vec<Option<usize>> = projection
8818 .iter()
8819 .map(|p| {
8820 let Expr::Column(c) = &p.expr else {
8821 return None;
8822 };
8823 if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
8824 return None;
8825 }
8826 let prefix = alloc::format!("{name}.", name = c.name);
8827 let abs = deferred
8828 .combined_schema
8829 .iter()
8830 .position(|s| s.name.starts_with(&prefix))?;
8831 deferred
8832 .offsets
8833 .partition_point(|&o| o <= abs)
8834 .checked_sub(1)
8835 })
8836 .collect();
8837 // ORDER BY (when present) still evaluates against a materialised
8838 // Row — keep the order-key encoder correct rather than fork it.
8839 let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
8840 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
8841 let mut proj_memo = memoize::MemoizeCache::default();
8842 let sources_ref = &deferred.sources;
8843 let stride = deferred.stride;
8844 let survivors_ref = &deferred.survivors;
8845 let n_surv = survivors_ref.len() / stride.max(1);
8846 // v7.38 (read01 B8) — streaming top-N budget (see the sibling
8847 // single-table path). Bounds this JOIN projection's accumulator
8848 // to O(keep) for `ORDER BY … LIMIT k`.
8849 let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
8850 && !stmt.distinct
8851 && !stmt.limit_with_ties
8852 && !self.env_cfg().disable_topk
8853 {
8854 stmt.limit_literal().and_then(|l| {
8855 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
8856 (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
8857 })
8858 } else {
8859 None
8860 };
8861 // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
8862 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
8863 hashbrown::HashMap::new();
8864 let distinct_hb = hashbrown::DefaultHashBuilder::default();
8865 for surv_i in 0..n_surv {
8866 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
8867 let row = &refs[surv_i];
8868 let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
8869 Some(row.as_row())
8870 } else {
8871 None
8872 };
8873 let mut values = Vec::with_capacity(projection.len());
8874 for (i, p) in projection.iter().enumerate() {
8875 if let Some((k, col_in_src)) = proj_decomposed[i] {
8876 // v7.36 — direct (source_k, col) lookup, no
8877 // partition_point. tuple[k] is the row index in
8878 // sources[k]; LEFT-NULL slots are `usize::MAX`.
8879 let ri = tuple[k];
8880 let v: Value<'static> = if ri == usize::MAX {
8881 Value::Null
8882 } else {
8883 sources_ref[k]
8884 .get(ri)
8885 .and_then(|r| r.values.get(col_in_src))
8886 .cloned()
8887 .map(Value::into_owned)
8888 .unwrap_or(Value::Null)
8889 };
8890 values.push(v);
8891 } else if let Some(pos) = proj_pos[i] {
8892 // Bound but couldn't decompose (shouldn't normally
8893 // happen — keep as a safe path).
8894 values.push(
8895 row.get(pos)
8896 .cloned()
8897 .map(Value::into_owned)
8898 .unwrap_or(Value::Null),
8899 );
8900 } else if let Some(k) = whole_row_src[i]
8901 && tuple[k] == usize::MAX
8902 {
8903 // v7.39 (round 962) — a whole-row reference to a side
8904 // an OUTER join null-extended is NULL, not a
8905 // composite whose fields are all NULL. PG18.4 answers
8906 // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
8907 // an empty cell; round 961 answered `(,)`.
8908 //
8909 // The evaluator below cannot tell the two apart: it
8910 // reads the MATERIALISED combined row, where a
8911 // null-extended side is indistinguishable from a real
8912 // row whose every column is NULL — and that row is
8913 // `(,)` in PG too, so guessing by "all fields NULL"
8914 // would trade one wrong answer for another. The
8915 // tuple, which is still in hand here, does know:
8916 // `usize::MAX` is the sentinel the join writes for
8917 // exactly this.
8918 values.push(Value::Null);
8919 } else {
8920 // Eval path — `materialised` is Some whenever any
8921 // projection item is non-bound (need_eval_row true).
8922 // v7.24 (round-16 B) — select-list subqueries under a
8923 // JOIN go through the correlated-aware evaluator too.
8924 let mrow = materialised.as_deref().expect("materialised for eval");
8925 values.push(self.eval_expr_with_correlated(
8926 &p.expr,
8927 mrow,
8928 &ctx,
8929 cancel,
8930 Some(&mut proj_memo),
8931 )?);
8932 }
8933 }
8934 let out_row = Row::new(values);
8935 // v7.37.16 — streaming DISTINCT (see the scan-path twin):
8936 // probe on the projected row; duplicates skip the
8937 // build_order_keys eval and never enter `tagged`.
8938 if stmt.distinct {
8939 let bucket = seen_distinct
8940 .entry(norm_hash_row(&out_row, &distinct_hb, ctx.mysql_dialect))
8941 .or_default();
8942 if bucket
8943 .iter()
8944 .any(|i| row_eq_norm(&tagged[i].1, &out_row, ctx.mysql_dialect))
8945 {
8946 continue;
8947 }
8948 bucket.push(tagged.len());
8949 }
8950 let order_keys = if stmt.order_by.is_empty() {
8951 Vec::new()
8952 } else {
8953 let mrow = materialised.as_deref().expect("materialised for order by");
8954 build_order_keys(&stmt.order_by, mrow, &ctx)?
8955 };
8956 budget.charge(approx_row_bytes(&out_row))?;
8957 tagged.push((order_keys, out_row));
8958 if let Some((k, descs)) = &topk_stream {
8959 topk_trim(&mut tagged, *k, descs);
8960 }
8961 }
8962 if !stmt.order_by.is_empty() {
8963 // v7.38 元机制 D acceptor — see other call site above.
8964 let keep = if self.env_cfg().disable_topk {
8965 None
8966 } else {
8967 stmt.limit_literal()
8968 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
8969 };
8970 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
8971 // v7.39 (round 688) — the join's ORDER BY resolves its keys
8972 // against `ctx`, which is built from `build_combined_schema`, so
8973 // this is where a declared collation reaches the sort. There was
8974 // exactly ONE resolver call in the engine before this — the
8975 // single-table scan's — which is why every other shape sorted by
8976 // bytes no matter what the schemas carried.
8977 let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
8978 crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
8979 }
8980 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
8981 apply_offset_and_limit(
8982 &mut output_rows,
8983 stmt.offset_literal(),
8984 stmt.limit_literal(),
8985 );
8986 let columns: Vec<ColumnSchema> = projection
8987 .into_iter()
8988 .map(|p| {
8989 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
8990 c.user_enum_type = p.user_enum_type;
8991 c.collation_name = p.collation_name;
8992 c.mysql_fsp = p.mysql_fsp;
8993 c
8994 })
8995 .collect();
8996 Ok(QueryResult::Rows {
8997 columns,
8998 rows: output_rows,
8999 })
9000 }
9001}
9002
9003impl Engine {
9004 /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
9005 /// by id, decodes each row body against the table's current
9006 /// schema, applies the SELECT's projection + optional WHERE +
9007 /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
9008 /// / ORDER BY are unsupported on this path (STABILITY carve-
9009 /// out); operators wanting them should restore the segment
9010 /// into a regular table first.
9011 fn exec_select_as_of_segment(
9012 &self,
9013 stmt: &SelectStatement,
9014 from: &spg_sql::ast::FromClause,
9015 segment_id: u32,
9016 ) -> Result<QueryResult, EngineError> {
9017 // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
9018 // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
9019 if !from.joins.is_empty()
9020 || stmt.group_by.is_some()
9021 || stmt.having.is_some()
9022 || !stmt.unions.is_empty()
9023 || !stmt.order_by.is_empty()
9024 || stmt.offset.is_some()
9025 || stmt.distinct
9026 || aggregate::uses_aggregate(stmt)
9027 {
9028 return Err(EngineError::Unsupported(
9029 "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
9030 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
9031 .into(),
9032 ));
9033 }
9034 let table = self
9035 .active_catalog()
9036 .get(&from.primary.name)
9037 .ok_or_else(|| StorageError::TableNotFound {
9038 name: from.primary.name.clone(),
9039 })?;
9040 let schema = table.schema().clone();
9041 let schema_cols = &schema.columns;
9042 let alias = from
9043 .primary
9044 .alias
9045 .as_deref()
9046 .unwrap_or(from.primary.name.as_str());
9047 let ctx = self.ev_ctx(schema_cols, Some(alias));
9048 let seg = self
9049 .active_catalog()
9050 .cold_segment(segment_id)
9051 .ok_or_else(|| {
9052 EngineError::Unsupported(alloc::format!(
9053 "AS OF SEGMENT: cold segment {segment_id} not registered"
9054 ))
9055 })?;
9056 let mut out_rows: Vec<Row<'static>> = Vec::new();
9057 let mut limit_remaining: Option<usize> =
9058 stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
9059 for (_key, body) in seg.scan() {
9060 let (row, _consumed) =
9061 spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
9062 .map_err(EngineError::Storage)?;
9063 if let Some(where_expr) = &stmt.where_ {
9064 let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
9065 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9066 continue;
9067 }
9068 }
9069 // Projection.
9070 let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
9071 out_rows.push(projected);
9072 if let Some(rem) = limit_remaining.as_mut() {
9073 if *rem == 0 {
9074 out_rows.pop();
9075 break;
9076 }
9077 *rem -= 1;
9078 }
9079 }
9080 // Output column schema: derive from SELECT items.
9081 let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
9082 Ok(QueryResult::Rows {
9083 columns,
9084 rows: out_rows,
9085 })
9086 }
9087
9088 /// v6.10.2 — simple-path WHERE eval that doesn't go through
9089 /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
9090 /// scan paths predicate against a snapshot frozen segment, no
9091 /// cross-row state.
9092 fn eval_expr_simple(
9093 &self,
9094 expr: &Expr,
9095 row: &Row<'static>,
9096 ctx: &EvalContext,
9097 ) -> Result<Value<'static>, EngineError> {
9098 let cancel = CancelToken::none();
9099 self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
9100 }
9101}
9102
9103// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
9104
9105/// One row-producing projection: an expression to evaluate, the resulting
9106/// column's user-visible name, its inferred type, and nullability.
9107#[derive(Debug, Clone)]
9108pub(crate) struct ProjectedItem {
9109 pub(crate) expr: Expr,
9110 pub(crate) output_name: String,
9111 pub(crate) ty: DataType,
9112 pub(crate) nullable: bool,
9113 /// v7.39 (read01 round 54) — a projected enum column keeps its enum
9114 /// identity. Enum-ness lives outside the DataType lattice (the value is a
9115 /// Text), so a projection that dropped this made the RESULT schema forget
9116 /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
9117 /// that schema, silently fell back to TEXT order instead of member order.
9118 pub(crate) user_enum_type: Option<String>,
9119 /// v7.39 (round 425) — a projected MySQL temporal column keeps its
9120 /// declared fractional-seconds precision, so the renderer can pad to
9121 /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
9122 /// a whole second). Like `user_enum_type` this lives outside the
9123 /// DataType lattice, so a projection that dropped it made the RESULT
9124 /// schema forget how wide the fraction should print.
9125 pub(crate) mysql_fsp: Option<u8>,
9126 /// v7.39 (round 688) — and its declared collation, the third thing to
9127 /// live outside the DataType lattice and the third to be lost the same
9128 /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
9129 /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
9130 /// projection rebuilt the output column and the ORDER BY resolves
9131 /// against THAT schema.
9132 pub(crate) collation_name: Option<String>,
9133}
9134
9135/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
9136/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
9137/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
9138/// the spec's "two NULLs are not distinct"; the second is a tolerated
9139/// quirk for v1 (no NaN literals are reachable from the SQL surface).
9140/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
9141fn expr_is_aggregate_call(e: &Expr) -> bool {
9142 match e {
9143 Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
9144 Expr::AggregateOrdered { .. } => true,
9145 _ => false,
9146 }
9147}
9148
9149/// Collect distinct top-level aggregate call expressions (dedup by value). Does
9150/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
9151/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
9152/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
9153/// than today — never a regression on a working query).
9154fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
9155 if expr_is_aggregate_call(e) {
9156 if !out.iter().any(|x| x == e) {
9157 out.push(e.clone());
9158 }
9159 return;
9160 }
9161 match e {
9162 Expr::Binary { lhs, rhs, .. } => {
9163 collect_agg_exprs(lhs, out);
9164 collect_agg_exprs(rhs, out);
9165 }
9166 Expr::Unary { expr, .. }
9167 | Expr::Cast { expr, .. }
9168 | Expr::IsNull { expr, .. }
9169 | Expr::BoolTest { expr, .. }
9170 | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
9171 Expr::FunctionCall { args, .. } => {
9172 for a in args {
9173 collect_agg_exprs(a, out);
9174 }
9175 }
9176 Expr::Like { expr, pattern, .. } => {
9177 collect_agg_exprs(expr, out);
9178 collect_agg_exprs(pattern, out);
9179 }
9180 Expr::Extract { source, .. } => collect_agg_exprs(source, out),
9181 Expr::WindowFunction {
9182 args,
9183 partition_by,
9184 order_by,
9185 ..
9186 } => {
9187 for a in args {
9188 collect_agg_exprs(a, out);
9189 }
9190 for p in partition_by {
9191 collect_agg_exprs(p, out);
9192 }
9193 for (o, _, _) in order_by {
9194 collect_agg_exprs(o, out);
9195 }
9196 }
9197 _ => {}
9198 }
9199}
9200
9201/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
9202fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
9203 if expr_is_aggregate_call(e) {
9204 if let Some(idx) = aggs.iter().position(|x| x == e) {
9205 *e = Expr::Column(ColumnName {
9206 qualifier: None,
9207 name: alloc::format!("__agg{idx}"),
9208 });
9209 }
9210 return;
9211 }
9212 match e {
9213 Expr::Binary { lhs, rhs, .. } => {
9214 replace_agg_exprs(lhs, aggs);
9215 replace_agg_exprs(rhs, aggs);
9216 }
9217 Expr::Unary { expr, .. }
9218 | Expr::Cast { expr, .. }
9219 | Expr::IsNull { expr, .. }
9220 | Expr::BoolTest { expr, .. }
9221 | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
9222 Expr::FunctionCall { args, .. } => {
9223 for a in args {
9224 replace_agg_exprs(a, aggs);
9225 }
9226 }
9227 Expr::Like { expr, pattern, .. } => {
9228 replace_agg_exprs(expr, aggs);
9229 replace_agg_exprs(pattern, aggs);
9230 }
9231 Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
9232 Expr::WindowFunction {
9233 args,
9234 partition_by,
9235 order_by,
9236 ..
9237 } => {
9238 for a in args {
9239 replace_agg_exprs(a, aggs);
9240 }
9241 for p in partition_by {
9242 replace_agg_exprs(p, aggs);
9243 }
9244 for (o, _, _) in order_by {
9245 replace_agg_exprs(o, aggs);
9246 }
9247 }
9248 _ => {}
9249 }
9250}
9251
9252/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
9253/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
9254/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
9255/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
9256/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
9257/// Returns None outside the bounded subset (leaves current behaviour). Only fires
9258/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
9259/// window-only / aggregate-only queries.
9260fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
9261 if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
9262 return None;
9263 }
9264 // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
9265 if !stmt.unions.is_empty() {
9266 return None;
9267 }
9268 let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
9269 if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
9270 return None;
9271 }
9272 stmt.from.as_ref()?;
9273 // Collect the aggregate calls to hoist from projection + outer ORDER BY.
9274 let mut aggs: Vec<Expr> = Vec::new();
9275 for item in &stmt.items {
9276 if let SelectItem::Expr { expr, .. } = item {
9277 collect_agg_exprs(expr, &mut aggs);
9278 }
9279 }
9280 for ob in &stmt.order_by {
9281 collect_agg_exprs(&ob.expr, &mut aggs);
9282 }
9283 // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
9284 let mut inner_items: Vec<SelectItem> = Vec::new();
9285 for g in &group_cols {
9286 inner_items.push(SelectItem::Expr {
9287 expr: g.clone(),
9288 alias: None,
9289 });
9290 }
9291 for (i, a) in aggs.iter().enumerate() {
9292 inner_items.push(SelectItem::Expr {
9293 expr: a.clone(),
9294 alias: Some(alloc::format!("__agg{i}")),
9295 });
9296 }
9297 let inner = SelectStatement {
9298 items: inner_items,
9299 distinct: false,
9300 distinct_on: Vec::new(),
9301 unions: Vec::new(),
9302 order_by: Vec::new(),
9303 limit: None,
9304 offset: None,
9305 limit_with_ties: false,
9306 window_check_exprs: Vec::new(),
9307 ..stmt.clone()
9308 };
9309 let derived = TableRef {
9310 name: "__aggwin".into(),
9311 alias: Some("__aggwin".into()),
9312 only: false,
9313 as_of_segment: None,
9314 unnest_expr: None,
9315 unnest_column_aliases: Vec::new(),
9316 with_ordinality: false,
9317 generate_series_args: None,
9318 lateral_subquery: Some(alloc::boxed::Box::new(inner)),
9319 jsonb_each_text_arg: None,
9320 table_fn_call: None,
9321 rows_from: None,
9322 json_table: None,
9323 scalar_fn_item: false,
9324 };
9325 // Outer window query over the derived rows: aggregates → __aggN column refs.
9326 let mut outer_items = stmt.items.clone();
9327 for item in &mut outer_items {
9328 if let SelectItem::Expr { expr, alias } = item {
9329 // Preserve PG's column label for a bare aggregate projection.
9330 if alias.is_none()
9331 && let Expr::FunctionCall { name, .. } = expr
9332 && crate::aggregate::is_aggregate_name(name)
9333 {
9334 *alias = Some(name.to_ascii_lowercase());
9335 }
9336 replace_agg_exprs(expr, &aggs);
9337 }
9338 }
9339 let mut outer_order = stmt.order_by.clone();
9340 for ob in &mut outer_order {
9341 replace_agg_exprs(&mut ob.expr, &aggs);
9342 }
9343 let mut outer_distinct_on = stmt.distinct_on.clone();
9344 for e in &mut outer_distinct_on {
9345 replace_agg_exprs(e, &aggs);
9346 }
9347 Some(SelectStatement {
9348 locking: None,
9349 ctes: Vec::new(),
9350 distinct: stmt.distinct,
9351 distinct_on: outer_distinct_on,
9352 items: outer_items,
9353 from: Some(FromClause {
9354 primary: derived,
9355 joins: Vec::new(),
9356 }),
9357 where_: None,
9358 group_by: None,
9359 group_by_all: false,
9360 having: None,
9361 unions: Vec::new(),
9362 order_by: outer_order,
9363 limit: stmt.limit.clone(),
9364 offset: stmt.offset.clone(),
9365 limit_with_ties: stmt.limit_with_ties,
9366 window_check_exprs: Vec::new(),
9367 })
9368}
9369
9370/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
9371/// membership.
9372///
9373/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
9374/// there?", and all four answered by scanning the whole right side once per
9375/// left row. The cost was (left rows x right rows), which is why
9376/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
9377/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
9378/// row that does not pays for all of it. Over 100k left rows, raising the
9379/// right side from 100 to 10,000 took 35 ms to 2848.
9380///
9381/// This is the shape round 485 already solved for DISTINCT, and it reuses
9382/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
9383/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
9384/// every bucket with the exact comparator, so a collision costs time and
9385/// never an answer.
9386struct PeerIndex<'r> {
9387 bh: hashbrown::DefaultHashBuilder,
9388 buckets: hashbrown::HashMap<u64, Vec<usize>>,
9389 rows: &'r [Row<'static>],
9390 mysql: bool,
9391}
9392
9393impl<'r> PeerIndex<'r> {
9394 fn build(rows: &'r [Row<'static>], mysql: bool) -> Self {
9395 // ONE hasher for the whole pass: the default builder is seeded per
9396 // instance, so a fresh one per row would put equal rows in different
9397 // buckets.
9398 let bh = hashbrown::DefaultHashBuilder::default();
9399 let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
9400 hashbrown::HashMap::with_capacity(rows.len());
9401 for (i, r) in rows.iter().enumerate() {
9402 buckets
9403 .entry(norm_hash_row(r, &bh, mysql))
9404 .or_default()
9405 .push(i);
9406 }
9407 Self {
9408 bh,
9409 buckets,
9410 rows,
9411 mysql,
9412 }
9413 }
9414
9415 fn contains(&self, r: &Row<'static>) -> bool {
9416 let h = norm_hash_row(r, &self.bh, self.mysql);
9417 self.buckets
9418 .get(&h)
9419 .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.mysql)))
9420 }
9421
9422 /// Remove ONE occurrence, so the multiset forms cancel row for row the
9423 /// way the pool they replaced did.
9424 fn take_one(&mut self, r: &Row<'static>) -> bool {
9425 let h = norm_hash_row(r, &self.bh, self.mysql);
9426 let Some(b) = self.buckets.get_mut(&h) else {
9427 return false;
9428 };
9429 let Some(pos) = b
9430 .iter()
9431 .position(|&i| row_eq_norm(&self.rows[i], r, self.mysql))
9432 else {
9433 return false;
9434 };
9435 b.swap_remove(pos);
9436 true
9437 }
9438}
9439
9440pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, mysql: bool) -> Vec<Row<'static>> {
9441 dedup_by_row(rows, |r| r, mysql)
9442}
9443
9444/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
9445/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
9446/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
9447/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
9448/// order is preserved, and correctness needs only the one-way guarantee
9449/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
9450/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
9451fn dedup_by_row<T>(items: Vec<T>, row_of: impl Fn(&T) -> &Row<'static>, mysql: bool) -> Vec<T> {
9452 if items.len() <= 32 {
9453 let mut out: Vec<T> = Vec::with_capacity(items.len());
9454 for it in items {
9455 if !out
9456 .iter()
9457 .any(|seen| row_eq_norm(row_of(seen), row_of(&it), mysql))
9458 {
9459 out.push(it);
9460 }
9461 }
9462 return out;
9463 }
9464 // ONE BuildHasher instance for the whole pass — the default builder
9465 // is randomly seeded PER INSTANCE, so a fresh one per row would give
9466 // equal rows different hashes and never dedup.
9467 let bh = hashbrown::DefaultHashBuilder::default();
9468 let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
9469 let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9470 hashbrown::HashMap::with_capacity(items.len());
9471 for it in items {
9472 let h = norm_hash_row(row_of(&it), &bh, mysql);
9473 let bucket = buckets.entry(h).or_default();
9474 if !bucket
9475 .iter()
9476 .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), mysql))
9477 {
9478 bucket.push(out.len());
9479 out.push(it);
9480 }
9481 }
9482 out
9483}
9484
9485/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
9486/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
9487/// rows may collide (buckets are re-checked with the exact comparator).
9488///
9489/// Domain design mirrors `value_cmp`'s equivalence classes:
9490/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
9491/// shares one domain: a value that is an integer fitting i64 hashes the
9492/// i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
9493/// anything else hashes the f64 approximation computed by THE SAME
9494/// formula the value_cmp float arms use (`numeric_to_f64`), so
9495/// `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
9496/// hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
9497/// Known un-closable corner: an integer in [2^53, 2^63) can compare
9498/// Equal to a float via value_cmp's lossy f64 arm while hashing in the
9499/// exact-i64 domain — mixed int/float rows at that magnitude may miss a
9500/// dedup (PG itself compares int8↔float8 in the lossy float8 domain).
9501/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
9502/// compares them blank-insensitively; plain Text pairs that differ only
9503/// in trailing blanks merely collide and are separated exactly).
9504/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
9505/// hash their fields under a distinct tag.
9506/// - Everything value_cmp falls back to debug-format ordering for
9507/// (Json, arrays, vectors, geometry, ranges, …) shares one constant
9508/// bucket — degrades to the exact linear scan, never wrong.
9509fn norm_hash_row(row: &Row<'static>, bh: &hashbrown::DefaultHashBuilder, mysql: bool) -> u64 {
9510 norm_hash_values(&row.values, bh, mysql)
9511}
9512
9513/// v7.39 (round 485) — the same hash over a bare value slice, so the
9514/// DISTINCT probe can run against a reused buffer instead of demanding a
9515/// `Row` that has to be allocated first (see `values_eq_norm`).
9516fn norm_hash_values(
9517 values: &[Value<'static>],
9518 bh: &hashbrown::DefaultHashBuilder,
9519 mysql: bool,
9520) -> u64 {
9521 use core::hash::{BuildHasher, Hash, Hasher};
9522 let mut h = bh.build_hasher();
9523 for v in values {
9524 // v7.39 (round 410) — hash the folded key when the MySQL collation
9525 // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
9526 // `'A'` vs `'a '`) share a hash bucket.
9527 if mysql {
9528 if let Some(folded) = mysql_dedup_fold(v) {
9529 folded.hash(&mut h);
9530 continue;
9531 }
9532 }
9533 norm_hash_value(v, &mut h);
9534 }
9535 h.finish()
9536}
9537
9538fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
9539 const TAG_NULL: u8 = 0;
9540 const TAG_BOOL: u8 = 1;
9541 const TAG_NUM_I64: u8 = 2;
9542 const TAG_NUM_F64: u8 = 3;
9543 const TAG_TEXT: u8 = 4;
9544 const TAG_DATE: u8 = 6;
9545 const TAG_TIME: u8 = 7;
9546 const TAG_TIMESTAMP: u8 = 8;
9547 const TAG_TIMETZ: u8 = 10;
9548 const TAG_UUID: u8 = 11;
9549 const TAG_MONEY: u8 = 12;
9550 const TAG_BYTES: u8 = 13;
9551 const TAG_INTERVAL: u8 = 14;
9552 const TAG_CHAR1: u8 = 15;
9553 const TAG_OPAQUE: u8 = 255;
9554 // One shared writer for the numeric family: an integer value
9555 // representable as i64 goes exact (round-trip probe — no_std, so no
9556 // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
9557 // through 0i64, folding it into 0.0 as value_cmp requires.
9558 let num_f64 = |h: &mut H, x: f64| {
9559 if x.is_nan() {
9560 h.write_u8(TAG_NUM_F64);
9561 h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
9562 return;
9563 }
9564 const TWO63: f64 = 9_223_372_036_854_775_808.0;
9565 if (-TWO63..TWO63).contains(&x) {
9566 #[allow(clippy::cast_possible_truncation)]
9567 let n = x as i64;
9568 #[allow(clippy::cast_precision_loss)]
9569 if (n as f64) == x {
9570 h.write_u8(TAG_NUM_I64);
9571 h.write_i64(n);
9572 return;
9573 }
9574 }
9575 h.write_u8(TAG_NUM_F64);
9576 h.write_u64(x.to_bits());
9577 };
9578 match v {
9579 Value::Null => h.write_u8(TAG_NULL),
9580 Value::Bool(b) => {
9581 h.write_u8(TAG_BOOL);
9582 h.write_u8(u8::from(*b));
9583 }
9584 Value::SmallInt(n) => {
9585 h.write_u8(TAG_NUM_I64);
9586 h.write_i64(i64::from(*n));
9587 }
9588 Value::Int(n) => {
9589 h.write_u8(TAG_NUM_I64);
9590 h.write_i64(i64::from(*n));
9591 }
9592 Value::BigInt(n) => {
9593 h.write_u8(TAG_NUM_I64);
9594 h.write_i64(*n);
9595 }
9596 Value::Float(x) => num_f64(h, *x),
9597 Value::Numeric {
9598 scaled,
9599 scale,
9600 kind,
9601 } => match kind {
9602 spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
9603 spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
9604 spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
9605 spg_storage::NumericKind::Finite => {
9606 // Reduce trailing fractional zeros so 1.50 and 1.5 share a
9607 // representation, then: exact integers fitting i64 go to the
9608 // i64 domain; everything else uses numeric_to_f64 — the SAME
9609 // formula value_cmp's Numeric↔Float arm compares with.
9610 let (mut s, mut sc) = (*scaled, *scale);
9611 while sc > 0 && s % 10 == 0 {
9612 s /= 10;
9613 sc -= 1;
9614 }
9615 if sc == 0 {
9616 if let Ok(n) = i64::try_from(s) {
9617 h.write_u8(TAG_NUM_I64);
9618 h.write_i64(n);
9619 } else {
9620 num_f64(h, crate::orderby::numeric_to_f64(s, 0));
9621 }
9622 } else {
9623 num_f64(h, crate::orderby::numeric_to_f64(s, sc));
9624 }
9625 }
9626 },
9627 // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
9628 // value that also fits i128 reuses the Numeric path above so
9629 // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
9630 // any i128-representable value — constant bucket is safe.
9631 Value::NumericBig(b) => match b.to_i128() {
9632 Some(s) => norm_hash_value(
9633 &Value::Numeric {
9634 scaled: s,
9635 scale: b.scale(),
9636 kind: spg_storage::NumericKind::Finite,
9637 },
9638 h,
9639 ),
9640 None => h.write_u8(TAG_OPAQUE),
9641 },
9642 // value_cmp compares Text↔BpChar blank-insensitively (both sides
9643 // trimmed), so both hash the trimmed bytes. Text pairs differing
9644 // only in trailing blanks collide and are split exactly in-bucket.
9645 Value::Text(s) | Value::BpChar(s) => {
9646 h.write_u8(TAG_TEXT);
9647 h.write(s.trim_end_matches(' ').as_bytes());
9648 }
9649 Value::Char1(c) => {
9650 h.write_u8(TAG_CHAR1);
9651 h.write_u8(*c);
9652 }
9653 Value::Date(d) => {
9654 h.write_u8(TAG_DATE);
9655 h.write_i32(*d);
9656 }
9657 Value::Time(t) => {
9658 h.write_u8(TAG_TIME);
9659 h.write_i64(*t);
9660 }
9661 Value::Timestamp(t) => {
9662 h.write_u8(TAG_TIMESTAMP);
9663 h.write_i64(*t);
9664 }
9665 Value::TimeTz { us, offset_secs } => {
9666 h.write_u8(TAG_TIMETZ);
9667 h.write_i64(*us);
9668 h.write_i32(*offset_secs);
9669 }
9670 Value::Uuid(u) => {
9671 h.write_u8(TAG_UUID);
9672 h.write(u);
9673 }
9674 Value::Money(c) => {
9675 h.write_u8(TAG_MONEY);
9676 h.write_i64(*c);
9677 }
9678 Value::Bytes(b) => {
9679 h.write_u8(TAG_BYTES);
9680 h.write(b.as_ref());
9681 }
9682 Value::Interval {
9683 months,
9684 days,
9685 micros,
9686 } => {
9687 h.write_u8(TAG_INTERVAL);
9688 h.write_i32(*months);
9689 h.write_i32(*days);
9690 h.write_i64(*micros);
9691 }
9692 // v7.37.16 — REAL joined the numeric value_cmp family (widened
9693 // to f64, same formulas as the arms), so it hashes in the shared
9694 // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
9695 // f32→f64 is exact, so equal-under-cmp implies equal bits here.
9696 Value::Real(x) => num_f64(h, f64::from(*x)),
9697 // Json (structural equality), vector families (float rendering),
9698 // arrays / geometry / net / ranges / composites (debug-format
9699 // fallback): one constant bucket — exact linear within.
9700 _ => h.write_u8(TAG_OPAQUE),
9701 }
9702}
9703
9704/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
9705/// treats numerically-equal exact values as one regardless of type or scale
9706/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
9707/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
9708/// `Row` `==` would keep them distinct.
9709/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
9710/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
9711/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
9712/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
9713/// the folded comparison key for a text value, None for anything else (which
9714/// keeps the byte-exact `value_cmp` path).
9715fn mysql_dedup_fold(v: &Value) -> Option<String> {
9716 match v {
9717 Value::Text(s) | Value::BpChar(s) => {
9718 Some(spg_storage::mysql_ci_fold(s.trim_end_matches(' ')))
9719 }
9720 _ => None,
9721 }
9722}
9723
9724/// v7.39 (round 485) — how many projected rows the single-table scan
9725/// builds, and how many of those the DISTINCT probe throws away again.
9726///
9727/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
9728/// 21 % of all samples in malloc/free called straight from the scan
9729/// closure. The closure's one per-row allocation is the projected
9730/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
9731/// instructions later — but "most" is a guess until it is a number, so
9732/// these count it. (Round 480 was spent acting on an inference about a
9733/// branch that turned out never to run.)
9734/// v7.39 (round 488) — reachability counters for round 487's projection
9735/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
9736/// and a never-called-function probe rules out code layout — so the
9737/// question is whether that shape reaches this code at all, which is a
9738/// number, not an inference.
9739pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
9740pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
9741
9742pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
9743pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
9744 core::sync::atomic::AtomicU64::new(0);
9745
9746pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, mysql: bool) -> bool {
9747 values_eq_norm(&a.values, &b.values, mysql)
9748}
9749
9750/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
9751/// DISTINCT probe can compare a reused projection buffer against a kept
9752/// row without building a `Row` for it.
9753pub(crate) fn values_eq_norm(a: &[Value<'static>], b: &[Value<'static>], mysql: bool) -> bool {
9754 a.len() == b.len()
9755 && a.iter().zip(b).all(|(x, y)| {
9756 if mysql {
9757 if let (Some(fx), Some(fy)) = (mysql_dedup_fold(x), mysql_dedup_fold(y)) {
9758 return fx == fy;
9759 }
9760 }
9761 crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
9762 })
9763}
9764
9765/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
9766/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
9767/// order via the byte values; vectors are not sortable.
9768pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
9769 // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
9770 // so values sharing a ≥6-byte common prefix (`product_001` vs
9771 // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
9772 // order by their exact bytes instead of the old lossy f64 coarse key.
9773 // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
9774 // matches PG's default C / binary text collation. Every other type
9775 // keeps the lossless-enough `f64` fast path below.
9776 if let Value::Text(s) = v {
9777 return Ok(OrderKey::Text(s.as_ref().into()));
9778 }
9779 // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
9780 // byte order (PG bpcharcmp under C collation), so mixed-pad values of
9781 // the same logical string order equal.
9782 if let Value::BpChar(s) = v {
9783 return Ok(OrderKey::Text(s.trim_end_matches(' ').into()));
9784 }
9785 // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
9786 // carry the parsed value and compare it structurally (see
9787 // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
9788 if let Value::Json(s) = v {
9789 return Ok(match crate::json::parse(s) {
9790 Ok(jv) => OrderKey::Json(jv),
9791 Err(_) => OrderKey::Text(s.as_ref().into()),
9792 });
9793 }
9794 // v7.37 — byte-orderable types PG sorts byte-wise but that have no
9795 // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
9796 // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
9797 // matching PG's network ordering.
9798 match v {
9799 Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
9800 // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
9801 Value::NumericBig(b) => return Ok(OrderKey::BigNum((**b).clone())),
9802 Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
9803 Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
9804 Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
9805 Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
9806 Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
9807 let mut key = alloc::vec::Vec::with_capacity(18);
9808 key.push(*family);
9809 key.extend_from_slice(addr);
9810 key.push(*bits);
9811 return Ok(OrderKey::Bytes(key));
9812 }
9813 _ => {}
9814 }
9815 // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
9816 // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
9817 // own OrderKey so integer arrays sort numerically; a NULL element rides to
9818 // the end via the +INF sentinel.
9819 let inf = || OrderKey::NullBig;
9820 let arr = match v {
9821 Value::IntArray(a) => Some(
9822 a.iter()
9823 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
9824 .collect(),
9825 ),
9826 Value::SmallIntArray(a) => Some(
9827 a.iter()
9828 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
9829 .collect(),
9830 ),
9831 Value::BigIntArray(a) => Some(
9832 a.iter()
9833 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
9834 .collect(),
9835 ),
9836 Value::BoolArray(a) => Some(
9837 a.iter()
9838 .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
9839 .collect(),
9840 ),
9841 Value::TextArray(a) => Some(
9842 a.iter()
9843 .map(|o| o.as_ref().map_or_else(inf, |s| OrderKey::Text(s.clone())))
9844 .collect(),
9845 ),
9846 #[allow(clippy::cast_precision_loss)]
9847 Value::FloatArray(a) => Some(
9848 a.iter()
9849 .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
9850 .collect(),
9851 ),
9852 Value::NumericArray(a) => Some(
9853 a.iter()
9854 .map(|o| {
9855 o.map_or_else(inf, |(m, s)| {
9856 OrderKey::Num(crate::orderby::numeric_to_f64(m, s))
9857 })
9858 })
9859 .collect(),
9860 ),
9861 Value::DateArray(a) => Some(
9862 a.iter()
9863 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
9864 .collect(),
9865 ),
9866 _ => None,
9867 };
9868 if let Some(elements) = arr {
9869 return Ok(OrderKey::Array(elements));
9870 }
9871 // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
9872 // right, which is exactly the lexicographic element order an Array key
9873 // already gives: `(2,'b') < (9,'a')` because the leading field decides.
9874 if let Value::Composite(fields) = v {
9875 let elements = fields
9876 .iter()
9877 .map(|(_, fv)| value_to_order_key(fv))
9878 .collect::<Result<alloc::vec::Vec<_>, _>>()?;
9879 return Ok(OrderKey::Array(elements));
9880 }
9881 // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
9882 // Projecting these to f64 (the historic path) silently collapses BigInt /
9883 // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
9884 // the wrong order for large ids and microsecond timestamps.
9885 match v {
9886 Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
9887 Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
9888 Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
9889 // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
9890 // integer (days / micros / cents / calendar year); TIMETZ by the
9891 // UTC-equivalent micros (local wall - offset) so the same physical
9892 // instant in different zones sorts equal.
9893 Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
9894 Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
9895 Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
9896 Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
9897 Value::TimeTz { us, offset_secs } => {
9898 return Ok(OrderKey::Int(
9899 i128::from(*us) - i128::from(*offset_secs) * 1_000_000,
9900 ));
9901 }
9902 Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
9903 _ => {}
9904 }
9905 let num = match v {
9906 // Callers without NULLS FIRST/LAST context (array elements,
9907 // histogram sampling) put NULL last, as before.
9908 Value::Null => return Ok(OrderKey::NullBig),
9909 // v7.17.0 Phase 3.P0-38 — range ordering is not supported
9910 // in v7.17.0 (needs lex-then-inclusivity tiebreak).
9911 Value::Range { .. } => {
9912 return Err(EngineError::Unsupported(
9913 "ORDER BY of a range value is not supported in v7.17.0".into(),
9914 ));
9915 }
9916 // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
9917 Value::Hstore(_) => {
9918 return Err(EngineError::Unsupported(
9919 "ORDER BY of a hstore value is not supported".into(),
9920 ));
9921 }
9922 // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
9923 Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
9924 return Err(EngineError::Unsupported(
9925 "ORDER BY of a 2D array is not supported in v7.17.0".into(),
9926 ));
9927 }
9928 #[allow(clippy::cast_precision_loss)]
9929 Value::Numeric { scaled, scale, .. } => {
9930 // Scaled integer / 10^scale, computed via f64 for sort
9931 // ordering only. Precision losses here only matter for
9932 // ORDER BY tie-breaks well past 15 significant digits.
9933 // `f64::powi` lives in std; we hand-roll the loop so the
9934 // no_std engine crate doesn't need it.
9935 let mut divisor = 1.0_f64;
9936 for _ in 0..*scale {
9937 divisor *= 10.0;
9938 }
9939 (*scaled as f64) / divisor
9940 }
9941 Value::Float(x) => *x,
9942 // v7.37.16 — REAL sorts by its exact f64 widening (it had no
9943 // arm and fell through to the unsupported error).
9944 Value::Real(x) => f64::from(*x),
9945 Value::Bool(b) => {
9946 if *b {
9947 1.0
9948 } else {
9949 0.0
9950 }
9951 }
9952 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
9953 return Err(EngineError::Unsupported(
9954 "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
9955 ));
9956 }
9957 // v7.37 — PG orders INTERVAL by its total time, treating a month as
9958 // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
9959 // f64 is exact for any interval under ~285 years, and only ORDER BY
9960 // tie-breaks past that magnitude lose precision. Matches the
9961 // min/max(interval) comparator in aggregate.rs.
9962 #[allow(clippy::cast_precision_loss)]
9963 Value::Interval {
9964 months,
9965 days,
9966 micros,
9967 } => {
9968 let total = i128::from(*months) * 30 * 86_400_000_000
9969 + i128::from(*days) * 86_400_000_000
9970 + i128::from(*micros);
9971 total as f64
9972 }
9973 Value::Json(_) => {
9974 return Err(EngineError::Unsupported(
9975 "ORDER BY of a JSON value is not supported — cast the document to text first"
9976 .into(),
9977 ));
9978 }
9979 // v7.5.0 — Value is #[non_exhaustive]; future variants need
9980 // an explicit ORDER BY mapping. Surface as Unsupported until
9981 // engine support is added.
9982 _ => {
9983 return Err(EngineError::Unsupported(
9984 "ORDER BY of this value type is not supported".into(),
9985 ));
9986 }
9987 };
9988 Ok(OrderKey::Num(num))
9989}
9990
9991/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
9992/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
9993/// `EngineError` so the projection-build path keeps `UnknownQualifier`
9994/// vs `ColumnNotFound` distinct.
9995/// PG's name for the physical row identity. It is reserved there — no table
9996/// can have a column called this — which is what lets `*` skip it by name.
9997pub(crate) const CTID_COLUMN: &str = "ctid";
9998
9999/// v7.39 (round 512) — PG's system columns, in the order they are appended.
10000/// All six are reserved names there, which is what lets `*` skip them and
10001/// lets a scan tell them from a user column without a flag.
10002pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
10003
10004/// Is this name one of them?
10005pub(crate) fn is_system_column(name: &str) -> bool {
10006 SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
10007}
10008
10009/// Where the scan's appended system columns begin, if this schema carries
10010/// them: the trailing six, named in order. A catalog view with a column of
10011/// its own called `xmin` does not match, which is the point.
10012fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
10013 let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
10014 cols[start..]
10015 .iter()
10016 .zip(SYSTEM_COLUMNS)
10017 .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
10018 .then_some(start)
10019}
10020
10021/// v7.39 (round 540) — which positions `*` must skip.
10022///
10023/// The rule stays round 512's — the synthetic columns are the trailing
10024/// six of a relation's block, matched by POSITION so a genuine `xmin`
10025/// column is not lost — but a JOINED schema names its columns
10026/// `alias.column` and lays the peers out end to end, so a peer's six sit
10027/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
10028/// "trailing six" test back on the block it was written for.
10029fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
10030 let mut skip = alloc::vec![false; cols.len()];
10031 fn qualifier(n: &str) -> Option<&str> {
10032 n.rsplit_once('.').map(|(q, _)| q)
10033 }
10034 fn bare(n: &str) -> &str {
10035 n.rsplit('.').next().unwrap_or(n)
10036 }
10037 let mut i = 0;
10038 while i < cols.len() {
10039 let q = qualifier(&cols[i].name);
10040 let mut end = i;
10041 while end < cols.len() && qualifier(&cols[end].name) == q {
10042 end += 1;
10043 }
10044 if let Some(start) = (end - i)
10045 .checked_sub(SYSTEM_COLUMNS.len())
10046 .map(|off| i + off)
10047 && cols[start..end]
10048 .iter()
10049 .zip(SYSTEM_COLUMNS)
10050 .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
10051 {
10052 for s in skip.iter_mut().take(end).skip(start) {
10053 *s = true;
10054 }
10055 }
10056 i = end;
10057 }
10058 skip
10059}
10060
10061/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
10062/// read? Only then is the column materialised.
10063pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
10064 let mut found = false;
10065 crate::expr_analysis::visit_expr_columns_and_subqueries(
10066 e,
10067 &mut |c| {
10068 if is_system_column(&c.name) {
10069 found = true;
10070 }
10071 },
10072 &mut |_| {},
10073 );
10074 found
10075}
10076
10077fn references_ctid(stmt: &SelectStatement) -> bool {
10078 let in_expr = expr_references_ctid;
10079 stmt.items.iter().any(|i| match i {
10080 SelectItem::Expr { expr, .. } => in_expr(expr),
10081 _ => false,
10082 }) || stmt.where_.as_ref().is_some_and(in_expr)
10083 || stmt.order_by.iter().any(|o| in_expr(&o.expr))
10084 || stmt
10085 .group_by
10086 .as_ref()
10087 .is_some_and(|g| g.iter().any(in_expr))
10088 || stmt.having.as_ref().is_some_and(in_expr)
10089}
10090
10091/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
10092/// is a name the projection has to TYPE before any row exists.
10093///
10094/// Evaluation has answered this since round T9 (`resolve_column` builds a
10095/// `Value::Composite` of every column), but the typing side below had no
10096/// such branch and raised `column "t" does not exist` first — so the
10097/// feature was unreachable through a projection. Measured against PG18.4:
10098/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
10099///
10100/// The type is `Jsonb` + a composite marker, which is exactly how a
10101/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
10102/// the value travels as a `Value::Composite` and renders in the canonical
10103/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
10104/// so the marker names the alias and no rehydration keys off it — the
10105/// value arrives already built.
10106fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
10107 let mut s = ColumnSchema::new(
10108 alloc::string::String::from(alias),
10109 spg_storage::DataType::Jsonb,
10110 true,
10111 );
10112 s.user_composite_type = Some(alloc::string::String::from(alias));
10113 s
10114}
10115
10116pub(crate) fn resolve_projection_column<'a>(
10117 c: &ColumnName,
10118 schema_cols: &'a [ColumnSchema],
10119 table_alias: &str,
10120) -> Result<Cow<'a, ColumnSchema>, EngineError> {
10121 if let Some(q) = &c.qualifier {
10122 let composite = alloc::format!("{q}.{name}", name = c.name);
10123 if let Some(s) = schema_cols.iter().find(|s| s.name == composite) {
10124 return Ok(Cow::Borrowed(s));
10125 }
10126 // Single-table case: the qualifier may equal the active alias —
10127 // then look for the bare column name.
10128 if q == table_alias
10129 && let Some(s) = schema_cols.iter().find(|s| s.name == c.name)
10130 {
10131 return Ok(Cow::Borrowed(s));
10132 }
10133 // For multi-table schemas the qualifier is unknown only if no
10134 // column bears the "<q>." prefix. For single-table, the alias
10135 // mismatch alone is enough.
10136 let prefix = alloc::format!("{q}.");
10137 let qualifier_known =
10138 q == table_alias || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
10139 if !qualifier_known {
10140 return Err(EngineError::Eval(EvalError::UnknownQualifier {
10141 qualifier: q.clone(),
10142 }));
10143 }
10144 return Err(EngineError::Eval(EvalError::ColumnNotFound {
10145 name: c.name.clone(),
10146 }));
10147 }
10148 if let Some(s) = schema_cols.iter().find(|s| s.name == c.name) {
10149 return Ok(Cow::Borrowed(s));
10150 }
10151 let suffix = alloc::format!(".{name}", name = c.name);
10152 let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
10153 let first = matches.next();
10154 let extra = matches.next();
10155 match (first, extra) {
10156 (Some(s), None) => Ok(Cow::Borrowed(s)),
10157 (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
10158 detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
10159 })),
10160 // The whole-row reference, checked LAST so a real column carrying
10161 // the alias's name still wins — the same precedence
10162 // `resolve_column` applies on the evaluation side.
10163 //
10164 // Two schema shapes reach here. A single-table (or subquery, or
10165 // CTE) scan carries its alias and bare column names, so the name
10166 // has to equal the alias. A JOIN's combined schema carries no
10167 // alias at all and qualifies every column `alias.col`, so the
10168 // alias is identified by the prefix instead — which is exactly
10169 // how `whole_row_composite` picks the fields out on the
10170 // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
10171 // answers `(7,z)` on PG18.4 and errored here until this arm
10172 // covered the joined shape too.
10173 _ if !table_alias.is_empty() && c.name == table_alias => {
10174 Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
10175 }
10176 _ if table_alias.is_empty() && {
10177 let prefix = alloc::format!("{name}.", name = c.name);
10178 schema_cols.iter().any(|s| s.name.starts_with(&prefix))
10179 } =>
10180 {
10181 Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
10182 }
10183 _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
10184 name: c.name.clone(),
10185 })),
10186 }
10187}
10188
10189/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
10190/// parser to carry per-branch GROUPING() masks into a grouping-set query's
10191/// ORDER BY. They must never reach the output. No-op unless such a column is
10192/// present, so the common path is untouched.
10193/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
10194///
10195/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
10196/// a `LIMIT 2` that should have answered two groups answered one.
10197fn apply_deferred_limit(
10198 rows: alloc::vec::Vec<Row<'static>>,
10199 deferred: &(
10200 Option<spg_sql::ast::LimitExpr>,
10201 Option<spg_sql::ast::LimitExpr>,
10202 ),
10203) -> alloc::vec::Vec<Row<'static>> {
10204 let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
10205 Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
10206 _ => None,
10207 };
10208 let mut rows = rows;
10209 if let Some(off) = count(&deferred.1) {
10210 rows = rows.split_off(off.min(rows.len()));
10211 }
10212 if let Some(lim) = count(&deferred.0) {
10213 rows.truncate(lim);
10214 }
10215 rows
10216}
10217
10218fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
10219 let QueryResult::Rows { columns, rows } = result else {
10220 return result;
10221 };
10222 if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
10223 return QueryResult::Rows { columns, rows };
10224 }
10225 let keep: Vec<usize> = columns
10226 .iter()
10227 .enumerate()
10228 .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
10229 .map(|(i, _)| i)
10230 .collect();
10231 let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
10232 let new_rows: Vec<Row<'static>> = rows
10233 .into_iter()
10234 .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
10235 .collect();
10236 QueryResult::Rows {
10237 columns: new_cols,
10238 rows: new_rows,
10239 }
10240}
10241
10242/// v7.39 (round 487) — bind every projection item that is a bare column
10243/// reference to its position, once per query.
10244///
10245/// `#[inline(never)]` and out of line on purpose. Round 486 established
10246/// that adding code inside these scan bodies moves neighbouring hot
10247/// functions around under fat LTO: the first version of this had the loop
10248/// inline in `run_single_table_scan` and four aggregate shapes that never
10249/// touch that function — `full_agg`, `join_agg`, `group_500k`,
10250/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
10251/// the same machine. Keeping it out of line kept them still.
10252#[inline(never)]
10253fn bind_direct_columns(
10254 projection: &[ProjectedItem],
10255 ctx: &eval::EvalContext<'_>,
10256) -> Vec<Option<usize>> {
10257 projection
10258 .iter()
10259 .map(|p| match &p.expr {
10260 Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
10261 // Same exclusion `compile_into` makes: a composite column
10262 // has to be rehydrated from stored JSON, which is not a
10263 // cell read.
10264 ctx.columns
10265 .get(*pos)
10266 .is_none_or(|sc| sc.user_composite_type.is_none())
10267 }),
10268 _ => None,
10269 })
10270 .collect()
10271}
10272
10273/// v7.39 (round 505) — the name an un-aliased projected expression reports.
10274///
10275/// PG18 names a call for its function and everything else `?column?`;
10276/// measured with `\gdesc`. SPG used to print the parsed expression back
10277/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
10278/// name-keyed row access found nothing under `upper`.
10279///
10280/// The MySQL half is NOT this rule and is deliberately left alone here:
10281/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
10282/// which needs the parser to hand over spans the AST does not carry yet.
10283/// Until it does, a MySQL session keeps the printed form — closer to what
10284/// MariaDB answers than `?column?` would be.
10285pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
10286 if mysql {
10287 return expr.to_string();
10288 }
10289 spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
10290}
10291
10292pub(crate) fn build_projection(
10293 items: &[SelectItem],
10294 schema_cols: &[ColumnSchema],
10295 table_alias: &str,
10296 mysql: bool,
10297) -> Result<Vec<ProjectedItem>, EngineError> {
10298 build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0)
10299}
10300
10301/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
10302/// invisible to `*`.
10303///
10304/// The windowed-SELECT path appends a synthetic `__win_N` column per window
10305/// function so the rewritten projection can reference the computed values as
10306/// ordinary columns. `*` then expanded them too, and
10307/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
10308/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
10309/// silent one: the row simply had one more field than the client asked for.
10310///
10311/// Hidden by POSITION rather than by name, for the reason round 512 recorded
10312/// about the system columns: a name test looks safe until a real column
10313/// happens to carry the name. These are appended last, so the count is what
10314/// identifies them.
10315pub(crate) fn build_projection_hiding_tail(
10316 items: &[SelectItem],
10317 schema_cols: &[ColumnSchema],
10318 table_alias: &str,
10319 mysql: bool,
10320 hidden_tail: usize,
10321) -> Result<Vec<ProjectedItem>, EngineError> {
10322 let visible = schema_cols.len().saturating_sub(hidden_tail);
10323 // v7.39 (round 462) — a join's combined schema qualifies every column
10324 // `alias.col` so the deferred-join cell lookups resolve by composite
10325 // name. That is an internal convention, and `*` was handing it to the
10326 // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
10327 // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
10328 // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
10329 // already learned this for `q.*`; plain `*` never got the same rule.
10330 //
10331 // The signal is the schema itself, not the call site: only a combined
10332 // join schema arrives with no table alias AND every column qualified.
10333 // A single-table schema carries its alias, an empty schema has nothing
10334 // to strip, and a synthetic schema's names carry no dot.
10335 let joined_schema = table_alias.is_empty()
10336 && !schema_cols.is_empty()
10337 && schema_cols.iter().all(|c| c.name.contains('.'));
10338 let bare_name = |name: &str| -> String {
10339 if !joined_schema {
10340 return name.to_string();
10341 }
10342 match name.split_once('.') {
10343 Some((_, rest)) if !rest.is_empty() => rest.to_string(),
10344 _ => name.to_string(),
10345 }
10346 };
10347 let mut out = Vec::new();
10348 for item in items {
10349 match item {
10350 SelectItem::Wildcard => {
10351 // v7.39 (round 511) — `*` never expands a system column, as
10352 // PG's does not. They join the schema only when the statement
10353 // asked for them, so this matters for the mixed shape
10354 // `SELECT *, ctid FROM t`.
10355 //
10356 // v7.39 (round 512) — by POSITION, not by name. Matching on
10357 // the name alone looked safe because PG reserves them, and it
10358 // is not: `pg_replication_slots` genuinely has a column called
10359 // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
10360 // Only the trailing six, in the order the scan appends them,
10361 // are the synthetic ones.
10362 let sys_skip = synthetic_system_positions(schema_cols);
10363 for (idx, col) in schema_cols.iter().enumerate() {
10364 if sys_skip[idx] || idx >= visible {
10365 continue;
10366 }
10367 out.push(ProjectedItem {
10368 expr: Expr::Column(ColumnName {
10369 qualifier: None,
10370 name: col.name.clone(),
10371 }),
10372 output_name: bare_name(&col.name),
10373 ty: col.ty,
10374 nullable: col.nullable,
10375 user_enum_type: col.user_enum_type.clone(),
10376 mysql_fsp: col.mysql_fsp,
10377 collation_name: col.collation_name.clone(),
10378 });
10379 }
10380 }
10381 // v7.39 (round 128) — `q.*` expands to every column belonging to
10382 // the qualifier `q`. Single-table schemas carry bare column names
10383 // reachable via `table_alias`; a join's combined schema carries
10384 // `alias.col` names, so a column belongs to `q` when its name has
10385 // the `q.` prefix. PG labels the expanded columns by their bare
10386 // name, so the `alias.` prefix is stripped from the output name.
10387 SelectItem::QualifiedWildcard(q) => {
10388 let prefix = alloc::format!("{q}.");
10389 let single_table = !table_alias.is_empty() && q == table_alias;
10390 let mut matched = 0usize;
10391 for col in &schema_cols[..visible] {
10392 let belongs =
10393 col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
10394 if !belongs {
10395 continue;
10396 }
10397 matched += 1;
10398 let output_name = col
10399 .name
10400 .strip_prefix(&prefix)
10401 .unwrap_or(&col.name)
10402 .to_string();
10403 out.push(ProjectedItem {
10404 expr: Expr::Column(ColumnName {
10405 qualifier: None,
10406 name: col.name.clone(),
10407 }),
10408 output_name,
10409 ty: col.ty,
10410 nullable: col.nullable,
10411 user_enum_type: col.user_enum_type.clone(),
10412 mysql_fsp: col.mysql_fsp,
10413 collation_name: col.collation_name.clone(),
10414 });
10415 }
10416 if matched == 0 {
10417 return Err(EngineError::Eval(EvalError::UnknownQualifier {
10418 qualifier: q.clone(),
10419 }));
10420 }
10421 }
10422 SelectItem::Expr { expr, alias } => {
10423 // Plain column ref keeps full schema info (real type +
10424 // nullability). For compound expressions try the
10425 // describe-side function-return-type table first
10426 // (e.g. `SELECT now()` → Timestamptz, `SELECT
10427 // concat(…)` → Text). Falls back to nullable Text
10428 // for shapes the describe path can't resolve.
10429 if let Expr::Column(c) = expr {
10430 let sch = resolve_projection_column(c, schema_cols, table_alias)?;
10431 let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
10432 out.push(ProjectedItem {
10433 expr: expr.clone(),
10434 output_name,
10435 ty: sch.ty,
10436 nullable: sch.nullable,
10437 // v7.39 (read01 round 54) — a bare enum column keeps
10438 // its enum identity through the projection.
10439 user_enum_type: sch.user_enum_type.clone(),
10440 mysql_fsp: sch.mysql_fsp,
10441 collation_name: sch.collation_name.clone(),
10442 });
10443 } else if let Some(shape) = describe::describe_expr(expr, schema_cols) {
10444 let output_name = alias
10445 .clone()
10446 .unwrap_or_else(|| default_output_name(expr, mysql));
10447 out.push(ProjectedItem {
10448 expr: expr.clone(),
10449 output_name,
10450 ty: shape.ty,
10451 // v7.39 (round 258) — a projected EXPRESSION keeps its
10452 // enum identity too, not just a bare column. `FROM
10453 // (VALUES ('happy'::mood), …) t(m)` lowers to constant
10454 // SELECTs, so the derived column arrived here as a cast
10455 // and lost the enum — making the outer ORDER BY / min /
10456 // max / array_agg sort by the label's TEXT.
10457 nullable: shape.nullable,
10458 user_enum_type: None,
10459 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
10460 // A bare column reference keeps its collation; any
10461 // other expression produces a new value and has none.
10462 collation_name: match expr {
10463 Expr::Column(c) => schema_cols
10464 .iter()
10465 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
10466 .and_then(|sc| sc.collation_name.clone()),
10467 _ => None,
10468 },
10469 });
10470 } else {
10471 let output_name = alias
10472 .clone()
10473 .unwrap_or_else(|| default_output_name(expr, mysql));
10474 out.push(ProjectedItem {
10475 expr: expr.clone(),
10476 output_name,
10477 // A user ENUM has no DataType of its own, so
10478 // `describe_expr` cannot type `'ok'::mood` and the
10479 // item lands HERE, defaulting to text — which is why
10480 // pg_typeof answered `text` and a derived table sorted
10481 // enum values by their label.
10482 ty: DataType::Text,
10483 nullable: true,
10484 user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
10485 .map(alloc::string::String::from),
10486 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
10487 collation_name: match expr {
10488 Expr::Column(c) => schema_cols
10489 .iter()
10490 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
10491 .and_then(|sc| sc.collation_name.clone()),
10492 _ => None,
10493 },
10494 });
10495 }
10496 }
10497 }
10498 }
10499 Ok(out)
10500}
10501
10502// ---- v4.12 window-function helpers ----
10503// The (partition-key, order-key, original-index) tuple shape used
10504// across these helpers is intrinsic to the planner. Factoring it
10505// into a typedef adds indirection without making the code clearer,
10506// so several lints are allowed inline on the affected functions
10507// rather than module-wide.
10508
10509/// v4.22: pick more specific column types from observed rows when
10510/// the projection builder defaulted to Text (the v1.x behavior for
10511/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
10512/// land an Int column in the CTE storage table rather than failing
10513/// the insert with "expected TEXT, got INT".
10514pub(crate) fn infer_column_types(
10515 columns: &[ColumnSchema],
10516 rows: &[Row<'static>],
10517) -> Vec<ColumnSchema> {
10518 let mut out = columns.to_vec();
10519 for (col_idx, col) in out.iter_mut().enumerate() {
10520 if col.ty != DataType::Text {
10521 continue;
10522 }
10523 let mut inferred: Option<DataType> = None;
10524 let mut all_null = true;
10525 for row in rows {
10526 let Some(v) = row.values.get(col_idx) else {
10527 continue;
10528 };
10529 let ty = match v {
10530 Value::Null => continue,
10531 Value::SmallInt(_) => DataType::SmallInt,
10532 Value::Int(_) => DataType::Int,
10533 Value::BigInt(_) => DataType::BigInt,
10534 Value::Float(_) => DataType::Float,
10535 Value::Bool(_) => DataType::Bool,
10536 Value::Vector(_) => DataType::Vector {
10537 dim: 0,
10538 encoding: VecEncoding::F32,
10539 },
10540 // v7.38 (read01 U16) — carry array values through with an
10541 // array type so a recursive CTE that projects an array
10542 // (e.g. a SEARCH/CYCLE ord / path column) types the working
10543 // column as an array, not Text.
10544 Value::TextArray(_) => DataType::TextArray,
10545 Value::IntArray(_) => DataType::IntArray,
10546 Value::BigIntArray(_) => DataType::BigIntArray,
10547 Value::SmallIntArray(_) => DataType::SmallIntArray,
10548 Value::FloatArray(_) => DataType::FloatArray,
10549 Value::BoolArray(_) => DataType::BoolArray,
10550 // v7.39 (GUC knife 2) — an interval projection describes
10551 // as INTERVAL (typed drivers read the RowDescription OID).
10552 Value::Interval { .. } => DataType::Interval,
10553 _ => DataType::Text,
10554 };
10555 all_null = false;
10556 inferred = Some(match inferred {
10557 None => ty,
10558 Some(prev) if prev == ty => prev,
10559 Some(_) => DataType::Text,
10560 });
10561 }
10562 if let Some(t) = inferred {
10563 col.ty = t;
10564 col.nullable = true;
10565 } else if all_null {
10566 col.nullable = true;
10567 }
10568 }
10569 out
10570}
10571
10572/// Numeric widening rank for UNION type resolution (higher = wider).
10573fn numeric_rank(t: DataType) -> Option<u8> {
10574 match t {
10575 DataType::SmallInt => Some(1),
10576 DataType::Int => Some(2),
10577 DataType::BigInt => Some(3),
10578 DataType::Numeric { .. } => Some(4),
10579 DataType::Float => Some(5),
10580 _ => None,
10581 }
10582}
10583
10584/// Resolve the common result type for a UNION / VALUES column from the
10585/// set of concrete (non-NULL) branch types, following the safe subset
10586/// of PG's type resolution:
10587/// * all-numeric → the widest numeric (int ∪ bigint → bigint, … ∪
10588/// numeric → numeric, … ∪ float → float);
10589/// * DATE ∪ TIMESTAMP → TIMESTAMP;
10590/// * exactly one concrete non-TEXT type mixed with TEXT literals →
10591/// that concrete type (the TEXT cells get parsed into it).
10592/// Returns `None` for anything ambiguous, so the caller leaves the
10593/// column untouched rather than risk a wrong or failing coercion.
10594fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
10595 // NB: types are collected from RUNTIME values, which are coarser
10596 // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
10597 // a single-concrete-type fast path must NOT overwrite the column
10598 // type — it would downgrade tstz to ts. NULL-only unification (PG:
10599 // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
10600 // row's pg_typeof) needs schema-level resolution — recorded, not
10601 // attempted here.
10602 if types.len() < 2 {
10603 return None;
10604 }
10605 if types.iter().all(|t| numeric_rank(*t).is_some()) {
10606 return types
10607 .iter()
10608 .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
10609 .copied();
10610 }
10611 let non_text: Vec<&DataType> = types
10612 .iter()
10613 .filter(|t| !matches!(t, DataType::Text))
10614 .collect();
10615 // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
10616 // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
10617 // if any is timestamp the result is timestamp (ts ∪ date). All values are
10618 // the same UTC-micros instant, so widening date/ts to tstz is lossless.
10619 if non_text.iter().all(|t| {
10620 matches!(
10621 t,
10622 DataType::Date | DataType::Timestamp | DataType::Timestamptz
10623 )
10624 }) && non_text
10625 .iter()
10626 .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
10627 {
10628 if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
10629 return Some(DataType::Timestamptz);
10630 }
10631 return Some(DataType::Timestamp);
10632 }
10633 // A single concrete non-TEXT type mixed with TEXT literals.
10634 if non_text.len() == 1 {
10635 return Some(*non_text[0]);
10636 }
10637 // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
10638 // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
10639 // text): resolve the concrete set first (PG treats the unknown-
10640 // typed string literals as castable to whatever the knowns
10641 // resolve to), then the TEXT cells parse into that target — the
10642 // caller's coercion dry-run still abandons the column if any
10643 // literal doesn't parse.
10644 if !non_text.is_empty() && non_text.len() < types.len() {
10645 let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
10646 return resolve_union_common_type(&concrete);
10647 }
10648 None
10649}
10650
10651/// Coerce every cell of a UNION / VALUES result column to one common
10652/// type (see [`resolve_union_common_type`]). Conservative: a column
10653/// whose branches already agree, or whose types don't resolve, or where
10654/// any cell fails to coerce, is left exactly as it was — this never
10655/// turns a previously-working query into an error.
10656fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
10657 for col_idx in 0..columns.len() {
10658 let mut seen: Vec<DataType> = Vec::new();
10659 for row in rows.iter() {
10660 if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
10661 if !seen.contains(&dt) {
10662 seen.push(dt);
10663 }
10664 }
10665 }
10666 // v7.37.16 — a single concrete runtime type under a TEXT-typed
10667 // column means the column type came off a NULL (or unknown-text)
10668 // branch: NULL literals describe as TEXT (`L::Null → Text`), so
10669 // `VALUES (NULL),(1.5)` left the column "text" while every
10670 // non-NULL cell is numeric. Adopt the concrete type — schema
10671 // only, no cell changes. tstz-safe by construction: a real
10672 // timestamptz column's schema type is Timestamptz, not Text, so
10673 // the coarser runtime type (Value::Timestamp) can't downgrade it
10674 // through this arm; and a real text column's non-NULL cells are
10675 // Text, which keeps seen == [Text] and skips it.
10676 if seen.len() == 1
10677 && matches!(columns[col_idx].ty, DataType::Text)
10678 && !matches!(seen[0], DataType::Text)
10679 {
10680 columns[col_idx].ty = seen[0];
10681 continue;
10682 }
10683 let Some(target) = resolve_union_common_type(&seen) else {
10684 continue;
10685 };
10686 // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
10687 // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
10688 // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
10689 // existing numeric cell untouched and only promote integers (to scale 0)
10690 // rather than rescaling everything to the widest scale.
10691 let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
10692 // Dry-run the coercion; abandon the whole column if any fails.
10693 let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
10694 let mut ok = true;
10695 for row in rows.iter() {
10696 match row.values.get(col_idx) {
10697 Some(Value::Numeric { .. }) if scale_preserving_numeric => {
10698 coerced.push(Some(row.values[col_idx].clone()));
10699 }
10700 Some(v) => {
10701 let cell_target = if scale_preserving_numeric {
10702 DataType::Numeric {
10703 precision: 0,
10704 scale: 0,
10705 }
10706 } else {
10707 target
10708 };
10709 match crate::conversions::coerce_value(
10710 v.clone(),
10711 cell_target,
10712 &columns[col_idx].name,
10713 col_idx,
10714 ) {
10715 Ok(cv) => coerced.push(Some(cv)),
10716 Err(_) => {
10717 ok = false;
10718 break;
10719 }
10720 }
10721 }
10722 None => coerced.push(None),
10723 }
10724 }
10725 if !ok {
10726 continue;
10727 }
10728 for (row, cv) in rows.iter_mut().zip(coerced) {
10729 if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
10730 *slot = nv;
10731 }
10732 }
10733 columns[col_idx].ty = target;
10734 }
10735}
10736
10737/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
10738/// dedup inside the recursive iteration. Crude but deterministic
10739/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
10740fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
10741 let mut out = Vec::new();
10742 for v in &row.values {
10743 // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
10744 // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
10745 // like PG (and like GROUP BY, which already normalizes). The old
10746 // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
10747 // the exact-decimal family through one scale-stripped canonical form.
10748 match v {
10749 Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
10750 Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
10751 Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
10752 Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
10753 other => {
10754 let s = alloc::format!("{other:?}|");
10755 out.extend_from_slice(s.as_bytes());
10756 }
10757 }
10758 }
10759 out
10760}
10761
10762/// Append a scale-independent canonical key for an exact-decimal value: strip
10763/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
10764/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
10765fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
10766 while scale > 0 && scaled % 10 == 0 {
10767 scaled /= 10;
10768 scale -= 1;
10769 }
10770 let s = alloc::format!("\u{1}{scaled}e-{scale}|");
10771 out.extend_from_slice(s.as_bytes());
10772}
10773
10774/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
10775/// (uncorrelated; outer refs were substituted upstream), then zip
10776/// them in parallel, NULL-padding shorter arrays to the longest
10777/// (PG's ROWS FROM shorthand). Shared by the primary-position
10778/// executor and the join-position materialiser, which both detect
10779/// the parser's `__unnest_zip` marker call.
10780pub(crate) fn unnest_zip_rows(
10781 args: &[Expr],
10782) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
10783 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
10784 let ctx = EvalContext::new(&empty_schema, None);
10785 let dummy_row = Row::new(alloc::vec::Vec::new());
10786 let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
10787 let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
10788 alloc::vec::Vec::with_capacity(args.len());
10789 for a in args {
10790 let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
10791 let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = match v {
10792 Value::Null => (DataType::Text, alloc::vec::Vec::new()),
10793 Value::TextArray(xs) => (
10794 DataType::Text,
10795 xs.into_iter()
10796 .map(|x| x.map(Value::text).unwrap_or(Value::Null))
10797 .collect(),
10798 ),
10799 Value::IntArray(xs) => (
10800 DataType::Int,
10801 xs.into_iter()
10802 .map(|x| x.map(Value::Int).unwrap_or(Value::Null))
10803 .collect(),
10804 ),
10805 Value::BigIntArray(xs) => (
10806 DataType::BigInt,
10807 xs.into_iter()
10808 .map(|x| x.map(Value::BigInt).unwrap_or(Value::Null))
10809 .collect(),
10810 ),
10811 other => {
10812 return Err(EngineError::Unsupported(alloc::format!(
10813 "unnest() expects array arguments, got {}",
10814 crate::conversions::pg_type_name_for_error_opt(other.data_type())
10815 )));
10816 }
10817 };
10818 dtypes.push(dt);
10819 columns.push(items);
10820 }
10821 let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
10822 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
10823 for i in 0..max_len {
10824 let vals: alloc::vec::Vec<Value<'static>> = columns
10825 .iter()
10826 .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
10827 .collect();
10828 rows.push(Row::new(vals));
10829 }
10830 Ok((dtypes, rows))
10831}
10832
10833/// Detect the parser's multi-arg unnest marker on an unnest_expr.
10834pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
10835 match expr {
10836 Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
10837 _ => None,
10838 }
10839}
10840
10841/// Evaluate generate_series arguments (uncorrelated — outer refs
10842/// were substituted upstream where applicable) and build the row
10843/// stream. Dispatches on the start value's shape and rejects
10844/// mixed-shape calls early (e.g. start = timestamp, stop =
10845/// integer) so the caller gets a clean error rather than a panic.
10846/// Shared by the primary-position executor and the join-position
10847/// materialiser.
10848pub(crate) fn generate_series_rows(
10849 args: &[Expr],
10850 cancel: &CancelToken<'_>,
10851) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
10852 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
10853 let ctx = EvalContext::new(&empty_schema, None);
10854 let dummy_row = Row::new(alloc::vec::Vec::new());
10855 let mut arg_values: alloc::vec::Vec<Value<'static>> =
10856 alloc::vec::Vec::with_capacity(args.len());
10857 for a in args {
10858 arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
10859 }
10860 generate_series_from_values(arg_values, args, cancel)
10861}
10862
10863/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
10864/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
10865/// full integer / numeric / timestamp overload set with the FROM-clause path.
10866/// Before this split the target-list arm reimplemented only the integer case,
10867/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
10868/// NULL for the timestamp column instead of the series. `arg_values` are the
10869/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
10870/// timestamp type resolution (it inspects the argument expressions' types).
10871pub(crate) fn generate_series_from_values(
10872 mut arg_values: alloc::vec::Vec<Value<'static>>,
10873 args: &[Expr],
10874 cancel: &CancelToken<'_>,
10875) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
10876 // PG: a NULL bound or step yields zero rows (also keeps the
10877 // NULL-padded lateral probe alive — schema without data).
10878 if arg_values.iter().any(|v| matches!(v, Value::Null)) {
10879 return Ok((DataType::BigInt, alloc::vec::Vec::new()));
10880 }
10881 // PG resolves `generate_series(date, date, interval)` to the
10882 // timestamp/timestamptz overload by implicitly casting each date
10883 // bound up to a timestamp at midnight (verified vs live PG18.4:
10884 // date args yield rows anchored at 00:00:00). SPG's TZ-naive
10885 // timestamp model renders the same instants, so fold any Date
10886 // bound to its midnight Timestamp (canonical `days *
10887 // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
10888 // the shape match so the existing timestamp arm drives the walk.
10889 // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
10890 // `generate_series(date, date, interval)` has no date overload, and among
10891 // the two candidates PG prefers the timestamptz one (timestamptz is the
10892 // preferred type of the datetime category), so the column comes back
10893 // `timestamp with time zone` — the rows render with a `+00` offset. A
10894 // timestamptz bound obviously lands there too. Only genuinely
10895 // timestamp-typed bounds keep the TZ-naive result type.
10896 let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
10897 let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
10898 || args.iter().any(|a| {
10899 crate::describe::describe_expr(a, &empty_cols)
10900 .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
10901 });
10902 for v in &mut arg_values {
10903 if let Value::Date(d) = *v {
10904 *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
10905 }
10906 }
10907 match arg_values.as_slice() {
10908 [Value::Timestamp(start), Value::Timestamp(stop), step] => {
10909 let interval_step = match step {
10910 Value::Interval { .. } => step.clone(),
10911 // v7.38 (read01) — PG resolves an unknown-type string step
10912 // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
10913 // a bare text step by parsing it the same way `::interval` does.
10914 Value::Text(s) => crate::conversions::coerce_value(
10915 Value::text(s.as_ref()),
10916 DataType::Interval,
10917 "",
10918 0,
10919 )
10920 .map_err(|_| {
10921 EngineError::Unsupported(alloc::format!(
10922 "generate_series(timestamp, timestamp, …): \
10923 could not parse step {s:?} as INTERVAL"
10924 ))
10925 })?,
10926 other => {
10927 return Err(EngineError::Unsupported(alloc::format!(
10928 "generate_series(timestamp, timestamp, …): \
10929 step must be INTERVAL, got {}",
10930 crate::conversions::pg_type_name_for_error_opt(other.data_type())
10931 )));
10932 }
10933 };
10934 let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
10935 Ok((
10936 if tz {
10937 DataType::Timestamptz
10938 } else {
10939 DataType::Timestamp
10940 },
10941 rows,
10942 ))
10943 }
10944 [start, stop, step]
10945 if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
10946 {
10947 let s = value_to_i64(start);
10948 let e = value_to_i64(stop);
10949 let st = value_to_i64(step);
10950 // PG types the series by the argument type: int4 args → int4
10951 // elements, int8 (bigint) args → int8. Any BigInt operand widens.
10952 let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
10953 let rows = generate_series_integers(s, e, st, wide, cancel)?;
10954 Ok((
10955 if wide {
10956 DataType::BigInt
10957 } else {
10958 DataType::Int
10959 },
10960 rows,
10961 ))
10962 }
10963 [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
10964 let s = value_to_i64(start);
10965 let e = value_to_i64(stop);
10966 let wide = value_is_bigint(start) || value_is_bigint(stop);
10967 let rows = generate_series_integers(s, e, 1, wide, cancel)?;
10968 Ok((
10969 if wide {
10970 DataType::BigInt
10971 } else {
10972 DataType::Int
10973 },
10974 rows,
10975 ))
10976 }
10977 // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
10978 // series in exact numeric arithmetic; NaN / infinity bounds and a
10979 // zero step get dedicated wordings, and a mixed int/numeric call
10980 // resolves here via the implicit int→numeric cast.
10981 [_, _] | [_, _, _]
10982 if arg_values
10983 .iter()
10984 .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
10985 && arg_values.iter().all(|v| {
10986 matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
10987 }) =>
10988 {
10989 use spg_storage::NumericKind as K;
10990 let words: [(&str, &str); 3] = [
10991 (
10992 "start value cannot be NaN",
10993 "start value cannot be infinity",
10994 ),
10995 ("stop value cannot be NaN", "stop value cannot be infinity"),
10996 ("step size cannot be NaN", "step size cannot be infinity"),
10997 ];
10998 for (i, v) in arg_values.iter().enumerate() {
10999 if let Value::Numeric { kind, .. } = v {
11000 if *kind != K::Finite {
11001 let (nan_w, inf_w) = words[i];
11002 return Err(EngineError::Unsupported(
11003 if *kind == K::NaN { nan_w } else { inf_w }.into(),
11004 ));
11005 }
11006 }
11007 }
11008 let big =
11009 |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
11010 let start = big(&arg_values[0]);
11011 let stop = big(&arg_values[1]);
11012 let step = if arg_values.len() == 3 {
11013 big(&arg_values[2])
11014 } else {
11015 spg_storage::bignum::BigNumeric::from_i128(1, 0)
11016 };
11017 if step.is_zero() {
11018 return Err(EngineError::Unsupported(
11019 "step size cannot equal zero".into(),
11020 ));
11021 }
11022 let descending = step.parts().0;
11023 let mut rows = alloc::vec::Vec::new();
11024 let mut cur = start;
11025 const MAX_ROWS: usize = 10_000_000;
11026 loop {
11027 cancel.check()?;
11028 let c = cur.cmp(&stop);
11029 if descending {
11030 if c == core::cmp::Ordering::Less {
11031 break;
11032 }
11033 } else if c == core::cmp::Ordering::Greater {
11034 break;
11035 }
11036 if rows.len() >= MAX_ROWS {
11037 return Err(EngineError::Unsupported(alloc::format!(
11038 "generate_series() result exceeds {MAX_ROWS} rows"
11039 )));
11040 }
11041 rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
11042 cur.clone()
11043 )]));
11044 cur = cur.add(&step);
11045 }
11046 Ok((
11047 DataType::Numeric {
11048 precision: 0,
11049 scale: 0,
11050 },
11051 rows,
11052 ))
11053 }
11054 _ => Err(EngineError::Unsupported(alloc::format!(
11055 "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
11056 argument shapes; got {}",
11057 arg_values
11058 .iter()
11059 .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
11060 .collect::<alloc::vec::Vec<_>>()
11061 .join(", ")
11062 ))),
11063 }
11064}
11065
11066/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
11067/// Step direction follows the sign: positive step iterates upward
11068/// (stops when current > stop); negative iterates downward; zero
11069/// errors. Caller-facing row stream is `BigInt`-typed so a single
11070/// projection schema covers SmallInt / Int / BigInt callers.
11071fn generate_series_integers(
11072 start: i64,
11073 stop: i64,
11074 step: i64,
11075 wide: bool,
11076 cancel: &CancelToken<'_>,
11077) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11078 if step == 0 {
11079 return Err(EngineError::Unsupported(
11080 "step size cannot equal zero".into(),
11081 ));
11082 }
11083 let mut out = alloc::vec::Vec::new();
11084 let mut cur = start;
11085 // Hard cap to keep a runaway call from eating all memory. PG
11086 // has no such cap but does honour query timeout; SPG's cancel
11087 // token will fire too — this is a defense-in-depth backstop.
11088 const MAX_ROWS: usize = 10_000_000;
11089 loop {
11090 cancel.check()?;
11091 if step > 0 && cur > stop {
11092 break;
11093 }
11094 if step < 0 && cur < stop {
11095 break;
11096 }
11097 out.push(Row::new(alloc::vec![if wide {
11098 Value::BigInt(cur)
11099 } else {
11100 Value::Int(cur as i32)
11101 }]));
11102 if out.len() > MAX_ROWS {
11103 return Err(EngineError::Unsupported(alloc::format!(
11104 "generate_series(): exceeded {MAX_ROWS} rows; \
11105 narrow start/stop or use a larger step"
11106 )));
11107 }
11108 cur = match cur.checked_add(step) {
11109 Some(n) => n,
11110 None => break,
11111 };
11112 }
11113 Ok(out)
11114}
11115
11116/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
11117/// `Value::Interval { months, micros }` per the caller's guard;
11118/// each iteration adds the interval via `apply_binary_interval`
11119/// so month-shifting handles short-month rollover (PG semantics).
11120fn generate_series_timestamps(
11121 start: i64,
11122 stop: i64,
11123 step: Value,
11124 cancel: &CancelToken<'_>,
11125) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11126 let (months, days, micros) = match &step {
11127 Value::Interval {
11128 months,
11129 days,
11130 micros,
11131 } => (*months, *days, *micros),
11132 _ => unreachable!("caller guards step.is_interval"),
11133 };
11134 if months == 0 && days == 0 && micros == 0 {
11135 return Err(EngineError::Unsupported(
11136 "generate_series(): INTERVAL step cannot be zero".into(),
11137 ));
11138 }
11139 let ascending = months > 0 || days > 0 || micros > 0;
11140 let mut out = alloc::vec::Vec::new();
11141 let mut cur = Value::Timestamp(start);
11142 const MAX_ROWS: usize = 10_000_000;
11143 loop {
11144 cancel.check()?;
11145 let cur_t = match cur {
11146 Value::Timestamp(t) => t,
11147 _ => unreachable!("loop invariant: cur is Timestamp"),
11148 };
11149 if ascending && cur_t > stop {
11150 break;
11151 }
11152 if !ascending && cur_t < stop {
11153 break;
11154 }
11155 out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
11156 if out.len() > MAX_ROWS {
11157 return Err(EngineError::Unsupported(alloc::format!(
11158 "generate_series(): exceeded {MAX_ROWS} rows; \
11159 narrow start/stop or use a larger step"
11160 )));
11161 }
11162 let next = eval::apply_binary_interval(
11163 spg_sql::ast::BinOp::Add,
11164 &cur,
11165 &Value::Interval {
11166 months,
11167 days,
11168 micros,
11169 },
11170 )
11171 .map_err(EngineError::Eval)?;
11172 cur = match next {
11173 Some(v) => v,
11174 None => break,
11175 };
11176 }
11177 Ok(out)
11178}
11179
11180/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
11181/// WITH TIES` requires an `ORDER BY`. Without one, there's no
11182/// way to identify "ties" deterministically, so PG errors at
11183/// plan time. SPG mirrors that surface so the same DDL / app
11184/// behaviour holds on cutover.
11185fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
11186 if stmt.limit_with_ties && stmt.order_by.is_empty() {
11187 return Err(EngineError::Unsupported(alloc::string::String::from(
11188 "WITH TIES cannot be specified without ORDER BY clause",
11189 )));
11190 }
11191 Ok(())
11192}
11193
11194/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
11195/// (case-insensitive). Used by `exec_select_cancel`'s
11196/// projection loop to detect Set-Returning-Function rows that
11197/// need per-row expansion. Only the top-level call counts —
11198/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
11199/// projection's perspective; it would surface as an "unknown
11200/// function" mismatch downstream, which is what we want
11201/// (multi-SRF / nested SRF is documented carve-out for v7.19).
11202fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
11203 top_level_srf_kind(expr).is_some()
11204}
11205
11206/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
11207/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
11208/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
11209/// source row.
11210#[derive(Clone, Copy, PartialEq, Eq)]
11211pub(crate) enum SrfKind {
11212 Unnest,
11213 /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
11214 /// list. It used to be handled ONLY by the parser's lift into FROM, so a
11215 /// second one in the same list came back as "unknown function".
11216 GenerateSeries,
11217 GenerateSubscripts,
11218 /// `_text` variants unwrap scalars to their lexeme; the plain forms render
11219 /// every value as compact JSON text.
11220 ArrayElements {
11221 as_text: bool,
11222 },
11223 PathQuery,
11224 RegexpMatches,
11225 Each {
11226 as_text: bool,
11227 },
11228 ObjectKeys,
11229}
11230
11231/// Case-insensitive match against any of `names`.
11232fn name_is(name: &str, names: &[&str]) -> bool {
11233 names.iter().any(|n| name.eq_ignore_ascii_case(n))
11234}
11235
11236pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
11237 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
11238 return None;
11239 };
11240 let n = args.len();
11241 // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
11242 // SELECT list (it returned an array there before) and shares the unnest
11243 // expansion machinery.
11244 if n == 1 && name.eq_ignore_ascii_case("unnest") {
11245 return Some(SrfKind::Unnest);
11246 }
11247 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
11248 return Some(SrfKind::GenerateSeries);
11249 }
11250 if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
11251 return Some(SrfKind::GenerateSubscripts);
11252 }
11253 // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
11254 // per element / match in the SELECT list; they collapsed to a single row
11255 // (a TextArray, or an "unknown function" error for `each`) before.
11256 if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
11257 return Some(SrfKind::ArrayElements { as_text: false });
11258 }
11259 if n == 1
11260 && name_is(
11261 name,
11262 &["jsonb_array_elements_text", "json_array_elements_text"],
11263 )
11264 {
11265 return Some(SrfKind::ArrayElements { as_text: true });
11266 }
11267 // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
11268 if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
11269 return Some(SrfKind::PathQuery);
11270 }
11271 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
11272 return Some(SrfKind::RegexpMatches);
11273 }
11274 if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
11275 return Some(SrfKind::Each { as_text: false });
11276 }
11277 if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
11278 return Some(SrfKind::Each { as_text: true });
11279 }
11280 if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
11281 return Some(SrfKind::ObjectKeys);
11282 }
11283 None
11284}
11285
11286/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
11287/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
11288/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
11289/// rows, as in PG).
11290pub(crate) fn top_level_srf_output(
11291 expr: &spg_sql::ast::Expr,
11292 row: &Row<'static>,
11293 ctx: &EvalContext<'_>,
11294) -> Result<Vec<Value<'static>>, EngineError> {
11295 let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
11296 (top_level_srf_kind(expr), expr)
11297 else {
11298 return Err(EngineError::Unsupported(
11299 "expected a SELECT-list SRF call".into(),
11300 ));
11301 };
11302 match kind {
11303 SrfKind::Unnest => {
11304 // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
11305 // the elements DIRECTLY: the old path built the whole
11306 // Value::Array (one eval + a clone per element) only for
11307 // array_value_to_elements to clone every element back out.
11308 // Any other argument shape (a column, a function result)
11309 // keeps the build-then-split path.
11310 if let spg_sql::ast::Expr::Array(items) = &args[0] {
11311 return items
11312 .iter()
11313 .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
11314 .collect();
11315 }
11316 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11317 array_value_to_elements(&arr)
11318 }
11319 SrfKind::GenerateSeries => {
11320 // v7.39 (read01 round 96) — evaluate the args against the actual
11321 // row, then hand off to the shared core so the numeric and
11322 // timestamp/timestamptz overloads work here too (this arm used to
11323 // handle only integers, silently NULLing a temporal/numeric series
11324 // when it shared a target list with another SRF).
11325 let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
11326 for a in args {
11327 arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
11328 }
11329 let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
11330 Ok(rows
11331 .into_iter()
11332 .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
11333 .collect())
11334 }
11335 SrfKind::GenerateSubscripts => {
11336 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11337 let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
11338 if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
11339 return Ok(Vec::new());
11340 }
11341 let len = array_value_to_elements(&arr)?.len();
11342 Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
11343 }
11344 // One Value per array element (`_text` → text / SQL NULL, plain → the
11345 // element's compact JSON text) — the element list the FROM-clause form
11346 // materialises.
11347 SrfKind::ArrayElements { as_text } => {
11348 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11349 if matches!(arg, Value::Null) {
11350 return Ok(Vec::new());
11351 }
11352 let items =
11353 crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
11354 Ok(items
11355 .into_iter()
11356 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
11357 .collect())
11358 }
11359 // The scalar form already yields a TextArray of the keys (or errors on
11360 // a non-object, like PG); expand it into rows.
11361 SrfKind::ObjectKeys => {
11362 let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
11363 array_value_to_elements(&v)
11364 }
11365 // One row per match, each a text[] of the pattern's capture groups.
11366 SrfKind::RegexpMatches => {
11367 let vals: Vec<Value<'static>> = args
11368 .iter()
11369 .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
11370 .collect::<Result<_, _>>()?;
11371 crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
11372 }
11373 // One composite `(key, value)` row per object member (plain → jsonb
11374 // value, `_text` → text / SQL NULL).
11375 SrfKind::Each { as_text } => {
11376 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11377 if matches!(arg, Value::Null) {
11378 return Ok(Vec::new());
11379 }
11380 let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
11381 Ok(pairs
11382 .into_iter()
11383 .map(|(k, v)| {
11384 let val = if as_text {
11385 v.map(Value::text).unwrap_or(Value::Null)
11386 } else {
11387 v.map(Value::json).unwrap_or(Value::Null)
11388 };
11389 Value::Composite(alloc::vec![
11390 ("key".to_string(), Value::text(k)),
11391 ("value".to_string(), val),
11392 ])
11393 })
11394 .collect())
11395 }
11396 // One Value per matched JSON value.
11397 SrfKind::PathQuery => {
11398 let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11399 let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
11400 // v7.39 — optional vars document (3rd arg).
11401 let vars = match args.get(2) {
11402 Some(a) => {
11403 let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
11404 crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
11405 }
11406 None => None,
11407 };
11408 match crate::json::path_query_vars(&doc, &path, vars.as_ref())
11409 .map_err(EngineError::Eval)?
11410 {
11411 Value::Null => Ok(Vec::new()),
11412 Value::TextArray(items) => Ok(items
11413 .into_iter()
11414 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
11415 .collect()),
11416 other => Ok(alloc::vec![other]),
11417 }
11418 }
11419 }
11420}
11421
11422/// v7.19 P5 — turn an array-typed `Value` into the element list
11423/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
11424/// = (no rows)`). Non-array values fall through to a type-mismatch
11425/// error.
11426pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
11427 // v7.39 (round 236) — PG unnests a multidimensional array into its
11428 // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
11429 // rows). SPG stores 2-D arrays as their own variants, which fell
11430 // through to the type-mismatch arm below.
11431 if let Some(flat) = crate::eval::values::flatten_2d(v) {
11432 return array_value_to_elements(&flat);
11433 }
11434 match v {
11435 Value::Null => Ok(Vec::new()),
11436 Value::TextArray(items) => Ok(items
11437 .iter()
11438 .map(|opt| {
11439 opt.as_ref()
11440 .map(|s| Value::text(s.clone()))
11441 .unwrap_or(Value::Null)
11442 })
11443 .collect()),
11444 Value::IntArray(items) => Ok(items
11445 .iter()
11446 .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
11447 .collect()),
11448 Value::BigIntArray(items) => Ok(items
11449 .iter()
11450 .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
11451 .collect()),
11452 // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
11453 // range per canonical span.
11454 Value::Multirange { kind, ranges } => Ok(ranges
11455 .iter()
11456 .map(|s| Value::Range {
11457 kind: *kind,
11458 lower: s.lower.clone(),
11459 upper: s.upper.clone(),
11460 lower_inc: s.lower_inc,
11461 upper_inc: s.upper_inc,
11462 empty: false,
11463 })
11464 .collect()),
11465 other => Err(EngineError::Eval(EvalError::TypeMismatch {
11466 detail: alloc::format!(
11467 "unnest() expects an array argument, got {}",
11468 crate::conversions::pg_type_name_for_error_opt(other.data_type())
11469 ),
11470 })),
11471 }
11472}
11473
11474impl Engine {
11475 /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
11476 /// the SELECT's FROM / JOIN graph, re-parse each view's body
11477 /// source, and prepend it as a synthetic CTE on the
11478 /// returned SelectStatement. Returns `None` when no view
11479 /// references are found (caller proceeds with the original
11480 /// statement); returns `Some(rewritten)` otherwise (caller
11481 /// re-runs exec_select_cancel on the rewritten form so the
11482 /// regular CTE materialiser handles it).
11483 fn expand_views_in_select(
11484 &self,
11485 stmt: &SelectStatement,
11486 ) -> Result<Option<SelectStatement>, EngineError> {
11487 let cat = self.active_catalog();
11488 let mut referenced: Vec<String> = Vec::new();
11489 if let Some(from) = &stmt.from {
11490 collect_view_refs(&from.primary, cat, &mut referenced);
11491 for j in &from.joins {
11492 collect_view_refs(&j.table, cat, &mut referenced);
11493 }
11494 }
11495 // Don't expand a view name that's already shadowed by a
11496 // CTE on the same SELECT — the CTE wins per PG.
11497 referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
11498 if referenced.is_empty() {
11499 return Ok(None);
11500 }
11501 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
11502 for name in &referenced {
11503 let view = cat.view(name).ok_or_else(|| {
11504 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
11505 "view {name:?} disappeared mid-expansion"
11506 )))
11507 })?;
11508 let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
11509 EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
11510 })?;
11511 let Statement::Select(body) = parsed else {
11512 return Err(EngineError::Unsupported(alloc::format!(
11513 "view {name:?} body is not a SELECT (catalog corruption)"
11514 )));
11515 };
11516 new_ctes.push(spg_sql::ast::Cte {
11517 name: name.clone(),
11518 body: spg_sql::ast::CteBody::Select(body),
11519 recursive: false,
11520 column_overrides: view.columns.clone(),
11521 search: None,
11522 cycle: None,
11523 });
11524 }
11525 let mut out = stmt.clone();
11526 // Prepend so view CTEs are visible to caller-supplied CTEs.
11527 new_ctes.extend(out.ctes);
11528 out.ctes = new_ctes;
11529 Ok(Some(out))
11530 }
11531
11532 /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
11533 /// any partition-parent table, rewrite the SELECT so each parent
11534 /// reference resolves to a CTE whose body is a `UNION ALL` over the
11535 /// children that pass the WHERE-derived partition-key range. Returns
11536 /// `None`(no rewrite needed)when no parent is referenced or all
11537 /// references are shadowed by a same-name CTE.
11538 ///
11539 /// Pruning vocabulary at v7.37.6-B:
11540 /// * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
11541 /// and `<key> BETWEEN literal AND literal`.
11542 /// * Anything outside that(OR / nested IN / function call on the
11543 /// key)defaults to "no pruning" — every child + DEFAULT lands
11544 /// in the UNION. Correctness is preserved; only the plan size
11545 /// widens.
11546 fn expand_partition_parents_in_select(
11547 &self,
11548 stmt: &SelectStatement,
11549 ) -> Result<Option<SelectStatement>, EngineError> {
11550 let cat = self.active_catalog();
11551 let Some(from) = &stmt.from else {
11552 return Ok(None);
11553 };
11554 let mut parent_refs: Vec<String> = Vec::new();
11555 collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
11556 for j in &from.joins {
11557 collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
11558 }
11559 // Drop names shadowed by a CTE on the same SELECT(PG semantics
11560 // — same as view expansion above).
11561 parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
11562 if parent_refs.is_empty() {
11563 return Ok(None);
11564 }
11565 // Synthesise a CTE name per parent so the existing
11566 // "CTE shadows a real table" guard doesn't fire (the parent
11567 // IS a real table in the catalog, unlike VIEW expansion's
11568 // case). The FROM-clause TableRef walker below rewrites
11569 // every parent reference to point at the synthetic CTE.
11570 let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
11571 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
11572 let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
11573 for parent_name in &parent_refs {
11574 // No children = no rewrite. The parent itself is a real
11575 // (empty-rows) table — the regular FROM-resolution path
11576 // will scan it and return 0 rows, matching the
11577 // "partition parent with no children" plan. Skipping the
11578 // CTE here also avoids `SELECT * FROM parent` re-entering
11579 // this rewrite on the synthetic body (infinite recursion).
11580 let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
11581 continue;
11582 };
11583 new_ctes.push(spg_sql::ast::Cte {
11584 name: synth_name(parent_name),
11585 body: spg_sql::ast::CteBody::Select(body),
11586 recursive: false,
11587 column_overrides: Vec::new(),
11588 search: None,
11589 cycle: None,
11590 });
11591 expanded_parents.push(parent_name.clone());
11592 }
11593 if expanded_parents.is_empty() {
11594 return Ok(None);
11595 }
11596 let mut out = stmt.clone();
11597 if let Some(from) = out.from.as_mut() {
11598 rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
11599 for j in &mut from.joins {
11600 rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
11601 }
11602 }
11603 new_ctes.extend(out.ctes);
11604 out.ctes = new_ctes;
11605 Ok(Some(out))
11606 }
11607
11608 /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
11609 /// Children include every overlap-hit `Range` plus(always)the
11610 /// `Default` child(if any). Returns `Ok(None)` when no children
11611 /// would survive — caller skips the CTE injection and lets the
11612 /// parent fall through to the regular(empty-rows)scan path,
11613 /// avoiding the infinite recursion that an empty-body CTE
11614 /// referencing the parent name would trigger.
11615 /// v7.37.16 (16.10) — public helper invoked from explain.rs to
11616 /// surface "which children survive the WHERE-clause prune" in
11617 /// EXPLAIN output. Returns `None` when `parent_name` isn't
11618 /// actually a partition parent; otherwise returns the list of
11619 /// children the planner would scan (same algorithm as
11620 /// [`Self::build_partition_parent_union_body`] but without the
11621 /// SQL re-parse).
11622 /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
11623 /// expression (the PG-shaped EXPLAIN's scan builder has no full
11624 /// SelectStatement in hand). Wraps the original by synthesising a
11625 /// minimal statement carrying just the predicate.
11626 pub(crate) fn explain_partition_kept_children_by_where(
11627 &self,
11628 parent_name: &str,
11629 where_: Option<&spg_sql::ast::Expr>,
11630 ) -> Option<Vec<alloc::string::String>> {
11631 let mut synth = SelectStatement::default();
11632 synth.where_ = where_.cloned();
11633 self.explain_partition_kept_children(parent_name, &synth)
11634 }
11635
11636 pub(crate) fn explain_partition_kept_children(
11637 &self,
11638 parent_name: &str,
11639 outer: &SelectStatement,
11640 ) -> Option<Vec<alloc::string::String>> {
11641 use spg_storage::PartitionRole;
11642 let cat = self.active_catalog();
11643 let parent = cat.get(parent_name)?;
11644 let (key_position, parent_kind) = match &parent.schema().partition_role {
11645 Some(PartitionRole::Parent {
11646 key_column_positions,
11647 kind,
11648 ..
11649 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
11650 _ => return None,
11651 };
11652 let key_col_name = parent.schema().columns[key_position].name.clone();
11653 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
11654 Some(expr) => extract_key_range(expr, &key_col_name),
11655 None => (None, None),
11656 };
11657 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
11658 Some(expr) => extract_key_eq_value(expr, &key_col_name),
11659 None => None,
11660 };
11661 let children = crate::partition::children_of_parent(cat, parent_name);
11662 let mut kept: Vec<alloc::string::String> = Vec::new();
11663 let mut default_child: Option<alloc::string::String> = None;
11664 for child_name in &children {
11665 let Some(child) = cat.get(child_name) else {
11666 continue;
11667 };
11668 match &child.schema().partition_role {
11669 Some(PartitionRole::Range { lower, upper, .. }) => {
11670 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
11671 kept.push(child_name.clone());
11672 }
11673 }
11674 Some(PartitionRole::List { values, .. }) => match &eq_value {
11675 Some(v) => {
11676 if values.iter().any(|b| b.equals_value(v)) {
11677 kept.push(child_name.clone());
11678 }
11679 }
11680 None => kept.push(child_name.clone()),
11681 },
11682 Some(PartitionRole::Hash {
11683 modulus, remainder, ..
11684 }) => match &eq_value {
11685 Some(v) => {
11686 let h = crate::partition::pg_compatible_hash(v);
11687 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
11688 kept.push(child_name.clone());
11689 }
11690 }
11691 None => kept.push(child_name.clone()),
11692 },
11693 Some(PartitionRole::Default { .. }) => {
11694 default_child = Some(child_name.clone());
11695 }
11696 _ => {}
11697 }
11698 }
11699 let _ = parent_kind;
11700 if let Some(d) = default_child {
11701 if kept.is_empty() || eq_value.is_none() {
11702 kept.push(d);
11703 }
11704 }
11705 Some(kept)
11706 }
11707
11708 fn build_partition_parent_union_body(
11709 &self,
11710 parent_name: &str,
11711 outer: &SelectStatement,
11712 ) -> Result<Option<SelectStatement>, EngineError> {
11713 use spg_storage::PartitionRole;
11714 let cat = self.active_catalog();
11715 let parent = cat.get(parent_name).ok_or_else(|| {
11716 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
11717 "partition parent {parent_name:?} disappeared mid-expansion"
11718 )))
11719 })?;
11720 let (key_position, parent_kind) = match &parent.schema().partition_role {
11721 Some(PartitionRole::Parent {
11722 key_column_positions,
11723 kind,
11724 ..
11725 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
11726 // v7.39 (round 645) — an INHERITANCE parent, which has no
11727 // role of its own: the relationship is recorded only in the
11728 // children. Three things differ from a partition parent and
11729 // all three are in this body.
11730 //
11731 // * The parent HOLDS ROWS, so it is a term of the union —
11732 // `FROM ONLY`, or expanding it would recurse.
11733 // * There is no partition key, so there is nothing to
11734 // prune: every child is a term.
11735 // * A child may declare columns of its own, so the terms
11736 // name the PARENT's columns rather than `*`. PG's
11737 // `SELECT * FROM parent` returns the parent's shape.
11738 //
11739 // Answered from this match rather than a branch before it —
11740 // round 644 measured what an extra early return beside an
11741 // existing test costs in this file.
11742 _ if crate::partition::has_inheritance_children(cat, parent_name) => {
11743 let cols = parent
11744 .schema()
11745 .columns
11746 .iter()
11747 .map(|c| quote_ident_for_sql(&c.name))
11748 .collect::<Vec<_>>()
11749 .join(", ");
11750 let carry_sys = references_ctid(outer);
11751 let sys = if carry_sys {
11752 let mut t = alloc::string::String::new();
11753 for s in SYSTEM_COLUMNS {
11754 t.push_str(", ");
11755 t.push_str(s);
11756 }
11757 t
11758 } else {
11759 alloc::string::String::new()
11760 };
11761 let mut body = alloc::format!(
11762 "SELECT {cols}{sys} FROM ONLY {}",
11763 quote_ident_for_sql(parent_name)
11764 );
11765 for child in crate::partition::children_of_parent(cat, parent_name) {
11766 body.push_str(&alloc::format!(
11767 " UNION ALL SELECT {cols}{sys} FROM {}",
11768 quote_ident_for_sql(&child)
11769 ));
11770 }
11771 return parse_select_or_corrupt(&body).map(Some);
11772 }
11773 _ => {
11774 return Err(EngineError::Unsupported(alloc::format!(
11775 "partition expansion: {parent_name:?} is not a parent"
11776 )));
11777 }
11778 };
11779 let key_col_name = parent.schema().columns[key_position].name.clone();
11780 // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
11781 // off the WHERE; for LIST / HASH we extract a single `=`
11782 // literal (and the rest of the planner falls back to "keep
11783 // every child" — same conservative path as 16.1/16.2).
11784 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
11785 Some(expr) => extract_key_range(expr, &key_col_name),
11786 None => (None, None),
11787 };
11788 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
11789 Some(expr) => extract_key_eq_value(expr, &key_col_name),
11790 None => None,
11791 };
11792 let children = crate::partition::children_of_parent(cat, parent_name);
11793 let mut kept: Vec<String> = Vec::new();
11794 let mut default_child: Option<String> = None;
11795 // First pass — apply per-strategy gates, defer DEFAULT until
11796 // we know whether some non-DEFAULT child matched.
11797 for child_name in &children {
11798 let Some(child) = cat.get(child_name) else {
11799 continue;
11800 };
11801 match &child.schema().partition_role {
11802 Some(PartitionRole::Range { lower, upper, .. }) => {
11803 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
11804 kept.push(child_name.clone());
11805 }
11806 }
11807 // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
11808 // = <lit>`, only the child whose values contain that
11809 // literal survives. Otherwise (no equality predicate
11810 // or planner couldn't extract one) keep the child
11811 // conservatively.
11812 Some(PartitionRole::List { values, .. }) => match &eq_value {
11813 Some(v) => {
11814 if values.iter().any(|b| b.equals_value(v)) {
11815 kept.push(child_name.clone());
11816 }
11817 }
11818 None => kept.push(child_name.clone()),
11819 },
11820 // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
11821 // we know the residue class deterministically, so
11822 // only the matching REMAINDER child survives.
11823 Some(PartitionRole::Hash {
11824 modulus, remainder, ..
11825 }) => match &eq_value {
11826 Some(v) => {
11827 let h = crate::partition::pg_compatible_hash(v);
11828 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
11829 kept.push(child_name.clone());
11830 }
11831 }
11832 None => kept.push(child_name.clone()),
11833 },
11834 Some(PartitionRole::Default { .. }) => {
11835 default_child = Some(child_name.clone());
11836 }
11837 _ => {}
11838 }
11839 }
11840 // PG-style DEFAULT semantics: the DEFAULT child must be
11841 // scanned iff some row could fall outside every concrete
11842 // child's bound predicate. We approximate that as "no
11843 // concrete child matched" (== full prune) — strictly
11844 // conservative for LIST / HASH (DEFAULT also catches rows
11845 // outside the union of value-sets / residues), and matches
11846 // PG for the equality case where we *do* know the routing
11847 // outcome.
11848 let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
11849 if let Some(d) = default_child {
11850 if kept.is_empty() {
11851 kept.push(d);
11852 } else if eq_value.is_none() {
11853 // Without an equality literal, the DEFAULT child may
11854 // still hold matching rows (e.g. LIKE on TEXT keys
11855 // for which a LIST partition exists). Keep it.
11856 kept.push(d);
11857 }
11858 }
11859 // Build the UNION ALL body text and re-parse — keeps the
11860 // rewrite expressible in surface SQL so the engine's existing
11861 // parser path handles the AST shape uniformly.
11862 if kept.is_empty() {
11863 // No children survive — caller falls back to scanning the
11864 // (empty) parent table. Returning None here is what
11865 // prevents the synthetic CTE from referring back to the
11866 // parent name and re-entering this rewrite pass.
11867 let _ = parent_name;
11868 return Ok(None);
11869 }
11870 // v7.39 (round 622, S05a) — the system columns of the CHILD the row
11871 // actually lives in.
11872 //
11873 // The parent is read through a synthetic CTE, so a `tableoid` on it
11874 // resolved against that CTE: every row of every child reported
11875 // `__spg_partition_pm`, an internal name no user ever typed, where
11876 // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
11877 // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
11878 // one asks "which partition is this row in", answering 0 rows where
11879 // PG answers 1. `ctid` had the same shape: it numbered the CTE's
11880 // output, so rows in different children got distinct ctids instead
11881 // of each child's own physical position.
11882 //
11883 // Naming them in the term is what carries them: the child scan
11884 // materialises its own six because the statement now references
11885 // them, and they land in SYSTEM_COLUMNS order right after the user
11886 // columns — the exact layout the positional `*` skip already
11887 // expects. Only done when the outer statement asks for one, so a
11888 // plain `SELECT * FROM parent` scans exactly what it scanned.
11889 let carry_sys = references_ctid(outer);
11890 let mut body = alloc::string::String::new();
11891 for (i, child_name) in kept.iter().enumerate() {
11892 if i > 0 {
11893 body.push_str(" UNION ALL ");
11894 }
11895 body.push_str("SELECT *");
11896 if carry_sys {
11897 for sys in SYSTEM_COLUMNS {
11898 body.push_str(", ");
11899 body.push_str(sys);
11900 }
11901 }
11902 body.push_str(" FROM ");
11903 body.push_str("e_ident_for_sql(child_name));
11904 }
11905 parse_select_or_corrupt(&body).map(Some)
11906 }
11907}
11908
11909/// Rewrite a `TableRef` pointing at a partition parent so it
11910/// references the synthetic CTE created by the expansion. If the
11911/// original ref had no alias, preserve the parent name as an alias
11912/// so column references like `events_partitioned.received_at`
11913/// keep resolving.
11914fn rewrite_partition_parent_table_ref(
11915 t: &mut spg_sql::ast::TableRef,
11916 parents: &[alloc::string::String],
11917 synth_name: &impl Fn(&str) -> alloc::string::String,
11918) {
11919 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
11920 return;
11921 }
11922 // v7.39 (round 644) — an ONLY reference stays pointed at the parent
11923 // itself. The rewrite is keyed on the NAME, so in
11924 // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
11925 // parent list and this then rewrote BOTH — including the one that
11926 // asked not to descend. PG answers 0 for that join; SPG answered 2.
11927 // Folded into the existing test — see the note in
11928 // `collect_partition_parent_refs` for what a separate one cost.
11929 if t.only || !parents.iter().any(|p| p == &t.name) {
11930 return;
11931 }
11932 if t.alias.is_none() {
11933 t.alias = Some(t.name.clone());
11934 }
11935 t.name = synth_name(&t.name);
11936}
11937
11938/// Walk a `TableRef` and push its `name` if it resolves to a partition
11939/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
11940/// `generate_series_args` references — those aren't catalog tables.
11941fn collect_partition_parent_refs(
11942 t: &spg_sql::ast::TableRef,
11943 cat: &spg_storage::Catalog,
11944 out: &mut Vec<alloc::string::String>,
11945) {
11946 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
11947 return;
11948 }
11949 // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
11950 // The keyword used to be absorbed at parse time, so this fanned out
11951 // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
11952 // answered 2 where PG answers 0.
11953 //
11954 // Folded into the existing test rather than given an early return of
11955 // its own: as two extra lines in this function's body it cost
11956 // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
11957 // outside the panel. Rounds 641 and 643 met the same wall from the
11958 // other two directions — adding to a hot function and taking away
11959 // from a cold one. What goes in a body near the row loop is a
11960 // codegen decision whatever its shape.
11961 if !t.only && crate::partition::has_children(cat, &t.name) {
11962 out.push(t.name.clone());
11963 }
11964}
11965
11966/// v7.37.6-B partition-key range derived from a WHERE expression.
11967/// `i64` microseconds since epoch with the same sign convention as
11968/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
11969/// / `=`),`false` ⇒ exclusive(`>` / `<`).
11970#[derive(Debug, Clone, Copy)]
11971pub(crate) struct PartitionFilterBound {
11972 pub micros: i64,
11973 pub inclusive: bool,
11974}
11975
11976/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
11977/// shapes; tighten the running lo / hi as we go. Anything outside that
11978/// (OR / nested calls / non-key columns)is ignored — caller treats
11979/// `None` as "no constraint on that side."
11980fn extract_key_range(
11981 expr: &spg_sql::ast::Expr,
11982 key_col: &str,
11983) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
11984 let mut lo: Option<PartitionFilterBound> = None;
11985 let mut hi: Option<PartitionFilterBound> = None;
11986 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
11987 while let Some(e) = stack.pop() {
11988 match e {
11989 spg_sql::ast::Expr::Binary {
11990 lhs,
11991 op: spg_sql::ast::BinOp::And,
11992 rhs,
11993 } => {
11994 stack.push(lhs);
11995 stack.push(rhs);
11996 }
11997 // BETWEEN is desugared at parse time into `lhs >= low AND
11998 // lhs <= high`, so it lands here as two regular Binary
11999 // arms via the AND walker above.
12000 spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
12001 let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
12002 (Some(lhs.as_ref()), rhs.as_ref(), false)
12003 } else if is_column_ref(rhs, key_col) {
12004 (Some(rhs.as_ref()), lhs.as_ref(), true)
12005 } else {
12006 (None, lhs.as_ref(), false)
12007 };
12008 if col_ref.is_none() {
12009 continue;
12010 }
12011 let Some(lit) = literal_to_micros(lit_side) else {
12012 continue;
12013 };
12014 use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
12015 let effective_op = if swapped {
12016 match op {
12017 Lt => Gt,
12018 LtEq => GtEq,
12019 Gt => Lt,
12020 GtEq => LtEq,
12021 other => *other,
12022 }
12023 } else {
12024 *op
12025 };
12026 match effective_op {
12027 Eq => {
12028 tighten_lo(
12029 &mut lo,
12030 PartitionFilterBound {
12031 micros: lit,
12032 inclusive: true,
12033 },
12034 );
12035 tighten_hi(
12036 &mut hi,
12037 PartitionFilterBound {
12038 micros: lit,
12039 inclusive: true,
12040 },
12041 );
12042 }
12043 GtEq => {
12044 tighten_lo(
12045 &mut lo,
12046 PartitionFilterBound {
12047 micros: lit,
12048 inclusive: true,
12049 },
12050 );
12051 }
12052 Gt => {
12053 tighten_lo(
12054 &mut lo,
12055 PartitionFilterBound {
12056 micros: lit,
12057 inclusive: false,
12058 },
12059 );
12060 }
12061 LtEq => {
12062 tighten_hi(
12063 &mut hi,
12064 PartitionFilterBound {
12065 micros: lit,
12066 inclusive: true,
12067 },
12068 );
12069 }
12070 Lt => {
12071 tighten_hi(
12072 &mut hi,
12073 PartitionFilterBound {
12074 micros: lit,
12075 inclusive: false,
12076 },
12077 );
12078 }
12079 _ => {}
12080 }
12081 }
12082 _ => {}
12083 }
12084 }
12085 (lo, hi)
12086}
12087
12088fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12089 match slot {
12090 None => *slot = Some(new),
12091 Some(cur) => {
12092 if new.micros > cur.micros
12093 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12094 {
12095 *slot = Some(new);
12096 }
12097 }
12098 }
12099}
12100
12101fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12102 match slot {
12103 None => *slot = Some(new),
12104 Some(cur) => {
12105 if new.micros < cur.micros
12106 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12107 {
12108 *slot = Some(new);
12109 }
12110 }
12111 }
12112}
12113
12114fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
12115 if let spg_sql::ast::Expr::Column(c) = e {
12116 c.name.eq_ignore_ascii_case(key_col)
12117 } else {
12118 false
12119 }
12120}
12121
12122/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
12123/// `key_col = <literal>` predicate out for LIST/HASH partition
12124/// pruning. Returns `None` when no equality literal can be lifted
12125/// (planner then keeps every child — correctness preserved). The
12126/// returned `Value<'static>` is an owned coercion so the caller can
12127/// outlive any AST node it was extracted from.
12128pub(crate) fn extract_key_eq_value(
12129 expr: &spg_sql::ast::Expr,
12130 key_col: &str,
12131) -> Option<spg_storage::Value<'static>> {
12132 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12133 while let Some(e) = stack.pop() {
12134 match e {
12135 spg_sql::ast::Expr::Binary {
12136 lhs,
12137 op: spg_sql::ast::BinOp::And,
12138 rhs,
12139 } => {
12140 stack.push(lhs);
12141 stack.push(rhs);
12142 }
12143 spg_sql::ast::Expr::Binary {
12144 lhs,
12145 op: spg_sql::ast::BinOp::Eq,
12146 rhs,
12147 } => {
12148 let lit_side = if is_column_ref(lhs, key_col) {
12149 rhs.as_ref()
12150 } else if is_column_ref(rhs, key_col) {
12151 lhs.as_ref()
12152 } else {
12153 continue;
12154 };
12155 let cloned = lit_side.clone();
12156 let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
12157 continue;
12158 };
12159 // Coerce to an owned Value<'static> so the caller
12160 // can hold it past the WHERE expression's lifetime.
12161 let owned: spg_storage::Value<'static> = match v {
12162 spg_storage::Value::Text(s) => {
12163 spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
12164 }
12165 spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
12166 spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
12167 spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
12168 spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
12169 spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
12170 spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
12171 spg_storage::Value::Null => spg_storage::Value::Null,
12172 // Anything else (Vector / Json / Bytes / Numeric /
12173 // arrays / interval / …) isn't a current partition
12174 // key type; skip without pruning.
12175 _ => continue,
12176 };
12177 return Some(owned);
12178 }
12179 _ => {}
12180 }
12181 }
12182 None
12183}
12184
12185/// Coerce a literal Expr(after the parser folded sequence calls etc.)
12186/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
12187/// pruning and routing agree on the literal vocabulary. Returns
12188/// `None` when the literal isn't recognised(planner then skips
12189/// pruning on that branch — correctness preserved).
12190fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
12191 let cloned = e.clone();
12192 let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
12193 match value {
12194 spg_storage::Value::Timestamp(m) => Some(m),
12195 spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
12196 spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
12197 _ => None,
12198 }
12199}
12200
12201/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
12202/// satisfying the WHERE-derived filter range. PG-style half-open:
12203/// child upper exclusive. Filter inclusivity is honoured per-bound.
12204fn range_satisfies_filter(
12205 range_lo: &spg_storage::PartitionBound,
12206 range_hi: &spg_storage::PartitionBound,
12207 filter_lo: Option<&PartitionFilterBound>,
12208 filter_hi: Option<&PartitionFilterBound>,
12209) -> bool {
12210 use spg_storage::PartitionBound;
12211 // For each filter side, reject children that can't host any row
12212 // matching the predicate.
12213 if let Some(lo) = filter_lo {
12214 // child upper bound vs filter lower:
12215 // if filter is x >= L, child rejects iff child.hi <= L
12216 // if filter is x > L, child rejects iff child.hi <= L
12217 // (child.hi exclusive, so equality with L still rejects)
12218 match range_hi {
12219 PartitionBound::MinValue => return false,
12220 PartitionBound::MaxValue => {}
12221 PartitionBound::TimestampTz(hi) => {
12222 if *hi <= lo.micros {
12223 return false;
12224 }
12225 }
12226 // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
12227 // matched against TIMESTAMPTZ filters here; keep child
12228 // (conservative: don't prune).
12229 PartitionBound::BigInt(_)
12230 | PartitionBound::Int(_)
12231 | PartitionBound::SmallInt(_)
12232 | PartitionBound::Date(_)
12233 | PartitionBound::Text(_) => {}
12234 }
12235 }
12236 if let Some(hi) = filter_hi {
12237 // child lower bound vs filter upper:
12238 // if filter is x <= U, child rejects iff child.lo > U
12239 // if filter is x < U, child rejects iff child.lo >= U
12240 match range_lo {
12241 PartitionBound::MaxValue => return false,
12242 PartitionBound::MinValue => {}
12243 PartitionBound::TimestampTz(lo) => {
12244 let rejects = if hi.inclusive {
12245 *lo > hi.micros
12246 } else {
12247 *lo >= hi.micros
12248 };
12249 if rejects {
12250 return false;
12251 }
12252 }
12253 PartitionBound::BigInt(_)
12254 | PartitionBound::Int(_)
12255 | PartitionBound::SmallInt(_)
12256 | PartitionBound::Date(_)
12257 | PartitionBound::Text(_) => {}
12258 }
12259 }
12260 true
12261}
12262
12263fn quote_ident_for_sql(name: &str) -> alloc::string::String {
12264 // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
12265 // identifier, otherwise quoted). Conservative: always quote so
12266 // children with reserved names round-trip safely through the
12267 // CTE-body parse.
12268 let mut out = alloc::string::String::with_capacity(name.len() + 2);
12269 out.push('"');
12270 for c in name.chars() {
12271 if c == '"' {
12272 out.push('"');
12273 }
12274 out.push(c);
12275 }
12276 out.push('"');
12277 out
12278}
12279
12280fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
12281 let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
12282 EngineError::Unsupported(alloc::format!(
12283 "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
12284 ))
12285 })?;
12286 let Statement::Select(body) = parsed else {
12287 return Err(EngineError::Unsupported(alloc::format!(
12288 "partition expansion: generated SQL {sql:?} is not a SELECT"
12289 )));
12290 };
12291 Ok(body)
12292}
12293
12294/// v7.39 (read01 round 65/66) — the column shape a set-returning function
12295/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
12296/// yields ONE column named after the call's alias when there is one (`FROM
12297/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
12298/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
12299fn setof_column_shape_from(
12300 declared: &str,
12301 name: &str,
12302 alias: Option<&str>,
12303 got: &[ColumnSchema],
12304) -> alloc::vec::Vec<ColumnSchema> {
12305 let upper = declared.to_ascii_uppercase();
12306 if upper.starts_with("TABLE(") {
12307 let raw = &declared["TABLE(".len()..declared.len() - 1];
12308 return raw
12309 .split(',')
12310 .zip(got.iter())
12311 .map(|(decl, g)| {
12312 let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
12313 ColumnSchema::new(cname.to_string(), g.ty, true)
12314 })
12315 .collect();
12316 }
12317 let cname = alias.unwrap_or(name);
12318 got.first()
12319 .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
12320 .unwrap_or_default()
12321}
12322
12323/// The plpgsql twin: the interpreter hands back raw value rows, so the types
12324/// come off the first row.
12325fn setof_column_shape(
12326 declared: &str,
12327 name: &str,
12328 alias: Option<&str>,
12329 first_row: Option<&alloc::vec::Vec<Value<'static>>>,
12330) -> alloc::vec::Vec<ColumnSchema> {
12331 let got: alloc::vec::Vec<ColumnSchema> = first_row
12332 .map(|r| {
12333 r.iter()
12334 .enumerate()
12335 .map(|(i, v)| {
12336 ColumnSchema::new(
12337 alloc::format!("col{i}"),
12338 v.data_type().unwrap_or(DataType::Text),
12339 true,
12340 )
12341 })
12342 .collect()
12343 })
12344 .unwrap_or_default();
12345 setof_column_shape_from(declared, name, alias, &got)
12346}
12347
12348/// v7.39 (read01 round 67) — expand every set-returning call in a target list
12349/// for ONE input row, PG's ProjectSet semantics.
12350///
12351/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
12352/// output has as many rows as the LONGEST of them, and a shorter one is padded
12353/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
12354/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
12355/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
12356/// is zero rows, not one NULL row.
12357///
12358/// Non-SRF items repeat, evaluated once per output row from the same input row.
12359/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
12360/// used to reach the scalar function dispatcher, which reported the aggregate as
12361/// an *unknown function* — the same "symptom two layers above the cause" shape
12362/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
12363/// sees a call, not the clause it came from. The statement knows.
12364/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
12365/// clause may appear.
12366///
12367/// PG rejects `FOR UPDATE` on exactly the shapes that have no
12368/// identifiable base row to lock, each with its own wording. SPG
12369/// accepted all of them and locked nothing, so a query that PG refuses
12370/// outright came back looking like it had taken locks.
12371///
12372/// Every wording read off live PG 18.4.
12373fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
12374 let Some(lock) = &stmt.locking else {
12375 return Ok(());
12376 };
12377 let verb = lock_clause_verb(lock.strength);
12378 let refuse = |what: &str| {
12379 Err(EngineError::Unsupported(alloc::format!(
12380 "{verb} is not allowed with {what}"
12381 )))
12382 };
12383 if !stmt.unions.is_empty() {
12384 return refuse("UNION/INTERSECT/EXCEPT");
12385 }
12386 if stmt.distinct || !stmt.distinct_on.is_empty() {
12387 return refuse("DISTINCT clause");
12388 }
12389 if stmt.group_by.is_some() || stmt.group_by_all {
12390 return refuse("GROUP BY clause");
12391 }
12392 let has_agg = stmt.items.iter().any(|it| match it {
12393 spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
12394 _ => false,
12395 });
12396 if has_agg {
12397 return refuse("aggregate functions");
12398 }
12399 // `FOR UPDATE OF t` must name a relation that is actually in FROM.
12400 for want in &lock.of_tables {
12401 if !locking_from_names(stmt)
12402 .iter()
12403 .any(|n| n.eq_ignore_ascii_case(want))
12404 {
12405 return Err(EngineError::Unsupported(alloc::format!(
12406 "relation \"{want}\" in {verb} clause not found in FROM clause"
12407 )));
12408 }
12409 }
12410 Ok(())
12411}
12412
12413/// How PG names the clause in its diagnostics.
12414const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
12415 use spg_sql::ast::LockStrength as LS;
12416 match s {
12417 LS::Update => "FOR UPDATE",
12418 LS::NoKeyUpdate => "FOR NO KEY UPDATE",
12419 LS::Share => "FOR SHARE",
12420 LS::KeyShare => "FOR KEY SHARE",
12421 }
12422}
12423
12424/// Every relation name (or alias) the FROM clause exposes.
12425fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
12426 let mut out = alloc::vec::Vec::new();
12427 if let Some(f) = &stmt.from {
12428 let mut push = |t: &spg_sql::ast::TableRef| {
12429 if let Some(a) = &t.alias {
12430 out.push(a.clone());
12431 }
12432 out.push(t.name.clone());
12433 };
12434 push(&f.primary);
12435 for j in &f.joins {
12436 push(&j.table);
12437 }
12438 }
12439 out
12440}
12441
12442fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
12443 use spg_sql::ast::Expr;
12444 if let Some(w) = &stmt.where_
12445 && aggregate::contains_aggregate(w)
12446 {
12447 return Err(EngineError::Unsupported(
12448 "aggregate functions are not allowed in WHERE".into(),
12449 ));
12450 }
12451 let mut nested = false;
12452 let mut check = |e: &Expr| {
12453 let mut probe = e.clone();
12454 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
12455 let args = match n {
12456 Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
12457 _ => return false,
12458 };
12459 if args.iter().any(aggregate::contains_aggregate) {
12460 nested = true;
12461 }
12462 false
12463 });
12464 };
12465 for it in &stmt.items {
12466 if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
12467 check(expr);
12468 }
12469 }
12470 if let Some(h) = &stmt.having {
12471 check(h);
12472 }
12473 for o in &stmt.order_by {
12474 check(&o.expr);
12475 }
12476 if nested {
12477 return Err(EngineError::Unsupported(
12478 "aggregate function calls cannot be nested".into(),
12479 ));
12480 }
12481 Ok(())
12482}
12483
12484/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
12485/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
12486/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
12487/// to a set and then applies the enclosing expression once per element. SPG only
12488/// ever recognised an SRF that WAS the item, so everything above died on
12489/// "unknown function unnest" — the set-returning call, wrapped in anything at
12490/// all, fell through to the scalar function dispatcher which has no such name.
12491///
12492/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
12493/// rewritten to read that column, and the rewritten expression is evaluated once
12494/// per output row against the input row extended with the lifted values. The
12495/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
12496/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
12497/// executors (the single-table scan, the synthetic-table pipeline, and the
12498/// unnest FROM path) each evaluated the key as an ordinary expression, where the
12499/// literal `n` is just the constant n — the same sort key for every row. The
12500/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
12501/// back in input order, not in a wrong order. Statement prep resolves the common
12502/// case, but only when the SELECT item is an expression — a `*` is not one, and
12503/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
12504/// spelling landed on exactly the shape prep could not resolve.
12505///
12506/// A set-returning item is left alone: copying it into ORDER BY would make the
12507/// key "the whole set", evaluated once per INPUT row.
12508fn resolve_positional_order_by(
12509 order_by: &[spg_sql::ast::OrderBy],
12510 projection: &[ProjectedItem],
12511) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
12512 order_by
12513 .iter()
12514 .map(|o| {
12515 let mut o = o.clone();
12516 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
12517 && *n >= 1
12518 && let Ok(idx) = usize::try_from(*n - 1)
12519 && let Some(item) = projection.get(idx)
12520 && !expr_contains_builtin_srf(&item.expr)
12521 {
12522 o.expr = item.expr.clone();
12523 }
12524 o
12525 })
12526 .collect()
12527}
12528
12529/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
12530/// this expression? Statement preparation (`resolve_order_by_position`) runs
12531/// before any catalog is in hand, and it only needs to know "is this item's value
12532/// a set", which the builtin SRFs answer syntactically.
12533pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
12534 let mut found = false;
12535 let mut probe = e.clone();
12536 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
12537 if is_top_level_unnest(n) {
12538 found = true;
12539 return true;
12540 }
12541 false
12542 });
12543 found
12544}
12545
12546/// v7.39 (round 599) — everything about a target-list SRF that does not
12547/// depend on the row.
12548///
12549/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
12550/// each SRF-bearing projection expression, walked and rewrote the tree,
12551/// formatted a `__srf_N` name per node, and copied the whole column schema.
12552/// A counting allocator put the path at 24 allocations per input row for a
12553/// single-element `unnest`, against 0 for the same scan without one — 211 MB
12554/// where the plain scan took 4.3 — and the shape held whatever the array
12555/// contained, which is what invariant work looks like.
12556struct SrfPlan {
12557 /// The lifted SRF calls, in slot order.
12558 nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
12559 /// Per projection position, the expression with its SRF calls replaced
12560 /// by `__srf_N` column references. `None` means the item has none.
12561 rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
12562 /// The input schema followed by one column per slot. Only the slots'
12563 /// TYPES vary per row, and they are patched in place.
12564 ext_cols: alloc::vec::Vec<ColumnSchema>,
12565 /// v7.39 (round 743) — the rewritten projection COMPILED against the
12566 /// extended schema, once per plan. The per-output-row evaluation ran
12567 /// the interpreter (~560 ns/row on the unnest panel cell); the Step
12568 /// VM reads the `__srf_N` slots as plain columns. `None` = that item
12569 /// is not fully compilable and keeps the interpreter.
12570 compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
12571 base_cols: usize,
12572}
12573
12574fn build_srf_plan(
12575 engine: &Engine,
12576 projection: &[ProjectedItem],
12577 srf_idxs: &[usize],
12578 ctx: &EvalContext<'_>,
12579) -> Result<SrfPlan, EngineError> {
12580 // Lift every SRF node out of every item that contains one.
12581 let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
12582 let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
12583 let mut reject: Option<EngineError> = None;
12584 for &i in srf_idxs {
12585 let mut e = projection[i].expr.clone();
12586 crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
12587 if reject.is_some() {
12588 return true;
12589 }
12590 // PG refuses a set-returning function inside a conditional: the set
12591 // would have to be produced before anyone knows whether the branch
12592 // is even taken.
12593 let conditional = match n {
12594 spg_sql::ast::Expr::Case { .. } => Some("CASE"),
12595 spg_sql::ast::Expr::FunctionCall { name, .. }
12596 if name.eq_ignore_ascii_case("coalesce") =>
12597 {
12598 Some("COALESCE")
12599 }
12600 _ => None,
12601 };
12602 if let Some(kind) = conditional
12603 && engine.expr_contains_srf(n)
12604 {
12605 reject = Some(EngineError::Unsupported(alloc::format!(
12606 "set-returning functions are not allowed in {kind}"
12607 )));
12608 return true;
12609 }
12610 if !engine.is_srf_node(n) {
12611 return false;
12612 }
12613 let slot = nodes.len();
12614 nodes.push(n.clone());
12615 *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
12616 qualifier: None,
12617 name: alloc::format!("__srf_{slot}"),
12618 });
12619 true
12620 });
12621 rewritten[i] = Some(e);
12622 }
12623 if let Some(err) = reject {
12624 return Err(err);
12625 }
12626 let base_cols = ctx.columns.len();
12627 let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
12628 for slot in 0..nodes.len() {
12629 ext_cols.push(ColumnSchema::new(
12630 alloc::format!("__srf_{slot}"),
12631 DataType::Text,
12632 true,
12633 ));
12634 }
12635 // v7.39 (round 743) — compile the rewritten items against the
12636 // EXTENDED schema. The slot columns' declared type is a per-row
12637 // patched detail the compiled column read does not consult.
12638 let compiled: Vec<Option<eval::CompiledExpr>> = {
12639 let mut ext_ctx = ctx.clone();
12640 ext_ctx.columns = &ext_cols;
12641 projection
12642 .iter()
12643 .enumerate()
12644 .map(|(i, p)| {
12645 let e = rewritten[i].as_ref().unwrap_or(&p.expr);
12646 if eval::fully_compilable(e) {
12647 Some(eval::compile_expr(e, &ext_ctx))
12648 } else {
12649 None
12650 }
12651 })
12652 .collect()
12653 };
12654 Ok(SrfPlan {
12655 nodes,
12656 rewritten,
12657 ext_cols,
12658 compiled,
12659 base_cols,
12660 })
12661}
12662
12663/// One input row expanded through a plan built once for the whole scan.
12664/// v7.39 (round 621) — expand a projection whose target list contains
12665/// set-returning items, remembering which INPUT row each output row came from.
12666///
12667/// The three materialised-source tails — `FROM unnest(…)`, `FROM
12668/// generate_series(…)`, and the one that serves VALUES / a derived table /
12669/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
12670/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
12671/// v(x)` answered `function unnest(integer[]) does not exist` on all the
12672/// others, for a query PG answers. Sharing the expansion is the point: a
12673/// fourth copy would have been the fourth place to forget.
12674fn expand_projection_srfs(
12675 engine: &Engine,
12676 projection: &[ProjectedItem],
12677 srf_idxs: &[usize],
12678 filtered: &[Row<'static>],
12679 ctx: &EvalContext<'_>,
12680) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
12681 let mut out = alloc::vec::Vec::with_capacity(filtered.len());
12682 let mut src = alloc::vec::Vec::with_capacity(filtered.len());
12683 // v7.39 (round 726) — ONE plan for the whole scan. The per-row
12684 // spelling rebuilt it for every input row: a full clone of the
12685 // rewritten projection trees and the extended schema, 50k times on
12686 // the panel's unnest cell.
12687 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
12688 // v7.39 (round 733) — shard the expansion. Each shard clones the
12689 // plan (its ext_cols slot types are per-row mutable) and builds a
12690 // MINIMAL context — EvalContext is not Sync — which is sound only
12691 // when every expression involved is pure: the whole projection and
12692 // every SRF argument must be fully_compilable, or the row loop
12693 // stays serial with the full session context.
12694 // The projection is judged in its REWRITTEN form — the SRF call
12695 // itself is never compilable, but after the lift it is a plain
12696 // `__srf_N` column reference.
12697 let all_pure = projection
12698 .iter()
12699 .enumerate()
12700 .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
12701 && plan.nodes.iter().all(|n| match n {
12702 Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
12703 other => eval::fully_compilable(other),
12704 });
12705 if all_pure
12706 && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
12707 && let Some(r) = engine.parallel_runner.0.as_deref()
12708 {
12709 let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
12710 let chunk = filtered.len().div_ceil(n_shards);
12711 type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
12712 let schema_cols = ctx.columns;
12713 let alias = ctx.table_alias;
12714 let mysql = ctx.mysql_dialect;
12715 let style = ctx.render_style;
12716 let plan_ref = &plan;
12717 let results = r.run_shards(n_shards, &|si| {
12718 let lo = si * chunk;
12719 let hi = ((si + 1) * chunk).min(filtered.len());
12720 let mut sctx = eval::EvalContext::new(schema_cols, alias);
12721 sctx.mysql_dialect = mysql;
12722 sctx.render_style = style;
12723 // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
12724 // compiled programs); each shard rebuilds it, which also
12725 // recompiles against the shard's own context. Build errors
12726 // were already surfaced by the outer build above.
12727 let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
12728 Ok(p) => p,
12729 Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
12730 };
12731 let mut run = || -> ShardOut {
12732 let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
12733 let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
12734 for (i, row) in filtered[lo..hi].iter().enumerate() {
12735 let expanded =
12736 expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
12737 sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
12738 o.extend(expanded);
12739 }
12740 Ok((o, sidx))
12741 };
12742 alloc::boxed::Box::new(run())
12743 });
12744 for boxed in results {
12745 let shard = boxed
12746 .downcast::<ShardOut>()
12747 .expect("runner echoes the closure's box");
12748 let (o, sidx) = (*shard)?;
12749 out.extend(o);
12750 src.extend(sidx);
12751 }
12752 return Ok((out, src));
12753 }
12754 for (i, row) in filtered.iter().enumerate() {
12755 let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
12756 src.extend(core::iter::repeat_n(i, expanded.len()));
12757 out.extend(expanded);
12758 }
12759 Ok((out, src))
12760}
12761
12762/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
12763///
12764/// A key that names a select-list item reads it out of the EXPANDED row,
12765/// because PG sorts after the expansion. A key that names a source column the
12766/// query does not project is evaluated against the input row that output row
12767/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
12768fn srf_order_key(
12769 ob: &spg_sql::ast::OrderBy,
12770 out_col: Option<usize>,
12771 out: &Row<'static>,
12772 src: &Row<'static>,
12773 ctx: &EvalContext<'_>,
12774) -> Result<Value<'static>, EngineError> {
12775 match out_col {
12776 Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
12777 None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
12778 }
12779}
12780
12781fn expand_srf_row_with(
12782 engine: &Engine,
12783 plan: &mut SrfPlan,
12784 projection: &[ProjectedItem],
12785 row: &Row<'static>,
12786 ctx: &EvalContext<'_>,
12787) -> Result<Vec<Row<'static>>, EngineError> {
12788 let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
12789 for n in &plan.nodes {
12790 lists.push(engine.srf_values(n, row, ctx)?);
12791 }
12792 let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
12793 // Only the slots' element types depend on the row; the names and the
12794 // input schema around them do not.
12795 for (slot, list) in lists.iter().enumerate() {
12796 plan.ext_cols[plan.base_cols + slot].ty = list
12797 .iter()
12798 .find_map(|v| v.data_type())
12799 .unwrap_or(DataType::Text);
12800 }
12801 let mut ext_ctx = ctx.clone();
12802 ext_ctx.columns = &plan.ext_cols;
12803 let mut out = Vec::with_capacity(n_rows);
12804 // v7.39 (round 726) — the base columns are the SAME for every
12805 // expanded row; clone them once and rewrite only the SRF slots per
12806 // k. The old form cloned the whole input row per OUTPUT row — for
12807 // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
12808 // TEXT column the projection never reads.
12809 let base_len = row.values.len();
12810 let mut ext_vals = row.values.clone();
12811 ext_vals.resize(base_len + lists.len(), Value::Null);
12812 let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
12813 for k in 0..n_rows {
12814 for (slot, list) in lists.iter().enumerate() {
12815 // Past the end of THIS srf's rows → NULL (PG pads).
12816 ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
12817 }
12818 let ext_row = Row::new(core::mem::take(&mut ext_vals));
12819 let mut vals = Vec::with_capacity(projection.len());
12820 for (i, p) in projection.iter().enumerate() {
12821 // v7.39 (round 743) — compiled when possible; the
12822 // interpreter for the rest, with its exact wording.
12823 vals.push(match &plan.compiled[i] {
12824 Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
12825 .map_err(EngineError::Eval)?,
12826 None => {
12827 let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
12828 eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
12829 }
12830 });
12831 }
12832 ext_vals = ext_row.values;
12833 out.push(Row::new(vals));
12834 }
12835 Ok(out)
12836}
12837
12838/// The one-shot spelling, for the callers that expand a single row.
12839/// v7.39 (round 600) — which output column each ORDER BY key names, for a
12840/// query whose target list contains a set-returning function.
12841///
12842/// The keys used to be built from the INPUT row, before the SRF expanded, so
12843/// anything that named the SRF's own output was evaluated as a scalar call:
12844/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
12845/// "function unnest(integer[]) does not exist", and so did the spellings that
12846/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
12847/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
12848/// back in input order. PG sorts AFTER the expansion, so a key that names a
12849/// select-list item reads that item's value out of the expanded row.
12850///
12851/// `None` keeps the key on the input row, which is where an ORDER BY naming
12852/// a column the query does not project has to be evaluated.
12853fn srf_order_output_cols(
12854 order_by: &[spg_sql::ast::OrderBy],
12855 projection: &[ProjectedItem],
12856) -> Vec<Option<usize>> {
12857 order_by
12858 .iter()
12859 .map(|ob| {
12860 // A positive ordinal is the Nth output column, directly.
12861 // `resolve_positional_order_by` deliberately leaves an ordinal
12862 // pointing at a set-returning item alone — copying the call into
12863 // ORDER BY would have made the key "the whole set" back when keys
12864 // came from the input row. Reading the expanded row's column is
12865 // what it should have meant, and is what this does.
12866 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
12867 && *n >= 1
12868 && let Ok(idx) = usize::try_from(*n - 1)
12869 && idx < projection.len()
12870 {
12871 return Some(idx);
12872 }
12873 // An unqualified name matching exactly one output name. SQL
12874 // resolves ORDER BY against the select list first, so this wins
12875 // over an input column of the same name — which is the whole
12876 // point of `SELECT g AS id … ORDER BY id`.
12877 if let Expr::Column(c) = &ob.expr
12878 && c.qualifier.is_none()
12879 {
12880 let mut hit = None;
12881 for (i, p) in projection.iter().enumerate() {
12882 if p.output_name.eq_ignore_ascii_case(&c.name) {
12883 if hit.is_some() {
12884 hit = None;
12885 break;
12886 }
12887 hit = Some(i);
12888 }
12889 }
12890 if hit.is_some() {
12891 return hit;
12892 }
12893 }
12894 // Or the same expression as a select-list item — which is what
12895 // `ORDER BY 1` becomes once `resolve_positional_order_by` has
12896 // run, and what a repeated `ORDER BY unnest(…)` is.
12897 projection.iter().position(|p| p.expr == ob.expr)
12898 })
12899 .collect()
12900}
12901
12902fn expand_srf_row(
12903 engine: &Engine,
12904 projection: &[ProjectedItem],
12905 srf_idxs: &[usize],
12906 row: &Row<'static>,
12907 ctx: &EvalContext<'_>,
12908) -> Result<Vec<Row<'static>>, EngineError> {
12909 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
12910 expand_srf_row_with(engine, &mut plan, projection, row, ctx)
12911}
12912
12913impl Engine {
12914 /// The rows one target-list SRF yields for an input row. `None` from
12915 /// `srf_target_idxs` means the expression is not set-returning at all.
12916 fn srf_values(
12917 &self,
12918 expr: &spg_sql::ast::Expr,
12919 row: &Row<'static>,
12920 ctx: &EvalContext<'_>,
12921 ) -> Result<Vec<Value<'static>>, EngineError> {
12922 if top_level_srf_kind(expr).is_some() {
12923 return top_level_srf_output(expr, row, ctx);
12924 }
12925 // A user set-returning function. Its body runs through the real
12926 // executor, like every function body since round 63.
12927 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
12928 return Err(EngineError::Unsupported(
12929 "expected a SELECT-list SRF call".into(),
12930 ));
12931 };
12932 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
12933 for a in args {
12934 vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
12935 }
12936 let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
12937 // v7.39 (read01 round 68) — in a target list a multi-column function is
12938 // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
12939 // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
12940 // what it is for. A single-column function contributes its bare value.
12941 Ok(rows
12942 .into_iter()
12943 .map(|r| {
12944 if r.values.len() == 1 {
12945 r.values.into_iter().next().unwrap_or(Value::Null)
12946 } else {
12947 Value::Composite(
12948 cols.iter()
12949 .map(|c| c.name.clone())
12950 .zip(r.values)
12951 .collect::<alloc::vec::Vec<_>>(),
12952 )
12953 }
12954 })
12955 .collect())
12956 }
12957
12958 /// Is THIS node a set-returning call: one of the builtin kinds, or a user
12959 /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
12960 fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
12961 if is_top_level_unnest(e) {
12962 return true;
12963 }
12964 let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
12965 return false;
12966 };
12967 self.active_catalog().functions_named(name).iter().any(|f| {
12968 let r = f.returns.trim().to_ascii_uppercase();
12969 r.starts_with("SETOF") || r.starts_with("TABLE(")
12970 })
12971 }
12972
12973 /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
12974 fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
12975 let mut found = false;
12976 let mut probe = e.clone();
12977 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
12978 if self.is_srf_node(n) {
12979 found = true;
12980 return true;
12981 }
12982 false
12983 });
12984 found
12985 }
12986
12987 /// Which projection items CONTAIN a set-returning call. Before round 78 this
12988 /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
12989 /// ordinary scalar call all the way down to the function dispatcher, which
12990 /// then reported `unnest` as an unknown function.
12991 fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
12992 projection
12993 .iter()
12994 .enumerate()
12995 .filter(|(_, p)| self.expr_contains_srf(&p.expr))
12996 .map(|(i, _)| i)
12997 .collect()
12998 }
12999}
13000
13001impl Engine {
13002 /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
13003 /// no `(f(args)).*` item.
13004 fn lower_record_expansion(
13005 &self,
13006 stmt: &SelectStatement,
13007 ) -> Result<Option<SelectStatement>, EngineError> {
13008 use spg_sql::ast::{Expr, SelectItem};
13009 let is_marker = |it: &SelectItem| {
13010 matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
13011 if name == "__record_expand")
13012 };
13013 if !stmt.items.iter().any(is_marker) {
13014 return Ok(None);
13015 }
13016 let mut out = stmt.clone();
13017 let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
13018 let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
13019 for (n, item) in stmt.items.iter().enumerate() {
13020 if !is_marker(item) {
13021 items.push(item.clone());
13022 continue;
13023 }
13024 let SelectItem::Expr {
13025 expr: Expr::FunctionCall { args, .. },
13026 ..
13027 } = item
13028 else {
13029 unreachable!("checked by is_marker");
13030 };
13031 let Some(Expr::FunctionCall {
13032 name: fname,
13033 args: fargs,
13034 }) = args.first()
13035 else {
13036 return Err(EngineError::Unsupported(
13037 "(<expr>).* expands a function's record — it needs a function call".into(),
13038 ));
13039 };
13040 let cols = self.setof_declared_columns(fname)?;
13041 let alias = alloc::format!("__rec{n}");
13042 let mut tref = bare_table_ref_named(&alias);
13043 tref.table_fn_call = Some(alloc::boxed::Box::new((
13044 fname.to_ascii_lowercase(),
13045 fargs.clone(),
13046 )));
13047 tref.alias = Some(alias.clone());
13048 lateral_refs.push(tref);
13049 for c in cols {
13050 items.push(SelectItem::Expr {
13051 expr: Expr::Column(spg_sql::ast::ColumnName {
13052 qualifier: Some(alias.clone()),
13053 name: c,
13054 }),
13055 alias: None,
13056 });
13057 }
13058 }
13059 out.items = items;
13060 // The function joins the FROM. With no FROM it BECOMES the FROM; with one
13061 // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
13062 // (the arguments may reference the outer row — the round-69 correlation).
13063 for tref in lateral_refs {
13064 match &mut out.from {
13065 None => {
13066 out.from = Some(spg_sql::ast::FromClause {
13067 primary: tref,
13068 joins: alloc::vec::Vec::new(),
13069 });
13070 }
13071 Some(from) => from.joins.push(spg_sql::ast::FromJoin {
13072 kind: spg_sql::ast::JoinKind::Cross,
13073 table: tref,
13074 on: None,
13075 using_cols: None,
13076 natural: false,
13077 }),
13078 }
13079 }
13080 Ok(Some(out))
13081 }
13082
13083 /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
13084 /// v text)` names them; a `SETOF <scalar>` is one column named after the
13085 /// function.
13086 fn setof_declared_columns(
13087 &self,
13088 name: &str,
13089 ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
13090 let cat = self.active_catalog();
13091 let overloads = cat.functions_named(name);
13092 let def = overloads.first().ok_or_else(|| {
13093 EngineError::Unsupported(alloc::format!("function {name} does not exist"))
13094 })?;
13095 let declared = def.returns.trim();
13096 let upper = declared.to_ascii_uppercase();
13097 if upper.starts_with("TABLE(") {
13098 let raw = &declared["TABLE(".len()..declared.len() - 1];
13099 return Ok(raw
13100 .split(',')
13101 .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
13102 .collect());
13103 }
13104 Ok(alloc::vec![name.to_string()])
13105 }
13106}
13107
13108/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
13109/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
13110/// COLUMNS list (data-independent), NESTED children inlined in
13111/// declaration order (PG's flattened output shape).
13112/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
13113/// correlated JSON_TABLE's static schema without evaluating its doc.
13114pub(crate) fn json_table_schema_pub(
13115 cols: &[spg_sql::ast::JsonTableColumn],
13116) -> alloc::vec::Vec<ColumnSchema> {
13117 json_table_schema(cols)
13118}
13119
13120fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
13121 use spg_sql::ast::JsonTableColumn as C;
13122 let mut out = alloc::vec::Vec::new();
13123 for c in cols {
13124 match c {
13125 C::Ordinality { name } => {
13126 out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
13127 }
13128 C::Regular {
13129 name, ty, exists, ..
13130 } => {
13131 let dt = if *exists {
13132 DataType::Bool
13133 } else {
13134 crate::conversions::column_type_to_data_type(*ty)
13135 };
13136 out.push(ColumnSchema::new(name.clone(), dt, true));
13137 }
13138 C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
13139 }
13140 }
13141 out
13142}
13143
13144/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
13145/// JSON_TABLE column's declared type (the DEFAULT expr may be a
13146/// string literal like `'none'` that must land as the column type).
13147fn coerce_json_table_default(
13148 v: Value<'static>,
13149 ty: spg_sql::ast::ColumnTypeName,
13150 name: &str,
13151) -> Result<Value<'static>, EngineError> {
13152 if v.is_null() {
13153 return Ok(Value::Null);
13154 }
13155 let dt = crate::conversions::column_type_to_data_type(ty);
13156 crate::conversions::coerce_value(v, dt, name, 0)
13157}
13158
13159/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
13160fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
13161 use crate::json::JsonValue as J;
13162 match v {
13163 Value::Null => J::Null,
13164 Value::Bool(b) => J::Bool(*b),
13165 Value::SmallInt(n) => J::Number(f64::from(*n)),
13166 Value::Int(n) => J::Number(f64::from(*n)),
13167 Value::BigInt(n) => J::Number(*n as f64),
13168 Value::Float(x) => J::Number(*x),
13169 Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
13170 other => J::String(crate::eval::value_to_text(other)),
13171 }
13172}
13173
13174fn bare_table_ref_named(name: &str) -> TableRef {
13175 TableRef {
13176 name: name.to_string(),
13177 alias: None,
13178 only: false,
13179 as_of_segment: None,
13180 unnest_expr: None,
13181 unnest_column_aliases: alloc::vec::Vec::new(),
13182 with_ordinality: false,
13183 generate_series_args: None,
13184 lateral_subquery: None,
13185 jsonb_each_text_arg: None,
13186 table_fn_call: None,
13187 rows_from: None,
13188 json_table: None,
13189 scalar_fn_item: false,
13190 }
13191}
13192
13193impl Engine {
13194 /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
13195 /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
13196 /// entries are the array-able SRFs, already lowered by the parser into their
13197 /// scalar array form.
13198 fn rows_from_rows(
13199 &self,
13200 primary: &TableRef,
13201 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
13202 let entries = primary
13203 .rows_from
13204 .as_ref()
13205 .expect("caller guards rows_from.is_some()");
13206 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13207 let ctx = self.ev_ctx(&empty, None);
13208 let dummy = Row::new(alloc::vec::Vec::new());
13209 let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
13210 let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13211 for (name, args) in entries {
13212 let (vals, colname) = if name == "__array" {
13213 // The parser lowered this one to `<array expr>`; its rows are the
13214 // array's elements.
13215 let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
13216 (
13217 array_value_to_elements(&arr)?,
13218 alloc::string::String::from("unnest"),
13219 )
13220 } else {
13221 let call = spg_sql::ast::Expr::FunctionCall {
13222 name: name.clone(),
13223 args: args.clone(),
13224 };
13225 (self.srf_values(&call, &dummy, &ctx)?, name.clone())
13226 };
13227 let ty = vals
13228 .first()
13229 .and_then(spg_storage::Value::data_type)
13230 .unwrap_or(DataType::Text);
13231 cols.push(ColumnSchema::new(colname, ty, true));
13232 lists.push(vals);
13233 }
13234 let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
13235 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
13236 for k in 0..n {
13237 let mut vals: alloc::vec::Vec<Value<'static>> =
13238 alloc::vec::Vec::with_capacity(lists.len() + 1);
13239 for l in &lists {
13240 vals.push(l.get(k).cloned().unwrap_or(Value::Null));
13241 }
13242 rows.push(Row::new(vals));
13243 }
13244 if primary.with_ordinality {
13245 cols.push(ColumnSchema::new(
13246 "ordinality".to_string(),
13247 DataType::BigInt,
13248 false,
13249 ));
13250 rows = rows
13251 .into_iter()
13252 .enumerate()
13253 .map(|(i, r)| {
13254 let mut v = r.values;
13255 v.push(Value::BigInt(i as i64 + 1));
13256 Row::new(v)
13257 })
13258 .collect();
13259 }
13260 Ok((rows, cols))
13261 }
13262}
13263
13264/// v7.39 (round 232) — PG names the offending set operation in its
13265/// arity / type-mismatch messages ("each UNION query must have the same
13266/// number of columns"). `UNION ALL` is still spelled UNION there.
13267fn set_op_name(kind: UnionKind) -> &'static str {
13268 match kind {
13269 UnionKind::All | UnionKind::Distinct => "UNION",
13270 UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
13271 UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
13272 }
13273}
13274
13275/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
13276/// type: a bare string or NULL literal that no context has typed yet. SPG
13277/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
13278/// be the syntax. A wildcard or a non-literal expression is never unknown.
13279fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
13280 stmt.items
13281 .iter()
13282 .map(|item| match item {
13283 SelectItem::Expr { expr, .. } => matches!(
13284 expr,
13285 Expr::Literal(spg_sql::ast::Literal::String(_))
13286 | Expr::Literal(spg_sql::ast::Literal::Null)
13287 ),
13288 _ => false,
13289 })
13290 .collect()
13291}
13292
13293/// v7.39 (round 233) — retype one branch column's cells, reporting the
13294/// conversion failure the way PG does rather than leaving the column
13295/// half-converted. Used when the other branch typed an untyped literal.
13296fn coerce_branch_column(
13297 rows: &mut [Row<'static>],
13298 col_idx: usize,
13299 target: DataType,
13300 col_name: &str,
13301) -> Result<(), EngineError> {
13302 for row in rows.iter_mut() {
13303 let Some(slot) = row.values.get_mut(col_idx) else {
13304 continue;
13305 };
13306 if matches!(slot, Value::Null) {
13307 continue;
13308 }
13309 *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
13310 }
13311 Ok(())
13312}
13313
13314/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
13315/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
13316/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
13317/// reference to q's output columns substituted by the underlying column.
13318///
13319/// Admission is deliberately narrow — anything that changes cardinality,
13320/// order, or scope stays on the materialising path:
13321/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
13322/// FROM with no ordinality or positional column aliases, and no
13323/// subquery anywhere its expressions (an inner scope could reference
13324/// q too — descending is a later knife);
13325/// * inner: one stored table, bare-column projection only, no
13326/// CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
13327/// * every outer column reference must resolve inside q's output list —
13328/// a name that does not is an ERROR today, and flattening would
13329/// silently legalise it against the base table.
13330fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
13331 use spg_sql::ast::SelectItem;
13332 let inner = primary.lateral_subquery.as_deref()?;
13333 // Outer shape.
13334 if !stmt.ctes.is_empty()
13335 || !stmt.unions.is_empty()
13336 || stmt.distinct
13337 || !stmt.distinct_on.is_empty()
13338 || !stmt.window_check_exprs.is_empty()
13339 || stmt.locking.is_some()
13340 || primary.with_ordinality
13341 || !primary.unnest_column_aliases.is_empty()
13342 {
13343 return None;
13344 }
13345 // Inner shape.
13346 if !inner.ctes.is_empty()
13347 || !inner.unions.is_empty()
13348 || inner.distinct
13349 || !inner.distinct_on.is_empty()
13350 || inner.group_by.is_some()
13351 || inner.group_by_all
13352 || inner.having.is_some()
13353 || !inner.order_by.is_empty()
13354 || inner.limit.is_some()
13355 || inner.offset.is_some()
13356 || !inner.window_check_exprs.is_empty()
13357 || inner.locking.is_some()
13358 {
13359 return None;
13360 }
13361 let ifrom = inner.from.as_ref()?;
13362 let it = &ifrom.primary;
13363 if !ifrom.joins.is_empty()
13364 || it.name.is_empty()
13365 || it.lateral_subquery.is_some()
13366 || it.unnest_expr.is_some()
13367 || it.generate_series_args.is_some()
13368 || it.as_of_segment.is_some()
13369 || it.jsonb_each_text_arg.is_some()
13370 || it.table_fn_call.is_some()
13371 || it.rows_from.is_some()
13372 || it.json_table.is_some()
13373 || it.with_ordinality
13374 || !it.unnest_column_aliases.is_empty()
13375 {
13376 return None;
13377 }
13378 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
13379 return None;
13380 }
13381 // The output map: q's visible name -> the underlying column.
13382 let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
13383 let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
13384 alloc::collections::BTreeMap::new();
13385 for item in &inner.items {
13386 let SelectItem::Expr { expr, alias } = item else {
13387 return None;
13388 };
13389 let Expr::Column(c) = expr else {
13390 return None;
13391 };
13392 if let Some(q) = c.qualifier.as_deref()
13393 && !q.eq_ignore_ascii_case(&inner_alias)
13394 {
13395 return None;
13396 }
13397 let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
13398 // A duplicated output name would make substitution ambiguous.
13399 if map
13400 .insert(out_name.to_ascii_lowercase(), c.clone())
13401 .is_some()
13402 {
13403 return None;
13404 }
13405 }
13406 if map.is_empty() {
13407 return None;
13408 }
13409 let derived_alias = primary
13410 .alias
13411 .clone()
13412 .unwrap_or_else(|| primary.name.clone())
13413 .to_ascii_lowercase();
13414 // Substitute in a clone; bail (None) on the first reference the map
13415 // cannot answer.
13416 let mut out = stmt.clone();
13417 let ok = core::cell::Cell::new(true);
13418 let mut subst = |e: &mut Expr| -> bool {
13419 match e {
13420 Expr::Column(c) => {
13421 match c.qualifier.as_deref() {
13422 Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
13423 None => {}
13424 Some(_) => {
13425 ok.set(false);
13426 return true;
13427 }
13428 }
13429 match map.get(&c.name.to_ascii_lowercase()) {
13430 Some(target) => *c = target.clone(),
13431 None => ok.set(false),
13432 }
13433 true
13434 }
13435 // Any subquery could reference q from its own scope;
13436 // descending is a later knife — bail for now.
13437 Expr::ScalarSubquery(_)
13438 | Expr::Exists { .. }
13439 | Expr::InSubquery { .. }
13440 | Expr::RowInSubquery { .. }
13441 | Expr::RowCmpSubquery { .. } => {
13442 ok.set(false);
13443 true
13444 }
13445 _ => false,
13446 }
13447 };
13448 for item in &mut out.items {
13449 match item {
13450 SelectItem::Expr { expr, .. } => {
13451 crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
13452 }
13453 // `SELECT * FROM (…) q` means q's columns, in q's order.
13454 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
13455 }
13456 }
13457 if let Some(w) = &mut out.where_ {
13458 crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
13459 }
13460 if let Some(gs) = &mut out.group_by {
13461 for g in gs {
13462 crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
13463 }
13464 }
13465 if let Some(h) = &mut out.having {
13466 crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
13467 }
13468 for o in &mut out.order_by {
13469 crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
13470 }
13471 for d in &mut out.distinct_on {
13472 crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
13473 }
13474 if !ok.get() {
13475 return None;
13476 }
13477 // FROM becomes the stored table; the filters conjoin.
13478 out.from = Some(spg_sql::ast::FromClause {
13479 primary: it.clone(),
13480 joins: Vec::new(),
13481 });
13482 out.where_ = match (inner.where_.clone(), out.where_.take()) {
13483 (Some(a), Some(b)) => Some(Expr::Binary {
13484 lhs: alloc::boxed::Box::new(a),
13485 op: spg_sql::ast::BinOp::And,
13486 rhs: alloc::boxed::Box::new(b),
13487 }),
13488 (Some(a), None) => Some(a),
13489 (None, b) => b,
13490 };
13491 Some(out)
13492}
13493
13494/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
13495/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
13496/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
13497/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
13498/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
13499/// DISTINCT, an SRF, or an unprovable inner shape stays put.
13500fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
13501 use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
13502 let inner = primary.lateral_subquery.as_deref()?;
13503 // Outer: exactly `SELECT count(*)`, nothing else.
13504 if !stmt.ctes.is_empty()
13505 || !stmt.unions.is_empty()
13506 || stmt.distinct
13507 || !stmt.distinct_on.is_empty()
13508 || stmt.where_.is_some()
13509 || stmt.group_by.is_some()
13510 || stmt.having.is_some()
13511 || !stmt.order_by.is_empty()
13512 || stmt.limit.is_some()
13513 || stmt.offset.is_some()
13514 || stmt.items.len() != 1
13515 {
13516 return None;
13517 }
13518 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
13519 return None;
13520 };
13521 let E::FunctionCall { name, args } = expr else {
13522 return None;
13523 };
13524 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
13525 return None;
13526 }
13527 // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
13528 let Some(LimitExpr::Literal(k)) = &inner.offset else {
13529 return None;
13530 };
13531 let k = i64::from(*k);
13532 if inner.limit.is_some() || inner.order_by.is_empty() {
13533 return None;
13534 }
13535 let mut counted = inner.clone();
13536 counted.order_by = Vec::new();
13537 counted.offset = None;
13538 // The stripped inner must now be a provable simple shape (its
13539 // items become irrelevant — count(*) reads none of them — but an
13540 // SRF item would change the row count, so the flatten predicate's
13541 // scrutiny still applies).
13542 let base = matview_flatten_probe(&counted)?;
13543 let mut out = stmt.clone();
13544 out.items = alloc::vec![SelectItem::Expr {
13545 expr: E::FunctionCall {
13546 name: String::from("greatest"),
13547 args: alloc::vec![
13548 E::Binary {
13549 lhs: alloc::boxed::Box::new(E::FunctionCall {
13550 name: String::from("count_star"),
13551 args: alloc::vec![],
13552 }),
13553 op: spg_sql::ast::BinOp::Sub,
13554 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
13555 },
13556 E::Literal(spg_sql::ast::Literal::Integer(0)),
13557 ],
13558 },
13559 alias: Some(String::from("count")),
13560 }];
13561 out.from = Some(spg_sql::ast::FromClause {
13562 primary: base,
13563 joins: Vec::new(),
13564 });
13565 out.where_ = counted.where_.clone();
13566 Some(out)
13567}
13568
13569/// The inner-shape probe `try_count_over_offset` shares with the
13570/// flatten: single stored table, no modifiers, no subqueries, no SRF
13571/// items. Returns the base TableRef.
13572fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
13573 use spg_sql::ast::SelectItem;
13574 if !inner.ctes.is_empty()
13575 || !inner.unions.is_empty()
13576 || inner.distinct
13577 || !inner.distinct_on.is_empty()
13578 || inner.group_by.is_some()
13579 || inner.group_by_all
13580 || inner.having.is_some()
13581 || !inner.order_by.is_empty()
13582 || inner.limit.is_some()
13583 || inner.offset.is_some()
13584 || !inner.window_check_exprs.is_empty()
13585 || inner.locking.is_some()
13586 {
13587 return None;
13588 }
13589 let ifrom = inner.from.as_ref()?;
13590 let it = &ifrom.primary;
13591 if !ifrom.joins.is_empty()
13592 || it.name.is_empty()
13593 || it.lateral_subquery.is_some()
13594 || it.unnest_expr.is_some()
13595 || it.generate_series_args.is_some()
13596 || it.as_of_segment.is_some()
13597 || it.jsonb_each_text_arg.is_some()
13598 || it.table_fn_call.is_some()
13599 || it.rows_from.is_some()
13600 || it.json_table.is_some()
13601 || it.with_ordinality
13602 {
13603 return None;
13604 }
13605 for item in &inner.items {
13606 match item {
13607 SelectItem::Expr { expr, .. } => {
13608 if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
13609 return None;
13610 }
13611 }
13612 SelectItem::Wildcard => {}
13613 SelectItem::QualifiedWildcard(_) => return None,
13614 }
13615 }
13616 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
13617 return None;
13618 }
13619 Some(it.clone())
13620}
13621
13622/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
13623/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
13624/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
13625/// constant-LENGTH array literal unnests to exactly k rows per input
13626/// row (NULL elements are rows too). One SRF item only, elements
13627/// subquery-free, and the stripped inner must pass the same probe the
13628/// count-over-offset rewrite uses.
13629fn try_count_over_const_unnest(
13630 stmt: &SelectStatement,
13631 primary: &TableRef,
13632) -> Option<SelectStatement> {
13633 use spg_sql::ast::{Expr as E, SelectItem};
13634 let inner = primary.lateral_subquery.as_deref()?;
13635 if !stmt.ctes.is_empty()
13636 || !stmt.unions.is_empty()
13637 || stmt.distinct
13638 || !stmt.distinct_on.is_empty()
13639 || stmt.where_.is_some()
13640 || stmt.group_by.is_some()
13641 || stmt.having.is_some()
13642 || !stmt.order_by.is_empty()
13643 || stmt.limit.is_some()
13644 || stmt.offset.is_some()
13645 || stmt.items.len() != 1
13646 {
13647 return None;
13648 }
13649 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
13650 return None;
13651 };
13652 let E::FunctionCall { name, args } = expr else {
13653 return None;
13654 };
13655 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
13656 return None;
13657 }
13658 // Inner: exactly one item, and it is unnest(ARRAY[...]).
13659 if inner.items.len() != 1
13660 || !inner.order_by.is_empty()
13661 || inner.limit.is_some()
13662 || inner.offset.is_some()
13663 {
13664 return None;
13665 }
13666 let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
13667 return None;
13668 };
13669 let E::FunctionCall {
13670 name: fname,
13671 args: fargs,
13672 } = item
13673 else {
13674 return None;
13675 };
13676 if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
13677 return None;
13678 }
13679 let E::Array(elems) = &fargs[0] else {
13680 return None;
13681 };
13682 if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
13683 return None;
13684 }
13685 let k = elems.len() as i64;
13686 // The stripped inner (the SRF item replaced by a plain constant)
13687 // must be the provable simple shape.
13688 let mut counted = inner.clone();
13689 counted.items = alloc::vec![SelectItem::Expr {
13690 expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
13691 alias: None,
13692 }];
13693 let base = matview_flatten_probe(&counted)?;
13694 let mut out = stmt.clone();
13695 out.items = alloc::vec![SelectItem::Expr {
13696 expr: E::Binary {
13697 lhs: alloc::boxed::Box::new(E::FunctionCall {
13698 name: String::from("count_star"),
13699 args: alloc::vec![],
13700 }),
13701 op: spg_sql::ast::BinOp::Mul,
13702 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
13703 },
13704 alias: Some(String::from("count")),
13705 }];
13706 out.from = Some(spg_sql::ast::FromClause {
13707 primary: base,
13708 joins: Vec::new(),
13709 });
13710 out.where_ = counted.where_.clone();
13711 Some(out)
13712}