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, FoldSpec::dialect(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 // 7.38.1 S5.1 — pg_catalog.pg_opclass (pg_dump wall #1).
1165 "__spg_pg_opclass" => {
1166 let (schema, rows) =
1167 crate::system_catalog::synth_pg_opclass(self.active_catalog());
1168 materialise_meta_view(&mut catalog, view, schema, rows)?;
1169 }
1170 "__spg_pg_opfamily" => {
1171 let (schema, rows) =
1172 crate::system_catalog::synth_pg_opfamily(self.active_catalog());
1173 materialise_meta_view(&mut catalog, view, schema, rows)?;
1174 }
1175 "__spg_pg_amop" => {
1176 let (schema, rows) =
1177 crate::system_catalog::synth_pg_amop(self.active_catalog());
1178 materialise_meta_view(&mut catalog, view, schema, rows)?;
1179 }
1180 "__spg_pg_amproc" => {
1181 let (schema, rows) =
1182 crate::system_catalog::synth_pg_amproc(self.active_catalog());
1183 materialise_meta_view(&mut catalog, view, schema, rows)?;
1184 }
1185 // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1186 // ORM reflection + pg_dump read the deparsed default text).
1187 "__spg_pg_attrdef" => {
1188 let (schema, rows) =
1189 crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1190 materialise_meta_view(&mut catalog, view, schema, rows)?;
1191 }
1192 // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1193 "__spg_pg_policy" => {
1194 let (schema, rows) =
1195 crate::system_catalog::synth_pg_policy(self.active_catalog());
1196 materialise_meta_view(&mut catalog, view, schema, rows)?;
1197 }
1198 "__spg_pg_policies" => {
1199 let (schema, rows) =
1200 crate::system_catalog::synth_pg_policies(self.active_catalog());
1201 materialise_meta_view(&mut catalog, view, schema, rows)?;
1202 }
1203 // v7.37.24 (24.14) — pg_catalog.pg_collation.
1204 "__spg_pg_collation" => {
1205 let (schema, rows) =
1206 crate::system_catalog::synth_pg_collation(self.active_catalog());
1207 materialise_meta_view(&mut catalog, view, schema, rows)?;
1208 }
1209 // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1210 "__spg_pg_tablespace" => {
1211 let (schema, rows) =
1212 crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1213 materialise_meta_view(&mut catalog, view, schema, rows)?;
1214 }
1215 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1216 // for pgAdmin / DataGrip "indexes per table" listings.
1217 "__spg_pg_indexes" => {
1218 let (schema, rows) = synth_pg_indexes(self.active_catalog());
1219 materialise_meta_view(&mut catalog, view, schema, rows)?;
1220 }
1221 // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1222 // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1223 "__spg_pg_description" => {
1224 let (schema, rows) =
1225 crate::system_catalog::synth_pg_description(self.active_catalog());
1226 materialise_meta_view(&mut catalog, view, schema, rows)?;
1227 }
1228 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1229 // for index introspection by ORM compilers.
1230 "__spg_pg_index" => {
1231 let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1232 materialise_meta_view(&mut catalog, view, schema, rows)?;
1233 }
1234 // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1235 // for FK / UNIQUE / PK / CHECK introspection.
1236 "__spg_pg_constraint" => {
1237 let (schema, rows) = synth_pg_constraint(self.active_catalog());
1238 materialise_meta_view(&mut catalog, view, schema, rows)?;
1239 }
1240 // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1241 // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1242 "__spg_pg_sequence" => {
1243 let (schema, rows) = synth_pg_sequence(self.active_catalog());
1244 materialise_meta_view(&mut catalog, view, schema, rows)?;
1245 }
1246 // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1247 // pg_roles / pg_user. SPG is single-database so
1248 // pg_database surfaces just `postgres`; pg_roles
1249 // / pg_user walk the engine's UserStore.
1250 "__spg_pg_database" => {
1251 let (schema, rows) = synth_pg_database(self);
1252 materialise_meta_view(&mut catalog, view, schema, rows)?;
1253 }
1254 "__spg_pg_roles" => {
1255 let (schema, rows) = synth_pg_roles(self);
1256 materialise_meta_view(&mut catalog, view, schema, rows)?;
1257 }
1258 // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1259 // same roles, with PG's own `use*` column names. It used to
1260 // publish pg_roles' columns under this name.
1261 "__spg_pg_user" => {
1262 let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1263 materialise_meta_view(&mut catalog, view, schema, rows)?;
1264 }
1265 // v7.39 (read01 round 58) — role membership.
1266 "__spg_pg_auth_members" => {
1267 let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1268 materialise_meta_view(&mut catalog, view, schema, rows)?;
1269 }
1270 // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1271 // pg_views surfaces every CREATE VIEW result; SPG
1272 // ships one row per declared view from the catalog.
1273 "__spg_pg_views" => {
1274 let (schema, rows) = synth_pg_views(self.active_catalog());
1275 materialise_meta_view(&mut catalog, view, schema, rows)?;
1276 }
1277 // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1278 // catalogued query-rewrite RULE.
1279 "__spg_pg_rules" => {
1280 let (schema, rows) =
1281 crate::system_catalog::synth_pg_rules(self.active_catalog());
1282 materialise_meta_view(&mut catalog, view, schema, rows)?;
1283 }
1284 // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1285 // catalogue `pg_get_ruledef(oid)` resolves against.
1286 "__spg_pg_rewrite" => {
1287 let (schema, rows) =
1288 crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1289 materialise_meta_view(&mut catalog, view, schema, rows)?;
1290 }
1291 // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1292 // and PG's own column names.
1293 "__spg_pg_matviews" => {
1294 let (schema, rows) =
1295 crate::system_catalog::synth_pg_matviews(self.active_catalog());
1296 materialise_meta_view(&mut catalog, view, schema, rows)?;
1297 }
1298 // pg_catalog.pg_extension — native capability list
1299 // (mailrs embed round-12).
1300 // v7.39 (round 546) — the catalogs SPG has real content
1301 // for, from the facts it already holds.
1302 "__spg_pg_db_role_setting" => {
1303 let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1304 materialise_meta_view(&mut catalog, view, schema, rows)?;
1305 }
1306 "__spg_pg_language" => {
1307 let (schema, rows) = crate::system_catalog::synth_pg_language();
1308 materialise_meta_view(&mut catalog, view, schema, rows)?;
1309 }
1310 "__spg_pg_sequences" => {
1311 let (schema, rows) =
1312 crate::system_catalog::synth_pg_sequences(self.active_catalog());
1313 materialise_meta_view(&mut catalog, view, schema, rows)?;
1314 }
1315 "__spg_pg_range" => {
1316 let (schema, rows) = crate::system_catalog::synth_pg_range();
1317 materialise_meta_view(&mut catalog, view, schema, rows)?;
1318 }
1319 "__spg_pg_partitioned_table" => {
1320 let (schema, rows) =
1321 crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1322 materialise_meta_view(&mut catalog, view, schema, rows)?;
1323 }
1324 "__spg_pg_authid" => {
1325 let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1326 materialise_meta_view(&mut catalog, view, schema, rows)?;
1327 }
1328 "__spg_pg_group" => {
1329 let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1330 materialise_meta_view(&mut catalog, view, schema, rows)?;
1331 }
1332 "__spg_pg_shadow" => {
1333 let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1334 materialise_meta_view(&mut catalog, view, schema, rows)?;
1335 }
1336 // v7.39 (round 544) — pg_cast, probed from the real
1337 // cast implementation.
1338 "__spg_pg_cast" => {
1339 let (schema, rows) = crate::system_catalog::synth_pg_cast();
1340 materialise_meta_view(&mut catalog, view, schema, rows)?;
1341 }
1342 // v7.39 (round 541) — an empty catalog that exists.
1343 "__spg_pg_foreign_table" => {
1344 let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1345 materialise_meta_view(&mut catalog, view, schema, rows)?;
1346 }
1347 "__spg_pg_extension" => {
1348 let (schema, rows) = synth_pg_extension();
1349 materialise_meta_view(&mut catalog, view, schema, rows)?;
1350 }
1351 // v7.39 (round 502) — the timezone catalogues.
1352 "__spg_pg_timezone_names" => {
1353 let (schema, rows) = synth_pg_timezone_names(self);
1354 materialise_meta_view(&mut catalog, view, schema, rows)?;
1355 }
1356 "__spg_pg_timezone_abbrevs" => {
1357 let (schema, rows) = synth_pg_timezone_abbrevs(self);
1358 materialise_meta_view(&mut catalog, view, schema, rows)?;
1359 }
1360 // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1361 "__spg_pg_settings" => {
1362 let (schema, rows) = synth_pg_settings(self);
1363 materialise_meta_view(&mut catalog, view, schema, rows)?;
1364 }
1365 // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1366 // v7.39 (read01 round 51) — information_schema.role_table_grants
1367 // and .table_privileges. Both report the owner's seven implicit
1368 // table privileges; SPG's single role owns everything.
1369 // v7.39 (read01 round 59) — information_schema.column_privileges.
1370 "__spg_info_column_privileges" => {
1371 let (schema, rows) =
1372 crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1373 materialise_meta_view(&mut catalog, view, schema, rows)?;
1374 }
1375 "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1376 let grantee = self.current_role().to_string();
1377 let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1378 self.active_catalog(),
1379 &grantee,
1380 );
1381 materialise_meta_view(&mut catalog, view, schema, rows)?;
1382 }
1383 "__spg_info_key_column_usage" => {
1384 let (schema, rows) = synth_info_key_column_usage(self.active_catalog());
1385 materialise_meta_view(&mut catalog, view, schema, rows)?;
1386 }
1387 // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1388 "__spg_info_referential_constraints" => {
1389 let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1390 materialise_meta_view(&mut catalog, view, schema, rows)?;
1391 }
1392 // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1393 "__spg_info_statistics" => {
1394 let (schema, rows) = synth_info_statistics(self.active_catalog());
1395 materialise_meta_view(&mut catalog, view, schema, rows)?;
1396 }
1397 // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1398 "__spg_info_routines" => {
1399 let (schema, rows) = synth_info_routines();
1400 materialise_meta_view(&mut catalog, view, schema, rows)?;
1401 }
1402 // v7.37.24 (24.3) — information_schema.attributes.
1403 "__spg_info_attributes" => {
1404 let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1405 self.active_catalog(),
1406 );
1407 materialise_meta_view(&mut catalog, view, schema, rows)?;
1408 }
1409 // v7.37.24 (24.2) — information_schema.domains.
1410 "__spg_info_domains" => {
1411 let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1412 self.active_catalog(),
1413 );
1414 materialise_meta_view(&mut catalog, view, schema, rows)?;
1415 }
1416 // v7.37.24 (24.9) — information_schema.schemata.
1417 "__spg_info_schemata" => {
1418 let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1419 self.active_catalog(),
1420 );
1421 materialise_meta_view(&mut catalog, view, schema, rows)?;
1422 }
1423 // v7.37.24 (24.9) — information_schema.views.
1424 "__spg_info_views" => {
1425 let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1426 self.active_catalog(),
1427 );
1428 materialise_meta_view(&mut catalog, view, schema, rows)?;
1429 }
1430 // v7.37.24 (24.9) — information_schema.table_constraints.
1431 "__spg_info_table_constraints" => {
1432 let (schema, rows) =
1433 crate::system_catalog::synth_information_schema_table_constraints(
1434 self.active_catalog(),
1435 );
1436 materialise_meta_view(&mut catalog, view, schema, rows)?;
1437 }
1438 // v7.37.17 — information_schema.constraint_column_usage.
1439 "__spg_info_constraint_column_usage" => {
1440 let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1441 self.active_catalog(),
1442 );
1443 materialise_meta_view(&mut catalog, view, schema, rows)?;
1444 }
1445 // v7.37.17 — information_schema.triggers.
1446 "__spg_info_triggers" => {
1447 let (schema, rows) =
1448 crate::system_catalog::synth_info_triggers(self.active_catalog());
1449 materialise_meta_view(&mut catalog, view, schema, rows)?;
1450 }
1451 // v7.37.17 — information_schema.check_constraints.
1452 "__spg_info_check_constraints" => {
1453 let (schema, rows) =
1454 crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1455 materialise_meta_view(&mut catalog, view, schema, rows)?;
1456 }
1457 // v7.37.17 — information_schema.sequences.
1458 "__spg_info_sequences" => {
1459 let (schema, rows) =
1460 crate::system_catalog::synth_info_sequences(self.active_catalog());
1461 materialise_meta_view(&mut catalog, view, schema, rows)?;
1462 }
1463 // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1464 "__spg_mysql_user" => {
1465 let (schema, rows) = synth_mysql_user(self);
1466 materialise_meta_view(&mut catalog, view, schema, rows)?;
1467 }
1468 "__spg_mysql_db" => {
1469 let (schema, rows) = synth_mysql_db();
1470 materialise_meta_view(&mut catalog, view, schema, rows)?;
1471 }
1472 // v7.39 (round 541) — the catalogs PG has that SPG is
1473 // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1474 other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1475 let (schema, rows) =
1476 crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1477 materialise_meta_view(&mut catalog, view, schema, rows)?;
1478 }
1479 _ => {
1480 return Err(EngineError::Unsupported(alloc::format!(
1481 "meta view {view:?} is not yet materialisable; \
1482 v7.16.2 covers information_schema.columns / .tables \
1483 and pg_catalog.pg_class / pg_attribute; \
1484 v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1485 pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1486 pg_user / pg_views / pg_matviews / pg_settings"
1487 )));
1488 }
1489 }
1490 }
1491 Ok(catalog)
1492 }
1493
1494 pub(crate) fn exec_with_ctes(
1495 &self,
1496 stmt: &SelectStatement,
1497 cancel: CancelToken<'_>,
1498 ) -> Result<QueryResult, EngineError> {
1499 cancel.check()?;
1500 // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1501 // bodies are supported here. Writable CTEs on a SELECT
1502 // outer require `&mut self` and route through the
1503 // top-level `exec_select_cancel_mut` entry; sentori
1504 // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1505 // INSERT, not a SELECT, so this restriction is harmless
1506 // in practice.
1507 if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1508 // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1509 // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1510 // of a statement, not nested inside a subquery; this path is
1511 // reached exactly when one is nested. The old text described SPG's
1512 // own executor plumbing ("the top-level mutable entry"), which
1513 // means nothing to a client.
1514 return Err(EngineError::Unsupported(
1515 "WITH clause containing a data-modifying statement must be at the top level".into(),
1516 ));
1517 }
1518 let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1519 // Strip CTEs from the body before running on the temp engine
1520 // so we don't recurse forever.
1521 let mut body = stmt.clone();
1522 body.ctes = Vec::new();
1523 let mut temp = Engine::restore(catalog);
1524 if let Some(c) = self.clock {
1525 temp = temp.with_clock(c);
1526 }
1527 if let Some(f) = self.salt_fn {
1528 temp = temp.with_salt_fn(f);
1529 }
1530 temp.exec_select_cancel(&body, cancel)
1531 }
1532
1533 /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1534 /// `&self` SELECT path. Caller guarantees no modifying CTE
1535 /// bodies are present.
1536 pub(crate) fn materialise_ctes_readonly(
1537 &self,
1538 ctes: &[spg_sql::ast::Cte],
1539 cancel: CancelToken<'_>,
1540 ) -> Result<crate::Catalog, EngineError> {
1541 cancel.check()?;
1542 let mut catalog = self.active_catalog().clone();
1543 for cte in ctes {
1544 let body_select = cte.body.as_select().ok_or_else(|| {
1545 EngineError::Unsupported(alloc::format!(
1546 "data-modifying CTE not supported on this SELECT entry"
1547 ))
1548 })?;
1549 // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1550 // (PG scoping: the WITH name wins for the outer query and later
1551 // CTEs, while THIS body still sees the real table — a
1552 // non-recursive body's self-name is the table, probe P2). This
1553 // materialiser works on a CLONE, so the shadow is simply: run
1554 // the body against the untouched clone, then drop the real
1555 // table from the clone before installing the CTE's temp. A
1556 // RECURSIVE self-reference is the CTE itself (P6), so there the
1557 // drop happens before the iterating materialiser runs.
1558 let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1559 let synthetic = spg_sql::ast::Cte {
1560 name: cte.name.clone(),
1561 body: spg_sql::ast::CteBody::Select(body_select.clone()),
1562 recursive: true,
1563 column_overrides: cte.column_overrides.clone(),
1564 search: None,
1565 cycle: None,
1566 };
1567 if catalog.get(&cte.name).is_some() {
1568 let _ = catalog.drop_table(&cte.name);
1569 }
1570 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1571 } else {
1572 let mut cte_engine = Engine::restore(catalog.clone());
1573 if let Some(c) = self.clock {
1574 cte_engine = cte_engine.with_clock(c);
1575 }
1576 if let Some(f) = self.salt_fn {
1577 cte_engine = cte_engine.with_salt_fn(f);
1578 }
1579 let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1580 let QueryResult::Rows { columns, rows } = body_result else {
1581 return Err(EngineError::Unsupported(alloc::format!(
1582 "CTE {:?} body did not return rows",
1583 cte.name
1584 )));
1585 };
1586 (columns, rows)
1587 };
1588 let inferred = infer_column_types(&columns, &rows);
1589 let mut columns = inferred;
1590 if !cte.column_overrides.is_empty() {
1591 if cte.column_overrides.len() != columns.len() {
1592 return Err(EngineError::Unsupported(alloc::format!(
1593 "CTE {:?} column list has {} names but body returns {} columns",
1594 cte.name,
1595 cte.column_overrides.len(),
1596 columns.len()
1597 )));
1598 }
1599 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1600 col.name.clone_from(name);
1601 }
1602 }
1603 let schema = TableSchema::new(cte.name.clone(), columns);
1604 // v7.39 (round 156) — the body ran against the untouched clone;
1605 // from here on the CTE name resolves to the temp (PG scoping).
1606 if catalog.get(&cte.name).is_some() {
1607 let _ = catalog.drop_table(&cte.name);
1608 }
1609 catalog.create_table(schema).map_err(EngineError::Storage)?;
1610 let table = catalog
1611 .get_mut(&cte.name)
1612 .expect("just-created CTE table must exist");
1613 for row in rows {
1614 table.insert(row).map_err(EngineError::Storage)?;
1615 }
1616 }
1617 Ok(catalog)
1618 }
1619
1620 /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1621 /// Retained for non-DML callers; the DML path (writable CTE on
1622 /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1623 /// `dml.rs` which installs the CTE temps directly on the
1624 /// active catalog so the outer statement's writes hit real
1625 /// tables.
1626 #[allow(dead_code)]
1627 pub(crate) fn materialise_ctes(
1628 &mut self,
1629 ctes: &[spg_sql::ast::Cte],
1630 cancel: CancelToken<'_>,
1631 ) -> Result<crate::Catalog, EngineError> {
1632 cancel.check()?;
1633 // v7.37.43-T4.4 — modifying CTEs need to write through the
1634 // SAME catalog as the outer statement, not a clone (PG's
1635 // writable CTE puts all modifications in one transaction).
1636 // For the read-only case the original logic cloned, but
1637 // since the outer statement also goes through the cloned
1638 // engine and ALL writes must converge, we now drive the
1639 // accumulator off `self.active_catalog().clone()` and
1640 // commit the modifying writes directly to `self`'s active
1641 // catalog so the surface is consistent.
1642 let mut catalog = self.active_catalog().clone();
1643 // v7.39 (round 149) — a modifying CTE body's target must be a
1644 // real relation, never a sibling CTE (PG: relation does not
1645 // exist); checked before any alias lands in the accumulator.
1646 for cte in ctes {
1647 let body_target = match &cte.body {
1648 spg_sql::ast::CteBody::Select(_) => None,
1649 spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1650 spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1651 spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1652 spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1653 };
1654 if let Some(t) = body_target
1655 && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1656 && catalog.get(t).is_none()
1657 {
1658 return Err(EngineError::Storage(
1659 spg_storage::StorageError::TableNotFound { name: t.into() },
1660 ));
1661 }
1662 }
1663 for cte in ctes {
1664 if catalog.get(&cte.name).is_some() {
1665 return Err(EngineError::Unsupported(alloc::format!(
1666 "CTE name {:?} shadows an existing table; rename the CTE",
1667 cte.name
1668 )));
1669 }
1670 let (columns, rows) = match &cte.body {
1671 // v7.39 (round 145) — see the sibling site: only a body that
1672 // truly self-references takes the iterating materialiser.
1673 spg_sql::ast::CteBody::Select(body)
1674 if cte.recursive && select_refers_to(body, &cte.name) =>
1675 {
1676 // Recursive CTE — the existing helper takes a
1677 // SELECT body and the snapshot catalog.
1678 let synthetic = spg_sql::ast::Cte {
1679 name: cte.name.clone(),
1680 body: spg_sql::ast::CteBody::Select(body.clone()),
1681 recursive: true,
1682 column_overrides: cte.column_overrides.clone(),
1683 search: None,
1684 cycle: None,
1685 };
1686 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1687 }
1688 spg_sql::ast::CteBody::Select(body) => {
1689 // v7.25 (round-17) — run against the accumulated
1690 // catalog so later CTEs can reference earlier
1691 // ones in the same WITH clause.
1692 let mut cte_engine = Engine::restore(catalog.clone());
1693 if let Some(c) = self.clock {
1694 cte_engine = cte_engine.with_clock(c);
1695 }
1696 if let Some(f) = self.salt_fn {
1697 cte_engine = cte_engine.with_salt_fn(f);
1698 }
1699 let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1700 let QueryResult::Rows { columns, rows } = body_result else {
1701 return Err(EngineError::Unsupported(alloc::format!(
1702 "CTE {:?} body did not return rows",
1703 cte.name
1704 )));
1705 };
1706 (columns, rows)
1707 }
1708 spg_sql::ast::CteBody::Insert(body) => {
1709 self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1710 }
1711 spg_sql::ast::CteBody::Update(body) => {
1712 self.exec_modifying_cte_update(&cte.name, body, cancel)?
1713 }
1714 spg_sql::ast::CteBody::Delete(body) => {
1715 self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1716 }
1717 spg_sql::ast::CteBody::Merge(body) => {
1718 self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1719 }
1720 };
1721 // v4.22: the projection builder labels any non-column
1722 // expression as Text — including literal SELECT 1.
1723 // Promote each column's type to whatever the rows
1724 // actually carry so the CTE storage table accepts them.
1725 let inferred = infer_column_types(&columns, &rows);
1726 let mut columns = inferred;
1727 if !cte.column_overrides.is_empty() {
1728 if cte.column_overrides.len() != columns.len() {
1729 return Err(EngineError::Unsupported(alloc::format!(
1730 "CTE {:?} column list has {} names but body returns {} columns",
1731 cte.name,
1732 cte.column_overrides.len(),
1733 columns.len()
1734 )));
1735 }
1736 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1737 col.name.clone_from(name);
1738 }
1739 }
1740 let schema = TableSchema::new(cte.name.clone(), columns);
1741 catalog.create_table(schema).map_err(EngineError::Storage)?;
1742 let table = catalog
1743 .get_mut(&cte.name)
1744 .expect("just-created CTE table must exist");
1745 for row in rows {
1746 table.insert(row).map_err(EngineError::Storage)?;
1747 }
1748 }
1749 Ok(catalog)
1750 }
1751
1752 /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1753 /// against `self` (so the mutation lands in the active catalog
1754 /// inside the current transaction) and captures the RETURNING
1755 /// projection — column schema + rows — to materialise as the
1756 /// CTE alias's table. An INSERT without RETURNING produces a
1757 /// 0-row table with a synthetic single-column placeholder
1758 /// (matches PG: the CTE alias is still defined, but referencing
1759 /// it from the outer query without RETURNING raises a
1760 /// column-resolution error at scan time).
1761 fn exec_modifying_cte_insert(
1762 &mut self,
1763 cte_name: &str,
1764 body: &spg_sql::ast::InsertStatement,
1765 _cancel: CancelToken<'_>,
1766 ) -> Result<
1767 (
1768 Vec<spg_storage::ColumnSchema>,
1769 Vec<spg_storage::Row<'static>>,
1770 ),
1771 EngineError,
1772 > {
1773 // round 151 — a WITH-headed body keeps its own ctes; the body
1774 // statement routes through its writable-CTE entry (outer CTEs
1775 // are never copied into bodies, so no recursion risk).
1776 let body = body.clone();
1777 let result = self.exec_insert(body)?;
1778 match result {
1779 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1780 QueryResult::CommandOk { .. } => {
1781 // No RETURNING — emit a sentinel single-column
1782 // schema with zero rows so the alias is defined.
1783 let placeholder = spg_storage::ColumnSchema::new(
1784 alloc::format!("{cte_name}_returning_absent"),
1785 spg_storage::DataType::Text,
1786 true,
1787 );
1788 Ok((alloc::vec![placeholder], Vec::new()))
1789 }
1790 }
1791 }
1792
1793 /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1794 /// as INSERT above.
1795 fn exec_modifying_cte_update(
1796 &mut self,
1797 cte_name: &str,
1798 body: &spg_sql::ast::UpdateStatement,
1799 cancel: CancelToken<'_>,
1800 ) -> Result<
1801 (
1802 Vec<spg_storage::ColumnSchema>,
1803 Vec<spg_storage::Row<'static>>,
1804 ),
1805 EngineError,
1806 > {
1807 let body = body.clone();
1808 let result = self.exec_update_cancel(&body, cancel)?;
1809 match result {
1810 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1811 QueryResult::CommandOk { .. } => {
1812 let placeholder = spg_storage::ColumnSchema::new(
1813 alloc::format!("{cte_name}_returning_absent"),
1814 spg_storage::DataType::Text,
1815 true,
1816 );
1817 Ok((alloc::vec![placeholder], Vec::new()))
1818 }
1819 }
1820 }
1821
1822 /// v7.37.43-T4.4 — execute a DELETE CTE body.
1823 fn exec_modifying_cte_delete(
1824 &mut self,
1825 cte_name: &str,
1826 body: &spg_sql::ast::DeleteStatement,
1827 cancel: CancelToken<'_>,
1828 ) -> Result<
1829 (
1830 Vec<spg_storage::ColumnSchema>,
1831 Vec<spg_storage::Row<'static>>,
1832 ),
1833 EngineError,
1834 > {
1835 let body = body.clone();
1836 let result = self.exec_delete_cancel(&body, cancel)?;
1837 match result {
1838 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1839 QueryResult::CommandOk { .. } => {
1840 let placeholder = spg_storage::ColumnSchema::new(
1841 alloc::format!("{cte_name}_returning_absent"),
1842 spg_storage::DataType::Text,
1843 true,
1844 );
1845 Ok((alloc::vec![placeholder], Vec::new()))
1846 }
1847 }
1848 }
1849
1850 /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1851 fn exec_modifying_cte_merge(
1852 &mut self,
1853 cte_name: &str,
1854 body: &spg_sql::ast::MergeStatement,
1855 cancel: CancelToken<'_>,
1856 ) -> Result<
1857 (
1858 Vec<spg_storage::ColumnSchema>,
1859 Vec<spg_storage::Row<'static>>,
1860 ),
1861 EngineError,
1862 > {
1863 let body = body.clone();
1864 let result = self.exec_merge_cancel(&body, cancel)?;
1865 match result {
1866 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1867 QueryResult::CommandOk { .. } => {
1868 let placeholder = spg_storage::ColumnSchema::new(
1869 alloc::format!("{cte_name}_returning_absent"),
1870 spg_storage::DataType::Text,
1871 true,
1872 );
1873 Ok((alloc::vec![placeholder], Vec::new()))
1874 }
1875 }
1876 }
1877
1878 /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1879 /// UNION (or UNION ALL) of an anchor that does not reference
1880 /// the CTE name, and one or more recursive terms that do. The
1881 /// anchor runs first; each subsequent iteration runs the
1882 /// recursive term against a temp catalog where the CTE name is
1883 /// bound to the *previous* iteration's output. Iteration stops
1884 /// when the recursive term yields no rows; UNION (DISTINCT)
1885 /// deduplicates against the accumulated result, UNION ALL does
1886 /// not. A hard cap on total rows prevents runaway queries.
1887 #[allow(clippy::too_many_lines)]
1888 pub(crate) fn materialise_recursive_cte(
1889 &self,
1890 cte: &spg_sql::ast::Cte,
1891 base_catalog: &Catalog,
1892 cancel: CancelToken<'_>,
1893 ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1894 const MAX_TOTAL_ROWS: usize = 1_000_000;
1895 const MAX_ITERATIONS: usize = 100_000;
1896 cancel.check()?;
1897 // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1898 // a modifying recursive CTE is parser-rejectable but we
1899 // guard here defensively.
1900 let body_select = cte.body.as_select().ok_or_else(|| {
1901 EngineError::Unsupported(alloc::format!(
1902 "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1903 cte.name
1904 ))
1905 })?;
1906 if body_select.unions.is_empty() {
1907 return Err(EngineError::Unsupported(alloc::format!(
1908 "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1909 cte.name
1910 )));
1911 }
1912 // Anchor: the body's leading SELECT, with unions stripped.
1913 let mut anchor = body_select.clone();
1914 let all_union_terms = core::mem::take(&mut anchor.unions);
1915 anchor.ctes = Vec::new();
1916 // v7.37 D.42 — split the UNION members: those that do NOT reference the
1917 // CTE are additional ANCHOR terms, only the ones that do recurse. A
1918 // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1919 // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1920 // treating the non-recursive `SELECT r2` as a recursive term made it
1921 // re-emit its constant row every iteration → runaway loop.
1922 let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1923 .into_iter()
1924 .partition(|(_, t)| !select_refers_to(t, &cte.name));
1925 let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1926 let QueryResult::Rows {
1927 columns: anchor_cols,
1928 rows: mut anchor_rows,
1929 } = anchor_result
1930 else {
1931 return Err(EngineError::Unsupported(alloc::format!(
1932 "WITH RECURSIVE {:?}: anchor did not return rows",
1933 cte.name
1934 )));
1935 };
1936 // Append every non-recursive UNION member's rows to the anchor set.
1937 for (_, term) in &anchor_terms {
1938 let mut term = term.clone();
1939 term.ctes = Vec::new();
1940 if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
1941 anchor_rows.extend(rows);
1942 }
1943 }
1944 // The projection builder labels non-column expressions Text;
1945 // refine column types from the anchor's actual values so the
1946 // intermediate iter-catalog tables accept them.
1947 let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
1948 if !cte.column_overrides.is_empty() {
1949 if cte.column_overrides.len() != columns.len() {
1950 return Err(EngineError::Unsupported(alloc::format!(
1951 "CTE {:?} column list has {} names but anchor returns {} columns",
1952 cte.name,
1953 cte.column_overrides.len(),
1954 columns.len()
1955 )));
1956 }
1957 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1958 col.name.clone_from(name);
1959 }
1960 }
1961 let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
1962 let mut working_set: Vec<Row<'static>> = anchor_rows;
1963 let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
1964 // Track at least one "all UNION ALL" flag — if every union
1965 // kind is ALL we skip the dedup step (faster + matches PG).
1966 let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
1967 if !all_union_all {
1968 for r in &all_rows {
1969 seen.insert(encode_row_key(r));
1970 }
1971 }
1972 // v7.39 (round 598) — the engine and its catalog are built ONCE.
1973 // Each iteration used to clone the catalog, create the CTE table,
1974 // and construct a whole `Engine` — which initialises 82 fields — to
1975 // hold that round's working set. A counting allocator put the loop
1976 // at 63 allocations and 104 kB per iteration, or 1 GB for a
1977 // 10,000-row recursive CTE, and none of it varied with how much
1978 // else was in the catalog: the per-round rebuild WAS the cost. The
1979 // table is emptied and refilled instead.
1980 let mut iter_catalog = base_catalog.clone();
1981 let schema = TableSchema::new(cte.name.clone(), columns.clone());
1982 iter_catalog
1983 .create_table(schema)
1984 .map_err(EngineError::Storage)?;
1985 let mut iter_engine = Engine::restore(iter_catalog);
1986 if let Some(c) = self.clock {
1987 iter_engine = iter_engine.with_clock(c);
1988 }
1989 if let Some(f) = self.salt_fn {
1990 iter_engine = iter_engine.with_salt_fn(f);
1991 }
1992 // The recursive terms are cloned once too — the clone stripped the
1993 // CTE list off each of them, per term per iteration.
1994 let recursive_terms: Vec<SelectStatement> = union_terms
1995 .iter()
1996 .map(|(_, t)| {
1997 let mut t = t.clone();
1998 t.ctes = Vec::new();
1999 t
2000 })
2001 .collect();
2002 // v7.39 (round 618) — plan every recursive term once. Taken only if
2003 // ALL of them plan, so a query never runs half on each path.
2004 let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2005 .iter()
2006 .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2007 .collect();
2008 let fast_ctx = term_plans.as_ref().map(|plans| {
2009 let alias = plans[0].alias.clone();
2010 (alias, ())
2011 });
2012 for iter in 0..MAX_ITERATIONS {
2013 cancel.check()?;
2014 if working_set.is_empty() {
2015 break;
2016 }
2017 if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2018 // The worktable IS the working set: no table to empty and
2019 // refill, and no query execution per round.
2020 let mut next_set: Vec<Row<'static>> = Vec::new();
2021 for plan in plans {
2022 let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2023 for row in &working_set {
2024 cancel.check()?;
2025 if let Some(w) = plan.where_ {
2026 let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2027 if !matches!(v, Value::Bool(true)) {
2028 continue;
2029 }
2030 }
2031 let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2032 for it in &plan.items {
2033 vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2034 }
2035 let out = Row::new(vals);
2036 if !all_union_all {
2037 let key = encode_row_key(&out);
2038 if !seen.insert(key) {
2039 continue;
2040 }
2041 }
2042 next_set.push(out);
2043 }
2044 }
2045 if next_set.is_empty() {
2046 break;
2047 }
2048 all_rows.extend(next_set.iter().cloned());
2049 working_set = next_set;
2050 if all_rows.len() > MAX_TOTAL_ROWS {
2051 return Err(EngineError::Unsupported(alloc::format!(
2052 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2053 cte.name
2054 )));
2055 }
2056 if iter + 1 == MAX_ITERATIONS {
2057 return Err(EngineError::Unsupported(alloc::format!(
2058 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2059 cte.name
2060 )));
2061 }
2062 continue;
2063 }
2064 {
2065 // Truncated rather than dropped and recreated: the table's
2066 // own structure is what dropping it throws away, and it is
2067 // identical every round.
2068 let cat = iter_engine.base_catalog_mut();
2069 let table = cat.get_mut(&cte.name).expect("created above");
2070 table.truncate();
2071 for row in &working_set {
2072 table.insert(row.clone()).map_err(EngineError::Storage)?;
2073 }
2074 }
2075 // Run each recursive term in sequence and collect new rows.
2076 let mut next_set: Vec<Row<'static>> = Vec::new();
2077 for term in &recursive_terms {
2078 let r = iter_engine.exec_select_cancel(term, cancel)?;
2079 let QueryResult::Rows {
2080 columns: rc,
2081 rows: rs,
2082 } = r
2083 else {
2084 return Err(EngineError::Unsupported(alloc::format!(
2085 "WITH RECURSIVE {:?}: recursive term did not return rows",
2086 cte.name
2087 )));
2088 };
2089 if rc.len() != columns.len() {
2090 return Err(EngineError::Unsupported(alloc::format!(
2091 "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2092 cte.name,
2093 rc.len(),
2094 columns.len()
2095 )));
2096 }
2097 for row in rs {
2098 if !all_union_all {
2099 let key = encode_row_key(&row);
2100 if !seen.insert(key) {
2101 continue;
2102 }
2103 }
2104 next_set.push(row);
2105 }
2106 }
2107 if next_set.is_empty() {
2108 break;
2109 }
2110 all_rows.extend(next_set.iter().cloned());
2111 working_set = next_set;
2112 if all_rows.len() > MAX_TOTAL_ROWS {
2113 return Err(EngineError::Unsupported(alloc::format!(
2114 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2115 cte.name
2116 )));
2117 }
2118 if iter + 1 == MAX_ITERATIONS {
2119 return Err(EngineError::Unsupported(alloc::format!(
2120 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2121 cte.name
2122 )));
2123 }
2124 }
2125 Ok((columns, all_rows))
2126 }
2127
2128 pub(crate) fn resolve_select_subqueries(
2129 &self,
2130 stmt: &mut SelectStatement,
2131 cancel: CancelToken<'_>,
2132 ) -> Result<(), EngineError> {
2133 for item in &mut stmt.items {
2134 if let SelectItem::Expr { expr, alias } = item {
2135 // An UNCORRELATED subquery is replaced by its value right
2136 // here, and the shape the column was named for goes with
2137 // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2138 // boolean literal, so SPG answered `?column?` where PG18
2139 // answers `exists`. Only a subquery at the TOP of the item
2140 // loses its name this way — one nested inside a call still
2141 // reports the call.
2142 if alias.is_none()
2143 && matches!(
2144 expr,
2145 Expr::ScalarSubquery(_)
2146 | Expr::Exists { .. }
2147 | Expr::InSubquery { .. }
2148 | Expr::RowInSubquery { .. }
2149 | Expr::RowCmpSubquery { .. }
2150 )
2151 {
2152 *alias = Some(default_output_name(expr, self.backslash_escapes));
2153 }
2154 self.resolve_expr_subqueries(expr, cancel)?;
2155 }
2156 }
2157 if let Some(w) = &mut stmt.where_ {
2158 self.resolve_expr_subqueries(w, cancel)?;
2159 }
2160 // v7.24.1 — JOIN ON conditions can carry subqueries too;
2161 // they were never walked, so even an UNCORRELATED subquery
2162 // in ON hit "subquery reached row eval".
2163 if let Some(from) = &mut stmt.from {
2164 for j in &mut from.joins {
2165 if let Some(on) = &mut j.on {
2166 self.resolve_expr_subqueries(on, cancel)?;
2167 }
2168 }
2169 }
2170 if let Some(gs) = &mut stmt.group_by {
2171 for g in gs {
2172 self.resolve_expr_subqueries(g, cancel)?;
2173 }
2174 }
2175 if let Some(h) = &mut stmt.having {
2176 self.resolve_expr_subqueries(h, cancel)?;
2177 }
2178 for o in &mut stmt.order_by {
2179 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2180 }
2181 for (_, peer) in &mut stmt.unions {
2182 self.resolve_select_subqueries(peer, cancel)?;
2183 }
2184 Ok(())
2185 }
2186
2187 #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2188 pub(crate) fn resolve_expr_subqueries(
2189 &self,
2190 e: &mut Expr,
2191 cancel: CancelToken<'_>,
2192 ) -> Result<(), EngineError> {
2193 // Replace-on-this-node cases first.
2194 if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2195 *e = replacement;
2196 return Ok(());
2197 }
2198 match e {
2199 Expr::NamedArg { expr, .. } => self.resolve_expr_subqueries(expr, cancel)?,
2200 Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2201 Expr::AggregateOrdered { call, order_by, .. } => {
2202 self.resolve_expr_subqueries(call, cancel)?;
2203 for o in order_by.iter_mut() {
2204 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2205 }
2206 }
2207 Expr::Binary { lhs, rhs, .. } => {
2208 self.resolve_expr_subqueries(lhs, cancel)?;
2209 self.resolve_expr_subqueries(rhs, cancel)?;
2210 }
2211 Expr::Unary { expr, .. }
2212 | Expr::Cast { expr, .. }
2213 | Expr::IsNull { expr, .. }
2214 | Expr::BoolTest { expr, .. }
2215 | Expr::FieldAccess { base: expr, .. } => {
2216 self.resolve_expr_subqueries(expr, cancel)?;
2217 }
2218 Expr::FunctionCall { args, .. } => {
2219 for a in args {
2220 self.resolve_expr_subqueries(a, cancel)?;
2221 }
2222 }
2223 Expr::Like { expr, pattern, .. } => {
2224 self.resolve_expr_subqueries(expr, cancel)?;
2225 self.resolve_expr_subqueries(pattern, cancel)?;
2226 }
2227 Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2228 // v4.12 window functions — recurse into args + ORDER BY
2229 // + PARTITION BY in case they carry inner subqueries.
2230 Expr::WindowFunction {
2231 args,
2232 partition_by,
2233 order_by,
2234 ..
2235 } => {
2236 for a in args {
2237 self.resolve_expr_subqueries(a, cancel)?;
2238 }
2239 for p in partition_by {
2240 self.resolve_expr_subqueries(p, cancel)?;
2241 }
2242 for (e, _, _) in order_by {
2243 self.resolve_expr_subqueries(e, cancel)?;
2244 }
2245 }
2246 // Subquery nodes are handled in subquery_replacement
2247 // (which returned None — defensive no-op); Literal /
2248 // Column are leaves.
2249 Expr::ScalarSubquery(_)
2250 | Expr::Exists { .. }
2251 | Expr::InSubquery { .. }
2252 | Expr::RowInSubquery { .. }
2253 | Expr::RowCmpSubquery { .. }
2254 | Expr::Literal(_)
2255 | Expr::Placeholder(_)
2256 | Expr::Column(_) => {}
2257 // v7.30.2 — list elements can carry scalar subqueries
2258 // (`x IN (1, (SELECT …))`).
2259 Expr::InList { expr, list, .. } => {
2260 self.resolve_expr_subqueries(expr, cancel)?;
2261 for item in list {
2262 self.resolve_expr_subqueries(item, cancel)?;
2263 }
2264 }
2265 // v7.10.10 — recurse children.
2266 Expr::Array(items) => {
2267 for elem in items {
2268 self.resolve_expr_subqueries(elem, cancel)?;
2269 }
2270 }
2271 Expr::ArraySubscript { target, index } => {
2272 self.resolve_expr_subqueries(target, cancel)?;
2273 self.resolve_expr_subqueries(index, cancel)?;
2274 }
2275 Expr::ArraySlice { target, lo, hi } => {
2276 self.resolve_expr_subqueries(target, cancel)?;
2277 if let Some(l) = lo {
2278 self.resolve_expr_subqueries(l, cancel)?;
2279 }
2280 if let Some(h) = hi {
2281 self.resolve_expr_subqueries(h, cancel)?;
2282 }
2283 }
2284 Expr::AnyAll { expr, array, .. } => {
2285 self.resolve_expr_subqueries(expr, cancel)?;
2286 // Quantified subquery — an uncorrelated one
2287 // materialises up front; a correlated one stays for
2288 // the per-row resolver.
2289 if let Expr::ScalarSubquery(inner) = array.as_mut() {
2290 if !crate::subquery::select_is_correlated(inner) {
2291 let s = (**inner).clone();
2292 **array = self.materialize_quantified_rows(&s, cancel)?;
2293 }
2294 } else {
2295 self.resolve_expr_subqueries(array, cancel)?;
2296 }
2297 }
2298 Expr::Case {
2299 operand,
2300 branches,
2301 else_branch,
2302 } => {
2303 if let Some(o) = operand {
2304 self.resolve_expr_subqueries(o, cancel)?;
2305 }
2306 for (w, t) in branches {
2307 self.resolve_expr_subqueries(w, cancel)?;
2308 self.resolve_expr_subqueries(t, cancel)?;
2309 }
2310 if let Some(e) = else_branch {
2311 self.resolve_expr_subqueries(e, cancel)?;
2312 }
2313 }
2314 }
2315 Ok(())
2316 }
2317}
2318
2319impl Engine {
2320 /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2321 /// `SelectItem::Wildcard` to all schema columns and
2322 /// `SelectItem::Expr` via the regular eval path.
2323 pub(crate) fn project_row_simple(
2324 &self,
2325 row: &Row<'static>,
2326 items: &[SelectItem],
2327 schema_cols: &[ColumnSchema],
2328 alias: &str,
2329 ) -> Result<Row<'static>, EngineError> {
2330 let ctx = self.ev_ctx(schema_cols, Some(alias));
2331 let cancel = CancelToken::none();
2332 let mut out_vals = Vec::new();
2333 for item in items {
2334 match item {
2335 // In a single-table projection (AS OF SEGMENT / RETURNING) a
2336 // qualified `t.*` covers exactly the same columns as a bare `*`.
2337 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2338 out_vals.extend(row.values.iter().cloned());
2339 }
2340 SelectItem::Expr { expr, .. } => {
2341 let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2342 out_vals.push(v);
2343 }
2344 }
2345 }
2346 Ok(Row::new(out_vals))
2347 }
2348
2349 /// v6.10.2 — derive the output `ColumnSchema` list for an
2350 /// AS OF SEGMENT projection. Wildcards take the full schema;
2351 /// expressions take the alias if present or a synthetic
2352 /// `?column?` (PG convention) otherwise.
2353 pub(crate) fn derive_output_columns(
2354 &self,
2355 items: &[SelectItem],
2356 schema_cols: &[ColumnSchema],
2357 table_alias: &str,
2358 ) -> Vec<ColumnSchema> {
2359 let mut out = Vec::new();
2360 for item in items {
2361 match item {
2362 // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2363 // a single-table projection.
2364 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2365 out.extend(schema_cols.iter().cloned());
2366 }
2367 SelectItem::Expr { expr, alias } => {
2368 // Bare column references inherit the schema
2369 // column's name + type — PG names `RETURNING id`
2370 // "id" and types it BIGINT, and the sqlx embed
2371 // path type-checks RowDescription against the
2372 // Rust target (mailrs embed round-12).
2373 if let Expr::Column(col) = expr
2374 && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2375 {
2376 let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2377 let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2378 // v7.39 (read01 round 54) — carry the enum identity:
2379 // it lives outside the DataType lattice, so a derived
2380 // table built from this schema otherwise forgets it and
2381 // the OUTER `ORDER BY <enum col>` silently sorts by the
2382 // label's TEXT instead of member order.
2383 c.user_enum_type = sc.user_enum_type.clone();
2384 out.push(c);
2385 continue;
2386 }
2387 let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2388 // v7.30.4 (mailrs round-27, P0) — type the
2389 // expression with the same inference the SELECT
2390 // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2391 // The old Text default broke every typed decode
2392 // of `RETURNING uidnext - 1 AS uid`: four days
2393 // of inbound mail indexed nowhere. Inference
2394 // failure keeps the old Text fallback rather
2395 // than inventing new error paths here.
2396 // v7.39 (round 258) — take the enum identity from the
2397 // same projection build, not just the type: a constant
2398 // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2399 // VALUES row lowers to) is an EXPRESSION, so it landed
2400 // here and the derived table forgot the enum.
2401 let (ty, nullable) = build_projection(
2402 core::slice::from_ref(item),
2403 schema_cols,
2404 table_alias,
2405 self.backslash_escapes,
2406 )
2407 .ok()
2408 .and_then(|p| p.into_iter().next())
2409 .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2410 out.push(ColumnSchema::new(name, ty, nullable));
2411 }
2412 }
2413 }
2414 out
2415 }
2416
2417 /// v4.5: SELECT with cooperative cancellation. The token is
2418 /// honoured between UNION peers and inside the bare-SELECT row
2419 /// loop; HNSW kNN graph walks and the aggregate executor don't
2420 /// honour it yet (deferred — those paths bound their work
2421 /// internally by `LIMIT k` and `GROUP BY` cardinality).
2422 /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2423 /// its (lowercased) name, or None if the name isn't a virtual view.
2424 /// Callers decide whether to return it directly (`SELECT *`) or stage
2425 /// it as a temp table for the full query pipeline.
2426 fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2427 Some(match name {
2428 "spg_statistic" => self.exec_spg_statistic(),
2429 "spg_stat_replication" => self.exec_spg_stat_replication(),
2430 "spg_stat_segment" => self.exec_spg_stat_segment(),
2431 "spg_memory_stats" => self.exec_spg_memory_stats(),
2432 "spg_stat_query" => self.exec_spg_stat_query(),
2433 "pg_stat_statements" => self.exec_pg_stat_statements(),
2434 "spg_stat_activity" => self.exec_spg_stat_activity(),
2435 "pg_stat_activity" => self.exec_pg_stat_activity(),
2436 "pg_locks" => self.exec_pg_locks(),
2437 "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2438 "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2439 "spg_partition_health" => self.exec_spg_partition_health(),
2440 "spg_audit_chain" => self.exec_spg_audit_chain(),
2441 "spg_audit_verify" => self.exec_spg_audit_verify(),
2442 "spg_table_ddl" => self.exec_spg_table_ddl(),
2443 "spg_role_ddl" => self.exec_spg_role_ddl(),
2444 "spg_database_ddl" => self.exec_spg_database_ddl(),
2445 _ => return None,
2446 })
2447 }
2448
2449 /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2450 /// describes against: this engine's catalog with the view staged as a
2451 /// table, exactly as `exec_select_cancel_as` stages it for a
2452 /// non-bare query.
2453 ///
2454 /// These views never reach the catalog — each is a fixed row set built
2455 /// inside its own `exec_*` — so Describe reported no columns for all
2456 /// seventeen of them. Rows are deliberately not inserted: Describe
2457 /// only needs the shape, and `infer_column_types` reads the rows we
2458 /// already have in hand.
2459 pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2460 let from = stmt.from.as_ref()?;
2461 if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2462 return None;
2463 }
2464 let lower = from.primary.name.to_ascii_lowercase();
2465 let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2466 return None;
2467 };
2468 let mut catalog = self.active_catalog().clone();
2469 let cols = infer_column_types(&columns, &rows);
2470 catalog
2471 .create_table(TableSchema::new(from.primary.name.clone(), cols))
2472 .ok()?;
2473 Some(catalog)
2474 }
2475
2476 pub(crate) fn exec_select_cancel(
2477 &self,
2478 stmt: &SelectStatement,
2479 cancel: CancelToken<'_>,
2480 ) -> Result<QueryResult, EngineError> {
2481 self.exec_select_cancel_as(stmt, cancel, None)
2482 }
2483
2484 /// v7.39 (round 334, V55) — the same read core, authorised as
2485 /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2486 /// function's OWNER: that is the entire point of the form, and without
2487 /// it every definer function failed with "permission denied" on the
2488 /// very table it exists to expose.
2489 /// v7.39 (round 559) — see the call site. `None` for anything but
2490 /// the bare shape, so every other query keeps its old path.
2491 fn try_bare_count_star(
2492 &self,
2493 stmt: &SelectStatement,
2494 as_role: Option<&str>,
2495 ) -> Result<Option<QueryResult>, EngineError> {
2496 use spg_sql::ast::SelectItem;
2497 if as_role.is_some()
2498 || !stmt.ctes.is_empty()
2499 || !stmt.unions.is_empty()
2500 || stmt.where_.is_some()
2501 || stmt.group_by.is_some()
2502 || stmt.having.is_some()
2503 || stmt.distinct
2504 || !stmt.order_by.is_empty()
2505 || stmt.limit.is_some()
2506 || stmt.offset.is_some()
2507 || stmt.items.len() != 1
2508 {
2509 return Ok(None);
2510 }
2511 let Some(from) = &stmt.from else {
2512 return Ok(None);
2513 };
2514 if !from.joins.is_empty()
2515 || stmt.locking.is_some()
2516 || from.primary.lateral_subquery.is_some()
2517 || from.primary.unnest_expr.is_some()
2518 || from.primary.generate_series_args.is_some()
2519 || from.primary.name.is_empty()
2520 || from.primary.name.starts_with("__spg_")
2521 {
2522 return Ok(None);
2523 }
2524 // A partition PARENT holds no rows of its own — they live in the
2525 // children — so its header count is 0 and the ordinary path has
2526 // to fan out. Caught by the partition conformance cases.
2527 //
2528 // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2529 // of them, which is worse: its header count is a real number,
2530 // just not the answer. `SELECT count(*) FROM par` returned 1
2531 // where PG returns 2, because this shortcut fired before the
2532 // fan-out could. The question is "does anything descend from
2533 // this", not "was it declared a partition parent".
2534 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2535 return Ok(None);
2536 }
2537 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2538 return Ok(None);
2539 };
2540 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2541 return Ok(None);
2542 };
2543 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2544 return Ok(None);
2545 }
2546 // A row-security policy filters rows, so the header count is not
2547 // the answer; the ordinary path applies the policy.
2548 let Some(table) = self.active_catalog().get(&from.primary.name) else {
2549 return Ok(None);
2550 };
2551 if table.schema().row_security {
2552 return Ok(None);
2553 }
2554 // Rows frozen to the cold tier are not in `headers`, so the
2555 // header count would miss them. Caught by the cold-tier e2e.
2556 if table.has_cold_rows_fast() {
2557 return Ok(None);
2558 }
2559 let n = table.count_visible(&self.current_snapshot());
2560 let col = alias.clone().unwrap_or_else(|| String::from("count"));
2561 Ok(Some(QueryResult::Rows {
2562 columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2563 rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2564 i64::try_from(n).unwrap_or(i64::MAX)
2565 )])],
2566 }))
2567 }
2568
2569 /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2570 /// that col>` served from the index, never reading a row.
2571 ///
2572 /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2573 /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2574 /// count (2x at 1k). PG needs its visibility map for this — a heap
2575 /// tuple carries its own visibility, so an index entry alone cannot
2576 /// say whether the row is live, and PG reads the heap for any page
2577 /// the map does not mark all-visible. SPG keeps a header array
2578 /// beside the rows, so the locator answers it directly and there is
2579 /// no map to be stale.
2580 /// v7.39 (round 564) — the shape test, once, for both the
2581 /// materialising scan and the streaming one.
2582 ///
2583 /// Two callers asking the same question in two places is how a fact
2584 /// starts drifting; the answer here is the single copy. Returns the
2585 /// table, the alias the predicate is written against, the projected
2586 /// column's position, and the name the single output column takes.
2587 pub(crate) fn index_only_shape<'s>(
2588 &'s self,
2589 stmt: &'s SelectStatement,
2590 ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2591 use spg_sql::ast::SelectItem;
2592 if !stmt.ctes.is_empty()
2593 || !stmt.unions.is_empty()
2594 || stmt.group_by.is_some()
2595 || stmt.having.is_some()
2596 || stmt.distinct
2597 || stmt.locking.is_some()
2598 || !stmt.order_by.is_empty()
2599 || stmt.limit.is_some()
2600 || stmt.offset.is_some()
2601 || stmt.items.len() != 1
2602 {
2603 return None;
2604 }
2605 let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2606 return None;
2607 };
2608 if !from.joins.is_empty()
2609 || from.primary.lateral_subquery.is_some()
2610 || from.primary.unnest_expr.is_some()
2611 || from.primary.generate_series_args.is_some()
2612 || from.primary.name.is_empty()
2613 || from.primary.name.starts_with("__spg_")
2614 {
2615 return None;
2616 }
2617 // v7.39 (round 645) — see the note on the sibling shortcut above:
2618 // an inheritance parent's own header count is not the answer.
2619 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2620 return None;
2621 }
2622 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2623 return None;
2624 };
2625 let spg_sql::ast::Expr::Column(c) = expr else {
2626 return None;
2627 };
2628 let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2629 if let Some(q) = c.qualifier.as_deref()
2630 && !q.eq_ignore_ascii_case(alias_name)
2631 {
2632 return None;
2633 }
2634 let table = self.active_catalog().get(&from.primary.name)?;
2635 if table.schema().row_security {
2636 return None;
2637 }
2638 let cols = &table.schema().columns;
2639 let pos = cols
2640 .iter()
2641 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2642 let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2643 Some((table, alias_name, pos, out))
2644 }
2645
2646 /// v7.39 (round 565) — would this statement be answered out of the
2647 /// index alone?
2648 ///
2649 /// EXPLAIN has to name the node the executor will actually run, and
2650 /// the only honest way to know is to ask the same two questions the
2651 /// executor asks: the statement's shape, and everything decidable
2652 /// about the scan before it walks. Neither is re-stated here.
2653 pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2654 let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2655 return false;
2656 };
2657 let Some(where_) = stmt.where_.as_ref() else {
2658 return false;
2659 };
2660 crate::index_access::index_only_precheck(
2661 where_,
2662 &table.schema().columns,
2663 table,
2664 alias_name,
2665 pos,
2666 )
2667 .is_some()
2668 }
2669
2670 fn try_index_only_scan(
2671 &self,
2672 stmt: &SelectStatement,
2673 ) -> Result<Option<QueryResult>, EngineError> {
2674 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2675 return Ok(None);
2676 };
2677 // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2678 // are not materialised here, and a partition parent's own
2679 // heap/indexes are empty (its rows live in the children).
2680 if !stmt.ctes.is_empty() {
2681 return Ok(None);
2682 }
2683 if let Some(from) = &stmt.from
2684 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2685 {
2686 return Ok(None);
2687 }
2688 let where_ = stmt.where_.as_ref().expect("shape checked it");
2689 let cols = &table.schema().columns;
2690 let Some(values) = crate::index_access::try_index_only_range(
2691 where_,
2692 cols,
2693 table,
2694 alias_name,
2695 &self.current_snapshot(),
2696 pos,
2697 ) else {
2698 return Ok(None);
2699 };
2700 let schema = alloc::vec![ColumnSchema::new(
2701 out_name,
2702 cols[pos].ty,
2703 cols[pos].nullable
2704 )];
2705 Ok(Some(QueryResult::Rows {
2706 columns: schema,
2707 rows: values
2708 .into_iter()
2709 .map(|v| Row::new(alloc::vec![v]))
2710 .collect(),
2711 }))
2712 }
2713
2714 /// v7.39 (round 564) — the same scan, emitting each value instead of
2715 /// building a `Vec<Row>` for the encoder to walk once and drop.
2716 ///
2717 /// A profile of the server serving a 50k-row range put 10.2% of the
2718 /// connection thread's CPU on BUILDING that vector and another 9.7%
2719 /// on dropping it — a fifth of the query, spent allocating and
2720 /// freeing one single-element `Vec` per output row so that the wire
2721 /// encoder could borrow each value for a few nanoseconds. The
2722 /// streaming interface it then hands them to takes `&[Value]`
2723 /// already.
2724 ///
2725 /// Returns `None` when the shape does not apply, so the caller falls
2726 /// back before anything has been emitted.
2727 pub(crate) fn try_index_only_stream<F>(
2728 &self,
2729 stmt: &SelectStatement,
2730 emit: &mut F,
2731 ) -> Result<Option<usize>, EngineError>
2732 where
2733 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2734 {
2735 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2736 return Ok(None);
2737 };
2738 // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2739 // are not materialised here, and a partition parent's own
2740 // heap/indexes are empty (its rows live in the children).
2741 if !stmt.ctes.is_empty() {
2742 return Ok(None);
2743 }
2744 if let Some(from) = &stmt.from
2745 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2746 {
2747 return Ok(None);
2748 }
2749 let where_ = stmt.where_.as_ref().expect("shape checked it");
2750 let cols = &table.schema().columns;
2751 let schema = alloc::vec![ColumnSchema::new(
2752 out_name,
2753 cols[pos].ty,
2754 cols[pos].nullable
2755 )];
2756 let snapshot = self.current_snapshot();
2757 // The header goes out only once the walk has agreed to run — a
2758 // shape rejection after it would leave the client with a
2759 // RowDescription for a result that never comes.
2760 let mut wrote_header = false;
2761 let counted = crate::index_access::index_only_range_each(
2762 where_,
2763 cols,
2764 table,
2765 alias_name,
2766 &snapshot,
2767 pos,
2768 &mut |v: spg_storage::Value<'_>| {
2769 if !wrote_header {
2770 emit(crate::StreamItem::Header(&schema))?;
2771 wrote_header = true;
2772 }
2773 emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2774 },
2775 );
2776 match counted {
2777 None => Ok(None),
2778 Some(Err(e)) => Err(e),
2779 Some(Ok(n)) => {
2780 if !wrote_header {
2781 emit(crate::StreamItem::Header(&schema))?;
2782 }
2783 Ok(Some(n))
2784 }
2785 }
2786 }
2787
2788 /// `DISTINCT ON`'s de-duplication, which runs after the inner
2789 /// SELECT has produced its rows.
2790 ///
2791 /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2792 /// reason round 848 established: a debug build gives every branch's
2793 /// locals a slot in the frame whichever branch runs, and this one is
2794 /// eighty lines of hashing, key slicing and survivor sorting that a
2795 /// statement without `DISTINCT ON` never touches. Round 867
2796 /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2797 /// reaches none of it — the segment that had been blamed on
2798 /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2799 #[inline(never)]
2800 fn apply_distinct_on(
2801 &self,
2802 result: QueryResult,
2803 don_hidden: usize,
2804 don_limit: &(
2805 Option<spg_sql::ast::LimitExpr>,
2806 Option<spg_sql::ast::LimitExpr>,
2807 ),
2808 don_top1: usize,
2809 orig_order_by: &[spg_sql::ast::OrderBy],
2810 ) -> Result<QueryResult, EngineError> {
2811 let QueryResult::Rows { columns, rows } = result else {
2812 return Ok(result);
2813 };
2814 // The keys are the hidden trailing columns appended above.
2815 // v7.39 (round 729) — top-1 mode: the trailing columns are the
2816 // DON keys plus the ORDER tail; keep each group's best in one
2817 // hash pass, then sort the SURVIVORS with the original spec.
2818 let mut kept: alloc::vec::Vec<Row<'static>>;
2819 let key_start;
2820 if don_top1 > 0 {
2821 let tail = don_top1 - 1;
2822 key_start = columns.len().saturating_sub(don_hidden + tail);
2823 let ord_start = key_start + don_hidden;
2824 let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2825 .iter()
2826 .map(|o| (o.desc, o.nulls_first))
2827 .collect();
2828 let mysql = self.backslash_escapes;
2829 let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2830 for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2831 let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2832 let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2833 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2834 core::cmp::Ordering::Less => return true,
2835 core::cmp::Ordering::Greater => return false,
2836 core::cmp::Ordering::Equal => {}
2837 }
2838 }
2839 false
2840 };
2841 let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2842 let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2843 let mut keybuf = String::new();
2844 for row in rows {
2845 keybuf.clear();
2846 for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2847 aggregate::push_canonical_key(&mut keybuf, v);
2848 }
2849 match slot.get(keybuf.as_str()) {
2850 Some(&i) => {
2851 if better(&row, &best[i]) {
2852 best[i] = row;
2853 }
2854 }
2855 None => {
2856 slot.insert(keybuf.clone(), best.len());
2857 best.push(row);
2858 }
2859 }
2860 }
2861 // Survivors sort with the FULL original spec (keys are still
2862 // aboard as hidden columns).
2863 let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2864 .iter()
2865 .map(|o| (o.desc, o.nulls_first))
2866 .collect();
2867 best.sort_by(|a, b| {
2868 for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2869 let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2870 let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2871 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2872 core::cmp::Ordering::Equal => {}
2873 o => return o,
2874 }
2875 }
2876 core::cmp::Ordering::Equal
2877 });
2878 for r in &mut best {
2879 r.values.truncate(key_start);
2880 }
2881 kept = best;
2882 } else {
2883 key_start = columns.len().saturating_sub(don_hidden);
2884 let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2885 kept = alloc::vec::Vec::new();
2886 for mut row in rows {
2887 let key: alloc::vec::Vec<Value<'static>> =
2888 row.values.get(key_start..).unwrap_or(&[]).to_vec();
2889 if seen.iter().any(|k| k == &key) {
2890 continue;
2891 }
2892 seen.push(key);
2893 row.values.truncate(key_start);
2894 kept.push(row);
2895 }
2896 }
2897 let mut columns = columns;
2898 columns.truncate(key_start);
2899 // PG limits what DISTINCT ON left, not what fed it.
2900 let kept = apply_deferred_limit(kept, don_limit);
2901 Ok(QueryResult::Rows {
2902 columns,
2903 rows: kept,
2904 })
2905 }
2906
2907 pub(crate) fn exec_select_cancel_as(
2908 &self,
2909 stmt: &SelectStatement,
2910 cancel: CancelToken<'_>,
2911 as_role: Option<&str>,
2912 ) -> Result<QueryResult, EngineError> {
2913 // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2914 // <all columns>` is legal PG (the wildcard expands to grouped
2915 // columns); SPG refused the whole shape. Expand the wildcard
2916 // into explicit column refs up front — the aggregate layer's
2917 // existing "must appear in the GROUP BY clause" validation
2918 // then answers PG's sentence for any non-grouped column.
2919 if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2920 return self.exec_select_cancel_as(&expanded, cancel, as_role);
2921 }
2922 // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2923 // a row.
2924 //
2925 // The aggregate layer already short-circuits this to
2926 // `rows.len()`, so the O(1) part was never the problem — the
2927 // cost is UPSTREAM, materialising every visible row so that
2928 // layer can take its length. Measured over pgwire on 500k rows:
2929 // PG18 8.2 ms with two parallel workers, 10.3 ms with
2930 // parallelism off, SPG 16.5 ms — 1.6x slower than a
2931 // single-threaded PG on the commonest aggregate there is, and no
2932 // ledger entry recorded it.
2933 //
2934 // Counting visible HEADERS needs no row at all. PG cannot do
2935 // this: its visibility lives in the heap tuples themselves, so
2936 // it has to read them (that is why its own count(*) is a full
2937 // scan, parallel or not).
2938 // v7.39 (read01 round 57) — the table-privilege gate on the common
2939 // read core. A superuser session returns from it immediately.
2940 // v7.39 (round 529) — resolve an ORDER BY that names an output
2941 // ALIAS. The statement-level pass never reached a SELECT nested in
2942 // a FROM clause, a CTE or a scalar subquery, so the same query
2943 // worked on its own and failed the moment anything wrapped it —
2944 // which is what generated SQL does constantly.
2945 let aliased;
2946 let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
2947 let mut s = stmt.clone();
2948 crate::orderby::resolve_order_by_position(&mut s);
2949 aliased = s;
2950 &aliased
2951 } else {
2952 stmt
2953 };
2954 // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
2955 //
2956 // Its keys were evaluated against the PROJECTED row, so a key that
2957 // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
2958 // ORDER BY g, v DESC`, the canonical "latest row per group" — could
2959 // not be read at all and the query failed. PG evaluates them on the
2960 // input. They are projected as hidden columns here and stripped
2961 // again below, the same way the grouping-set ordering columns
2962 // already travel.
2963 //
2964 // And the dedup ran AFTER the inner statement's LIMIT, so
2965 // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
2966 // PG answers two: the limit had already taken two rows of the same
2967 // group before anything deduplicated them. A paginated DISTINCT ON
2968 // returned short pages, with no error. The limit is deferred to
2969 // after the dedup, which is PG's order.
2970 let don_stmt;
2971 // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
2972 // order spec (the rewritten stmt's is emptied).
2973 let orig_order_by = stmt.order_by.clone();
2974 let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
2975 (stmt, 0, (None, None), 0usize)
2976 } else {
2977 let mut s = stmt.clone();
2978 let hidden = s.distinct_on.len();
2979 for (i, e) in stmt.distinct_on.iter().enumerate() {
2980 s.items.push(SelectItem::Expr {
2981 expr: e.clone(),
2982 alias: Some(alloc::format!("__distinct_on_{i}")),
2983 });
2984 }
2985 // v7.39 (round 729) — group-top-1 short circuit. When the
2986 // DISTINCT ON keys are exactly the ORDER BY's leading keys,
2987 // the answer is "per group, the row that wins the remaining
2988 // order" — a single O(n) hash pass. The old path sorted the
2989 // ENTIRE input first (500k rows, ~180 ms on the panel cell)
2990 // to keep 100. The inner query runs UNSORTED with every
2991 // order key appended as a hidden column; the dedup below
2992 // keeps each group's best, then sorts the SURVIVORS.
2993 // Declared-collation order keys stay on the sorting path
2994 // (the value comparator here is collation-blind).
2995 let prefix_matches = s.order_by.len() >= hidden
2996 && stmt
2997 .distinct_on
2998 .iter()
2999 .zip(s.order_by.iter())
3000 .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3001 let colls_plain =
3002 crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3003 .map(|cs| cs.iter().all(Option::is_none))
3004 .unwrap_or(false);
3005 let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3006 let tail = s.order_by.len() - hidden;
3007 for (j, o) in s.order_by[hidden..].iter().enumerate() {
3008 s.items.push(SelectItem::Expr {
3009 expr: o.expr.clone(),
3010 alias: Some(alloc::format!("__don_ord_{j}")),
3011 });
3012 }
3013 // Carry the tail's direction flags through the aliases'
3014 // ORDER; the survivors re-sort below with the full spec.
3015 s.order_by = Vec::new();
3016 tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3017 } else {
3018 0
3019 };
3020 // Only a folded literal is deferred; a placeholder or an
3021 // expression keeps the path it has today rather than being
3022 // resolved a second way here.
3023 let deferrable = matches!(
3024 (&s.limit, &s.offset),
3025 (
3026 None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3027 None | Some(spg_sql::ast::LimitExpr::Literal(_))
3028 )
3029 );
3030 let deferred = if deferrable {
3031 (s.limit.take(), s.offset.take())
3032 } else {
3033 (None, None)
3034 };
3035 don_stmt = s;
3036 (&don_stmt, hidden, deferred, top1_tail)
3037 };
3038 self.acl_check_select_as(stmt, as_role)?;
3039 validate_aggregate_placement(stmt)?;
3040 // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3041 // privilege gate above. Placed before it at first, and the
3042 // security-definer e2e caught it immediately: a SECURITY INVOKER
3043 // function whose body is `SELECT count(*) FROM t` answered
3044 // instead of being refused, because the fast path never reached
3045 // the check.
3046 if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3047 return Ok(r);
3048 }
3049 // v7.39 (round 560) — an index-only range scan. Same placement
3050 // reasoning as the count above: after the privilege gate.
3051 if let Some(r) = self.try_index_only_scan(stmt)? {
3052 return Ok(r);
3053 }
3054 validate_locking_clause(stmt)?;
3055 let result = self.exec_select_cancel_inner(stmt, cancel)?;
3056 // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3057 // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3058 // They carry the per-branch mask through the UNION-ALL sort and must not
3059 // appear in the output. Stripped per SELECT level (grouping-set queries
3060 // are often wrapped in a derived subquery), before DISTINCT ON.
3061 let result = strip_synthetic_order_cols(result);
3062 // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3063 // rows arrive here already ORDER BY'd; keep the FIRST row of
3064 // each group the expressions define (PG semantics). The
3065 // expressions evaluate against the projected schema — an
3066 // expression that isn't in the select list errors honestly.
3067 if stmt.distinct_on.is_empty() {
3068 return Ok(result);
3069 }
3070 self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3071 }
3072
3073 /// The UNION chain: execute the head as a bare block, then fold each
3074 /// peer in with left-associative dedup.
3075 ///
3076 /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3077 /// reason round 848 established. A statement with no unions returns
3078 /// one line above the call — and every nested subquery on a deep
3079 /// path is such a statement, so each level of the recursion carried
3080 /// 170 lines of locals it could not reach. Round 867 measured that
3081 /// frame at 34,800 bytes, the largest single one on the descent,
3082 /// after two earlier attributions had blamed its caller and then its
3083 /// callee: the gap between two marks is the frame of everything
3084 /// BETWEEN them, and this function had no mark of its own.
3085 #[inline(never)]
3086 fn exec_union_chain(
3087 &self,
3088 stmt_ref: &SelectStatement,
3089 stmt: &SelectStatement,
3090 cancel: CancelToken<'_>,
3091 ) -> Result<QueryResult, EngineError> {
3092 // UNION path: clone-strip the head into a bare block (its own
3093 // DISTINCT and any inner ORDER BY are dropped by parser rule —
3094 // the wrapper SelectStatement carries them), execute, then chain
3095 // peers with left-associative dedup semantics.
3096 // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3097 // output columns; a position past their count is PG's 42P10.
3098 crate::orderby::check_order_by_positions(stmt_ref)?;
3099 let mut head_unknown = branch_unknown_mask(stmt_ref);
3100 let head_regcast = branch_regcast_mask(stmt_ref);
3101 let mut head = stmt_ref.clone();
3102 head.unions = Vec::new();
3103 head.order_by = Vec::new();
3104 head.limit = None;
3105 let QueryResult::Rows {
3106 mut columns,
3107 mut rows,
3108 } = self.exec_bare_select_cancel(&head, cancel)?
3109 else {
3110 unreachable!("bare SELECT cannot return CommandOk")
3111 };
3112 for (kind, peer) in &stmt_ref.unions {
3113 // v7.37.17 (17.6 siblings) — a peer carrying its own
3114 // unions is a nested INTERSECT group (the parser's
3115 // precedence regrouping); recurse through the
3116 // union-aware wrapper for it.
3117 let peer_result = if peer.unions.is_empty() {
3118 self.exec_bare_select_cancel(peer, cancel)?
3119 } else {
3120 self.exec_select_cancel(peer, cancel)?
3121 };
3122 let QueryResult::Rows {
3123 columns: peer_cols,
3124 rows: mut peer_rows,
3125 } = peer_result
3126 else {
3127 unreachable!("bare SELECT cannot return CommandOk")
3128 };
3129 if peer_cols.len() != columns.len() {
3130 // v7.39 (round 232) — PG's wording, which clients match on.
3131 return Err(EngineError::Unsupported(alloc::format!(
3132 "each {} query must have the same number of columns",
3133 set_op_name(*kind)
3134 )));
3135 }
3136 // v7.39 (round 232+233) — PG resolves each result column to one
3137 // type before it merges anything, and refuses the query when the
3138 // two branches have no common type. SPG's unifier
3139 // (`unify_union_columns`) is value-driven and deliberately
3140 // conservative — "a column where any cell fails to coerce is left
3141 // exactly as it was" — so a mismatch produced a column holding
3142 // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3143 // back with integers and text interleaved) instead of an error.
3144 //
3145 // The check has to read the branch ASTs, not just their schemas:
3146 // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3147 // as TEXT and is indistinguishable from a real text column by
3148 // schema alone — yet PG treats the two completely differently
3149 // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3150 // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3151 let peer_unknown = branch_unknown_mask(peer);
3152 let peer_regcast = branch_regcast_mask(peer);
3153 for i in 0..columns.len() {
3154 let hu = head_unknown.get(i).copied().unwrap_or(false);
3155 let pu = peer_unknown.get(i).copied().unwrap_or(false);
3156 let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3157 let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3158 || head_regcast.get(i).copied().unwrap_or(false);
3159 match (hu, pu) {
3160 // Both sides carry a real type: they must share a category.
3161 (false, false) => {
3162 if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3163 return Err(EngineError::Unsupported(alloc::format!(
3164 "{} types {} and {} cannot be matched",
3165 set_op_name(*kind),
3166 crate::conversions::pg_type_name_for_error(ht),
3167 crate::conversions::pg_type_name_for_error(pt),
3168 )));
3169 }
3170 }
3171 // One side is an untyped literal: it takes the other's
3172 // type, and failing to convert is the error PG reports.
3173 (true, false) => {
3174 coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3175 columns[i].ty = pt;
3176 head_unknown[i] = false;
3177 }
3178 (false, true) => {
3179 coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3180 }
3181 // Both untyped — nothing to resolve against yet.
3182 (true, true) => {}
3183 }
3184 }
3185 // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3186 // nullable (PG semantics). Previously the result kept only the head's
3187 // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3188 // non-null `1`) wrongly reported the column NOT NULL, which let
3189 // `count(col)`'s NOT-NULL fast-path count the NULL row.
3190 for (i, pc) in peer_cols.iter().enumerate() {
3191 if pc.nullable {
3192 columns[i].nullable = true;
3193 }
3194 }
3195 // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3196 // text by the session collation (CI + accent + PAD SPACE), like
3197 // GROUP BY. PG stays byte-exact.
3198 let mysql = self.backslash_escapes;
3199 // v7.38.13 — RESIDUAL, recorded rather than faked: a set
3200 // operation over a byte-wise column has the same folding hole
3201 // DISTINCT had, and this site has no output columns in scope
3202 // to build a mask from. Behaviour here is unchanged.
3203 let fold = FoldSpec::dialect(mysql);
3204 match kind {
3205 UnionKind::All => rows.extend(peer_rows),
3206 UnionKind::Distinct => {
3207 rows.extend(peer_rows);
3208 rows = dedup_rows(rows, fold);
3209 }
3210 // v7.37.17 (17.6 siblings) — PG set semantics.
3211 // v7.39 (round 591) — all four ask the same question of the
3212 // right side, and all four used to answer it by scanning it
3213 // once per left row. `PeerIndex` buckets it by the hash
3214 // DISTINCT already uses, so the answer is a lookup.
3215 // INTERSECT: distinct rows present on both sides.
3216 UnionKind::Intersect => {
3217 let idx = PeerIndex::build(&peer_rows, fold);
3218 rows = dedup_rows(rows, fold)
3219 .into_iter()
3220 .filter(|r| idx.contains(r))
3221 .collect();
3222 }
3223 // INTERSECT ALL: multiset intersection — each row
3224 // keeps min(left count, right count) occurrences.
3225 UnionKind::IntersectAll => {
3226 let mut idx = PeerIndex::build(&peer_rows, fold);
3227 let mut kept: Vec<Row<'static>> = Vec::new();
3228 for r in rows {
3229 if idx.take_one(&r) {
3230 kept.push(r);
3231 }
3232 }
3233 rows = kept;
3234 }
3235 // EXCEPT: distinct left rows absent from the right.
3236 UnionKind::Except => {
3237 let idx = PeerIndex::build(&peer_rows, fold);
3238 rows = dedup_rows(rows, fold)
3239 .into_iter()
3240 .filter(|r| !idx.contains(r))
3241 .collect();
3242 }
3243 // EXCEPT ALL: multiset subtraction — each right
3244 // occurrence cancels one left occurrence.
3245 UnionKind::ExceptAll => {
3246 let mut idx = PeerIndex::build(&peer_rows, fold);
3247 let mut kept: Vec<Row<'static>> = Vec::new();
3248 for r in rows {
3249 if !idx.take_one(&r) {
3250 kept.push(r);
3251 }
3252 }
3253 rows = kept;
3254 }
3255 }
3256 }
3257 // PG resolves a UNION / VALUES result column to one common type
3258 // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3259 // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3260 // built each branch independently, leaving mixed-type columns
3261 // that broke ORDER BY, comparisons, and value-based window
3262 // frames. Unify + coerce before the combined ORDER BY sees them.
3263 unify_union_columns(&mut columns, &mut rows);
3264 // ORDER BY at the top of a UNION applies to the combined result.
3265 // Eval against the projected schema (NOT the source table).
3266 if !stmt.order_by.is_empty() {
3267 // v7.39 (read01 round 54) — the combined-result ctx must carry the
3268 // catalog, and the projected columns must keep their enum identity
3269 // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3270 // by TEXT instead of member order — silently wrong rows, not an
3271 // error. (Same shape as the enum-order knife's GROUP BY fix.)
3272 let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3273 // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3274 // survive to here when the head projects a Wildcard (the
3275 // group-tail wrapper shape): map them onto the Nth
3276 // projected column so the combined sort works.
3277 let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3278 .order_by
3279 .iter()
3280 .map(|o| {
3281 let mut o = o.clone();
3282 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3283 && *n >= 1
3284 && let Ok(idx) = usize::try_from(*n - 1)
3285 && idx < columns.len()
3286 {
3287 o.expr = Expr::Column(spg_sql::ast::ColumnName {
3288 qualifier: None,
3289 name: columns[idx].name.clone(),
3290 });
3291 }
3292 o
3293 })
3294 .collect();
3295 let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3296 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3297 for r in rows {
3298 let keys = build_order_keys(&resolved_order, &r, &synth_ctx)?;
3299 tagged.push((keys, r));
3300 }
3301 sort_by_keys(&mut tagged, &descs);
3302 rows = tagged.into_iter().map(|(_, r)| r).collect();
3303 }
3304 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3305 Ok(QueryResult::Rows { columns, rows })
3306 }
3307
3308 fn exec_select_cancel_inner(
3309 &self,
3310 stmt: &SelectStatement,
3311 cancel: CancelToken<'_>,
3312 ) -> Result<QueryResult, EngineError> {
3313 cancel.check()?;
3314 // v7.38 P0 元机制 A — first observable point inside the
3315 // planner / executor. Tests use this to inject a delay or
3316 // a cancellation race before any row is produced. Release
3317 // build expands to `let _ = (...);` — zero cost.
3318 crate::injection_point!("planner_first_row_fetch", &stmt.from);
3319 // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3320 // PG analyses every definition, referenced or not, so `SELECT i FROM
3321 // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3322 // succeeded here (the parser used to drop the unreferenced defs
3323 // whole). The check is the CREATE VIEW check's shape (round 700): a
3324 // LIMIT-0 run of the same FROM with the definitions' key
3325 // expressions as the projection — it cannot disagree with what a
3326 // referencing window would have done, because it resolves the same
3327 // names the same way. Zero cost for the ordinary statement: the
3328 // list is empty unless a WINDOW clause left unreferenced defs.
3329 if !stmt.window_check_exprs.is_empty() {
3330 let mut probe = stmt.clone();
3331 probe.items = stmt
3332 .window_check_exprs
3333 .iter()
3334 .map(|e| spg_sql::ast::SelectItem::Expr {
3335 expr: e.clone(),
3336 alias: None,
3337 })
3338 .collect();
3339 probe.window_check_exprs = Vec::new();
3340 probe.distinct = false;
3341 probe.distinct_on = Vec::new();
3342 probe.group_by = None;
3343 probe.group_by_all = false;
3344 probe.having = None;
3345 probe.unions = Vec::new();
3346 probe.order_by = Vec::new();
3347 probe.locking = None;
3348 probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3349 probe.offset = None;
3350 probe.limit_with_ties = false;
3351 self.exec_select_cancel_inner(&probe, cancel)?;
3352 }
3353 // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3354 // takes the catalog, so the parser leaves a marker and the rewrite lands
3355 // here: the call moves into a LATERAL FROM item and the item becomes one
3356 // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3357 // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3358 // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3359 // second one.
3360 if let Some(lowered) = self.lower_record_expansion(stmt)? {
3361 return self.exec_select_cancel_inner(&lowered, cancel);
3362 }
3363 // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3364 // FROM / JOIN graph references any catalogued view name,
3365 // re-parse the view body and prepend it as a synthetic
3366 // CTE. Recurses on views-in-views via the regular CTE
3367 // dispatch below. Fast-path: skip the walker entirely when
3368 // the catalog has no views (the typical OLTP load).
3369 if !self.active_catalog().views_all().is_empty() {
3370 if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3371 return self.exec_select_cancel(&rewritten, cancel);
3372 }
3373 }
3374 // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3375 // gets rewritten to a UNION-ALL over the children that overlap
3376 // the WHERE-derived key range. Uses the same CTE-injection
3377 // trick as VIEW expansion above so downstream resolution
3378 // doesn't need a partition-aware code path.
3379 if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3380 return self.exec_select_cancel(&rewritten, cancel);
3381 }
3382 // v7.16.2 — information_schema / pg_catalog virtual
3383 // views (mailrs round-10 A.3). If the SELECT touches a
3384 // synthetic meta-table name (`__spg_info_*` /
3385 // `__spg_pg_*` — produced by the parser for
3386 // `information_schema.X` / `pg_catalog.X`), clone the
3387 // catalog, materialise the requested view as a real
3388 // temporary table, and re-execute against an enriched
3389 // engine. Same pattern as `exec_with_ctes` for CTEs.
3390 if !self.meta_views_materialised && select_references_meta_view(stmt) {
3391 return self.exec_select_with_meta_views(stmt, cancel);
3392 }
3393 // v6.10.2 — cold-tier time-travel short-circuit. When the
3394 // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3395 // dedicated cold-segment scan instead of the regular
3396 // hot+index path. The scope is intentionally narrow for
3397 // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3398 // optionally with a single-column-equality WHERE. JOINs /
3399 // aggregates / ORDER BY / subqueries on top of a time-
3400 // travelled scan are STABILITY § "Out of v6.10".
3401 if let Some(from) = &stmt.from
3402 && let Some(seg_id) = from.primary.as_of_segment
3403 {
3404 return self.exec_select_as_of_segment(stmt, from, seg_id);
3405 }
3406 // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3407 // pre-CTE because they don't read from the catalog and
3408 // shouldn't participate in regular FROM resolution.
3409 // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3410 // short-circuits. A meta-view FROM materialises to a fixed row
3411 // set. For a bare `SELECT *` we return it directly; otherwise we
3412 // stage it as a temp table and run the normal pipeline, so
3413 // projection / WHERE / ORDER BY / aggregates work over these views
3414 // (they were `SELECT *`-only before). A real table shadowing the
3415 // name wins (checked first), which also stops the staged re-run
3416 // from recursing back into meta-view detection.
3417 if let Some(from) = &stmt.from
3418 && from.joins.is_empty()
3419 && self.active_catalog().get(&from.primary.name).is_none()
3420 {
3421 let lower = from.primary.name.to_ascii_lowercase();
3422 if let Some(result) = self.meta_view_result(&lower) {
3423 let bare = stmt.where_.is_none()
3424 && stmt.group_by.is_none()
3425 && stmt.having.is_none()
3426 && stmt.unions.is_empty()
3427 && stmt.order_by.is_empty()
3428 && stmt.limit.is_none()
3429 && stmt.offset.is_none()
3430 && !stmt.distinct
3431 && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3432 if bare {
3433 return Ok(result);
3434 }
3435 if let QueryResult::Rows { columns, rows } = result {
3436 let mut catalog = self.active_catalog().clone();
3437 let cols = infer_column_types(&columns, &rows);
3438 let schema = TableSchema::new(from.primary.name.clone(), cols);
3439 catalog.create_table(schema).map_err(EngineError::Storage)?;
3440 let t = catalog
3441 .get_mut(&from.primary.name)
3442 .expect("just-created meta-view table must exist");
3443 for row in rows {
3444 t.insert(row).map_err(EngineError::Storage)?;
3445 }
3446 let mut eng = Engine::restore(catalog);
3447 if let Some(c) = self.clock {
3448 eng = eng.with_clock(c);
3449 }
3450 if let Some(f) = self.salt_fn {
3451 eng = eng.with_salt_fn(f);
3452 }
3453 // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3454 // connection identity so `WHERE pid = pg_backend_pid()`
3455 // matches inside the staged meta-view run.
3456 if let Some(f) = self.backend_pid_fn {
3457 eng.set_backend_pid_fn(f);
3458 }
3459 return eng.exec_select_cancel(stmt, cancel);
3460 }
3461 return Ok(result);
3462 }
3463 }
3464 // v4.11: CTEs materialise into a temporary enriched catalog
3465 // *before* anything else — the body SELECT can then refer
3466 // to CTE names via the regular FROM-clause resolution.
3467 // Uncorrelated only: each CTE body runs once against the
3468 // current catalog, not against later CTEs' results (left-
3469 // to-right materialisation would relax this, but we keep
3470 // it simple for v4.11 MVP).
3471 if !stmt.ctes.is_empty() {
3472 return self.exec_with_ctes(stmt, cancel);
3473 }
3474 // v4.10: subqueries (uncorrelated) are resolved here, before
3475 // the executor sees the row loop. We clone the statement so
3476 // we can mutate without disturbing the caller's AST — most
3477 // queries pass through with no subquery nodes and the clone
3478 // is cheap; with subqueries the materialisation cost
3479 // dominates anyway.
3480 let mut stmt_owned;
3481 let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3482 stmt_owned = stmt.clone();
3483 // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3484 // aggregate-wrapped correlated scalar subquery whose
3485 // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3486 // executor streams one join instead of splicing a per-row
3487 // subplan. Runs before the per-row/batch resolver, which then
3488 // only sees the subqueries the pull-up left behind.
3489 self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3490 // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3491 // the "per-key latest" scalar subquery shape (inbox / feed
3492 // / timeline applications) becomes a CTE + LEFT JOIN
3493 // against a GROUP BY pre-aggregation that reuses the v7.33
3494 // first_ordered argmax executor. Runs AFTER unique-key
3495 // pull-up (so the unique-key fast path still wins for
3496 // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3497 // Phase 1 (this commit) is skeleton only — no-op pass.
3498 self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3499 // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3500 // sublink pull-up to semi/anti-join, before the resolver gets
3501 // a chance to walk per-row.
3502 self.pull_up_exists_sublinks(&mut stmt_owned);
3503 // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3504 // exec_with_ctes so they materialise once before the body
3505 // SELECT runs. exec_with_ctes strips ctes from the body
3506 // clone, then re-enters select.
3507 if !stmt_owned.ctes.is_empty() {
3508 return self.exec_with_ctes(&stmt_owned, cancel);
3509 }
3510 // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3511 // SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3512 // BEFORE `resolve_select_subqueries` materialises the inner
3513 // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3514 // INSUBQ benchmark). Run the inner once, collect the result
3515 // values into a `HashSet<i64>` directly, then probe A.pk per
3516 // value and tally. Returns `Some` when the shape matches.
3517 if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3518 return Ok(out);
3519 }
3520 self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3521 &stmt_owned
3522 } else {
3523 stmt
3524 };
3525 if stmt_ref.unions.is_empty() {
3526 return self.exec_bare_select_cancel(stmt_ref, cancel);
3527 }
3528 self.exec_union_chain(stmt_ref, stmt, cancel)
3529 }
3530
3531 #[allow(clippy::too_many_lines)]
3532 #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3533 /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3534 /// Synthesises a single-column virtual table whose column type
3535 /// is TEXT and whose rows are the array elements. Routes
3536 /// through the regular projection / WHERE / ORDER BY / LIMIT
3537 /// machinery so set-returning UNNEST composes naturally with
3538 /// the rest of the SELECT surface.
3539 fn exec_select_unnest(
3540 &self,
3541 stmt: &SelectStatement,
3542 primary: &TableRef,
3543 cancel: CancelToken<'_>,
3544 ) -> Result<QueryResult, EngineError> {
3545 let expr = primary
3546 .unnest_expr
3547 .as_deref()
3548 .expect("caller guards unnest_expr.is_some()");
3549 // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3550 // N value columns instead of one; the shared builder does
3551 // the work and the tail below (WHERE / agg / projection)
3552 // runs against the wider schema.
3553 let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3554 match unnest_zip_args(expr) {
3555 Some(args) => Some(unnest_zip_rows(args)?),
3556 None => None,
3557 };
3558 // Evaluate the array expression once. Empty schema / empty
3559 // row — uncorrelated UNNEST cannot reference outer columns.
3560 // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3561 // introspection family (enum_range / enum_first / enum_last) resolves
3562 // its labels from the argument's STATIC enum type against the
3563 // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3564 // fell through to the generic arm, got NULL, and expanded to zero rows
3565 // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3566 // carry the catalog) worked.
3567 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3568 let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3569 let dummy_row = Row::new(alloc::vec::Vec::new());
3570 // v7.11.13 — unnest dispatches per array element type so
3571 // INT[] / BIGINT[] surface their PG types in projection.
3572 // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3573 // columns (PG: lexeme | positions | weights); everything else
3574 // keeps the alias / "unnest" defaults below.
3575 let mut composite_names: Option<&[&str]> = None;
3576 let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3577 if let Some(m) = multi {
3578 m
3579 } else {
3580 // v7.39 (round 236) — flatten a multidimensional array into
3581 // its row-major elements (PG) before the 1-D-only match.
3582 let unnest_src = {
3583 let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3584 crate::eval::values::flatten_2d(&v).unwrap_or(v)
3585 };
3586 let mut return_multi: Option<(
3587 alloc::vec::Vec<DataType>,
3588 alloc::vec::Vec<Row<'static>>,
3589 )> = None;
3590 let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3591 {
3592 Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3593 Value::TextArray(items) => {
3594 let rows = items
3595 .into_iter()
3596 .map(|item| {
3597 Row::new(alloc::vec![match item {
3598 Some(s) => Value::text(s),
3599 None => Value::Null,
3600 }])
3601 })
3602 .collect();
3603 (DataType::Text, rows)
3604 }
3605 Value::IntArray(items) => {
3606 let rows = items
3607 .into_iter()
3608 .map(|item| {
3609 Row::new(alloc::vec![match item {
3610 Some(n) => Value::Int(n),
3611 None => Value::Null,
3612 }])
3613 })
3614 .collect();
3615 (DataType::Int, rows)
3616 }
3617 Value::BigIntArray(items) => {
3618 let rows = items
3619 .into_iter()
3620 .map(|item| {
3621 Row::new(alloc::vec![match item {
3622 Some(n) => Value::BigInt(n),
3623 None => Value::Null,
3624 }])
3625 })
3626 .collect();
3627 (DataType::BigInt, rows)
3628 }
3629 Value::Multirange { kind, ranges } => {
3630 let rows = ranges
3631 .iter()
3632 .map(|sp| {
3633 Row::new(alloc::vec![Value::Range {
3634 kind,
3635 lower: sp.lower.clone(),
3636 upper: sp.upper.clone(),
3637 lower_inc: sp.lower_inc,
3638 upper_inc: sp.upper_inc,
3639 empty: false,
3640 }])
3641 })
3642 .collect();
3643 (DataType::Range(kind), rows)
3644 }
3645 // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3646 // one row per lexeme, PG18-measured columns
3647 // lexeme | positions | weights (`a | {1,3} |
3648 // {D,D}`); a position-less lexeme (a stripped
3649 // vector) reads NULL in both array columns.
3650 Value::TsVector(lexemes) => {
3651 composite_names = Some(&["lexeme", "positions", "weights"]);
3652 let rows = lexemes
3653 .iter()
3654 .map(|l| {
3655 let (pos, wts) = if l.positions.is_empty() {
3656 (Value::Null, Value::Null)
3657 } else {
3658 let letter = match l.weight {
3659 3 => "A",
3660 2 => "B",
3661 1 => "C",
3662 _ => "D",
3663 };
3664 (
3665 Value::SmallIntArray(
3666 l.positions
3667 .iter()
3668 .map(|p| {
3669 Some(i16::try_from(*p).unwrap_or(i16::MAX))
3670 })
3671 .collect(),
3672 ),
3673 Value::TextArray(
3674 l.positions
3675 .iter()
3676 .map(|_| Some(letter.into()))
3677 .collect(),
3678 ),
3679 )
3680 };
3681 Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3682 })
3683 .collect();
3684 return_multi = Some((
3685 alloc::vec![
3686 DataType::Text,
3687 DataType::SmallIntArray,
3688 DataType::TextArray
3689 ],
3690 rows,
3691 ));
3692 (DataType::Text, alloc::vec::Vec::new())
3693 }
3694 other => {
3695 // v7.39 (round 622, S05a) — see table_access.rs:
3696 // the same sentence, and it is a type mismatch.
3697 return Err(EngineError::Eval(EvalError::TypeMismatch {
3698 detail: alloc::format!(
3699 "unnest() expects an array argument, got {}",
3700 crate::conversions::pg_type_name_for_error_opt(other.data_type())
3701 ),
3702 }));
3703 }
3704 };
3705 if let Some(m) = return_multi {
3706 m
3707 } else {
3708 (alloc::vec![elem_dtype], rows)
3709 }
3710 };
3711 let alias = primary
3712 .alias
3713 .clone()
3714 .unwrap_or_else(|| "unnest".to_string());
3715 // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3716 // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3717 // entries map positionally over the value columns. Without
3718 // the column list, a single column falls back to the table
3719 // alias (pre-v7.13.2 behaviour); multi-arg columns default
3720 // to PG's `unnest`.
3721 let n_vals = dtypes.len();
3722 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3723 .iter()
3724 .enumerate()
3725 .map(|(i, dt)| {
3726 let name = primary
3727 .unnest_column_aliases
3728 .get(i)
3729 .cloned()
3730 .unwrap_or_else(|| {
3731 if let Some(names) = composite_names {
3732 names
3733 .get(i)
3734 .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3735 } else if n_vals == 1 {
3736 alias.clone()
3737 } else {
3738 "unnest".to_string()
3739 }
3740 });
3741 ColumnSchema::new(name, *dt, true)
3742 })
3743 .collect();
3744 // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3745 // parser desugared a base-type-returning function here (see
3746 // TableRef::scalar_fn_item); the marker rides the column so it survives
3747 // every EvalContext an inner stage rebuilds.
3748 if primary.scalar_fn_item && schema_cols.len() == 1 {
3749 schema_cols[0].scalar_row_source = true;
3750 }
3751 // WITH ORDINALITY — trailing BIGINT counting rows from 1
3752 // in element order. The alias entry after the value
3753 // columns renames it (PG default: `ordinality`).
3754 let rows = if primary.with_ordinality {
3755 let ord_name = primary
3756 .unnest_column_aliases
3757 .get(n_vals)
3758 .cloned()
3759 .unwrap_or_else(|| "ordinality".to_string());
3760 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3761 rows.into_iter()
3762 .enumerate()
3763 .map(|(i, row)| {
3764 let mut vals = row.values.clone();
3765 vals.push(Value::BigInt(i as i64 + 1));
3766 Row::new(vals)
3767 })
3768 .collect()
3769 } else {
3770 rows
3771 };
3772 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3773 // `EvalContext::new` drops it and every catalog-dependent cast
3774 // (regclass / enum / composite / domain) silently degrades.
3775 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3776 // Apply WHERE.
3777 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3778 let mut out = alloc::vec::Vec::with_capacity(rows.len());
3779 for row in rows {
3780 cancel.check()?;
3781 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3782 if matches!(v, Value::Bool(true)) {
3783 out.push(row);
3784 }
3785 }
3786 out
3787 } else {
3788 rows
3789 };
3790 // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3791 // unnest source. Same routing the relational scan path
3792 // already takes — without it `SELECT COUNT(*) FROM
3793 // unnest(ARRAY[…])` either errored at projection time or
3794 // returned the wrong shape.
3795 if aggregate::uses_aggregate(stmt) {
3796 // v7.29 — a per-query memo so correlated scalar
3797 // subqueries batch-evaluate once (group map) instead of
3798 // executing per group.
3799 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3800 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3801 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3802 .map_err(|err| match err {
3803 EngineError::Eval(ev) => ev,
3804 other => eval::EvalError::TypeMismatch {
3805 detail: alloc::format!("{other}"),
3806 },
3807 })
3808 };
3809 // v7.39 (round 656) — hand the rows over as they are rather than
3810 // collecting a second vector of `RowRef` wrappers. Note this is
3811 // a set-returning-function path, NOT the relational scan: the
3812 // measured O(rows) cost lived in `run_single_table_aggregate`,
3813 // and converting these four first was a miss that cost a full
3814 // round — every test stayed green and the number did not move.
3815 let agg = aggregate::run(
3816 stmt,
3817 crate::join::AggRows::Owned(&filtered),
3818 &schema_cols,
3819 Some(&alias),
3820 Some(&agg_correlated),
3821 self.parallel_runner.0.as_deref(),
3822 Some(self.active_catalog()),
3823 Some(self),
3824 )?;
3825 return self.finish_agg_result(agg, stmt, cancel);
3826 }
3827 // Projection.
3828 let projection =
3829 build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
3830 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3831 alloc::vec::Vec::with_capacity(filtered.len());
3832 // v7.19 P5 — Set-Returning-Function in projection
3833 // position (PG `SELECT unnest(arr) FROM t` shape). When a
3834 // SELECT item evaluates to a top-level unnest(arr) call,
3835 // expand it: for each input row, evaluate the array, emit
3836 // one output row per element, broadcasting non-SRF
3837 // projections from the same input row. Multi-SRF + LCM
3838 // padding stays a documented carve-out; mailrs uses
3839 // single-SRF for redirect_uris.
3840 // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3841 // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3842 let srf_idxs = self.srf_target_idxs(&projection);
3843 // v7.39 (round 621) — which input row each output row came from. An
3844 // SRF turns one input row into many, and the ORDER BY below used to
3845 // index the EXPANDED rows by the INPUT row's position: the result was
3846 // silently truncated to the input row count and left unsorted, so
3847 // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3848 // answered three of its six rows, in no order. Without the ORDER BY
3849 // the same query was already right.
3850 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3851 if !srf_idxs.is_empty() {
3852 let (rows, src) =
3853 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3854 projected_rows = rows;
3855 src_of_row = src;
3856 } else {
3857 // v7.24 (round-16 B) — select-list subqueries resolve
3858 // per row (correlated-aware; plain exprs take the fast
3859 // path inside).
3860 let mut proj_memo = memoize::MemoizeCache::default();
3861 for row in &filtered {
3862 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3863 for p in &projection {
3864 vals.push(self.eval_expr_with_correlated(
3865 &p.expr,
3866 row,
3867 &scan_ctx,
3868 cancel,
3869 Some(&mut proj_memo),
3870 )?);
3871 }
3872 projected_rows.push(Row::new(vals));
3873 }
3874 }
3875 // ORDER BY / LIMIT — apply on the projected rows (cheap;
3876 // unnest result sets are small by design).
3877 let columns: alloc::vec::Vec<ColumnSchema> = projection
3878 .iter()
3879 // v7.39 (read01 round 54) — keep the column's enum identity through
3880 // the projection (it lives outside the DataType lattice), or a
3881 // derived table / UNION / windowed result forgets it and any outer
3882 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
3883 .map(|p| {
3884 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
3885 c.user_enum_type = p.user_enum_type.clone();
3886 c.mysql_fsp = p.mysql_fsp;
3887 c
3888 })
3889 .collect();
3890 // Re-evaluate ORDER BY against the source schema (pre-projection
3891 // so col refs by name still resolve through `scan_ctx`).
3892 // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
3893 // column. Evaluated as an expression it is just the constant N: the same
3894 // key for every row, so the sort ran and changed nothing.
3895 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
3896 if !order_by.is_empty() {
3897 // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
3898 // A key that names a select-list item reads it out of the expanded
3899 // row (PG sorts AFTER the expansion); one that names a source
3900 // column the query does not project is evaluated on the input row
3901 // it came from, which is what `srf_order_output_cols` decides.
3902 let out_cols = if srf_idxs.is_empty() {
3903 alloc::vec![None; order_by.len()]
3904 } else {
3905 srf_order_output_cols(&order_by, &projection)
3906 };
3907 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
3908 .iter()
3909 .enumerate()
3910 .map(|(k, out)| -> Result<_, EngineError> {
3911 let src = src_of_row.get(k).copied().unwrap_or(k);
3912 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
3913 .iter()
3914 .zip(out_cols.iter())
3915 .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
3916 .collect();
3917 Ok((k, keys?))
3918 })
3919 .collect::<Result<_, _>>()?;
3920 indexed.sort_by(|a, b| {
3921 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
3922 let o = &order_by[idx];
3923 let cmp = order_by_value_cmp_in(
3924 o.desc,
3925 o.nulls_first,
3926 ka,
3927 kb,
3928 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
3929 );
3930 if cmp != core::cmp::Ordering::Equal {
3931 return cmp;
3932 }
3933 }
3934 core::cmp::Ordering::Equal
3935 });
3936 projected_rows = indexed
3937 .into_iter()
3938 .map(|(i, _)| projected_rows[i].clone())
3939 .collect();
3940 }
3941 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
3942 if stmt.distinct {
3943 projected_rows = dedup_rows(projected_rows, FoldSpec::dialect(scan_ctx.mysql_dialect));
3944 }
3945 // LIMIT / OFFSET — apply at the tail.
3946 if let Some(offset) = stmt.offset_literal() {
3947 let off = (offset as usize).min(projected_rows.len());
3948 projected_rows.drain(..off);
3949 }
3950 if let Some(limit) = stmt.limit_literal() {
3951 projected_rows.truncate(limit as usize);
3952 }
3953 Ok(QueryResult::Rows {
3954 columns,
3955 rows: projected_rows,
3956 })
3957 }
3958
3959 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
3960 /// step])` set-returning source. Mirrors `exec_select_unnest`'s
3961 /// shape: evaluate the arg list once against an empty row,
3962 /// materialise the row stream by stepping start → stop, then
3963 /// route through the standard WHERE / projection / ORDER BY /
3964 /// LIMIT pipeline. Two arg-type combos in v7.17:
3965 /// * integer / integer [/ integer] — SmallInt, Int, BigInt
3966 /// (widened to BigInt internally; step defaults to 1)
3967 /// * timestamp / timestamp / interval — date-range
3968 /// iteration (mailrs's daily-report pattern)
3969 fn exec_select_generate_series(
3970 &self,
3971 stmt: &SelectStatement,
3972 primary: &TableRef,
3973 cancel: CancelToken<'_>,
3974 ) -> Result<QueryResult, EngineError> {
3975 let args = primary
3976 .generate_series_args
3977 .as_ref()
3978 .expect("caller guards generate_series_args.is_some()");
3979 let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
3980 let alias = primary
3981 .alias
3982 .clone()
3983 .unwrap_or_else(|| "generate_series".to_string());
3984 // `AS t(n)` — the first column-alias entry renames the
3985 // series column (PG semantics); bare alias keeps the
3986 // pre-existing behaviour of naming the column after it.
3987 let col_name = primary
3988 .unnest_column_aliases
3989 .first()
3990 .cloned()
3991 .unwrap_or_else(|| alias.clone());
3992 let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
3993 let mut schema_cols = alloc::vec![col_schema.clone()];
3994 // WITH ORDINALITY — trailing BIGINT counting rows from 1;
3995 // the second column-alias entry renames it.
3996 let rows = if primary.with_ordinality {
3997 let ord_name = primary
3998 .unnest_column_aliases
3999 .get(1)
4000 .cloned()
4001 .unwrap_or_else(|| "ordinality".to_string());
4002 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4003 rows.into_iter()
4004 .enumerate()
4005 .map(|(i, row)| {
4006 let mut vals = row.values.clone();
4007 vals.push(Value::BigInt(i as i64 + 1));
4008 Row::new(vals)
4009 })
4010 .collect()
4011 } else {
4012 rows
4013 };
4014 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4015 // `EvalContext::new` drops it and every catalog-dependent cast
4016 // (regclass / enum / composite / domain) silently degrades.
4017 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4018 // WHERE.
4019 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4020 let mut out = alloc::vec::Vec::with_capacity(rows.len());
4021 for row in rows {
4022 cancel.check()?;
4023 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4024 if matches!(v, Value::Bool(true)) {
4025 out.push(row);
4026 }
4027 }
4028 out
4029 } else {
4030 rows
4031 };
4032 // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4033 // returning sources. When the SELECT projection contains
4034 // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4035 // …) we route the filtered row stream through the same
4036 // aggregate executor the relational scan path uses, so
4037 // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4038 // a single 100 row instead of erroring at projection
4039 // time. GROUP BY / HAVING / ORDER BY over the aggregate
4040 // output all ride through `aggregate::run`.
4041 if aggregate::uses_aggregate(stmt) {
4042 // v7.29 — a per-query memo so correlated scalar
4043 // subqueries batch-evaluate once (group map) instead of
4044 // executing per group.
4045 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4046 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4047 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4048 .map_err(|err| match err {
4049 EngineError::Eval(ev) => ev,
4050 other => eval::EvalError::TypeMismatch {
4051 detail: alloc::format!("{other}"),
4052 },
4053 })
4054 };
4055 // v7.39 (round 656) — hand the rows over as they are rather than
4056 // collecting a second vector of `RowRef` wrappers. Note this is
4057 // a set-returning-function path, NOT the relational scan: the
4058 // measured O(rows) cost lived in `run_single_table_aggregate`,
4059 // and converting these four first was a miss that cost a full
4060 // round — every test stayed green and the number did not move.
4061 let agg = aggregate::run(
4062 stmt,
4063 crate::join::AggRows::Owned(&filtered),
4064 &schema_cols,
4065 Some(&alias),
4066 Some(&agg_correlated),
4067 self.parallel_runner.0.as_deref(),
4068 Some(self.active_catalog()),
4069 Some(self),
4070 )?;
4071 return self.finish_agg_result(agg, stmt, cancel);
4072 }
4073 // Projection.
4074 let projection =
4075 build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
4076 // v7.39 (round 621) — and here, for the same reason.
4077 let srf_idxs = self.srf_target_idxs(&projection);
4078 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4079 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4080 alloc::vec::Vec::with_capacity(filtered.len());
4081 let mut proj_memo = memoize::MemoizeCache::default();
4082 if !srf_idxs.is_empty() {
4083 let (rows, src) =
4084 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4085 projected_rows = rows;
4086 src_of_row = src;
4087 } else {
4088 for row in &filtered {
4089 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4090 for p in &projection {
4091 // v7.24 (round-16 B) — correlated-aware.
4092 vals.push(self.eval_expr_with_correlated(
4093 &p.expr,
4094 row,
4095 &scan_ctx,
4096 cancel,
4097 Some(&mut proj_memo),
4098 )?);
4099 }
4100 projected_rows.push(Row::new(vals));
4101 }
4102 }
4103 let columns: alloc::vec::Vec<ColumnSchema> = projection
4104 .iter()
4105 // v7.39 (read01 round 54) — keep the column's enum identity through
4106 // the projection (it lives outside the DataType lattice), or a
4107 // derived table / UNION / windowed result forgets it and any outer
4108 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4109 .map(|p| {
4110 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
4111 c.user_enum_type = p.user_enum_type.clone();
4112 c.mysql_fsp = p.mysql_fsp;
4113 c
4114 })
4115 .collect();
4116 // ORDER BY against the source schema.
4117 // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4118 // more of them than there were inputs), and a positional key means the
4119 // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4120 // and what the other two synthetic-source tails already did.
4121 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4122 if !order_by.is_empty() {
4123 let out_cols = if srf_idxs.is_empty() {
4124 alloc::vec![None; order_by.len()]
4125 } else {
4126 srf_order_output_cols(&order_by, &projection)
4127 };
4128 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4129 .iter()
4130 .enumerate()
4131 .map(|(k, out)| -> Result<_, EngineError> {
4132 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4133 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4134 .iter()
4135 .zip(out_cols.iter())
4136 .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4137 .collect();
4138 Ok((k, keys?))
4139 })
4140 .collect::<Result<_, _>>()?;
4141 indexed.sort_by(|a, b| {
4142 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4143 let o = &stmt.order_by[idx];
4144 let cmp = order_by_value_cmp_in(
4145 o.desc,
4146 o.nulls_first,
4147 ka,
4148 kb,
4149 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4150 );
4151 if cmp != core::cmp::Ordering::Equal {
4152 return cmp;
4153 }
4154 }
4155 core::cmp::Ordering::Equal
4156 });
4157 projected_rows = indexed
4158 .into_iter()
4159 .map(|(i, _)| projected_rows[i].clone())
4160 .collect();
4161 }
4162 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4163 if stmt.distinct {
4164 projected_rows = dedup_rows(projected_rows, FoldSpec::dialect(scan_ctx.mysql_dialect));
4165 }
4166 if let Some(offset) = stmt.offset_literal() {
4167 let off = (offset as usize).min(projected_rows.len());
4168 projected_rows.drain(..off);
4169 }
4170 if let Some(limit) = stmt.limit_literal() {
4171 projected_rows.truncate(limit as usize);
4172 }
4173 Ok(QueryResult::Rows {
4174 columns,
4175 rows: projected_rows,
4176 })
4177 }
4178
4179 /// The FROM shapes that are not an ordinary table scan — joins, the
4180 /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4181 ///
4182 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4183 /// reason round 848 established in the parser: a debug build gives
4184 /// EVERY branch's locals a slot in the frame, whichever branch runs.
4185 /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4186 /// stacks several of them; a plain scan reaches none of these
4187 /// branches. Moving them out took the frame to 52,336.
4188 ///
4189 /// `Ok(None)` means "not one of these shapes, carry on".
4190 #[inline(never)]
4191 fn try_from_shape_paths(
4192 &self,
4193 stmt: &SelectStatement,
4194 from: &spg_sql::ast::FromClause,
4195 cancel: CancelToken<'_>,
4196 ) -> Result<Option<QueryResult>, EngineError> {
4197 if !from.joins.is_empty() {
4198 // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4199 // elimination: when a LEFT JOIN's right side is referenced
4200 // ONLY in the ON equality and the right-side join key is
4201 // UNIQUE/PK, the join preserves outer cardinality exactly
4202 // and contributes no values used downstream. Drop the
4203 // entire join. PG does this on the
4204 // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4205 // — A's row count is what survives, B never has to be
4206 // touched.
4207 if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4208 return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4209 }
4210 // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4211 // the v7.32 joinfold rewrite that turns inner JOINs into a
4212 // single-table scan when the catalogue can prove key-only
4213 // dependency. Tests use this to assert "without joinfold,
4214 // the join still executes correctly" (joinfold is a
4215 // semantically-equivalent rewrite, not a correctness fix).
4216 if !self.env_cfg().disable_joinfold {
4217 if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4218 return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4219 }
4220 }
4221 return self.exec_joined_select(stmt, from, cancel).map(Some);
4222 }
4223 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4224 // single-column table at SELECT entry by evaluating the
4225 // expression once against the empty row (UNNEST is
4226 // uncorrelated in v7.11; correlated / LATERAL unnest is a
4227 // v7.12 carve-out). Build a virtual `Table` in a heap-only
4228 // catalog, then route to the regular scan path.
4229 if from.primary.unnest_expr.is_some() {
4230 return self
4231 .exec_select_unnest(stmt, &from.primary, cancel)
4232 .map(Some);
4233 }
4234 // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4235 // returning function. Same dispatch shape as unnest but
4236 // emits a two-column (key TEXT, value TEXT) row stream.
4237 if from.primary.jsonb_each_text_arg.is_some() {
4238 return self
4239 .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4240 .map(Some);
4241 }
4242 // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4243 // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4244 // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4245 // array form. Each function runs; the results zip in LOCKSTEP with the
4246 // shorter padded to NULL — the SAME rule the target-list SRFs follow
4247 // (round 67), which is why `srf_values` is what evaluates each entry.
4248 if from.primary.rows_from.is_some() {
4249 let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4250 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4251 if let Some(col) = schema_cols.get_mut(i) {
4252 col.name = new_name.clone();
4253 }
4254 }
4255 let alias = from
4256 .primary
4257 .alias
4258 .clone()
4259 .unwrap_or_else(|| from.primary.name.clone());
4260 return self
4261 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4262 .map(Some);
4263 }
4264 // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4265 // COLUMNS (...))`. Materialise the row stream + schema by
4266 // walking the row path, then run the regular pipeline over it.
4267 if let Some(jt) = &from.primary.json_table {
4268 let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4269 let alias = from
4270 .primary
4271 .alias
4272 .clone()
4273 .unwrap_or_else(|| from.primary.name.clone());
4274 return self
4275 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4276 .map(Some);
4277 }
4278 if from.primary.table_fn_call.is_some() {
4279 let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4280 // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4281 // (from 1, in output order) AFTER the function's own columns. The
4282 // alias list names it like any other, which is why it is appended
4283 // BEFORE the renaming pass below.
4284 let rows = if from.primary.with_ordinality {
4285 schema_cols.push(ColumnSchema::new(
4286 "ordinality".to_string(),
4287 DataType::BigInt,
4288 false,
4289 ));
4290 rows.into_iter()
4291 .enumerate()
4292 .map(|(i, r)| {
4293 let mut vals = r.values;
4294 vals.push(Value::BigInt(i as i64 + 1));
4295 Row::new(vals)
4296 })
4297 .collect()
4298 } else {
4299 rows
4300 };
4301 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4302 if let Some(col) = schema_cols.get_mut(i) {
4303 col.name = new_name.clone();
4304 }
4305 }
4306 let alias = from
4307 .primary
4308 .alias
4309 .clone()
4310 .unwrap_or_else(|| from.primary.name.clone());
4311 return self
4312 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4313 .map(Some);
4314 }
4315 // v7.37.17 (17.6 siblings) — plain derived table in primary
4316 // position: `FROM ( SELECT … ) alias` (no joins). The inner
4317 // SELECT materialises once (it is uncorrelated by
4318 // construction), then the outer projection / WHERE /
4319 // aggregate / ORDER BY pipeline runs over the synthetic
4320 // table. Joined derived tables keep riding the LATERAL
4321 // machinery in join.rs.
4322 if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4323 // v7.39 (round 727) — flatten first. A simple derived table
4324 // (bare-column projection over one stored table, nothing that
4325 // changes cardinality or order) used to force the inner
4326 // SELECT through the SERIAL row-at-a-time projection pipeline
4327 // just to materialise a synthetic table the outer query then
4328 // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4329 // measured 18.6 ms against PG's 5 — and bare count over the
4330 // same filter WITHOUT the wrapper is 2 ms here, because it
4331 // rides the fused parallel lane. Rewriting to the unwrapped
4332 // form is PG's subquery pull-up; the whole tree gets the
4333 // fast lanes back.
4334 if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4335 return self.exec_select_cancel(&flat, cancel).map(Some);
4336 }
4337 // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4338 // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4339 // ORDER BY never changes the row count, and OFFSET drops
4340 // exactly k. The materialising path sorted 500k rows to
4341 // count 10k (57 ms); PG runs its parallel sort anyway
4342 // (28 ms). The rewrite skips the sort entirely on both
4343 // counts — a plan PG itself does not have.
4344 if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4345 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4346 }
4347 // v7.39 (round 743) — `count(*) OVER a derived whose only
4348 // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4349 // a constant-length array unnests to exactly k rows per
4350 // input row, NULL elements included. PG expands the set to
4351 // count it (6.6 ms on the panel cell); the identity doesn't.
4352 if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4353 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4354 }
4355 return self
4356 .exec_select_derived(stmt, &from.primary, cancel)
4357 .map(Some);
4358 }
4359 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4360 // [, step])` set-returning source. Dispatch mirrors UNNEST:
4361 // materialise the row stream from a single eval pass, then
4362 // run the regular projection / WHERE / ORDER BY / LIMIT
4363 // pipeline over the synthetic single-column table.
4364 if from.primary.generate_series_args.is_some() {
4365 return self
4366 .exec_select_generate_series(stmt, &from.primary, cancel)
4367 .map(Some);
4368 }
4369 Ok(None)
4370 }
4371
4372 /// Pick an index seek for this WHERE, if any of the four apply:
4373 /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4374 ///
4375 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4376 /// frame reason on `try_from_shape_paths`: in a debug build a
4377 /// closure's locals belong to the enclosing frame, and this one is
4378 /// four seek attempts wide on a function that nests.
4379 #[inline(never)]
4380 fn pick_indexed_rows<'r>(
4381 &'r self,
4382 stmt: &SelectStatement,
4383 table: &'r spg_storage::Table,
4384 schema_cols: &[spg_storage::ColumnSchema],
4385 alias: &str,
4386 ctx: &crate::eval::EvalContext<'_>,
4387 seek_snapshot: &crate::Snapshot,
4388 ) -> Option<Vec<Cow<'r, Row<'static>>>> {
4389 stmt.where_.as_ref().and_then(|w| {
4390 // BTree / col=literal seek first — covers the v7.11.3 multi-
4391 // column AND case and the leading-column equality lookup.
4392 try_index_seek(
4393 w,
4394 schema_cols,
4395 self.active_catalog(),
4396 table,
4397 alias,
4398 seek_snapshot,
4399 )
4400 .or_else(|| {
4401 // v7.12.3 — GIN-accelerated `WHERE col @@
4402 // tsquery` when the column has a `USING gin`
4403 // index. Returns an over-approximate candidate
4404 // set; the WHERE re-eval loop below verifies
4405 // the full `@@` predicate per row.
4406 try_gin_seek(
4407 w,
4408 schema_cols,
4409 self.active_catalog(),
4410 table,
4411 alias,
4412 ctx,
4413 seek_snapshot,
4414 )
4415 })
4416 .or_else(|| {
4417 // v7.15.0 — trigram-GIN-accelerated
4418 // `WHERE col LIKE / ILIKE '<pat>'` when the
4419 // column has a `gin_trgm_ops` GIN index.
4420 // Over-approximate candidate set; the WHERE
4421 // re-eval verifies the LIKE per row.
4422 try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4423 })
4424 .or_else(|| {
4425 // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4426 // accelerated `WHERE col @> <jsonb_literal>`
4427 // when the column has a `USING gin` index. The
4428 // posting-list intersection returns an over-
4429 // approximate candidate set; the WHERE re-eval
4430 // verifies the full `@>` predicate per row.
4431 try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4432 })
4433 })
4434 }
4435
4436 /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4437 /// the two `count(*)` short-circuits. Out-of-line for the frame
4438 /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4439 /// of them, and in a debug build their locals sit in the frame
4440 /// regardless.
4441 #[inline(never)]
4442 fn try_seek_fast_paths(
4443 &self,
4444 stmt: &SelectStatement,
4445 table: &spg_storage::Table,
4446 schema_cols: &[spg_storage::ColumnSchema],
4447 alias: &str,
4448 seek_snapshot: &crate::Snapshot,
4449 cancel: CancelToken<'_>,
4450 ) -> Result<Option<QueryResult>, EngineError> {
4451 if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4452 // NSW kNN dispatches against the hot-tier vector index only
4453 // (vector cells aren't promoted to cold segments), so wrap
4454 // the returned row indices as `Cow::Borrowed` for the
4455 // unified `materialise_in_order` shape.
4456 let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4457 .into_iter()
4458 .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4459 .collect();
4460 return materialise_in_order(
4461 stmt,
4462 schema_cols,
4463 alias,
4464 &ordered,
4465 self.backslash_escapes,
4466 )
4467 .map(Some);
4468 }
4469
4470 // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4471 // the scan via the BTree iterator in the requested direction
4472 // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4473 // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4474 // the load-bearing consumer; this skips the materialise-every-
4475 // row + partial-sort tail entirely. Walker output is already
4476 // in ORDER BY order so `materialise_in_order` (no extra sort)
4477 // is the natural sink.
4478 if let Some(walked) = try_pk_walk_top_n(
4479 stmt,
4480 self.active_catalog(),
4481 table,
4482 schema_cols,
4483 alias,
4484 self,
4485 cancel,
4486 ) {
4487 return materialise_in_order(stmt, schema_cols, alias, &walked, self.backslash_escapes)
4488 .map(Some);
4489 }
4490
4491 // Index seek: if WHERE is `col = literal` (or commuted) and the
4492 // referenced column has an index, dispatch each locator through
4493 // the catalog (hot tier → borrow, cold tier → page-read +
4494 // decode) and iterate just those rows. Otherwise fall back to a
4495 // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4496 // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4497 // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4498 // we don't pay the row materialisation cost twice. Returns
4499 // a bare `Rows{count}` if the shape matches.
4500 if aggregate::uses_aggregate(stmt)
4501 && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4502 {
4503 return Ok(Some(out));
4504 }
4505 // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4506 // locators directly, skipping row materialisation + WHERE re-eval.
4507 if aggregate::uses_aggregate(stmt)
4508 && let Some(out) = self.try_count_star_indexed_range_fast(
4509 stmt,
4510 table,
4511 schema_cols,
4512 alias,
4513 seek_snapshot,
4514 )
4515 {
4516 return Ok(Some(out));
4517 }
4518 Ok(None)
4519 }
4520
4521 /// The two rewrites that must happen before the FROM clause is even
4522 /// looked at: a meta-view reference needs the catalog views
4523 /// materialised, and a windowed projection belongs to the window
4524 /// executor. Out-of-line for the frame reason on
4525 /// `try_from_shape_paths`.
4526 #[inline(never)]
4527 fn try_pre_from_paths(
4528 &self,
4529 stmt: &SelectStatement,
4530 cancel: CancelToken<'_>,
4531 ) -> Result<Option<QueryResult>, EngineError> {
4532 if !self.meta_views_materialised && select_references_meta_view(stmt) {
4533 return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4534 }
4535 // v4.12: window-function path. When the projection contains
4536 // any `name(args) OVER (...)` we route to the dedicated
4537 // executor — partition + sort + per-row window value before
4538 // the regular projection.
4539 if select_has_window(stmt) {
4540 // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4541 // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4542 // needs the aggregation done first, then windows over the grouped
4543 // rows. Rewrite to an aggregate derived subquery + outer window query
4544 // (which the window-over-derived path, D.13, executes). Only fires on
4545 // the currently-erroring agg+window+GROUP BY shape, so it can't
4546 // regress working window-only or aggregate-only queries.
4547 if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4548 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4549 }
4550 return self.exec_select_with_window(stmt, cancel).map(Some);
4551 }
4552 Ok(None)
4553 }
4554
4555 /// A projection naming `ctid` or another system column: the schema
4556 /// has to be widened with them before the scan. Out-of-line for the
4557 /// frame reason on `try_from_shape_paths`.
4558 #[inline(never)]
4559 fn try_ctid_projection(
4560 &self,
4561 stmt: &SelectStatement,
4562 primary: &spg_sql::ast::TableRef,
4563 table: &spg_storage::Table,
4564 schema_cols: &[spg_storage::ColumnSchema],
4565 alias: &str,
4566 cancel: CancelToken<'_>,
4567 ) -> Result<Option<QueryResult>, EngineError> {
4568 if references_ctid(stmt) {
4569 let snapshot = self.current_snapshot();
4570 let mut ext_cols = schema_cols.to_vec();
4571 for name in SYSTEM_COLUMNS {
4572 ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4573 }
4574 let table_oid =
4575 crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4576 .unwrap_or(0);
4577 let headers = table.headers();
4578 let rows: Vec<Row<'static>> = table
4579 .scan_visible(&snapshot)
4580 .map(|(i, r)| {
4581 let mut vals = r.values.clone();
4582 // One block, offsets from 1, as PG numbers them.
4583 vals.push(Value::Tid(0, i as u32 + 1));
4584 let h = headers.get(i);
4585 vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4586 vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4587 // SPG keeps no per-statement command ids; PG shows 0 for
4588 // every row a reader can see, which is every row here.
4589 vals.push(Value::Cid(0));
4590 vals.push(Value::Cid(0));
4591 vals.push(Value::BigInt(table_oid));
4592 Row::new(vals)
4593 })
4594 .collect();
4595 return self
4596 .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4597 .map(Some);
4598 }
4599 Ok(None)
4600 }
4601
4602 /// A sequence read as a one-row relation (`SELECT last_value FROM
4603 /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4604 /// the frame reason on `try_from_shape_paths`.
4605 #[inline(never)]
4606 fn try_sequence_relation(
4607 &self,
4608 stmt: &SelectStatement,
4609 primary: &spg_sql::ast::TableRef,
4610 cancel: CancelToken<'_>,
4611 ) -> Result<Option<QueryResult>, EngineError> {
4612 if self.active_catalog().get(&primary.name).is_none()
4613 && let Some(seq) = self.active_catalog().sequence(&primary.name)
4614 {
4615 let rows = alloc::vec![Row::new(alloc::vec![
4616 Value::BigInt(seq.last_value),
4617 Value::BigInt(0),
4618 Value::Bool(seq.is_called),
4619 ])];
4620 let schema_cols = alloc::vec![
4621 ColumnSchema::new("last_value", DataType::BigInt, false),
4622 ColumnSchema::new("log_cnt", DataType::BigInt, false),
4623 ColumnSchema::new("is_called", DataType::Bool, false),
4624 ];
4625 let alias = primary
4626 .alias
4627 .clone()
4628 .unwrap_or_else(|| primary.name.clone());
4629 return self
4630 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4631 .map(Some);
4632 }
4633 Ok(None)
4634 }
4635
4636 pub(crate) fn exec_bare_select_cancel(
4637 &self,
4638 stmt: &SelectStatement,
4639 cancel: CancelToken<'_>,
4640 ) -> Result<QueryResult, EngineError> {
4641 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4642 // is meaningless without an ORDER BY; PG raises a hard
4643 // error and SPG mirrors the surface so the same DDL/app
4644 // path behaves identically on cutover.
4645 check_with_ties_requires_order_by(stmt)?;
4646 // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4647 // PG rejects window calls there outright. Checked here rather than
4648 // on the window path: `HAVING row_number() OVER () = 1` has no
4649 // window in its projection at all.
4650 crate::window::reject_window_in_row_clauses(stmt)?;
4651 // v7.39 (round 232) — the ORDER BY legality rules (positional
4652 // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4653 // check: before anything scans.
4654 crate::orderby::check_order_by_legality(stmt)?;
4655 // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4656 // equivalent statement the regular executor handles (merged join
4657 // columns collapse to a single unqualified output column; NATURAL
4658 // gets its common-column ON synthesised). The rewrite clears the
4659 // flags, so this re-entrant call is a no-op on the second pass.
4660 if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4661 return self.exec_bare_select_cancel(&rewritten, cancel);
4662 }
4663 // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4664 // exactly the group keys, IS a DISTINCT and was paying for the
4665 // aggregate executor to find that out. Same placement and shape
4666 // as the desugar above; the rewrite clears `group_by`, so the
4667 // re-entry is a no-op on the second pass. See `baregroup` for
4668 // what the gate rules out.
4669 if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4670 return self.exec_bare_select_cancel(&rewritten, cancel);
4671 }
4672 // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4673 // operand in a security-barrier subquery, then re-enter (the wrapped
4674 // operands are no longer bare RLS tables, so this is a no-op on the
4675 // second pass).
4676 if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4677 return self.exec_bare_select_cancel(&rewritten, cancel);
4678 }
4679 // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4680 // AND the RLS USING predicate into a single-table SELECT's WHERE.
4681 // Superuser sessions and non-RLS tables get `None` (no clone, no
4682 // change). Applied inline (shadowing `stmt`) rather than via re-entry
4683 // so it can't re-inject on a recursive pass.
4684 let rls_stmt;
4685 let stmt = match self.rls_select_predicate(stmt)? {
4686 Some(pred) => {
4687 let mut s = stmt.clone();
4688 s.where_ = Some(match s.where_.take() {
4689 Some(existing) => spg_sql::ast::Expr::Binary {
4690 lhs: alloc::boxed::Box::new(existing),
4691 op: spg_sql::ast::BinOp::And,
4692 rhs: alloc::boxed::Box::new(pred),
4693 },
4694 None => pred,
4695 });
4696 rls_stmt = s;
4697 &rls_stmt
4698 }
4699 None => stmt,
4700 };
4701 // v7.16.2 — same meta-view dispatch as
4702 // `exec_select_cancel`, applied here too because
4703 // `subquery_replacement` enters this function directly
4704 // for Exists / ScalarSubquery / InSubquery resolution
4705 // (bypassing the top-level entry to avoid double
4706 // subquery walking). Without this dispatch the subquery
4707 // hits `__spg_info_columns` and reports TableNotFound.
4708 if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4709 return Ok(done);
4710 }
4711 // Constant SELECT (no FROM) — evaluate each item once against an
4712 // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4713 // `SELECT '7'::INT`. Column references will surface as
4714 // ColumnNotFound on eval since the schema is empty.
4715 let Some(from) = &stmt.from else {
4716 return self.exec_constant_select(stmt);
4717 };
4718 // Multi-table FROM (one or more joined peers) goes through the
4719 // nested-loop join executor. Single-table FROM stays on the
4720 // existing scan + index-seek path.
4721 if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4722 return Ok(done);
4723 }
4724 // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4725 // tested — eight ORDER BY shapes byte-identical spilled against
4726 // in-memory, with 103 runs opened to prove the spill ran — and it
4727 // loses on wall clock, which is a hard stop whatever the memory
4728 // buys. Measured round 865, same psql client both sides, same
4729 // machine, row counts verified, and both sides confirmed to be
4730 // doing an external merge rather than an indexed walk:
4731 //
4732 // PG18 178.7 - 187.0 ms Sort Method: external merge, 85 MB
4733 // SPG spilled 269.7 - 299.6 ms 33 spill files at peak
4734 //
4735 // Non-overlapping, about 1.55x. Re-enable by restoring the call
4736 // below once that closes; nothing else has to change, which is
4737 // the point of it being a separate path.
4738 //
4739 // if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4740 // return Ok(done);
4741 // }
4742 //
4743 // v7.37 (round 882) — this walk stays unhooked, but its streaming
4744 // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4745 // bail in `try_exec_joined_streaming`. Collecting the answer was
4746 // most of what this one cost: handing rows over as the merge
4747 // produces them holds peak to the budget plus one row, and the
4748 // wall clock lands inside PG18's range rather than 1.55x outside
4749 // it. Numbers in `extsort.rs`'s header.
4750 let primary = &from.primary;
4751 // v7.39 (round 244) — a sequence is selectable as a one-row relation
4752 // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4753 // read it). Synthesize PG's three columns.
4754 if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4755 return Ok(done);
4756 }
4757 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4758 StorageError::TableNotFound {
4759 name: primary.name.clone(),
4760 }
4761 })?;
4762 let schema_cols = &table.schema().columns;
4763 // The qualifier accepted on column refs is the alias (if any) else the
4764 // bare table name.
4765 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4766 // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4767 // system columns at all: `SELECT ctid FROM t` answered "column
4768 // \"ctid\" does not exist", which takes out the dedup idiom every
4769 // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4770 // GROUP BY key)`.
4771 //
4772 // The value comes from the row's position, which the scan already
4773 // yields; the column is appended to the schema and the rows only
4774 // when the statement asks for it, so nothing else pays for it. That
4775 // also routes the query down the general path, past the index fast
4776 // paths below — they hand back rows without positions, and a ctid
4777 // that was sometimes right would be worse than none.
4778 if let Some(done) =
4779 self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4780 {
4781 return Ok(done);
4782 }
4783 let ctx = self.ev_ctx(schema_cols, Some(alias));
4784
4785 // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4786 // WHERE and an NSW index on `col` skips the full scan. The
4787 // walk returns rows already in ascending-distance order, so
4788 // ORDER BY / LIMIT are honoured implicitly.
4789 // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4790 // and thread it into every index-seek fast path below. No-op
4791 // today (every hot header is committed-alive).
4792 let seek_snapshot = self.current_snapshot();
4793 if let Some(done) =
4794 self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4795 {
4796 return Ok(done);
4797 }
4798 // full scan over the hot tier (cold-tier rows are only reached
4799 // via index seek in v5.1 — full table scans against cold-tier
4800 // data ship in v5.2 with the freezer's per-segment scan API).
4801 let indexed_rows =
4802 self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4803
4804 // Aggregate path: filter rows first, then hand off to the
4805 // aggregate executor which does its own projection + ORDER BY.
4806 if aggregate::uses_aggregate(stmt) {
4807 return self.run_single_table_aggregate(
4808 stmt,
4809 table,
4810 schema_cols,
4811 alias,
4812 indexed_rows,
4813 cancel,
4814 );
4815 }
4816 self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4817 }
4818
4819 /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4820 /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4821 /// uncorrelated FROM-primary case is the simpler shape, used by
4822 /// e2e pins. Materialises the (key, value) pair stream into a
4823 /// synthetic two-column TEXT table, then routes through the
4824 /// regular projection / WHERE / ORDER BY pipeline.
4825 /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4826 /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4827 /// item into (rows, schema). `outer_doc` is `Some` only when this
4828 /// is a NESTED level being expanded against a parent row item's
4829 /// already-parsed sub-document; the top-level call parses the doc
4830 /// expr itself. Row/column paths reuse the existing jsonpath
4831 /// evaluator (`json::json_table_path`); coercion reuses
4832 /// `coerce_value` on the JSON scalar text, so a json string
4833 /// coerces to DATE by its content, matching PG.
4834 #[allow(clippy::type_complexity)]
4835 pub(crate) fn json_table_rows(
4836 &self,
4837 jt: &spg_sql::ast::JsonTable,
4838 outer_doc: Option<&crate::json::JsonValue>,
4839 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4840 // Column schema is static (independent of data): flatten the
4841 // COLUMNS tree in declaration order (NESTED contributes its
4842 // children inline, the PG output shape).
4843 let schema = json_table_schema(&jt.columns);
4844
4845 // PASSING variables → a single JsonValue object the jsonpath
4846 // engine reads `$name` from.
4847 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4848 let ctx = EvalContext::new(&empty_schema, None);
4849 let dummy = Row::new(alloc::vec::Vec::new());
4850 let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4851 None
4852 } else {
4853 let mut entries = alloc::vec::Vec::new();
4854 for (name, e) in &jt.passing {
4855 let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
4856 entries.push((name.clone(), value_to_json_value(&v)));
4857 }
4858 Some(crate::json::JsonValue::Object(entries))
4859 };
4860
4861 // The document root: a NESTED level gets it from the parent;
4862 // the top level parses its doc expr.
4863 let root_owned;
4864 let root: &crate::json::JsonValue = match outer_doc {
4865 Some(d) => d,
4866 None => {
4867 let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
4868 let src = match &doc_val {
4869 Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
4870 Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
4871 other => {
4872 return Err(EngineError::Unsupported(alloc::format!(
4873 "JSON_TABLE document must be json/text, got {}",
4874 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4875 )));
4876 }
4877 };
4878 root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
4879 &root_owned
4880 }
4881 };
4882
4883 let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
4884 .map_err(EngineError::Eval)?;
4885 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
4886 for (idx, item) in items.iter().enumerate() {
4887 self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
4888 }
4889 Ok((rows, schema))
4890 }
4891
4892 /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
4893 /// Regular columns produce one value each; a NESTED column expands
4894 /// as an outer join (each nested match → one row sharing the
4895 /// parent cells; no nested match → one row with the nested cells
4896 /// NULL). Sibling NESTED at one level cross by concatenation of
4897 /// their independent expansions (PG's UNION-of-outer shape).
4898 fn json_table_emit_item(
4899 &self,
4900 jt: &spg_sql::ast::JsonTable,
4901 item: &crate::json::JsonValue,
4902 ordinality: usize,
4903 vars: Option<&crate::json::JsonValue>,
4904 out: &mut alloc::vec::Vec<Row<'static>>,
4905 ) -> Result<(), EngineError> {
4906 use spg_sql::ast::JsonTableColumn as C;
4907 // Parent cells (regular + ordinality), left-to-right; NESTED
4908 // columns contribute a run of child cells appended after.
4909 let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
4910 let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
4911 alloc::vec::Vec::new();
4912 let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4913 for col in &jt.columns {
4914 match col {
4915 C::Ordinality { .. } => {
4916 parent_cells.push(Value::BigInt(ordinality as i64 + 1));
4917 }
4918 C::Regular { .. } => {
4919 parent_cells.push(self.json_table_column_value(col, item, vars)?);
4920 }
4921 C::Nested { path, columns } => {
4922 // Recurse: a nested JSON_TABLE over `item` filtered
4923 // by `path`, with the same PASSING vars.
4924 let sub = spg_sql::ast::JsonTable {
4925 doc: jt.doc.clone(), // unused (outer_doc provided)
4926 row_path: path.clone(),
4927 columns: columns.clone(),
4928 passing: alloc::vec::Vec::new(),
4929 };
4930 let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
4931 nested_widths.push(nschema.len());
4932 nested_runs.push(nrows);
4933 }
4934 }
4935 }
4936 if nested_runs.is_empty() {
4937 out.push(Row::new(parent_cells));
4938 return Ok(());
4939 }
4940 // PG sibling-NESTED semantics: each sibling expands
4941 // INDEPENDENTLY and the results CONCATENATE — a row from
4942 // sibling s fills only s's cells, every other sibling's cells
4943 // NULL. An empty sibling contributes ZERO rows (not a NULL
4944 // row). Only when EVERY sibling is empty does the parent still
4945 // emit one all-NULL row (the outer-join guarantee that a parent
4946 // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
4947 // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
4948 let before = out.len();
4949 for (s_idx, run) in nested_runs.iter().enumerate() {
4950 for nrow in run {
4951 let mut cells = parent_cells.clone();
4952 for (o_idx, w) in nested_widths.iter().enumerate() {
4953 if o_idx == s_idx {
4954 cells.extend(nrow.values.iter().cloned());
4955 } else {
4956 for _ in 0..*w {
4957 cells.push(Value::Null);
4958 }
4959 }
4960 }
4961 out.push(Row::new(cells));
4962 }
4963 }
4964 if out.len() == before {
4965 // Every sibling empty → one all-NULL nested row.
4966 let mut cells = parent_cells.clone();
4967 for w in &nested_widths {
4968 for _ in 0..*w {
4969 cells.push(Value::Null);
4970 }
4971 }
4972 out.push(Row::new(cells));
4973 }
4974 Ok(())
4975 }
4976
4977 /// v7.39 (round 205) — evaluate one Regular column against a row
4978 /// item: EXISTS → bool; else path → at most one value, coerced to
4979 /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
4980 fn json_table_column_value(
4981 &self,
4982 col: &spg_sql::ast::JsonTableColumn,
4983 item: &crate::json::JsonValue,
4984 vars: Option<&crate::json::JsonValue>,
4985 ) -> Result<Value<'static>, EngineError> {
4986 use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
4987 let C::Regular {
4988 name,
4989 ty,
4990 path,
4991 exists,
4992 format_json,
4993 wrapper,
4994 on_empty,
4995 on_error,
4996 } = col
4997 else {
4998 unreachable!("caller guards Regular");
4999 };
5000 let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5001 if *exists {
5002 return Ok(Value::Bool(!matches.is_empty()));
5003 }
5004 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5005 let ctx = EvalContext::new(&empty_schema, None);
5006 let dummy = Row::new(alloc::vec::Vec::new());
5007 let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5008 match b {
5009 B::Null => Ok(Some(Value::Null)),
5010 B::Error => Ok(None),
5011 B::Default(e) => Ok(Some(
5012 eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5013 )),
5014 }
5015 };
5016 // Empty match set → ON EMPTY.
5017 if matches.is_empty() {
5018 return match default_of(on_empty)? {
5019 Some(v) => coerce_json_table_default(v, *ty, name),
5020 None => Err(EngineError::Unsupported(alloc::format!(
5021 "no SQL/JSON item found for JSON_TABLE column {name:?}"
5022 ))),
5023 };
5024 }
5025 let first = &matches[0];
5026 // FORMAT JSON: return the PG-canonical json representation.
5027 // WITH WRAPPER wraps the whole match SET in an array (even a
5028 // single scalar → `[5]`); without it, the single match's json.
5029 if *format_json {
5030 let text = if *wrapper {
5031 crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5032 } else {
5033 first.canonical_json_text()
5034 };
5035 return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5036 }
5037 if first.is_json_null() {
5038 return Ok(Value::Null);
5039 }
5040 // Coerce the scalar text to the declared type; on failure → ON
5041 // ERROR (default NULL, DEFAULT expr, or raise).
5042 let dt = crate::conversions::column_type_to_data_type(*ty);
5043 let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5044 match crate::conversions::coerce_value(scalar, dt, name, 0) {
5045 Ok(v) => Ok(v),
5046 Err(e) => match default_of(on_error)? {
5047 Some(v) => coerce_json_table_default(v, *ty, name),
5048 None => Err(e),
5049 },
5050 }
5051 }
5052
5053 /// table function into (rows, default schema). Dispatch by name.
5054 pub(crate) fn table_fn_rows(
5055 &self,
5056 primary: &TableRef,
5057 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5058 let (fn_name, args) = primary
5059 .table_fn_call
5060 .as_deref()
5061 .expect("caller guards table_fn_call.is_some()");
5062 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5063 let ctx = EvalContext::new(&empty_schema, None);
5064 let dummy_row = Row::new(alloc::vec::Vec::new());
5065 let arg0: Option<Value<'static>> = match args.first() {
5066 Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5067 None => None,
5068 };
5069 match fn_name.as_str() {
5070 // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5071 // `…_recordset` (+ json_ variants). The row shape is the BASE
5072 // argument's declared type — a table's or a composite type's
5073 // column list — which only the catalog knows, so the parser hands
5074 // the raw arguments here rather than desugaring blind.
5075 "jsonb_populate_record"
5076 | "json_populate_record"
5077 | "jsonb_populate_recordset"
5078 | "json_populate_recordset" => {
5079 let type_name = match args.first() {
5080 Some(Expr::Cast {
5081 target: spg_sql::ast::CastTarget::Named(n),
5082 ..
5083 }) => n.clone(),
5084 _ => {
5085 return Err(EngineError::Unsupported(alloc::format!(
5086 "{fn_name}(): first argument must name a row type, \
5087 e.g. NULL::mytable"
5088 )));
5089 }
5090 };
5091 let cat = self.active_catalog();
5092 let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5093 t.schema().columns.clone()
5094 } else if let Some(c) = cat.composite_types().get(&type_name) {
5095 c.fields
5096 .iter()
5097 .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5098 .collect()
5099 } else {
5100 return Err(EngineError::Unsupported(alloc::format!(
5101 "type \"{type_name}\" does not exist"
5102 )));
5103 };
5104 let json_arg = match args.get(1) {
5105 Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5106 None => Value::Null,
5107 };
5108 // The set form iterates the JSON array; the scalar form is
5109 // the one-element case of the same walk.
5110 let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5111 crate::json::array_element_rows(&json_arg, false, fn_name)
5112 .map_err(EngineError::Eval)?
5113 .into_iter()
5114 .map(|s| s.map_or(Value::Null, Value::json))
5115 .collect()
5116 } else if matches!(json_arg, Value::Null) {
5117 alloc::vec::Vec::new()
5118 } else {
5119 alloc::vec![json_arg]
5120 };
5121 let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5122 for doc in &docs {
5123 let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5124 for c in &cols {
5125 // `->>` semantics: a missing key is NULL, present keys
5126 // arrive as text and cast to the declared column type.
5127 let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5128 .map_err(EngineError::Eval)?;
5129 let v = if matches!(raw, Value::Null) {
5130 Value::Null
5131 } else {
5132 crate::conversions::coerce_value(raw, c.ty, "", 0)
5133 .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5134 };
5135 vals.push(v);
5136 }
5137 rows.push(Row::new(vals));
5138 }
5139 Ok((rows, cols))
5140 }
5141 // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5142 // a text[] of 'name=value' reloptions/fdw options → one
5143 // (option_name, option_value) row per element. NULL or an
5144 // empty array yields zero rows (PG); an element without
5145 // '=' carries a NULL option_value, matching PG's split.
5146 "pg_options_to_table" => {
5147 let schema = alloc::vec![
5148 ColumnSchema::new("option_name", DataType::Text, true),
5149 ColumnSchema::new("option_value", DataType::Text, true),
5150 ];
5151 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5152 if let Some(Value::TextArray(items)) = arg0 {
5153 for item in items.into_iter().flatten() {
5154 let (name, value) = match item.split_once('=') {
5155 Some((n, v)) => (Value::text(n), Value::text(v)),
5156 None => (Value::text(item.as_str()), Value::Null),
5157 };
5158 rows.push(Row::new(alloc::vec![name, value]));
5159 }
5160 }
5161 Ok((rows, schema))
5162 }
5163 // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5164 // PG18's per-sequence state SRF, (last_value, is_called).
5165 // pg_dump reads it joined to pg_sequence for every dumped
5166 // sequence's setval line. The oid resolves through the
5167 // same relation_oid mapping seqrelid publishes.
5168 "pg_get_sequence_data" => {
5169 let schema = alloc::vec![
5170 ColumnSchema::new("last_value", DataType::BigInt, false),
5171 ColumnSchema::new("is_called", DataType::Bool, false),
5172 ];
5173 let want = match arg0 {
5174 Some(Value::Int(n)) => i64::from(n),
5175 Some(Value::BigInt(n)) => n,
5176 _ => {
5177 return Err(EngineError::Unsupported(
5178 "pg_get_sequence_data(): argument must be a sequence oid".into(),
5179 ));
5180 }
5181 };
5182 let cat = self.active_catalog();
5183 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5184 for (name, def) in cat.sequences_all() {
5185 if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5186 rows.push(Row::new(alloc::vec![
5187 Value::BigInt(def.last_value),
5188 Value::Bool(def.is_called),
5189 ]));
5190 break;
5191 }
5192 }
5193 Ok((rows, schema))
5194 }
5195 "pg_partition_tree" => {
5196 let cols = alloc::vec![
5197 ColumnSchema::new("relid".to_string(), DataType::Text, true),
5198 ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5199 ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5200 ColumnSchema::new("level".to_string(), DataType::Int, true),
5201 ];
5202 let Some(Value::Text(name)) = &arg0 else {
5203 // NULL (or missing) argument → zero rows (PG).
5204 return Ok((alloc::vec::Vec::new(), cols));
5205 };
5206 let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5207 if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5208 return Err(EngineError::Unsupported(alloc::format!(
5209 "relation \"{name}\" does not exist"
5210 )));
5211 }
5212 let rows = entries
5213 .into_iter()
5214 .map(|(relid, parent, isleaf, level)| {
5215 Row::new(alloc::vec![
5216 Value::text(relid),
5217 parent.map_or(Value::Null, Value::text),
5218 Value::Bool(isleaf),
5219 #[allow(clippy::cast_possible_truncation)]
5220 Value::Int(level as i32),
5221 ])
5222 })
5223 .collect();
5224 Ok((rows, cols))
5225 }
5226 "pg_partition_ancestors" => {
5227 let cols =
5228 alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5229 let Some(Value::Text(name)) = &arg0 else {
5230 return Ok((alloc::vec::Vec::new(), cols));
5231 };
5232 let cat = self.active_catalog();
5233 if cat.get(name.as_ref()).is_none() {
5234 return Err(EngineError::Unsupported(alloc::format!(
5235 "relation \"{name}\" does not exist"
5236 )));
5237 }
5238 // A relation outside any partition tree yields no rows (PG).
5239 let in_tree = cat
5240 .get(name.as_ref())
5241 .is_some_and(|t| t.schema().partition_role.is_some());
5242 let rows = if in_tree {
5243 crate::partition_walks::ancestors_of(cat, name.as_ref())
5244 .into_iter()
5245 .map(|n| Row::new(alloc::vec![Value::text(n)]))
5246 .collect()
5247 } else {
5248 alloc::vec::Vec::new()
5249 };
5250 Ok((rows, cols))
5251 }
5252 // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5253 // saw, what each token was called, which dictionary took it
5254 // and what came out. It is a projection of the same tokenizer
5255 // and the same map the indexer uses, so it cannot describe a
5256 // pipeline other than the one that runs.
5257 "ts_debug" => {
5258 use crate::fts::{TokenType, TsDict};
5259 let cols = alloc::vec![
5260 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5261 ColumnSchema::new("description".to_string(), DataType::Text, false),
5262 ColumnSchema::new("token".to_string(), DataType::Text, false),
5263 ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5264 ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5265 ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5266 ];
5267 // PG's one-arg form uses the session configuration; the
5268 // two-arg form names one.
5269 let (cfg_name, text) = match (&arg0, args.get(1)) {
5270 (Some(Value::Text(c)), Some(t)) => {
5271 let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5272 (c.to_string(), crate::eval::value_to_text(&v))
5273 }
5274 (Some(v), None) => (
5275 alloc::string::String::from("english"),
5276 crate::eval::value_to_text(v),
5277 ),
5278 _ => return Ok((alloc::vec::Vec::new(), cols)),
5279 };
5280 let english = match cfg_name
5281 .trim()
5282 .trim_start_matches("pg_catalog.")
5283 .to_ascii_lowercase()
5284 .as_str()
5285 {
5286 "english" => true,
5287 "simple" => false,
5288 other => {
5289 return Err(EngineError::Unsupported(alloc::format!(
5290 "text search configuration \"{other}\" does not exist"
5291 )));
5292 }
5293 };
5294 let rows = crate::fts::tokenize_typed(&text)
5295 .into_iter()
5296 .map(|tok| {
5297 let dict = tok.ty.dictionary(english);
5298 let dname = dict.map(|d| match d {
5299 TsDict::Simple => "simple",
5300 TsDict::EnglishStem => "english_stem",
5301 });
5302 let folded = tok.text.to_lowercase();
5303 let lexemes = dict.map(|d| match d {
5304 TsDict::Simple => alloc::vec![Some(folded.clone())],
5305 TsDict::EnglishStem => {
5306 if crate::fts::is_english_stopword(&folded) {
5307 alloc::vec::Vec::new()
5308 } else {
5309 alloc::vec![Some(crate::fts::porter_stem(&folded))]
5310 }
5311 }
5312 });
5313 Row::new(alloc::vec![
5314 Value::text(tok.ty.alias()),
5315 Value::text(tok.ty.description()),
5316 Value::text(tok.text),
5317 Value::TextArray(
5318 dname
5319 .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5320 .unwrap_or_default(),
5321 ),
5322 dname.map_or(Value::Null, Value::text),
5323 lexemes.map_or(Value::Null, Value::TextArray),
5324 ])
5325 })
5326 .collect();
5327 let _ = TokenType::AsciiWord;
5328 Ok((rows, cols))
5329 }
5330 // v7.39 (round 651) — `ts_token_type('default')`, the list the
5331 // parser actually produces. It is a projection of the
5332 // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5333 // read, so the three cannot disagree about what a token is.
5334 "ts_token_type" => {
5335 use crate::fts::TokenType as T;
5336 let cols = alloc::vec![
5337 ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5338 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5339 ColumnSchema::new("description".to_string(), DataType::Text, false),
5340 ];
5341 // PG takes the parser by name or oid; SPG has the one.
5342 if let Some(Value::Text(p)) = &arg0
5343 && !p.eq_ignore_ascii_case("default")
5344 && !p.eq_ignore_ascii_case("pg_catalog.default")
5345 {
5346 return Err(EngineError::Unsupported(alloc::format!(
5347 "text search parser \"{p}\" does not exist"
5348 )));
5349 }
5350 const TYPES: &[T] = &[
5351 T::AsciiWord,
5352 T::Word,
5353 T::NumWord,
5354 T::Email,
5355 T::Url,
5356 T::Host,
5357 T::SFloat,
5358 T::Version,
5359 T::HwordNumPart,
5360 T::HwordPart,
5361 T::HwordAsciiPart,
5362 T::Blank,
5363 T::Tag,
5364 T::Protocol,
5365 T::NumHword,
5366 T::AsciiHword,
5367 T::Hword,
5368 T::UrlPath,
5369 T::File,
5370 T::Float,
5371 T::Int,
5372 T::Uint,
5373 T::Entity,
5374 ];
5375 let rows = TYPES
5376 .iter()
5377 .map(|t| {
5378 Row::new(alloc::vec![
5379 Value::Int(*t as i32),
5380 Value::text(t.alias()),
5381 Value::text(t.description()),
5382 ])
5383 })
5384 .collect();
5385 Ok((rows, cols))
5386 }
5387 // v7.39 (read01 round 65) — a set-returning USER function in FROM
5388 // (`FROM rows_of(2)`). Its body runs through the real executor, like
5389 // every other function body since round 63.
5390 other => {
5391 if !self.active_catalog().functions_named(other).is_empty() {
5392 return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5393 }
5394 Err(EngineError::Unsupported(alloc::format!(
5395 "table function {other}() is not supported in FROM"
5396 )))
5397 }
5398 }
5399 }
5400
5401 /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5402 /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5403 /// are bound into it as literals and it goes through the read path, so the
5404 /// rows it yields are exactly the rows a hand-written query would see.
5405 ///
5406 /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5407 /// text)` names them, and a `SETOF <scalar>` yields a single column named
5408 /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5409 /// shows.
5410 fn exec_setof_user_function(
5411 &self,
5412 name: &str,
5413 args: &[spg_sql::ast::Expr],
5414 // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5415 // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5416 alias: Option<&str>,
5417 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5418 // The call's arguments belong to the ENCLOSING query, so they are
5419 // evaluated here and the body sees values.
5420 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5421 let arg_ctx = self.ev_ctx(&empty, None);
5422 let dummy = Row::new(alloc::vec::Vec::new());
5423 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5424 for a in args {
5425 vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5426 }
5427 self.setof_rows_of(name, &vals, alias)
5428 }
5429
5430 /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5431 /// arguments. Shared by the FROM position and the target-list expansion, so
5432 /// a function cannot behave differently depending on where it is called.
5433 pub(crate) fn setof_rows_of(
5434 &self,
5435 name: &str,
5436 arg_values: &[Value<'static>],
5437 alias: Option<&str>,
5438 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5439 let cat = self.active_catalog();
5440 let overloads = cat.functions_named(name);
5441 let def = overloads
5442 .iter()
5443 .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5444 .ok_or_else(|| {
5445 EngineError::Unsupported(alloc::format!(
5446 "function {name} does not exist with {} argument(s)",
5447 arg_values.len()
5448 ))
5449 })?;
5450 let declared = def.returns.trim().to_string();
5451 let upper = declared.to_ascii_uppercase();
5452 if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5453 return Err(EngineError::Unsupported(alloc::format!(
5454 "function {name}() does not return a set — it cannot be used in FROM"
5455 )));
5456 }
5457
5458 let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5459 // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5460 // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5461 if def.language.eq_ignore_ascii_case("plpgsql") {
5462 let out_rows = self
5463 .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5464 .map_err(EngineError::Eval)?;
5465 let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5466 let rows = out_rows.into_iter().map(Row::new).collect();
5467 return Ok((rows, cols));
5468 }
5469 let body = def.body.trim().trim_end_matches(';');
5470 let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5471 EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5472 })?;
5473 let spg_sql::ast::Statement::Select(body_select) = stmt else {
5474 return Err(EngineError::Unsupported(alloc::format!(
5475 "function {name}(): a set-returning body must be a SELECT"
5476 )));
5477 };
5478 let arg_names = spg_storage::function_arg_names(&def.args_repr);
5479 let bound = crate::eval::bind_user_fn_args(
5480 self.active_catalog(),
5481 &body_select,
5482 &arg_names,
5483 arg_values,
5484 )
5485 .map_err(EngineError::Eval)?;
5486 let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5487 let QueryResult::Rows { columns, rows } = out else {
5488 return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5489 };
5490 // Name the columns from the DECLARED shape — the same rule the plpgsql
5491 // path above uses, so a body's language cannot change the row shape.
5492 let cols = setof_column_shape_from(&declared, name, alias, &columns);
5493 Ok((rows, cols))
5494 }
5495
5496 fn exec_select_jsonb_each_text(
5497 &self,
5498 stmt: &SelectStatement,
5499 primary: &TableRef,
5500 cancel: CancelToken<'_>,
5501 ) -> Result<QueryResult, EngineError> {
5502 let (each_fn, arg_expr) = primary
5503 .jsonb_each_text_arg
5504 .as_ref()
5505 .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5506 .expect("caller guards jsonb_each_text_arg.is_some()");
5507 // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5508 // forms keep JSON rendering in the value column (JSON null
5509 // stays jsonb 'null', strings keep their quotes).
5510 let as_text = each_fn.ends_with("_text");
5511 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5512 let ctx = EvalContext::new(&empty_schema, None);
5513 let dummy_row = Row::new(alloc::vec::Vec::new());
5514 let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5515 let pairs =
5516 crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5517 let rows: alloc::vec::Vec<Row<'static>> = pairs
5518 .into_iter()
5519 .map(|(k, v)| {
5520 let key_val = Value::text(k);
5521 let value_val = match v {
5522 Some(s) if as_text => Value::text(s),
5523 Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5524 None => Value::Null,
5525 };
5526 Row::new(alloc::vec![key_val, value_val])
5527 })
5528 .collect();
5529 let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5530 let value_dtype = if as_text {
5531 spg_storage::DataType::Text
5532 } else {
5533 spg_storage::DataType::Json
5534 };
5535 let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5536 let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5537 let mut schema_cols = alloc::vec![key_col, value_col];
5538 // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5539 // LATERAL-position form of the same call already honours it.
5540 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5541 if let Some(col) = schema_cols.get_mut(i) {
5542 col.name = new_name.clone();
5543 }
5544 }
5545 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5546 // `EvalContext::new` drops it and every catalog-dependent cast
5547 // (regclass / enum / composite / domain) silently degrades.
5548 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5549 // WHERE.
5550 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5551 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5552 for row in rows {
5553 cancel.check()?;
5554 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5555 if matches!(v, Value::Bool(true)) {
5556 out.push(row);
5557 }
5558 }
5559 out
5560 } else {
5561 rows
5562 };
5563 // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5564 if aggregate::uses_aggregate(stmt) {
5565 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5566 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5567 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5568 .map_err(|err| match err {
5569 EngineError::Eval(ev) => ev,
5570 other => eval::EvalError::TypeMismatch {
5571 detail: alloc::format!("{other}"),
5572 },
5573 })
5574 };
5575 // v7.39 (round 656) — hand the rows over as they are rather than
5576 // collecting a second vector of `RowRef` wrappers. Note this is
5577 // a set-returning-function path, NOT the relational scan: the
5578 // measured O(rows) cost lived in `run_single_table_aggregate`,
5579 // and converting these four first was a miss that cost a full
5580 // round — every test stayed green and the number did not move.
5581 let agg = aggregate::run(
5582 stmt,
5583 crate::join::AggRows::Owned(&filtered),
5584 &schema_cols,
5585 Some(&alias),
5586 Some(&agg_correlated),
5587 self.parallel_runner.0.as_deref(),
5588 Some(self.active_catalog()),
5589 Some(self),
5590 )?;
5591 return self.finish_agg_result(agg, stmt, cancel);
5592 }
5593 // Projection.
5594 let projection =
5595 build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
5596 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5597 alloc::vec::Vec::with_capacity(filtered.len());
5598 for row in &filtered {
5599 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5600 for p in &projection {
5601 let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5602 vals.push(v);
5603 }
5604 projected_rows.push(Row::new(vals));
5605 }
5606 let columns: alloc::vec::Vec<ColumnSchema> = projection
5607 .iter()
5608 // v7.39 (read01 round 54) — keep the column's enum identity through
5609 // the projection (it lives outside the DataType lattice), or a
5610 // derived table / UNION / windowed result forgets it and any outer
5611 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5612 .map(|p| {
5613 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5614 c.user_enum_type = p.user_enum_type.clone();
5615 c.mysql_fsp = p.mysql_fsp;
5616 c
5617 })
5618 .collect();
5619 // ORDER BY.
5620 if !stmt.order_by.is_empty() {
5621 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5622 .iter()
5623 .enumerate()
5624 .map(|(i, r)| -> Result<_, EngineError> {
5625 let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5626 .order_by
5627 .iter()
5628 .map(|ob| {
5629 eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5630 })
5631 .collect();
5632 Ok((i, keys?))
5633 })
5634 .collect::<Result<_, _>>()?;
5635 indexed.sort_by(|a, b| {
5636 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5637 let o = &stmt.order_by[idx];
5638 let cmp = order_by_value_cmp_in(
5639 o.desc,
5640 o.nulls_first,
5641 ka,
5642 kb,
5643 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5644 );
5645 if cmp != core::cmp::Ordering::Equal {
5646 return cmp;
5647 }
5648 }
5649 core::cmp::Ordering::Equal
5650 });
5651 projected_rows = indexed
5652 .into_iter()
5653 .map(|(i, _)| projected_rows[i].clone())
5654 .collect();
5655 }
5656 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5657 if stmt.distinct {
5658 projected_rows = dedup_rows(projected_rows, FoldSpec::dialect(scan_ctx.mysql_dialect));
5659 }
5660 if let Some(offset) = stmt.offset_literal() {
5661 let off = (offset as usize).min(projected_rows.len());
5662 projected_rows.drain(..off);
5663 }
5664 if let Some(limit) = stmt.limit_literal() {
5665 projected_rows.truncate(limit as usize);
5666 }
5667 Ok(QueryResult::Rows {
5668 columns,
5669 rows: projected_rows,
5670 })
5671 }
5672
5673 /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5674 /// ( SELECT … ) alias` in primary position. The inner SELECT
5675 /// materialises once through the regular bare-select executor
5676 /// (UNION tails included), then the outer WHERE / aggregate /
5677 /// projection / ORDER BY / LIMIT pipeline runs over the
5678 /// synthetic table — the same post-materialisation shape as
5679 /// exec_select_jsonb_each_text, generalised to N columns.
5680 fn exec_select_derived(
5681 &self,
5682 stmt: &SelectStatement,
5683 primary: &TableRef,
5684 cancel: CancelToken<'_>,
5685 ) -> Result<QueryResult, EngineError> {
5686 let inner = primary
5687 .lateral_subquery
5688 .as_deref()
5689 .expect("caller guards lateral_subquery.is_some()");
5690 // exec_select_cancel is the union-aware wrapper — the inner
5691 // SELECT may carry UNION tails on stmt.unions.
5692 let QueryResult::Rows {
5693 columns: inner_cols,
5694 rows,
5695 } = self.exec_select_cancel(inner, cancel)?
5696 else {
5697 return Err(EngineError::Unsupported(
5698 "derived table subquery must return rows".into(),
5699 ));
5700 };
5701 let alias = primary
5702 .alias
5703 .clone()
5704 .unwrap_or_else(|| primary.name.clone());
5705 // `AS t(a, b)` renames the materialised columns positionally
5706 // (extra inner columns keep their own names, PG behaviour).
5707 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5708 // v7.39 (read01 round 78) — a column-alias list longer than the item is
5709 // the error PG reports; SPG used to let the extra names through and then
5710 // fail two layers downstream with "column not found: <the extra name>".
5711 let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5712 if primary.unnest_column_aliases.len() > n_out {
5713 return Err(EngineError::Unsupported(alloc::format!(
5714 "table \"{alias}\" has {n_out} columns available but {} columns specified",
5715 primary.unnest_column_aliases.len()
5716 )));
5717 }
5718 if primary.scalar_fn_item && schema_cols.len() == 1 {
5719 schema_cols[0].scalar_row_source = true;
5720 }
5721 // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5722 // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5723 // The column-alias list, if given, names it like any other column.
5724 let mut rows = rows;
5725 if primary.with_ordinality {
5726 schema_cols.push(ColumnSchema::new(
5727 "ordinality".to_string(),
5728 DataType::BigInt,
5729 false,
5730 ));
5731 rows = rows
5732 .into_iter()
5733 .enumerate()
5734 .map(|(i, r)| {
5735 let mut v = r.values;
5736 #[allow(clippy::cast_possible_wrap)]
5737 v.push(Value::BigInt(i as i64 + 1));
5738 Row::new(v)
5739 })
5740 .collect();
5741 }
5742 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5743 if let Some(col) = schema_cols.get_mut(i) {
5744 col.name = new_name.clone();
5745 }
5746 }
5747 self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5748 }
5749
5750 /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5751 /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5752 /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5753 /// derived-table executor and the FROM-position table functions.
5754 fn exec_select_over_rows(
5755 &self,
5756 stmt: &SelectStatement,
5757 rows: alloc::vec::Vec<Row<'static>>,
5758 schema_cols: alloc::vec::Vec<ColumnSchema>,
5759 alias: &str,
5760 cancel: CancelToken<'_>,
5761 ) -> Result<QueryResult, EngineError> {
5762 let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5763 // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5764 // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5765 // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5766 // (the same path the aggregate branch uses); the old plain eval_expr let
5767 // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5768 let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5769 // WHERE.
5770 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5771 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5772 for row in rows {
5773 cancel.check()?;
5774 let v = self.eval_expr_with_correlated(
5775 w,
5776 &row,
5777 &scan_ctx,
5778 cancel,
5779 Some(&mut corr_memo.borrow_mut()),
5780 )?;
5781 if matches!(v, Value::Bool(true)) {
5782 out.push(row);
5783 }
5784 }
5785 out
5786 } else {
5787 rows
5788 };
5789 // Aggregate dispatch.
5790 if aggregate::uses_aggregate(stmt) {
5791 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5792 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5793 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5794 .map_err(|err| match err {
5795 EngineError::Eval(ev) => ev,
5796 other => eval::EvalError::TypeMismatch {
5797 detail: alloc::format!("{other}"),
5798 },
5799 })
5800 };
5801 // v7.39 (round 656) — hand the rows over as they are rather than
5802 // collecting a second vector of `RowRef` wrappers. Note this is
5803 // a set-returning-function path, NOT the relational scan: the
5804 // measured O(rows) cost lived in `run_single_table_aggregate`,
5805 // and converting these four first was a miss that cost a full
5806 // round — every test stayed green and the number did not move.
5807 let agg = aggregate::run(
5808 stmt,
5809 crate::join::AggRows::Owned(&filtered),
5810 &schema_cols,
5811 Some(alias),
5812 Some(&agg_correlated),
5813 self.parallel_runner.0.as_deref(),
5814 Some(self.active_catalog()),
5815 Some(self),
5816 )?;
5817 return self.finish_agg_result(agg, stmt, cancel);
5818 }
5819 // Projection.
5820 let projection =
5821 build_projection(&stmt.items, &schema_cols, alias, self.backslash_escapes)?;
5822 // v7.39 (round 621) — a target-list SRF expands here too. This tail
5823 // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5824 // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5825 // answered `function unnest(integer[]) does not exist` for a query PG
5826 // answers.
5827 let srf_idxs = self.srf_target_idxs(&projection);
5828 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5829 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5830 alloc::vec::Vec::with_capacity(filtered.len());
5831 if !srf_idxs.is_empty() {
5832 let (rows, src) =
5833 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5834 projected_rows = rows;
5835 src_of_row = src;
5836 } else {
5837 for row in &filtered {
5838 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5839 for p in &projection {
5840 let v = self.eval_expr_with_correlated(
5841 &p.expr,
5842 row,
5843 &scan_ctx,
5844 cancel,
5845 Some(&mut corr_memo.borrow_mut()),
5846 )?;
5847 vals.push(v);
5848 }
5849 projected_rows.push(Row::new(vals));
5850 }
5851 }
5852 let columns: alloc::vec::Vec<ColumnSchema> = projection
5853 .iter()
5854 // v7.39 (read01 round 54) — keep the column's enum identity through
5855 // the projection (it lives outside the DataType lattice), or a
5856 // derived table / UNION / windowed result forgets it and any outer
5857 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5858 .map(|p| {
5859 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5860 c.user_enum_type = p.user_enum_type.clone();
5861 c.mysql_fsp = p.mysql_fsp;
5862 c
5863 })
5864 .collect();
5865 // ORDER BY over the source rows (same shape as the other
5866 // synthetic-table executors).
5867 // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
5868 // OUTPUT column. Evaluated as an expression, as it was here, the literal
5869 // `1` is just the constant 1: the same sort key for every row, so the
5870 // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
5871 // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
5872 // landing on this executor) came back in input order.
5873 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
5874 if !order_by.is_empty() {
5875 // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
5876 // SRF makes more of them than there were inputs.
5877 let out_cols = if srf_idxs.is_empty() {
5878 alloc::vec![None; order_by.len()]
5879 } else {
5880 srf_order_output_cols(&order_by, &projection)
5881 };
5882 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
5883 .iter()
5884 .enumerate()
5885 .map(|(k, out)| -> Result<_, EngineError> {
5886 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
5887 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
5888 .iter()
5889 .zip(out_cols.iter())
5890 .map(|(ob, oc)| {
5891 // v7.39 (read01 round 54) — this path builds its
5892 // sort keys itself instead of going through
5893 // `build_order_keys`, so it skipped the enum-ordinal
5894 // substitution: an OUTER `ORDER BY <enum col>` over
5895 // a DERIVED TABLE sorted by the label TEXT, not by
5896 // member order. Silently wrong rows, not an error.
5897 let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
5898 Ok(
5899 match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
5900 Some(ord) => Value::Float(ord),
5901 None => v,
5902 },
5903 )
5904 })
5905 .collect();
5906 Ok((k, keys?))
5907 })
5908 .collect::<Result<_, _>>()?;
5909 indexed.sort_by(|a, b| {
5910 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5911 let o = &stmt.order_by[idx];
5912 let cmp = order_by_value_cmp_in(
5913 o.desc,
5914 o.nulls_first,
5915 ka,
5916 kb,
5917 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5918 );
5919 if cmp != core::cmp::Ordering::Equal {
5920 return cmp;
5921 }
5922 }
5923 core::cmp::Ordering::Equal
5924 });
5925 projected_rows = indexed
5926 .into_iter()
5927 .map(|(i, _)| projected_rows[i].clone())
5928 .collect();
5929 }
5930 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5931 if stmt.distinct {
5932 projected_rows = dedup_rows(projected_rows, FoldSpec::dialect(scan_ctx.mysql_dialect));
5933 }
5934 if let Some(offset) = stmt.offset_literal() {
5935 let off = (offset as usize).min(projected_rows.len());
5936 projected_rows.drain(..off);
5937 }
5938 if let Some(limit) = stmt.limit_literal() {
5939 projected_rows.truncate(limit as usize);
5940 }
5941 Ok(QueryResult::Rows {
5942 columns,
5943 rows: projected_rows,
5944 })
5945 }
5946
5947 /// Constant `SELECT` with no FROM: evaluate each projection item
5948 /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
5949 fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
5950 let empty_schema: Vec<ColumnSchema> = Vec::new();
5951 let ctx = self.ev_ctx(&empty_schema, None);
5952 // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
5953 // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
5954 // `SELECT string_agg('x',',')` → x). Before this it fell through to the
5955 // scalar projection, where the aggregate name looked like an unknown
5956 // function. The WHERE filters that one row, so `… WHERE false` leaves
5957 // the aggregate zero input rows (`count(*)` → 0).
5958 if aggregate::uses_aggregate(stmt) {
5959 let dummy = Row::new(Vec::new());
5960 let passes = match &stmt.where_ {
5961 Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
5962 None => true,
5963 };
5964 let rows: Vec<RowRef<'_>> = if passes {
5965 alloc::vec![RowRef::Owned(&dummy)]
5966 } else {
5967 Vec::new()
5968 };
5969 let agg = aggregate::run(
5970 stmt,
5971 crate::join::AggRows::Refs(&rows),
5972 &empty_schema,
5973 None,
5974 None,
5975 self.parallel_runner.0.as_deref(),
5976 Some(self.active_catalog()),
5977 Some(self),
5978 )?;
5979 return self.finish_agg_result(agg, stmt, CancelToken::none());
5980 }
5981 let projection = build_projection(&stmt.items, &empty_schema, "", self.backslash_escapes)?;
5982 // `SELECT … WHERE cond` with no FROM — the one conceptual
5983 // row survives only when the condition is true (previously
5984 // the WHERE was silently ignored: `SELECT 1 WHERE false`
5985 // returned a row).
5986 let dummy_row = Row::new(Vec::new());
5987 if let Some(w) = &stmt.where_ {
5988 let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
5989 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
5990 let columns: Vec<ColumnSchema> = projection
5991 .into_iter()
5992 .map(|p| {
5993 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5994 c.user_enum_type = p.user_enum_type;
5995 c.collation_name = p.collation_name;
5996 c.mysql_fsp = p.mysql_fsp;
5997 c
5998 })
5999 .collect();
6000 return Ok(QueryResult::Rows {
6001 columns,
6002 rows: Vec::new(),
6003 });
6004 }
6005 }
6006 // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6007 // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6008 // desugar to unnest) expands here: one output row per SRF row, sibling
6009 // scalar columns repeated. unnest / array_elements / path_query reach a
6010 // real FROM via the parser rewrite and never land here.
6011 // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6012 let srf_idxs = self.srf_target_idxs(&projection);
6013 if !srf_idxs.is_empty() {
6014 let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6015 let columns: Vec<ColumnSchema> = projection
6016 .into_iter()
6017 .map(|p| {
6018 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
6019 c.user_enum_type = p.user_enum_type;
6020 c.collation_name = p.collation_name;
6021 c.mysql_fsp = p.mysql_fsp;
6022 c
6023 })
6024 .collect();
6025 // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6026 // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6027 // to. This returned straight out of the expansion, so
6028 // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6029 // input order — the sort was not wrong, it never ran. (There is
6030 // exactly one conceptual input row here, which is why the ordinary
6031 // scan pipeline is not on this path at all.)
6032 if !stmt.order_by.is_empty() {
6033 let synth_ctx =
6034 EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6035 let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6036 .order_by
6037 .iter()
6038 .map(|o| {
6039 let mut o = o.clone();
6040 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6041 && *n >= 1
6042 && let Ok(idx) = usize::try_from(*n - 1)
6043 && idx < columns.len()
6044 {
6045 o.expr = Expr::Column(spg_sql::ast::ColumnName {
6046 qualifier: None,
6047 name: columns[idx].name.clone(),
6048 });
6049 }
6050 o
6051 })
6052 .collect();
6053 let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6054 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6055 for r in rows {
6056 let keys = build_order_keys(&resolved, &r, &synth_ctx)?;
6057 tagged.push((keys, r));
6058 }
6059 sort_by_keys(&mut tagged, &descs);
6060 rows = tagged.into_iter().map(|(_, r)| r).collect();
6061 }
6062 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6063 return Ok(QueryResult::Rows { columns, rows });
6064 }
6065 let mut values = Vec::with_capacity(projection.len());
6066 for p in &projection {
6067 values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6068 }
6069 let columns: Vec<ColumnSchema> = projection
6070 .into_iter()
6071 .map(|p| {
6072 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
6073 c.user_enum_type = p.user_enum_type;
6074 c.collation_name = p.collation_name;
6075 c.mysql_fsp = p.mysql_fsp;
6076 c
6077 })
6078 .collect();
6079 // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6080 // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6081 // returns none. (The SRF and aggregate arms above already applied
6082 // them; this tail was the one that didn't.)
6083 let mut rows = alloc::vec![Row::new(values)];
6084 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6085 Ok(QueryResult::Rows { columns, rows })
6086 }
6087
6088 /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6089 /// circuit. Catches
6090 /// SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6091 /// BEFORE `resolve_select_subqueries` materialises the inner result
6092 /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6093 /// values into a `HashSet<i64>` directly, then probes A.pk per
6094 /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6095 /// (~150 µs / query at INSUBQ benchmark scale).
6096 pub(crate) fn try_count_star_pk_in_subquery_fast(
6097 &self,
6098 stmt: &SelectStatement,
6099 cancel: CancelToken<'_>,
6100 ) -> Result<Option<QueryResult>, EngineError> {
6101 use spg_sql::ast::SelectItem;
6102 if stmt.distinct
6103 || stmt.limit_with_ties
6104 || stmt.group_by.is_some()
6105 || stmt.having.is_some()
6106 || !stmt.unions.is_empty()
6107 || !stmt.order_by.is_empty()
6108 || stmt.limit.is_some()
6109 || stmt.offset.is_some()
6110 || stmt.items.len() != 1
6111 {
6112 return Ok(None);
6113 }
6114 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6115 return Ok(None);
6116 };
6117 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6118 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6119 if !is_count_star {
6120 return Ok(None);
6121 }
6122 let Some(from) = stmt.from.as_ref() else {
6123 return Ok(None);
6124 };
6125 if !from.joins.is_empty()
6126 || from.primary.lateral_subquery.is_some()
6127 || from.primary.unnest_expr.is_some()
6128 || from.primary.generate_series_args.is_some()
6129 || from.primary.table_fn_call.is_some()
6130 || from.primary.as_of_segment.is_some()
6131 {
6132 return Ok(None);
6133 }
6134 let Some(where_expr) = stmt.where_.as_ref() else {
6135 return Ok(None);
6136 };
6137 // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6138 // negated=false; no other predicates.
6139 let Expr::InSubquery {
6140 expr: col_expr,
6141 subquery,
6142 negated: false,
6143 } = where_expr
6144 else {
6145 return Ok(None);
6146 };
6147 let Expr::Column(c) = col_expr.as_ref() else {
6148 return Ok(None);
6149 };
6150 let outer_alias = from
6151 .primary
6152 .alias
6153 .as_deref()
6154 .unwrap_or(from.primary.name.as_str());
6155 if let Some(q) = c.qualifier.as_deref()
6156 && !q.eq_ignore_ascii_case(outer_alias)
6157 {
6158 return Ok(None);
6159 }
6160 // Outer column must be a single-column PK on integer family.
6161 let catalog = self.active_catalog();
6162 let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6163 return Ok(None);
6164 };
6165 let outer_schema = outer_table.schema();
6166 let Some(outer_pos) = outer_schema
6167 .columns
6168 .iter()
6169 .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6170 else {
6171 return Ok(None);
6172 };
6173 if !matches!(
6174 outer_schema.columns[outer_pos].ty,
6175 spg_storage::DataType::BigInt
6176 | spg_storage::DataType::Int
6177 | spg_storage::DataType::SmallInt
6178 ) {
6179 return Ok(None);
6180 }
6181 if !outer_schema
6182 .uniqueness_constraints
6183 .iter()
6184 .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6185 {
6186 return Ok(None);
6187 }
6188 let Some(idx) = outer_table.index_on(outer_pos) else {
6189 return Ok(None);
6190 };
6191 // Inner must be uncorrelated. The cheap-correlation pre-check
6192 // exists upstream; here we just attempt the bare exec.
6193 if crate::subquery::select_is_correlated(subquery) {
6194 return Ok(None);
6195 }
6196 let mut inner = (**subquery).clone();
6197 self.resolve_select_subqueries(&mut inner, cancel)?;
6198 let r = match self.exec_bare_select_cancel(&inner, cancel) {
6199 Ok(r) => r,
6200 Err(_) => return Ok(None),
6201 };
6202 let QueryResult::Rows { columns, rows, .. } = r else {
6203 return Ok(None);
6204 };
6205 if columns.len() != 1 {
6206 return Ok(None);
6207 }
6208 // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6209 // subquery projects a column known to be UNIQUE/PK on its table
6210 // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6211 // in `tbl.uniqueness_constraints`), survivor values are
6212 // guaranteed distinct and the per-survivor `HashSet::insert`
6213 // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6214 //
6215 // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6216 // projection that is a bare Column ref, table-column lookup in
6217 // catalog confirms the column appears as a unique constraint's
6218 // sole member. UNIQUE NOT NULL is required — a nullable unique
6219 // column may have multiple NULLs, but NULLs are already skipped
6220 // above (`Value::Null => continue`), so a UNIQUE-only column is
6221 // still safe to dedup-skip.
6222 let inner_unique = (|| -> bool {
6223 if inner.distinct
6224 || inner.group_by.is_some()
6225 || !inner.unions.is_empty()
6226 || inner.having.is_some()
6227 || inner.items.len() != 1
6228 {
6229 return false;
6230 }
6231 let Some(inner_from) = inner.from.as_ref() else {
6232 return false;
6233 };
6234 if !inner_from.joins.is_empty()
6235 || inner_from.primary.lateral_subquery.is_some()
6236 || inner_from.primary.unnest_expr.is_some()
6237 || inner_from.primary.generate_series_args.is_some()
6238 || inner_from.primary.table_fn_call.is_some()
6239 {
6240 return false;
6241 }
6242 let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6243 return false;
6244 };
6245 let Expr::Column(pc) = proj else {
6246 return false;
6247 };
6248 let inner_alias = inner_from
6249 .primary
6250 .alias
6251 .as_deref()
6252 .unwrap_or(inner_from.primary.name.as_str());
6253 if let Some(q) = pc.qualifier.as_deref()
6254 && !q.eq_ignore_ascii_case(inner_alias)
6255 {
6256 return false;
6257 }
6258 let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6259 return false;
6260 };
6261 let isch = inner_table.schema();
6262 let Some(ipos) = isch
6263 .columns
6264 .iter()
6265 .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6266 else {
6267 return false;
6268 };
6269 isch.uniqueness_constraints
6270 .iter()
6271 .any(|u| u.columns.as_slice() == [ipos])
6272 })();
6273 // Collect inner i64 values directly into a HashSet, then probe.
6274 let mut count: i64 = 0;
6275 let mut probed = if inner_unique {
6276 hashbrown::HashSet::<i64>::new()
6277 } else {
6278 hashbrown::HashSet::<i64>::with_capacity(rows.len())
6279 };
6280 for row in &rows {
6281 let v = row.values.first().cloned().unwrap_or(Value::Null);
6282 let n = match v {
6283 Value::BigInt(n) => n,
6284 Value::Int(n) => i64::from(n),
6285 Value::SmallInt(n) => i64::from(n),
6286 Value::Null => continue,
6287 _ => return Ok(None),
6288 };
6289 // De-duplicate inner key set so a duplicate inner value
6290 // doesn't double-count the same outer row. Skipped when
6291 // the inner projection is statically unique.
6292 if !inner_unique && !probed.insert(n) {
6293 continue;
6294 }
6295 // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6296 // the `IndexKey::from_value` enum-dispatch and the per-call
6297 // `IndexKey` wrapper construction. The outer column is
6298 // already gated to integer-family above, so an i64 key
6299 // always corresponds to a valid PK lookup.
6300 if !idx.lookup_eq_i64(n).is_empty() {
6301 count += 1;
6302 }
6303 }
6304 let columns_out = alloc::vec![ColumnSchema::new(
6305 "count".to_string(),
6306 spg_storage::DataType::BigInt,
6307 false,
6308 )];
6309 let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6310 Ok(Some(QueryResult::Rows {
6311 columns: columns_out,
6312 rows: rows_out,
6313 }))
6314 }
6315
6316 /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6317 /// SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6318 /// (the post-subquery-replacement shape of the INSUBQ probe
6319 /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6320 /// The general aggregate path materialises every seeked row into
6321 /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6322 /// For COUNT(*) we only care how many keys hit; iterate the list
6323 /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6324 /// row materialisation, the aggregate state machine, and the per-
6325 /// row WHERE re-eval (the seek already filtered by the same list).
6326 /// Returns `None` when the shape doesn't match.
6327 fn try_count_star_pk_in_list_fast(
6328 &self,
6329 stmt: &SelectStatement,
6330 table: &spg_storage::Table,
6331 schema_cols: &[ColumnSchema],
6332 alias: &str,
6333 ) -> Option<QueryResult> {
6334 use spg_sql::ast::{ColumnName, SelectItem};
6335 // Gates on the SELECT shape.
6336 if stmt.distinct
6337 || stmt.limit_with_ties
6338 || stmt.group_by.is_some()
6339 || stmt.having.is_some()
6340 || !stmt.unions.is_empty()
6341 || !stmt.order_by.is_empty()
6342 || stmt.limit.is_some()
6343 || stmt.offset.is_some()
6344 || stmt.items.len() != 1
6345 {
6346 return None;
6347 }
6348 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6349 return None;
6350 };
6351 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6352 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6353 if !is_count_star {
6354 return None;
6355 }
6356 // WHERE must be `<col> IN (literal list)` with no other
6357 // conjuncts (the seek result is a true subset of the row
6358 // population for this predicate).
6359 let where_expr = stmt.where_.as_ref()?;
6360 let Expr::InList {
6361 expr: col_expr,
6362 list,
6363 negated: false,
6364 } = where_expr
6365 else {
6366 return None;
6367 };
6368 let Expr::Column(c) = col_expr.as_ref() else {
6369 return None;
6370 };
6371 if let Some(q) = c.qualifier.as_deref()
6372 && !q.eq_ignore_ascii_case(alias)
6373 {
6374 return None;
6375 }
6376 let col_pos = schema_cols
6377 .iter()
6378 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6379 // The column must be a single-column PK on an integer family
6380 // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6381 // so the antiset stays collision-free under `HashSet<i64>`.
6382 let schema = table.schema();
6383 if !matches!(
6384 schema.columns[col_pos].ty,
6385 spg_storage::DataType::BigInt
6386 | spg_storage::DataType::Int
6387 | spg_storage::DataType::SmallInt
6388 ) {
6389 return None;
6390 }
6391 if !schema
6392 .uniqueness_constraints
6393 .iter()
6394 .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6395 {
6396 return None;
6397 }
6398 let idx = table.index_on(col_pos)?;
6399 // Tally non-empty seek results across all literal values.
6400 let mut count: i64 = 0;
6401 for lit in list {
6402 let Expr::Literal(l) = lit else {
6403 return None;
6404 };
6405 // r1039 — through the shared resolver, so a literal spelled
6406 // in another type ('5' against an integer PK) is read as the
6407 // column's before it becomes a key. This tally answers from
6408 // the index alone, so a key in the wrong space would return a
6409 // COUNT of zero rather than fall back to a scan.
6410 let col = schema.columns.get(col_pos)?;
6411 let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6412 let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6413 if !idx.lookup_eq(&key).is_empty() {
6414 count += 1;
6415 }
6416 }
6417 let columns = alloc::vec![ColumnSchema::new(
6418 "count".to_string(),
6419 spg_storage::DataType::BigInt,
6420 false,
6421 )];
6422 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6423 let _ = ColumnName {
6424 qualifier: None,
6425 name: String::new(),
6426 };
6427 Some(QueryResult::Rows { columns, rows })
6428 }
6429
6430 /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6431 /// BETWEEN a AND b` on an indexed column. The index range walk yields
6432 /// exactly the matching (visible) rows, so we count locators directly —
6433 /// skipping the row materialisation, the aggregate state machine, and the
6434 /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6435 /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6436 /// when the shape doesn't match.
6437 fn try_count_star_indexed_range_fast(
6438 &self,
6439 stmt: &SelectStatement,
6440 table: &spg_storage::Table,
6441 schema_cols: &[ColumnSchema],
6442 alias: &str,
6443 snapshot: &spg_storage::snapshot::Snapshot,
6444 ) -> Option<QueryResult> {
6445 use spg_sql::ast::SelectItem;
6446 if stmt.distinct
6447 || stmt.limit_with_ties
6448 || stmt.group_by.is_some()
6449 || stmt.having.is_some()
6450 || !stmt.unions.is_empty()
6451 || !stmt.order_by.is_empty()
6452 || stmt.limit.is_some()
6453 || stmt.offset.is_some()
6454 || stmt.items.len() != 1
6455 {
6456 return None;
6457 }
6458 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6459 return None;
6460 };
6461 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6462 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6463 if !is_count_star {
6464 return None;
6465 }
6466 let where_expr = stmt.where_.as_ref()?;
6467 let count =
6468 crate::index_access::try_range_count(where_expr, schema_cols, table, alias, snapshot)?;
6469 let columns = alloc::vec![ColumnSchema::new(
6470 "count".to_string(),
6471 spg_storage::DataType::BigInt,
6472 false,
6473 )];
6474 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6475 Some(QueryResult::Rows { columns, rows })
6476 }
6477
6478 /// Single-table aggregate path: filter the (optionally index-seeked)
6479 /// rows, then hand off to the aggregate executor which does its own
6480 /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6481 fn run_single_table_aggregate<'a>(
6482 &self,
6483 stmt: &SelectStatement,
6484 table: &'a spg_storage::Table,
6485 schema_cols: &'a [ColumnSchema],
6486 alias: &str,
6487 indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6488 cancel: CancelToken<'_>,
6489 ) -> Result<QueryResult, EngineError> {
6490 // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6491 // REPEATABLE (see run_single_table_scan). Aggregates
6492 // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6493 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6494 let ctx = self
6495 .ev_ctx(schema_cols, Some(alias))
6496 .with_sample_rng(&sample_cell);
6497 // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6498 // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6499 // and every abandoned buffer on the way stays resident: RSS is a
6500 // high-water mark, so the intermediates are paid for even though
6501 // they are freed. Round 656 measured the scan at 17 bytes/row
6502 // where the survivor list itself only needs 8.
6503 let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6504 Vec::with_capacity(table.rows().len())
6505 } else {
6506 // With a WHERE, the row count is an UPPER bound and reserving it
6507 // is the worse trade: `… WHERE id = 5` over 50M rows would take
6508 // 400 MB of pointers to hold one survivor. Let it grow.
6509 Vec::new()
6510 };
6511 // v6.2.6 — Memoize: per-query LRU cache for correlated
6512 // scalar subqueries. Fresh per row-loop entry so each
6513 // SELECT execution gets an isolated cache.
6514 let mut memo = memoize::MemoizeCache::new();
6515 // v7.37 (perf) — single-table aggregate's WHERE filter
6516 // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6517 // correlated`) per row, even for subquery-free WHEREs that
6518 // the single-table SCAN path has compiled since v7.32
6519 // (perf knife D). The asymmetry meant a fold-to-filter
6520 // rewrite (joinfold) that swapped a JOIN for a single-table
6521 // aggregate over a compiled WHERE saw the tree-walker
6522 // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6523 // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6524 // step. Compile once if eligible; fall back to the walker
6525 // for subquery-bearing or non-compilable WHEREs.
6526 let compiled_where: Option<eval::CompiledExpr> = stmt
6527 .where_
6528 .as_ref()
6529 .filter(|w| eval::fully_compilable(w))
6530 .map(|w| {
6531 // v7.38.8 — the scan filter runs the cheap half of its
6532 // conjunction first. Called from HERE and not from
6533 // `eval::compiled`, deliberately: the row loop lives in
6534 // that file, and adding a function to it cost this
6535 // query 11 % through layout alone while doing no work
6536 // for it. See `crate::qualorder`.
6537 match crate::qualorder::reordered(w) {
6538 Some(r) => eval::compile_expr(&r, &ctx),
6539 None => eval::compile_expr(w, &ctx),
6540 }
6541 });
6542 let mut eval_stack: Vec<Value<'static>> = Vec::new();
6543 let mut row_passes_where = |row: &Row<'static>,
6544 eval_stack: &mut Vec<Value<'static>>,
6545 memo: &mut memoize::MemoizeCache|
6546 -> Result<bool, EngineError> {
6547 match (&compiled_where, &stmt.where_) {
6548 (Some(cw), _) => {
6549 // v7.39 (round 479) — the predicate wants a bool, not a
6550 // Value. The owned entry ended in `Value::into_owned`
6551 // and the caller then dropped it, once per row; round
6552 // 478's profile put that pair above the comparison
6553 // itself.
6554 Ok(eval::compiled::eval_compiled_pred(
6555 cw,
6556 row,
6557 &ctx,
6558 eval_stack,
6559 ctx.mysql_dialect,
6560 )
6561 .map_err(EngineError::Eval)?)
6562 }
6563 (None, Some(w)) => {
6564 let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6565 Ok(crate::eval::predicate_is_true(
6566 &cond,
6567 "WHERE",
6568 ctx.mysql_dialect,
6569 )?)
6570 }
6571 (None, None) => Ok(true),
6572 }
6573 };
6574 if let Some(rows) = &indexed_rows {
6575 for cow in rows {
6576 let row = cow.as_ref();
6577 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6578 continue;
6579 }
6580 filtered.push(row);
6581 }
6582 }
6583 // v7.36 (cold-tier coverage) — single-table aggregate's
6584 // non-indexed full scan was hot-only and silently lost cold
6585 // rows on COUNT/SUM/etc. Materialise cold rows once into
6586 // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6587 // shape stays unchanged; the cold rows live until the end of
6588 // the aggregate run.
6589 let cold_rows_storage = if indexed_rows.is_none() {
6590 self.iter_cold_rows_of_table(table)
6591 } else {
6592 Vec::new()
6593 };
6594 if indexed_rows.is_none() {
6595 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6596 // single-table aggregate full-scan path. Mirrors the gate on
6597 // `run_single_table_scan`: this is a user-query result path,
6598 // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6599 // reader's snapshot cannot see (e.g. tombstoned versions),
6600 // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6601 // under the default gate-off: every hot row is frozen or
6602 // committed-and-alive, so `is_row_visible` returns true.
6603 // Cold-tier rows are frozen (visible) by definition — left
6604 // ungated, matching the plain-scan path.
6605 let scan_snapshot = self.current_snapshot();
6606 // v7.39 (pg_stat knife B) — this full-scan branch walks
6607 // headers directly (serial and sharded alike); count the
6608 // sequential scan here.
6609 table.note_seq_scan();
6610 // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6611 // filter dominate the pre-aggregate wall time on big
6612 // scans (P1's ground truth: accumulation is only ~17%).
6613 // Shard THAT work when the host injected an executor and
6614 // the WHERE is compiled (the compiled evaluator is pure
6615 // over &row; the tree-walker fallback can hit correlated
6616 // subqueries and stays serial). Shards return surviving
6617 // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6618 // 'static bound — and the main thread only dereferences.
6619 let n = table.row_count();
6620 let par = self.parallel_runner.0.as_deref().filter(|_| {
6621 n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6622 });
6623 // v7.38.11 — ask the BRIN summary first. When it prunes,
6624 // the work left is a few thousand rows and sharding it
6625 // costs more than it saves, so the serial pruned loop below
6626 // takes it; the shard machinery is left exactly as it was
6627 // rather than taught about slots.
6628 let brin_slots = stmt
6629 .where_
6630 .as_ref()
6631 .and_then(|w| crate::brin::candidate_slots(w, table));
6632 let brin_prunes = brin_slots
6633 .as_ref()
6634 .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6635 if let Some(r) = par
6636 && !brin_prunes
6637 {
6638 let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6639 let chunk = n.div_ceil(n_shards);
6640 type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6641 let cw = &compiled_where;
6642 let snap_ref = &scan_snapshot;
6643 let results = r.run_shards(n_shards, &|s| {
6644 let lo = s * chunk;
6645 let hi = ((s + 1) * chunk).min(n);
6646 let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6647 // EvalContext carries Cells (sampler / row counters)
6648 // and is !Sync — each shard builds its own from the
6649 // same Sync inputs. The compiled WHERE is gated to
6650 // the pure-scalar whitelist, which reads none of the
6651 // session state the engine-built ctx would add
6652 // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6653 // sampled scans never take this branch).
6654 let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6655 let mut stack: Vec<Value<'static>> = Vec::new();
6656 let out: ShardOut = (|| {
6657 for i in lo..hi {
6658 if !table.is_row_visible(i, snap_ref) {
6659 continue;
6660 }
6661 let row = &table.rows()[i];
6662 // v7.39 (round 480) — the parallel full-scan
6663 // shard is the path the aggregate benchmark
6664 // actually takes, and it was still on the OWNED
6665 // entry: round 480's profile attributed 68.7 %
6666 // of `drop_glue<Value>` to this closure, which
6667 // is why round 479's fix to the indexed path
6668 // barely moved the total.
6669 //
6670 // The `matches!(…, Value::Bool(true))` form was
6671 // also a narrower reading than the rest of the
6672 // engine uses — `predicate_is_true` is what
6673 // handles NULL and MySQL truthiness — so the
6674 // bool entry fixes the shape as well as the cost.
6675 let pass = match cw {
6676 Some(c) => eval::compiled::eval_compiled_pred(
6677 c,
6678 row,
6679 &shard_ctx,
6680 &mut stack,
6681 shard_ctx.mysql_dialect,
6682 )
6683 .map_err(EngineError::Eval)?,
6684 None => true,
6685 };
6686 if pass {
6687 keep.push(i);
6688 }
6689 }
6690 Ok(keep)
6691 })();
6692 alloc::boxed::Box::new(out)
6693 });
6694 // v7.39 (round 567) — `rows()` is a 32-way trie, so
6695 // indexing it is four dependent loads and a scan that
6696 // reads every row paid them every row. A profile of
6697 // `SELECT sum(id)` over 500k rows put 37.8% of the
6698 // connection thread's CPU on THIS ONE LINE. The cursor
6699 // holds the leaf, making that one descent per 32.
6700 let mut rows_cur = table.rows().run_cursor();
6701 for boxed in results {
6702 let shard = boxed
6703 .downcast::<ShardOut>()
6704 .expect("runner echoes the closure's box");
6705 for i in (*shard)? {
6706 if let Some(row) = rows_cur.get(i) {
6707 filtered.push(row);
6708 }
6709 }
6710 }
6711 } else {
6712 let mut rows_cur = table.rows().run_cursor();
6713 // v7.38.11 — the slots the BRIN summary could not rule
6714 // out. The predicate still runs on every row that
6715 // survives: the summary decides what to SKIP, never
6716 // what to return.
6717 let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6718 for range in ranges {
6719 for i in range {
6720 if !table.is_row_visible(i, &scan_snapshot) {
6721 continue;
6722 }
6723 let Some(row) = rows_cur.get(i) else { continue };
6724 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6725 continue;
6726 }
6727 filtered.push(row);
6728 }
6729 }
6730 }
6731 for row in &cold_rows_storage {
6732 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6733 continue;
6734 }
6735 filtered.push(row);
6736 }
6737 }
6738 // v7.29 — a per-query memo so correlated scalar
6739 // subqueries batch-evaluate once (group map) instead of
6740 // executing per group.
6741 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6742 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6743 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6744 .map_err(|err| match err {
6745 EngineError::Eval(ev) => ev,
6746 other => eval::EvalError::TypeMismatch {
6747 detail: alloc::format!("{other}"),
6748 },
6749 })
6750 };
6751 // v7.39 (round 656) — the plain relational scan. This collect() was
6752 // the measured defect: one 64-byte `RowRef` per surviving row to
6753 // wrap an 8-byte pointer `filtered` already holds. Scalar
6754 // aggregates measured ~81 bytes/row of working memory because of
6755 // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6756 // one number. `AggRows::Ptrs` reads the pointers directly.
6757 let agg = aggregate::run(
6758 stmt,
6759 crate::join::AggRows::Ptrs(&filtered),
6760 schema_cols,
6761 Some(alias),
6762 Some(&agg_correlated),
6763 self.parallel_runner.0.as_deref(),
6764 Some(self.active_catalog()),
6765 Some(self),
6766 )?;
6767 self.finish_agg_result(agg, stmt, cancel)
6768 }
6769
6770 /// Single-table scan + projection path: WHERE filter (compiled when
6771 /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6772 /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6773 fn run_single_table_scan<'a>(
6774 &self,
6775 stmt: &SelectStatement,
6776 table: &'a spg_storage::Table,
6777 schema_cols: &'a [ColumnSchema],
6778 alias: &str,
6779 indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6780 cancel: CancelToken<'_>,
6781 ) -> Result<QueryResult, EngineError> {
6782 // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6783 // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6784 // deterministic `__tsm_fract(seed)` draws share one scan-local
6785 // state (isolated from the global random() PRNG); a fresh cell per
6786 // scan makes a repeat / rescan reproduce the same sample. Unused
6787 // and cheap when the query carries no sample.
6788 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6789 let ctx = self
6790 .ev_ctx(schema_cols, Some(alias))
6791 .with_sample_rng(&sample_cell);
6792 let projection = build_projection(&stmt.items, schema_cols, alias, self.backslash_escapes)?;
6793 // v7.19 P5 — single-table SELECT path for SRF
6794 // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6795 // unnest in the projection list. When present, the
6796 // per-row processor emits one output row per array
6797 // element (broadcasting non-SRF projections from the
6798 // same input row). Empty / NULL arrays emit zero rows
6799 // for that input — PG semantics.
6800 // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
6801 let srf_idxs = self.srf_target_idxs(&projection);
6802 let srf_position = srf_idxs.first().copied();
6803 // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
6804 let mut srf_plan = if srf_position.is_some() {
6805 Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
6806 } else {
6807 None
6808 };
6809
6810 // Materialise the filter pass into `(order_key, projected_row)`
6811 // tuples. The order key is `None` when there's no ORDER BY clause.
6812 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
6813 // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
6814 // output row to the per-query byte budget as it is built, so a
6815 // fat single-table scan / sort REJECTS with QueryBytesExceeded
6816 // at ~the ceiling instead of materialising the whole table and
6817 // only noticing at the final enforce_row_limit check. Without
6818 // this, N concurrent fat scans peak at N×table and OOM the host.
6819 // `max_query_bytes = None` (the embedded default) = no ceiling,
6820 // so existing unbudgeted behaviour is byte-identical.
6821 let mut budget = ByteBudget::new(self.max_query_bytes);
6822 // v6.2.6 — Memoize per-row WHERE eval shares one cache.
6823 let mut memo = memoize::MemoizeCache::new();
6824 // v7.32 (perf knife D) — subquery-free WHERE compiles once;
6825 // the row loop then runs a flat step program instead of a
6826 // tree interpretation per row.
6827 let compiled_where: Option<eval::CompiledExpr> = stmt
6828 .where_
6829 .as_ref()
6830 .filter(|w| eval::fully_compilable(w))
6831 .map(|w| {
6832 // v7.38.8 — the scan filter runs the cheap half of its
6833 // conjunction first. Called from HERE and not from
6834 // `eval::compiled`, deliberately: the row loop lives in
6835 // that file, and adding a function to it cost this
6836 // query 11 % through layout alone while doing no work
6837 // for it. See `crate::qualorder`.
6838 match crate::qualorder::reordered(w) {
6839 Some(r) => eval::compile_expr(&r, &ctx),
6840 None => eval::compile_expr(w, &ctx),
6841 }
6842 });
6843 let mut eval_stack: Vec<Value<'static>> = Vec::new();
6844 // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
6845 // SELECT-item scalar subquery for the PK-probe fast path. The
6846 // analysis (gate checks + catalog lookups) takes ~500 ns; doing
6847 // it once per query instead of once per row × 100 rows saves
6848 // ~50 µs and lets the per-row evaluation reduce to a single
6849 // index probe + outer-column read.
6850 let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
6851 .iter()
6852 .map(|p| {
6853 if let Expr::ScalarSubquery(inner) = &p.expr {
6854 self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
6855 } else {
6856 None
6857 }
6858 })
6859 .collect();
6860 let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
6861 // v7.39 (round 487) — a projection item that is a bare column
6862 // reference binds its position ONCE per query.
6863 //
6864 // Per row it used to walk `eval_expr_with_correlated` (a memo
6865 // lookup for "does this have a subquery", then an un-memoised
6866 // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
6867 // then `resolve_column`, which finds the column by scanning the
6868 // schema and comparing NAMES. On `SELECT g FROM h` that chain was
6869 // 19 % of self time for what is ultimately one cell read.
6870 //
6871 // `compile_column_pos` is the Step VM's resolver, already
6872 // `pub(crate)` and already reused by the aggregate's bind-once
6873 // path: it mirrors `resolve_column`'s happy layers and returns
6874 // None for anything that would reach an error, an ambiguity, or a
6875 // miss, so those still go the interpreter's way and keep its
6876 // exact message. A composite column is excluded for the same
6877 // reason `compile_into` excludes it — it must be rehydrated from
6878 // stored JSON, which is not a cell read.
6879 let proj_direct = bind_direct_columns(&projection, &ctx);
6880 let any_proj_direct = proj_direct.iter().any(Option::is_some);
6881 // v7.39 (round 605) — a projection item that cannot depend on the row
6882 // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
6883 // allocations a row against one for a plain column, `'abc' || 'def'`
6884 // six and `upper('abc')` five, all of them producing the same value
6885 // 50,000 times. An item that fails to evaluate is left alone, so its
6886 // error still comes from the row loop in the interpreter's wording.
6887 let proj_const: Vec<Option<Value<'static>>> = projection
6888 .iter()
6889 .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
6890 .collect();
6891 let any_proj_const = proj_const.iter().any(Option::is_some);
6892 crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
6893 // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
6894 // projection. Statement prep (`resolve_order_by_position`) can only map
6895 // `ORDER BY 1` onto the first SELECT item when that item is an
6896 // expression; a `*` is not one, so the literal survived to here and was
6897 // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
6898 // at all. The parser rewrites `SELECT unnest(a) x` into
6899 // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
6900 // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
6901 // back in input order. The projection is built by now, so the Nth output
6902 // column is known — resolve against it.
6903 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6904 // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
6905 // EXPANDED rows, so a key naming a select-list item reads that item.
6906 let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
6907 srf_order_output_cols(&order_by, &projection)
6908 } else {
6909 Vec::new()
6910 };
6911 let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
6912 // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
6913 // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
6914 // Hoisted above the closure so the projection-eval path can
6915 // gate `memo` passing on it: the SELECT-item correlated-scalar
6916 // batch path scans the FULL inner table once (~5 ms for 12.5 k
6917 // rows) and is only a win when N outer rows is large; for small
6918 // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
6919 let early_cap: Option<usize> = if order_by.is_empty()
6920 && !stmt.distinct
6921 && !stmt.limit_with_ties
6922 && srf_position.is_none()
6923 && stmt.where_.is_none()
6924 {
6925 stmt.limit_literal()
6926 .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
6927 } else {
6928 None
6929 };
6930 // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
6931 // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
6932 // full-sort by the test gate) keep only the running top-`keep`
6933 // rows in memory instead of materialising every projected row,
6934 // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
6935 // space, not O(rows). `None` = accumulate everything (the prior
6936 // behaviour). The final `partial_sort_tagged(keep)` below still
6937 // runs and produces the identical rows.
6938 // v7.39 (round 683) — the declared collation for each ORDER BY
6939 // position, resolved once and carried beside `descs` for the same
6940 // reason `descs` is carried: it is per key position, not per row.
6941 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
6942 let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
6943 && !stmt.distinct
6944 && !stmt.limit_with_ties
6945 && srf_position.is_none()
6946 && !self.env_cfg().disable_topk
6947 {
6948 stmt.limit_literal().and_then(|l| {
6949 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
6950 (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
6951 })
6952 } else {
6953 None
6954 };
6955 // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
6956 // kept rows in `tagged`. Probing on the PROJECTED row as soon as
6957 // it is built means a duplicate costs neither a build_order_keys
6958 // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
6959 // a tagged slot, and the sort below runs over u survivors, not
6960 // n input rows — PG's hash-distinct-then-sort plan shape.
6961 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
6962 hashbrown::HashMap::new();
6963 let distinct_hb = hashbrown::DefaultHashBuilder::default();
6964 // v7.38.13 — which output positions must NOT fold. Built once per
6965 // scan from the projection, which carries the source column's
6966 // byte-wise-ness; see `FoldSpec`.
6967 let distinct_mask = fold_mask(&projection);
6968 // v7.39 (round 485) — one projection buffer for the whole scan
6969 // rather than a fresh `Vec` per input row. A row that survives
6970 // the DISTINCT probe takes the buffer with it (`mem::take`) and
6971 // the next row allocates a new one; a row that duplicates an
6972 // earlier one leaves the buffer — and its capacity — in place.
6973 // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
6974 // projected rows are duplicates, so that is 49 900 allocate /
6975 // free pairs the scan no longer performs. Shapes where every row
6976 // survives (plain projection, `DISTINCT` over a unique column)
6977 // allocate exactly as often as before.
6978 let mut proj_buf: Vec<Value<'static>> = Vec::new();
6979 // v7.39 (round 571) — buffers handed back by the top-N trim.
6980 // Round 485 made the scan share ONE projection buffer, but a
6981 // surviving row takes it (`mem::take`) and without DISTINCT
6982 // almost every row survives, so the next one starts from zero
6983 // capacity and allocates. The trim drops `keep` rows at a time
6984 // and their buffers come back here instead of being freed.
6985 let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
6986 let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
6987 // v7.39 (round 581) — the worst row the accumulator is currently
6988 // keeping. Anything that loses to it cannot reach the answer, so
6989 // it is dropped before its projection is ever built.
6990 let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
6991 // v7.39 (round 582) — resolve each ORDER BY column once, not
6992 // once per row. See `order_by_bound_positions`.
6993 let order_bound =
6994 crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
6995 // v7.39 (round 581) — and it stops asking when the answer is
6996 // always "keep".
6997 //
6998 // The check earns its place only on rows it rejects. Over
6999 // ascending ids, `ORDER BY id DESC` never rejects one — every
7000 // row beats the current worst — so the comparison is pure
7001 // overhead there, measured at +5.5% in three batches out of
7002 // three. After a window of rows it looks at what it has
7003 // actually rejected and switches itself off if the shape is not
7004 // paying. The answers do not depend on it either way.
7005 const BOUNDARY_WINDOW: u32 = 8192;
7006 let mut boundary_checks: u32 = 0;
7007 let mut boundary_rejects: u32 = 0;
7008 let mut boundary_check_on = true;
7009 // Inline the per-row work in a closure so the indexed and full-
7010 // scan branches share the body.
7011 let mut process_row = |row: &Row<'static>, loop_idx: usize| -> Result<(), EngineError> {
7012 if loop_idx.is_multiple_of(256) {
7013 cancel.check()?;
7014 }
7015 if let Some(cw) = &compiled_where {
7016 let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7017 .map_err(EngineError::Eval)?;
7018 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7019 return Ok(());
7020 }
7021 } else if let Some(where_expr) = &stmt.where_ {
7022 let cond =
7023 self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7024 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7025 return Ok(());
7026 }
7027 }
7028 // Under DISTINCT the keys are built AFTER the dup probe
7029 // (survivors only); the non-distinct order is unchanged.
7030 // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7031 // row further down, and building them here would evaluate the
7032 // ORDER BY against the INPUT row: a key naming the SRF's own
7033 // output became a scalar call to it, which is where
7034 // "function unnest(integer[]) does not exist" came from.
7035 let order_keys = if order_by.is_empty() || stmt.distinct || srf_position.is_some() {
7036 Vec::new()
7037 } else {
7038 let mut buf = key_pool.pop().unwrap_or_default();
7039 crate::orderby::build_order_keys_bound(
7040 &order_by,
7041 &order_bound,
7042 &order_colls,
7043 row,
7044 &ctx,
7045 &mut buf,
7046 )?;
7047 // v7.39 (round 581) — reject before projecting.
7048 //
7049 // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7050 // 50 distinct `g` decides nearly every row on the FIRST
7051 // key, and PG answers it FASTER than the single-key form
7052 // (7.4 ms against 10.4) because a rejected row costs it
7053 // one comparison. SPG built both keys AND the projected
7054 // row for all 500k before throwing them away. The keys
7055 // are needed to compare; the projection is not.
7056 if boundary_check_on
7057 && let Some((_, descs)) = &topk_stream
7058 && let Some(b) = &topk_boundary
7059 {
7060 boundary_checks += 1;
7061 let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7062 == core::cmp::Ordering::Greater;
7063 if loses {
7064 boundary_rejects += 1;
7065 }
7066 if boundary_checks == BOUNDARY_WINDOW {
7067 // Keep asking only if it has been rejecting at
7068 // least a quarter of what it saw.
7069 boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7070 }
7071 if loses {
7072 buf.clear();
7073 key_pool.push(buf);
7074 return Ok(());
7075 }
7076 }
7077 buf
7078 };
7079 if srf_position.is_some() {
7080 let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7081 for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7082 if stmt.distinct {
7083 let bucket = seen_distinct
7084 .entry(norm_hash_row(
7085 &out,
7086 &distinct_hb,
7087 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7088 ))
7089 .or_default();
7090 if bucket.iter().any(|i| {
7091 row_eq_norm(
7092 &tagged[i].1,
7093 &out,
7094 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7095 )
7096 }) {
7097 continue;
7098 }
7099 bucket.push(tagged.len());
7100 }
7101 budget.charge(approx_row_bytes(&out))?;
7102 // The keys come from THIS expanded row: a key naming a
7103 // select-list item reads its value, anything else is
7104 // still evaluated against the input row.
7105 let keys = if order_by.is_empty() {
7106 Vec::new()
7107 } else {
7108 let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7109 for (k, ob) in order_by.iter().enumerate() {
7110 kv.push(match srf_order_cols.get(k).copied().flatten() {
7111 Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7112 None => eval::eval_expr(&ob.expr, row, &ctx)
7113 .map_err(EngineError::Eval)?,
7114 });
7115 }
7116 // Packed by the same code every other ORDER BY uses,
7117 // so DESC / NULLS FIRST / the MySQL rule are not
7118 // restated here.
7119 let key_row = Row::new(kv);
7120 let mut buf = Vec::new();
7121 crate::orderby::build_order_keys_bound(
7122 &order_by,
7123 &srf_key_bound,
7124 &order_colls,
7125 &key_row,
7126 &ctx,
7127 &mut buf,
7128 )?;
7129 buf
7130 };
7131 tagged.push((keys, out));
7132 }
7133 } else {
7134 let values = &mut proj_buf;
7135 values.clear();
7136 values.reserve(projection.len());
7137 for (i, p) in projection.iter().enumerate() {
7138 // v7.37.x (docker-fair SCALARSQ attack) — pre-
7139 // analysed PK-probe fast path. The per-row work is
7140 // a read of outer.col from the row plus an index
7141 // probe — no Expr clone, no walker, no
7142 // `eval_expr_with_correlated` framework.
7143 if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7144 values.push(self.probe_with_pk_fast_path(fp, row));
7145 continue;
7146 }
7147 // v7.39 (round 605) — the same value every row.
7148 if any_proj_const && let Some(v) = &proj_const[i] {
7149 values.push(v.clone());
7150 continue;
7151 }
7152 // v7.39 (round 487) — bound column: read the cell.
7153 // This is `rehydrate_cell`'s body for a non-composite
7154 // column, which is what the whole chain below reduces
7155 // to once the name has been resolved.
7156 if any_proj_direct && let Some(pos) = proj_direct[i] {
7157 crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7158 values.push(row.values[pos].clone().into_owned());
7159 continue;
7160 }
7161 // v7.24 (round-16 B) — correlated-aware.
7162 // v7.37.x (docker-fair SCALARSQ attack) — share the
7163 // per-row memo with projection. Required for the
7164 // batch-evaluated correlated-scalar path to fire on
7165 // SELECT-item scalar subqueries; otherwise each row
7166 // re-executes the inner.
7167 //
7168 // Skip the memo when the outer row count is small
7169 // (early-limited): the batch path scans the FULL
7170 // inner table to build a GroupMap (~5 ms for a
7171 // 12.5 k-row inner), while per-row execution with a
7172 // PK index seek is ~5 µs per call — much cheaper for
7173 // N ≤ ~1000 outer rows.
7174 let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7175 let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7176 values.push(
7177 self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7178 );
7179 }
7180 crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7181 if stmt.distinct {
7182 let bucket = seen_distinct
7183 .entry(norm_hash_values(
7184 &proj_buf,
7185 &distinct_hb,
7186 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7187 ))
7188 .or_default();
7189 if bucket.iter().any(|i| {
7190 values_eq_norm(
7191 &tagged[i].1.values,
7192 &proj_buf,
7193 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7194 )
7195 }) {
7196 crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7197 return Ok(());
7198 }
7199 bucket.push(tagged.len());
7200 }
7201 let out = Row::new(core::mem::replace(
7202 &mut proj_buf,
7203 proj_pool.pop().unwrap_or_default(),
7204 ));
7205 let order_keys = if stmt.distinct && !order_by.is_empty() {
7206 // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7207 // the bound-cell path precisely so an ORDER BY key that
7208 // names a column is READ instead of evaluated, and the
7209 // non-DISTINCT branch above has passed it ever since;
7210 // this branch never did, so `SELECT DISTINCT k .. ORDER
7211 // BY k` resolved "k" by string for every surviving row.
7212 let mut buf = key_pool.pop().unwrap_or_default();
7213 crate::orderby::build_order_keys_bound(
7214 &order_by,
7215 &order_bound,
7216 &order_colls,
7217 row,
7218 &ctx,
7219 &mut buf,
7220 )?;
7221 buf
7222 } else {
7223 order_keys
7224 };
7225 budget.charge(approx_row_bytes(&out))?;
7226 tagged.push((order_keys, out));
7227 }
7228 // Streaming top-N: bound the accumulator to O(keep) rows.
7229 if let Some((k, descs)) = &topk_stream {
7230 crate::orderby::topk_trim_recycling(
7231 &mut tagged,
7232 *k,
7233 descs,
7234 &mut proj_pool,
7235 &mut key_pool,
7236 &mut topk_boundary,
7237 );
7238 }
7239 Ok(())
7240 };
7241 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7242 // load-bearing full-scan path. This is the primary single-table
7243 // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7244 // in-place writers retain dead/old versions, an ungated scan
7245 // here would return them, so the gate must land BEFORE the
7246 // writers flip (see the plan's activation-order rule). A no-op
7247 // today: every hot row is frozen or committed-and-alive under
7248 // the reader's snapshot, so `is_row_visible` returns true for
7249 // all of them (verified by the full e2e suite staying green).
7250 let scan_snapshot = self.current_snapshot();
7251 let mut emitted: usize = 0;
7252 if let Some(rows) = &indexed_rows {
7253 for (loop_idx, cow) in rows.iter().enumerate() {
7254 if let Some(cap) = early_cap
7255 && emitted >= cap
7256 {
7257 break;
7258 }
7259 process_row(cow.as_ref(), loop_idx)?;
7260 emitted = emitted.saturating_add(1);
7261 }
7262 } else {
7263 // v7.39 (round 570) — the row store is a 32-way trie, so
7264 // indexing it is four dependent loads. Round 567 measured
7265 // -18% on the aggregate scan from holding the leaf between
7266 // rows; this is the same loop for the projecting scan.
7267 let mut rows_cur = table.rows().run_cursor();
7268 // v7.38.11 — see the aggregate scan above: a BRIN index on a
7269 // column this WHERE bounds says which slots cannot match.
7270 let brin_slots = stmt
7271 .where_
7272 .as_ref()
7273 .and_then(|w| crate::brin::candidate_slots(w, table))
7274 .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7275 for i in brin_slots.into_iter().flatten() {
7276 if let Some(cap) = early_cap
7277 && emitted >= cap
7278 {
7279 break;
7280 }
7281 // Skip rows this snapshot cannot see (invisible rows do
7282 // not count toward the LIMIT).
7283 if !table.is_row_visible(i, &scan_snapshot) {
7284 continue;
7285 }
7286 let Some(row) = rows_cur.get(i) else { continue };
7287 process_row(row, i)?;
7288 emitted = emitted.saturating_add(1);
7289 }
7290 // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7291 // rows into the same loop. The full-scan path here is the
7292 // load-bearing single-table SELECT executor, and pre-
7293 // 7.35.1 it only walked `table.rows()` (hot), so any
7294 // `SELECT … FROM t` against a table with cold segments
7295 // silently returned a subset.
7296 let cold_rows = self.iter_cold_rows_of_table(table);
7297 for (offset, row) in cold_rows.iter().enumerate() {
7298 if let Some(cap) = early_cap
7299 && emitted >= cap
7300 {
7301 break;
7302 }
7303 process_row(row, table.row_count() + offset)?;
7304 emitted = emitted.saturating_add(1);
7305 }
7306 }
7307
7308 // (DISTINCT already de-duped STREAMING inside process_row, so the
7309 // sort below only sees the u survivors and the partial-sort
7310 // budget applies to DISTINCT too.)
7311 if !order_by.is_empty() {
7312 // Partial-sort fast path: when LIMIT is small relative to
7313 // the row count, select_nth_unstable + sort just the
7314 // prefix is O(n + k log k) instead of O(n log n).
7315 // WITH TIES needs the full sort so the tie extension can
7316 // scan past `limit` to find rows that share the last-kept
7317 // row's key.
7318 let keep = if stmt.limit_with_ties
7319 // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7320 // forces the full-sort fallback by suppressing the
7321 // partial-sort `keep` budget. See
7322 // `xtests/sigil/test-mode-gucs.md`.
7323 || self.env_cfg().disable_topk
7324 {
7325 None
7326 } else {
7327 stmt.limit_literal()
7328 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7329 };
7330 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7331 crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7332 }
7333
7334 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7335 // past the truncated tail through every row that shares the
7336 // last-kept row's ORDER BY key. The tie check uses the
7337 // already-computed `(order_keys, row)` pairs so it matches
7338 // the sort comparator exactly. DISTINCT + WITH TIES falls
7339 // through to the no-ties path (PG also disallows their
7340 // combination; SPG silently drops the tie extension here so
7341 // the customer doesn't see a hard error mid-query — the
7342 // user-visible result is still correct, just narrower).
7343 let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7344 apply_offset_and_limit_tagged(
7345 &mut tagged,
7346 stmt.offset_literal(),
7347 stmt.limit_literal(),
7348 true,
7349 );
7350 tagged.into_iter().map(|(_, r)| r).collect()
7351 } else {
7352 // DISTINCT already de-duped pre-sort above.
7353 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7354 apply_offset_and_limit(
7355 &mut output_rows,
7356 stmt.offset_literal(),
7357 stmt.limit_literal(),
7358 );
7359 output_rows
7360 };
7361
7362 let columns: Vec<ColumnSchema> = projection
7363 .into_iter()
7364 .map(|p| {
7365 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
7366 c.user_enum_type = p.user_enum_type;
7367 c.collation_name = p.collation_name;
7368 c.mysql_fsp = p.mysql_fsp;
7369 c
7370 })
7371 .collect();
7372
7373 Ok(QueryResult::Rows {
7374 columns,
7375 rows: output_rows,
7376 })
7377 }
7378
7379 /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7380 /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7381 /// select items for the surviving rows only — PG's Result-above-
7382 /// Limit shape, where SubPlan loops equal the OUTPUT row count
7383 /// (50) instead of the group count (24k).
7384 fn finish_agg_result(
7385 &self,
7386 mut agg: aggregate::AggResult,
7387 stmt: &SelectStatement,
7388 cancel: CancelToken<'_>,
7389 ) -> Result<QueryResult, EngineError> {
7390 apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7391 if !agg.deferred.is_empty() {
7392 apply_offset_and_limit(
7393 &mut agg.synth_rows,
7394 stmt.offset_literal(),
7395 stmt.limit_literal(),
7396 );
7397 let ctx = EvalContext::new(&agg.synth_schema, None);
7398 let mut memo = memoize::MemoizeCache::default();
7399 // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7400 // Deferred subqueries are referenced only by surviving
7401 // select-list rows (≤ LIMIT), so their correlation keys are
7402 // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7403 // each batchable subquery's group map over just those keys
7404 // via per-key index seek; the per-row splice loop below then
7405 // reuses the seeded map. A join-shaped or un-indexed inner
7406 // falls through to the all-keys batch inside the call (built
7407 // eagerly here instead of lazily on row 0 — same cost), so
7408 // it still pays the full scan, never the 715 ms per-row
7409 // direct eval; its index-nested-loop probe is the next
7410 // knife. Genuinely non-batchable shapes return None and are
7411 // left unseeded for the loop's per-row resolver, as before.
7412 for (_, expr) in &agg.deferred {
7413 let mut subs: Vec<&SelectStatement> = Vec::new();
7414 collect_scalar_subqueries(expr, &mut subs);
7415 for sub in subs {
7416 let repr = alloc::format!("{sub}");
7417 if memo.group_maps.contains_key(&repr) {
7418 continue;
7419 }
7420 if let Some(gm) = self.try_batch_correlated_scalar(
7421 sub,
7422 Some((&agg.synth_rows, &ctx)),
7423 cancel,
7424 )? {
7425 memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7426 }
7427 }
7428 }
7429 for (ri, srow) in agg.synth_rows.iter().enumerate() {
7430 cancel.check()?;
7431 for (col, expr) in &agg.deferred {
7432 let v =
7433 self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7434 if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7435 *cell = v;
7436 }
7437 }
7438 }
7439 }
7440 Ok(QueryResult::Rows {
7441 columns: agg.columns,
7442 rows: agg.rows,
7443 })
7444 }
7445
7446 /// v7.37 — streaming projection for the joined-non-aggregate
7447 /// shape (multi-table FROM, all projection items bound, no
7448 /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
7449 /// UNION). Walks the deferred join survivors and emits
7450 /// `&[&Value]` borrowed straight out of the source tables — no
7451 /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
7452 /// on the mailrs `PROJ` shape (about 4 ms saved).
7453 ///
7454 /// Returns `Ok(None)` when the shape doesn't qualify; the caller
7455 /// then falls back to the materialising path.
7456 /// v7.37 (round 831) — stream a joinless SELECT straight off the
7457 /// stored table, one row at a time, without ever building a row set.
7458 ///
7459 /// Returns `Ok(None)` for anything this cannot serve, and the caller
7460 /// falls through to the deferred-join path exactly as before: a
7461 /// missing table, or a cold tier whose hydration the fallback handles.
7462 /// Sort a single-table scan through the external sorter, so the
7463 /// answer's size is bounded by `work_mem` and not by the input.
7464 ///
7465 /// Sorting held every row twice — the scan's `Vec<Row>` and the
7466 /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
7467 /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
7468 /// enough ORDER BY took the server down, which is a liveness
7469 /// problem before it is a performance one.
7470 ///
7471 /// A SEPARATE walk rather than a change to `run_single_table_scan`,
7472 /// following what round 831 did for the joinless shape. That
7473 /// function is 552 lines whose projection loop is entangled with
7474 /// DISTINCT (which indexes back into the tagged vector) and with
7475 /// streaming top-N (whose boundary moves as the scan runs); both
7476 /// assume the projection has already happened when a row is
7477 /// pushed, which is exactly what spilling has to defer. Two earlier
7478 /// attempts tried to rework that loop and were reverted. Here the
7479 /// existing path is untouched and this one only claims shapes it
7480 /// can serve, so a decline costs nothing.
7481 ///
7482 /// Records are SOURCE rows, not projected ones: `finish` re-derives
7483 /// keys from what it decodes, and an ORDER BY key need not be in
7484 /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
7485 fn try_spill_sorted_scan(
7486 &self,
7487 stmt: &SelectStatement,
7488 from: &FromClause,
7489 cancel: CancelToken<'_>,
7490 ) -> Result<Option<QueryResult>, EngineError> {
7491 // Shapes this walk does not serve. Each one either needs the
7492 // whole tagged vector addressable (DISTINCT probes back into
7493 // it, WITH TIES re-reads its tail) or is already bounded
7494 // without spilling (a LIMIT makes the partial sort O(keep)).
7495 if !self.can_spill()
7496 || stmt.order_by.is_empty()
7497 || stmt.distinct
7498 || stmt.limit_with_ties
7499 || stmt.limit_literal().is_some()
7500 || !from.joins.is_empty()
7501 || from.primary.lateral_subquery.is_some()
7502 || from.primary.unnest_expr.is_some()
7503 || from.primary.generate_series_args.is_some()
7504 || select_has_window(stmt)
7505 {
7506 return Ok(None);
7507 }
7508 // A parent's rows are its children's. These walks scan the named
7509 // relation alone, so a partitioned or inherited parent comes back
7510 // short — and silently: the corpus caught `SELECT id FROM pr
7511 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
7512 // parent's own rows instead of the partitions'. `ONLY` is exactly
7513 // the case that does not fan out, so it stays, which is the test
7514 // the FROM-clause fan-out itself makes.
7515 if !from.primary.only
7516 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7517 {
7518 return Ok(None);
7519 }
7520 let Some(table) = self.active_catalog().get(&from.primary.name) else {
7521 return Ok(None);
7522 };
7523 // Cold-tier rows live outside `rows()`; this walk would drop
7524 // them silently, the same reason round 831's walk declines.
7525 if table.has_cold_rows_fast() {
7526 return Ok(None);
7527 }
7528
7529 let alias = from
7530 .primary
7531 .alias
7532 .as_deref()
7533 .unwrap_or(from.primary.name.as_str());
7534 let cols = table.schema().columns.clone();
7535 let sess = self.dml_session();
7536 let ctx = EvalContext::new(&cols, Some(alias))
7537 .with_catalog(self.active_catalog())
7538 .with_session(&sess);
7539 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7540 let order_by = stmt.order_by.clone();
7541 // The same one-shot resolution the general path does (round
7542 // 582): each ORDER BY column is bound once, not once per row.
7543 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
7544 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7545 // Resolved BEFORE the scan, because it now decides what the sort
7546 // STORES and not just what it decodes (round 995).
7547 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
7548
7549 let mut sorter = crate::extsort::ExternalSorter::new(
7550 self.temp_run_factory,
7551 self.session_work_mem_bytes(),
7552 cols.clone(),
7553 &descs,
7554 )
7555 .with_stats(&self.spill_stats)
7556 .with_pruned(&needed);
7557 let snapshot = self.current_snapshot();
7558 // One key buffer for the whole scan: `push` drains it and leaves
7559 // the capacity behind.
7560 let mut keys: Vec<OrderKey> = Vec::new();
7561 // r1024 — compile the predicate once for the scan.
7562 //
7563 // These two sorted-spill scans are the paths a single-table SELECT
7564 // with an ORDER BY takes, and they were the last row-returning ones
7565 // still walking the expression tree per row. r1023 did the
7566 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
7567 // exactly this shape.
7568 //
7569 // Found from the profile's CALL TREE rather than its leaves. The
7570 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
7571 // 261, `mod_op` 178 — and two attempts at reasoning out which
7572 // function asked for it were both wrong. The tree names the caller
7573 // chain, and it named this one.
7574 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7575 .where_
7576 .as_ref()
7577 .filter(|w| crate::eval::fully_compilable(w))
7578 .map(|w| crate::eval::compile_expr(w, &ctx));
7579 let mut eval_stack: Vec<Value<'static>> = Vec::new();
7580 for (i, row) in table.scan_visible_from(0, &snapshot) {
7581 if i.is_multiple_of(256) {
7582 cancel.check()?;
7583 }
7584 if let Some(c) = &compiled_where {
7585 if !crate::eval::compiled::eval_compiled_pred(
7586 c,
7587 row,
7588 &ctx,
7589 &mut eval_stack,
7590 ctx.mysql_dialect,
7591 )? {
7592 continue;
7593 }
7594 } else if let Some(w) = &stmt.where_ {
7595 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
7596 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7597 continue;
7598 }
7599 }
7600 keys.clear();
7601 // `&[]`: this sorter compares with `cmp_multi_key_in(.., &[])`
7602 // (extsort.rs:213/543/1221), so the key must stay folded or the
7603 // two would disagree. See `build_order_keys_bound`.
7604 crate::orderby::build_order_keys_bound(
7605 &order_by,
7606 &order_bound,
7607 &[],
7608 row,
7609 &ctx,
7610 &mut keys,
7611 )?;
7612 sorter.push(&mut keys, row)?;
7613 }
7614
7615 let key_ctx = &ctx;
7616 let rows = sorter.finish(
7617 |src, buf| {
7618 crate::orderby::build_order_keys_bound(
7619 &order_by,
7620 &order_bound,
7621 &[],
7622 src,
7623 key_ctx,
7624 buf,
7625 )
7626 },
7627 |src| {
7628 let mut values = Vec::with_capacity(projection.len());
7629 for p in &projection {
7630 values.push(
7631 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
7632 );
7633 }
7634 Ok(Row::new(values))
7635 },
7636 )?;
7637
7638 let columns: Vec<ColumnSchema> = projection
7639 .iter()
7640 .map(|p| {
7641 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7642 c.user_enum_type = p.user_enum_type.clone();
7643 c.mysql_fsp = p.mysql_fsp;
7644 c
7645 })
7646 .collect();
7647 Ok(Some(QueryResult::Rows { columns, rows }))
7648 }
7649
7650 /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
7651 /// handing each row to the consumer instead of collecting the answer.
7652 ///
7653 /// That walk bounds the SORT and then returns `QueryResult::Rows`,
7654 /// which holds every output row. Measured at `work_mem = 4 MB` over
7655 /// 200-byte rows, RSS above the server's own baseline while the
7656 /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
7657 /// at 400k — linear — while the spill underneath worked correctly
7658 /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
7659 /// removes each file, so a count taken afterwards reads 0 whatever
7660 /// happened, and an earlier reading of "no spill at all" was that
7661 /// blind witness). The growth is the collected result, not the sort.
7662 ///
7663 /// Emitting makes peak the budget, one buffer per run and a single
7664 /// row — the state a merge already holds at every step. It also
7665 /// frees each projected row as the next is built rather than
7666 /// accumulating them, which is where the time is: a profile of the
7667 /// collecting walk put the allocator at 586 samples, more than every
7668 /// sort comparison combined (420), against 19 for `push` itself.
7669 /// v7.37 (round 923) — which of a sort record's columns the output half
7670 /// reads. The record is the SOURCE row (round 836), so a narrow projection
7671 /// decoded every column: skipping one 200-byte text halves a decode
7672 /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
7673 ///
7674 /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
7675 /// column reads NULL. Answers only when every projection item is a bare
7676 /// column reference AND every ORDER BY key is a bound column; anything
7677 /// else returns empty, decoding everything as before.
7678 /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
7679 /// drops references from expression kinds it does not enumerate.
7680 ///
7681 /// ORDER BY columns are included — the merge re-derives keys from the
7682 /// decoded row on the spilled path, so pruning one would sort NULLs.
7683 pub(crate) fn sort_record_columns_needed(
7684 items: &[SelectItem],
7685 order_bound: &[Option<usize>],
7686 arity: usize,
7687 ctx: &EvalContext,
7688 ) -> Vec<bool> {
7689 let all_bare = items.iter().all(|i| {
7690 matches!(
7691 i,
7692 SelectItem::Expr {
7693 expr: Expr::Column(_),
7694 ..
7695 }
7696 )
7697 });
7698 if !all_bare || order_bound.iter().any(Option::is_none) {
7699 return Vec::new();
7700 }
7701 let mut mask = alloc::vec![false; arity];
7702 for item in items {
7703 if let SelectItem::Expr {
7704 expr: Expr::Column(c),
7705 ..
7706 } = item
7707 {
7708 match crate::eval::find_column_pos(c, ctx) {
7709 Some(p) if p < arity => mask[p] = true,
7710 _ => return Vec::new(),
7711 }
7712 }
7713 }
7714 for p in order_bound.iter().flatten() {
7715 if *p < arity {
7716 mask[*p] = true;
7717 } else {
7718 return Vec::new();
7719 }
7720 }
7721 mask
7722 }
7723
7724 /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
7725 /// of sorting.
7726 ///
7727 /// PG serves such an ordering from the index and never sorts. We sorted:
7728 /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
7729 /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
7730 /// the sorter's own round trip — `ExternalSorter::finish_each` →
7731 /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
7732 /// Every row is encoded into the sorter's arena and decoded back out,
7733 /// for an order the index already holds.
7734 ///
7735 /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
7736 /// because it was built for top-N. This is the unbounded sibling.
7737 ///
7738 /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
7739 /// from a btree, so walking one would silently drop those rows. That is
7740 /// exactly the defect r1020 fixed on the top-N path, where it had
7741 /// shipped.
7742 /// r1044 — the index this statement's ORDER BY can be WALKED on,
7743 /// instead of sorted, or `None`.
7744 ///
7745 /// Extracted so `EXPLAIN` can ask the same question the executor
7746 /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
7747 /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
7748 /// while the executor walked the primary key — 34.9 ms against
7749 /// 147.0 for the same query ordered by an unindexed column, so the
7750 /// walk was plainly running. Round 551 fixed a different case of
7751 /// this and wrote the reason down: EXPLAIN is the first thing any
7752 /// performance question opens, and an instrument that misnames the
7753 /// access path is worse than one that says nothing.
7754 ///
7755 /// The gate is here once. Two copies of it is how the plan and the
7756 /// executor come to disagree again.
7757 pub(crate) fn index_order_walk_target(
7758 &self,
7759 stmt: &SelectStatement,
7760 from: &FromClause,
7761 ) -> Option<(String, usize)> {
7762 if stmt.order_by.len() != 1
7763 || !stmt.distinct_on.is_empty()
7764 || stmt.limit_with_ties
7765 || stmt.limit.is_some()
7766 || stmt.offset.is_some()
7767 || stmt.having.is_some()
7768 || stmt.group_by.is_some()
7769 || !stmt.unions.is_empty()
7770 || !from.joins.is_empty()
7771 || from.primary.lateral_subquery.is_some()
7772 || from.primary.unnest_expr.is_some()
7773 || from.primary.as_of_segment.is_some()
7774 || from.primary.generate_series_args.is_some()
7775 || select_has_window(stmt)
7776 || aggregate::uses_aggregate(stmt)
7777 {
7778 return None;
7779 }
7780 if stmt
7781 .items
7782 .iter()
7783 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7784 {
7785 return None;
7786 }
7787 let table = self.active_catalog().get(&from.primary.name)?;
7788 if table.has_cold_rows_fast() {
7789 return None;
7790 }
7791 if !from.primary.only
7792 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7793 {
7794 return None;
7795 }
7796 let alias = from
7797 .primary
7798 .alias
7799 .as_deref()
7800 .unwrap_or(from.primary.name.as_str());
7801 let cols = &table.schema().columns;
7802 let order = &stmt.order_by[0];
7803 let Expr::Column(oc) = &order.expr else {
7804 return None;
7805 };
7806 if let Some(q) = &oc.qualifier
7807 && !q.eq_ignore_ascii_case(alias)
7808 {
7809 return None;
7810 }
7811 let order_pos = cols
7812 .iter()
7813 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
7814 // r1047 — DISTINCT joins the walk when the projection IS the
7815 // order column, and only then. The index's keys are canonical
7816 // (r1039: representation equality is value equality — the
7817 // property every seek already depends on), so one key is one
7818 // distinct value and the walk can emit the first passing row of
7819 // each key group instead of hashing every row. On the release
7820 // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
7821 // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
7822 // with an ablation floor of 14.8, because the hash must
7823 // normalize and probe ALL the rows; the walk visits each key
7824 // once. A wider projection makes DISTINCT about the whole tuple,
7825 // not the key, so anything else still declines.
7826 if stmt.distinct {
7827 let only_the_order_column = stmt.items.len() == 1
7828 && match &stmt.items[0] {
7829 SelectItem::Expr {
7830 expr: Expr::Column(c),
7831 ..
7832 } => {
7833 c.name.eq_ignore_ascii_case(&oc.name)
7834 && match &c.qualifier {
7835 Some(q) => q.eq_ignore_ascii_case(alias),
7836 None => true,
7837 }
7838 }
7839 _ => false,
7840 };
7841 if !only_the_order_column {
7842 return None;
7843 }
7844 }
7845 // r1046 — a nullable key no longer refuses the walk; it changes
7846 // what the walk has to do. A NULL key is not in the btree, so
7847 // walking alone would silently drop those rows — the r1020
7848 // defect, which shipped once. The walk emits them separately, at
7849 // the end SQL puts them.
7850 //
7851 // Refusing was costing every nullable indexed column a 3.4x:
7852 // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
7853 // 72.0 ms with the column nullable and 20.2 with the same data
7854 // under NOT NULL. `NOT NULL` is not the default, so that was the
7855 // common case paying for the uncommon one.
7856 let index = table.index_on(order_pos)?;
7857 if !matches!(index.kind, spg_storage::IndexKind::BTree(_))
7858 || index.expression.is_some()
7859 || index.partial_predicate.is_some()
7860 {
7861 return None;
7862 }
7863 Some((index.name.clone(), order_pos))
7864 }
7865
7866 fn try_index_order_stream<F>(
7867 &self,
7868 stmt: &SelectStatement,
7869 from: &FromClause,
7870 cancel: CancelToken<'_>,
7871 emit: &mut F,
7872 ) -> Result<Option<usize>, EngineError>
7873 where
7874 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7875 {
7876 // r1044 — the shape gate lives in `index_order_walk_target`, so
7877 // `EXPLAIN` answers the same question. What stays here is the
7878 // part that RAISES (an illegal ORDER BY has to keep erroring
7879 // from where it did) and the bindings the walk needs.
7880 crate::orderby::check_order_by_legality(stmt)?;
7881 crate::orderby::check_order_by_positions(stmt)?;
7882 crate::window::reject_window_in_row_clauses(stmt)?;
7883 let Some((_, order_pos)) = self.index_order_walk_target(stmt, from) else {
7884 return Ok(None);
7885 };
7886 let Some(table) = self.active_catalog().get(&from.primary.name) else {
7887 return Ok(None);
7888 };
7889 let alias = from
7890 .primary
7891 .alias
7892 .as_deref()
7893 .unwrap_or(from.primary.name.as_str());
7894 let cols = table.schema().columns.clone();
7895 let order = &stmt.order_by[0];
7896 let Some(index) = table.index_on(order_pos) else {
7897 return Ok(None);
7898 };
7899
7900 let sess = self.dml_session();
7901 let ctx = EvalContext::new(&cols, Some(alias))
7902 .with_catalog(self.active_catalog())
7903 .with_session(&sess);
7904 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7905 let columns: Vec<ColumnSchema> = projection
7906 .iter()
7907 .map(|p| {
7908 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7909 c.user_enum_type = p.user_enum_type.clone();
7910 c.mysql_fsp = p.mysql_fsp;
7911 c
7912 })
7913 .collect();
7914 emit(crate::StreamItem::Header(&columns))?;
7915 let bound_pos: Vec<Option<usize>> = projection
7916 .iter()
7917 .map(|p| match &p.expr {
7918 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
7919 Ok(Some(pos)) => Some(pos),
7920 _ => None,
7921 },
7922 _ => None,
7923 })
7924 .collect();
7925
7926 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7927 .where_
7928 .as_ref()
7929 .filter(|w| crate::eval::fully_compilable(w))
7930 .map(|w| crate::eval::compile_expr(w, &ctx));
7931 let mut eval_stack: Vec<Value<'static>> = Vec::new();
7932 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
7933 let snapshot = self.current_snapshot();
7934
7935 // A btree holds one locator per row VERSION, so a row whose key was
7936 // updated can sit under two keys and a dead one can sit beside its
7937 // replacement. The visibility gate drops the dead; `seen` drops a
7938 // live row that the walk reaches twice, which would otherwise be a
7939 // duplicated output row rather than a slow one.
7940 let mut emitted_rows = alloc::vec![false; table.rows().len()];
7941
7942 // r1046 — the rows the index cannot hold.
7943 //
7944 // A NULL key is not in the btree, so the walk below never reaches
7945 // those rows; they are emitted here, at the end SQL puts them.
7946 // PG's default is NULLS LAST ascending and NULLS FIRST
7947 // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
7948 // the same rule `order_by_value_cmp_raw` applies to the sort this
7949 // replaces, so the two orders agree.
7950 //
7951 // Finding them costs one pass over the column. That pass is why
7952 // this is still worth doing: the sort it replaces encodes and
7953 // decodes every row, and the walk plus the pass measured 72.0 ms
7954 // down to about 22 on 400,000 rows.
7955 let nulls_first = order.nulls_first.unwrap_or(order.desc);
7956 // r1047 — under DISTINCT the walk emits the FIRST passing row of
7957 // each key group and skips the rest; the gate admits DISTINCT
7958 // only when the projection is the order column itself, so one
7959 // canonical key is one output row. NULL is one distinct value,
7960 // so the NULL pass stops at its first emit too.
7961 let distinct = stmt.distinct;
7962 let mut count = 0usize;
7963 let mut visited = 0usize;
7964 let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
7965 eval_stack: &mut Vec<Value<'static>>,
7966 values: &mut Vec<Value<'static>>,
7967 visited: &mut usize,
7968 emit: &mut F|
7969 -> Result<usize, EngineError> {
7970 if !cols[order_pos].nullable {
7971 return Ok(0);
7972 }
7973 let mut n = 0usize;
7974 for (ri, row) in table.rows().iter().enumerate() {
7975 if !matches!(row.values.get(order_pos), Some(Value::Null)) {
7976 continue;
7977 }
7978 if emitted_rows.get(ri).copied().unwrap_or(true) {
7979 continue;
7980 }
7981 if !table.is_row_visible(ri, &snapshot) {
7982 continue;
7983 }
7984 *visited += 1;
7985 if visited.is_multiple_of(256) {
7986 cancel.check()?;
7987 }
7988 emitted_rows[ri] = true;
7989 if Self::stream_project_row(
7990 row,
7991 stmt.where_.as_ref(),
7992 compiled_where.as_ref(),
7993 eval_stack,
7994 &projection,
7995 &bound_pos,
7996 &ctx,
7997 values,
7998 emit,
7999 )? {
8000 n += 1;
8001 if distinct {
8002 break;
8003 }
8004 }
8005 }
8006 Ok(n)
8007 };
8008
8009 if nulls_first {
8010 count += emit_null_rows(
8011 &mut emitted_rows,
8012 &mut eval_stack,
8013 &mut values,
8014 &mut visited,
8015 emit,
8016 )?;
8017 }
8018
8019 let walker: alloc::boxed::Box<
8020 dyn Iterator<Item = (&spg_storage::IndexKey, &spg_storage::PostingList)>,
8021 > = if order.desc {
8022 alloc::boxed::Box::new(index.iter_desc())
8023 } else {
8024 alloc::boxed::Box::new(index.iter_asc())
8025 };
8026 for (_key, locators) in walker {
8027 for loc in locators {
8028 let spg_storage::RowLocator::Hot(ri) = *loc else {
8029 continue;
8030 };
8031 if emitted_rows.get(ri).copied().unwrap_or(true) {
8032 continue;
8033 }
8034 if !table.is_row_visible(ri, &snapshot) {
8035 continue;
8036 }
8037 let Some(row) = table.rows().get(ri) else {
8038 continue;
8039 };
8040 visited += 1;
8041 if visited.is_multiple_of(256) {
8042 cancel.check()?;
8043 }
8044 emitted_rows[ri] = true;
8045 if Self::stream_project_row(
8046 row,
8047 stmt.where_.as_ref(),
8048 compiled_where.as_ref(),
8049 &mut eval_stack,
8050 &projection,
8051 &bound_pos,
8052 &ctx,
8053 &mut values,
8054 emit,
8055 )? {
8056 count += 1;
8057 // One row per key group: the rest are the same value.
8058 if distinct {
8059 break;
8060 }
8061 }
8062 }
8063 }
8064
8065 if !nulls_first {
8066 count += emit_null_rows(
8067 &mut emitted_rows,
8068 &mut eval_stack,
8069 &mut values,
8070 &mut visited,
8071 emit,
8072 )?;
8073 }
8074 Ok(Some(count))
8075 }
8076
8077 /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
8078 /// building an `OrderKey` vector per row.
8079 ///
8080 /// The row-returning sorted scan allocates twice per row: one
8081 /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
8082 /// projection. Counted over 400 k rows (r1030,
8083 /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
8084 /// allocations and 208 MB of traffic for an answer of four hundred
8085 /// thousand integers.
8086 ///
8087 /// The key half is pure ceremony on this shape.
8088 /// `sort_tagged_by_inline_int_key` already sorts indices rather than
8089 /// rows, so the per-row vector is built, has one integer taken out of
8090 /// it, and is then dragged through the permutation — it exists to carry
8091 /// a number the row's column already held. This lane carries the number
8092 /// instead, in a fixed-size array that lives inside the buffer element
8093 /// and allocates nothing. Same idea as the predicate VM's integer lane.
8094 ///
8095 /// Declines to `None` for anything it does not cover, and every caller
8096 /// falls through to the general path, so the gate list is the
8097 /// specification.
8098 ///
8099 /// Ties: equal keys keep scan order, as the stable sort on the general
8100 /// path does. Rows that tie on every ORDER BY term are entitled to any
8101 /// order among themselves either way — see `STABILITY.md`.
8102 fn try_int_key_sorted_stream<F>(
8103 &self,
8104 stmt: &SelectStatement,
8105 from: &FromClause,
8106 cancel: CancelToken<'_>,
8107 emit: &mut F,
8108 ) -> Result<Option<usize>, EngineError>
8109 where
8110 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8111 {
8112 /// Sort terms this lane carries inline. Four covers every ORDER BY
8113 /// in the endpoint sweep and in the dogfood corpus; wider ones fall
8114 /// through rather than growing the buffer element for everybody.
8115 const MAX_KEYS: usize = 4;
8116
8117 if stmt.order_by.is_empty()
8118 || stmt.order_by.len() > MAX_KEYS
8119 || stmt.distinct
8120 || stmt.limit_with_ties
8121 || stmt.limit.is_some()
8122 || stmt.offset.is_some()
8123 || stmt.having.is_some()
8124 || stmt.group_by.is_some()
8125 || !stmt.unions.is_empty()
8126 || !from.joins.is_empty()
8127 || from.primary.lateral_subquery.is_some()
8128 || from.primary.unnest_expr.is_some()
8129 || from.primary.as_of_segment.is_some()
8130 || from.primary.generate_series_args.is_some()
8131 || select_has_window(stmt)
8132 || aggregate::uses_aggregate(stmt)
8133 {
8134 return Ok(None);
8135 }
8136 if stmt
8137 .items
8138 .iter()
8139 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8140 {
8141 return Ok(None);
8142 }
8143 crate::orderby::check_order_by_legality(stmt)?;
8144 crate::orderby::check_order_by_positions(stmt)?;
8145 crate::window::reject_window_in_row_clauses(stmt)?;
8146 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8147 return Ok(None);
8148 };
8149 if table.has_cold_rows_fast() {
8150 return Ok(None);
8151 }
8152 if !from.primary.only
8153 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8154 {
8155 return Ok(None);
8156 }
8157 let alias = from
8158 .primary
8159 .alias
8160 .as_deref()
8161 .unwrap_or(from.primary.name.as_str());
8162 let cols = table.schema().columns.clone();
8163
8164 // Every ORDER BY term must be a NOT NULL integer column of this
8165 // table. NOT NULL is what lets the key be a bare integer: with
8166 // NULLs the lane would have to carry their ordering too, and
8167 // getting that subtly wrong is the r1020 defect.
8168 let mut key_pos = [0usize; MAX_KEYS];
8169 let mut descs = [false; MAX_KEYS];
8170 // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
8171 // which the AST records as `None`; `unwrap_or(desc)` is how the
8172 // rest of the engine resolves it.
8173 let mut nulls_first = [false; MAX_KEYS];
8174 let n_keys = stmt.order_by.len();
8175 for (slot, order) in stmt.order_by.iter().enumerate() {
8176 let Expr::Column(oc) = &order.expr else {
8177 return Ok(None);
8178 };
8179 if let Some(q) = &oc.qualifier
8180 && !q.eq_ignore_ascii_case(alias)
8181 {
8182 return Ok(None);
8183 }
8184 let Some(pos) = cols
8185 .iter()
8186 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
8187 else {
8188 return Ok(None);
8189 };
8190 if !matches!(
8191 cols[pos].ty,
8192 spg_storage::DataType::SmallInt
8193 | spg_storage::DataType::Int
8194 | spg_storage::DataType::BigInt
8195 ) {
8196 return Ok(None);
8197 }
8198 key_pos[slot] = pos;
8199 descs[slot] = order.desc;
8200 nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
8201 }
8202
8203 let sess = self.dml_session();
8204 let ctx = EvalContext::new(&cols, Some(alias))
8205 .with_catalog(self.active_catalog())
8206 .with_session(&sess);
8207 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8208 let columns: Vec<ColumnSchema> = projection
8209 .iter()
8210 .map(|p| {
8211 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8212 c.user_enum_type = p.user_enum_type.clone();
8213 c.mysql_fsp = p.mysql_fsp;
8214 c
8215 })
8216 .collect();
8217 let bound_pos: Vec<Option<usize>> = projection
8218 .iter()
8219 .map(|p| match &p.expr {
8220 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8221 Ok(Some(pos)) => Some(pos),
8222 _ => None,
8223 },
8224 _ => None,
8225 })
8226 .collect();
8227 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8228 .where_
8229 .as_ref()
8230 .filter(|w| crate::eval::fully_compilable(w))
8231 .map(|w| crate::eval::compile_expr(w, &ctx));
8232
8233 // The same first-observable point the materialising planner fires,
8234 // placed after the gates so it fires exactly once: this lane runs
8235 // BEFORE that planner and would otherwise be a hole in the
8236 // panic-isolation and cancellation-race coverage rather than a
8237 // faster path through it.
8238 crate::injection_point!("planner_first_row_fetch", &stmt.from);
8239
8240 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8241 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8242 let mut budget = ByteBudget::new(self.max_query_bytes);
8243 let snapshot = self.current_snapshot();
8244 // Keys, a NULL bit per key slot, and the row. The bitmask keeps
8245 // the element small: a nullable key still costs one bit rather
8246 // than a second array.
8247 let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
8248
8249 for (ri, row) in table.rows().iter().enumerate() {
8250 if ri.is_multiple_of(256) {
8251 cancel.check()?;
8252 }
8253 if !table.is_row_visible(ri, &snapshot) {
8254 continue;
8255 }
8256 // The key comes from the STORED row, before projection: an
8257 // ORDER BY column need not appear in the select list.
8258 let mut keys = [0i64; MAX_KEYS];
8259 let mut nulls = 0u8;
8260 let mut keyed = true;
8261 for slot in 0..n_keys {
8262 match row.values.get(key_pos[slot]) {
8263 Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
8264 Some(Value::Int(v)) => keys[slot] = i64::from(*v),
8265 Some(Value::BigInt(v)) => keys[slot] = *v,
8266 Some(Value::Null) | None => nulls |= 1 << slot,
8267 // An integer column holding something else is a row
8268 // this lane cannot order; hand the whole query back
8269 // rather than guess at it.
8270 _ => {
8271 keyed = false;
8272 break;
8273 }
8274 }
8275 }
8276 if !keyed {
8277 return Ok(None);
8278 }
8279 if !Self::stream_filter_project(
8280 row,
8281 stmt.where_.as_ref(),
8282 compiled_where.as_ref(),
8283 &mut eval_stack,
8284 &projection,
8285 &bound_pos,
8286 &ctx,
8287 &mut values,
8288 )? {
8289 continue;
8290 }
8291 budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
8292 sorted.push((keys, nulls, core::mem::take(&mut values)));
8293 values.reserve(projection.len());
8294 }
8295
8296 sorted.sort_by(|a, b| {
8297 use core::cmp::Ordering;
8298 for slot in 0..n_keys {
8299 let bit = 1u8 << slot;
8300 let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
8301 (true, true) => Ordering::Equal,
8302 // Where the NULLs go is already decided — `nulls_first`
8303 // resolved DESC's default when it was read. Reversing
8304 // this for DESC as well would apply the direction
8305 // twice and put them at the wrong end.
8306 (true, false) => {
8307 if nulls_first[slot] {
8308 Ordering::Less
8309 } else {
8310 Ordering::Greater
8311 }
8312 }
8313 (false, true) => {
8314 if nulls_first[slot] {
8315 Ordering::Greater
8316 } else {
8317 Ordering::Less
8318 }
8319 }
8320 (false, false) => {
8321 let o = a.0[slot].cmp(&b.0[slot]);
8322 if descs[slot] { o.reverse() } else { o }
8323 }
8324 };
8325 if ord != Ordering::Equal {
8326 return ord;
8327 }
8328 }
8329 Ordering::Equal
8330 });
8331
8332 emit(crate::StreamItem::Header(&columns))?;
8333 let count = sorted.len();
8334 for (_, _, vals) in &sorted {
8335 emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
8336 }
8337 Ok(Some(count))
8338 }
8339
8340 fn try_spill_sorted_stream<F>(
8341 &self,
8342 stmt: &SelectStatement,
8343 from: &FromClause,
8344 cancel: CancelToken<'_>,
8345 emit: &mut F,
8346 ) -> Result<Option<usize>, EngineError>
8347 where
8348 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8349 {
8350 // The shapes `try_spill_sorted_scan` declines, plus the ones the
8351 // streaming executor does not carry (a LIMIT is already bounded
8352 // by a partial sort; the rest need the answer addressable).
8353 if !self.can_spill()
8354 || stmt.order_by.is_empty()
8355 || stmt.distinct
8356 || stmt.limit_with_ties
8357 || stmt.limit.is_some()
8358 || stmt.offset.is_some()
8359 || stmt.having.is_some()
8360 || stmt.group_by.is_some()
8361 || !stmt.unions.is_empty()
8362 || !from.joins.is_empty()
8363 || from.primary.lateral_subquery.is_some()
8364 || from.primary.unnest_expr.is_some()
8365 || from.primary.as_of_segment.is_some()
8366 || from.primary.generate_series_args.is_some()
8367 || select_has_window(stmt)
8368 || aggregate::uses_aggregate(stmt)
8369 {
8370 return Ok(None);
8371 }
8372 if stmt
8373 .items
8374 .iter()
8375 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8376 {
8377 return Ok(None);
8378 }
8379 // Everything `exec_bare_select_cancel` does before it scans runs
8380 // BELOW this path, so a statement claimed here skips it. Three of
8381 // those were missed on the way in and each was caught by a
8382 // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
8383 // ORDER BY 2` sorted happily instead of raising 42P10), the
8384 // cancellation check by another, the partition fan-out by the
8385 // differential corpus. What is reconciled, item by item: with-ties
8386 // needs ORDER BY (gated above), USING/NATURAL and RLS join
8387 // rewrites (joins gated above), the single-table RLS predicate
8388 // (the dispatcher declines a policy-subject table before this is
8389 // reached), the meta-view dispatch (those names are not in the
8390 // catalog, so the lookup below declines). These three are calls,
8391 // so the message and SQLSTATE are the ones the fall-back gives —
8392 // `select_has_window` above reads the select list and ORDER BY but
8393 // not WHERE, which is the case the third one covers.
8394 crate::orderby::check_order_by_legality(stmt)?;
8395 crate::orderby::check_order_by_positions(stmt)?;
8396 crate::window::reject_window_in_row_clauses(stmt)?;
8397 // A parent's rows are its children's. These walks scan the named
8398 // relation alone, so a partitioned or inherited parent comes back
8399 // short — and silently: the corpus caught `SELECT id FROM pr
8400 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8401 // parent's own rows instead of the partitions'. `ONLY` is exactly
8402 // the case that does not fan out, so it stays, which is the test
8403 // the FROM-clause fan-out itself makes.
8404 if !from.primary.only
8405 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8406 {
8407 return Ok(None);
8408 }
8409 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8410 return Ok(None);
8411 };
8412 // Cold-tier rows live outside `rows()`; this walk would drop
8413 // them silently, the same reason round 831's walk declines.
8414 if table.has_cold_rows_fast() {
8415 return Ok(None);
8416 }
8417
8418 let alias = from
8419 .primary
8420 .alias
8421 .as_deref()
8422 .unwrap_or(from.primary.name.as_str());
8423 let cols = table.schema().columns.clone();
8424 let sess = self.dml_session();
8425 let ctx = EvalContext::new(&cols, Some(alias))
8426 .with_catalog(self.active_catalog())
8427 .with_session(&sess);
8428 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8429 let order_by = stmt.order_by.clone();
8430 // The same one-shot resolution the general path does (round
8431 // 582): each ORDER BY column is bound once, not once per row.
8432 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8433 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8434 // Resolved BEFORE the scan, because it now decides what the sort
8435 // STORES and not just what it decodes (round 995).
8436 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8437
8438 let mut sorter = crate::extsort::ExternalSorter::new(
8439 self.temp_run_factory,
8440 self.session_work_mem_bytes(),
8441 cols.clone(),
8442 &descs,
8443 )
8444 .with_stats(&self.spill_stats)
8445 .with_pruned(&needed);
8446 let snapshot = self.current_snapshot();
8447 // One key buffer for the whole scan: `push` drains it and leaves
8448 // the capacity behind.
8449 let mut keys: Vec<OrderKey> = Vec::new();
8450 // r1024 — compile the predicate once for the scan.
8451 //
8452 // These two sorted-spill scans are the paths a single-table SELECT
8453 // with an ORDER BY takes, and they were the last row-returning ones
8454 // still walking the expression tree per row. r1023 did the
8455 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8456 // exactly this shape.
8457 //
8458 // Found from the profile's CALL TREE rather than its leaves. The
8459 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8460 // 261, `mod_op` 178 — and two attempts at reasoning out which
8461 // function asked for it were both wrong. The tree names the caller
8462 // chain, and it named this one.
8463 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8464 .where_
8465 .as_ref()
8466 .filter(|w| crate::eval::fully_compilable(w))
8467 .map(|w| crate::eval::compile_expr(w, &ctx));
8468 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8469 for (i, row) in table.scan_visible_from(0, &snapshot) {
8470 if i.is_multiple_of(256) {
8471 cancel.check()?;
8472 }
8473 if let Some(c) = &compiled_where {
8474 if !crate::eval::compiled::eval_compiled_pred(
8475 c,
8476 row,
8477 &ctx,
8478 &mut eval_stack,
8479 ctx.mysql_dialect,
8480 )? {
8481 continue;
8482 }
8483 } else if let Some(w) = &stmt.where_ {
8484 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8485 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8486 continue;
8487 }
8488 }
8489 keys.clear();
8490 // `&[]`: this sorter compares with `cmp_multi_key_in(.., &[])`
8491 // (extsort.rs:213/543/1221), so the key must stay folded or the
8492 // two would disagree. See `build_order_keys_bound`.
8493 crate::orderby::build_order_keys_bound(
8494 &order_by,
8495 &order_bound,
8496 &[],
8497 row,
8498 &ctx,
8499 &mut keys,
8500 )?;
8501 sorter.push(&mut keys, row)?;
8502 }
8503
8504 let columns: Vec<ColumnSchema> = projection
8505 .iter()
8506 .map(|p| {
8507 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8508 c.user_enum_type = p.user_enum_type.clone();
8509 c.mysql_fsp = p.mysql_fsp;
8510 c
8511 })
8512 .collect();
8513 emit(crate::StreamItem::Header(&columns))?;
8514
8515 let key_ctx = &ctx;
8516 let mut emitted_since_check = 0usize;
8517 let n = sorter.finish_each(
8518 |src, buf| {
8519 crate::orderby::build_order_keys_bound(
8520 &order_by,
8521 &order_bound,
8522 &[],
8523 src,
8524 key_ctx,
8525 buf,
8526 )
8527 },
8528 |src, values| {
8529 for p in &projection {
8530 values.push(
8531 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8532 );
8533 }
8534 Ok(())
8535 },
8536 |cells| {
8537 // The merge is the long half of a big sort, and the scan's
8538 // check above stops running once it ends: a cancelled
8539 // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
8540 // anyway. Same stride as the scan.
8541 emitted_since_check += 1;
8542 if emitted_since_check >= 256 {
8543 emitted_since_check = 0;
8544 cancel.check()?;
8545 }
8546 emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
8547 },
8548 )?;
8549 Ok(Some(n))
8550 }
8551
8552 /// One row of the single-table streaming walk: the WHERE test, the
8553 /// projection, the emit. Returns whether a row was emitted.
8554 ///
8555 /// v7.39 (round 970) — factored out because the walk now has two ways
8556 /// to reach a row, the sequential scan and an index seek's candidate
8557 /// positions, and both must do IDENTICALLY this. A copy in each is how
8558 /// two paths for one job drift; this file already carries the cost of
8559 /// that lesson twice (rounds 823 and 961, both resolvers).
8560 ///
8561 /// `#[inline]` so the scan loop keeps the shape round 957 measured it
8562 /// in — a shared hot path pays for a new abstraction whether or not it
8563 /// uses it, and this one is on the scan.
8564 #[inline]
8565 #[allow(clippy::too_many_arguments)]
8566 fn stream_filter_project(
8567 row: &spg_storage::Row<'static>,
8568 where_: Option<&Expr>,
8569 // r1023 — the same WHERE, compiled once by the caller. `None` means
8570 // the expression did not qualify and `where_` is evaluated as before.
8571 compiled_where: Option<&crate::eval::CompiledExpr>,
8572 eval_stack: &mut Vec<Value<'static>>,
8573 projection: &[ProjectedItem],
8574 bound_pos: &[Option<usize>],
8575 ctx: &crate::eval::EvalContext<'_>,
8576 values: &mut Vec<Value<'static>>,
8577 ) -> Result<bool, EngineError> {
8578 // r1023 — this scan ran its predicate through the TREE INTERPRETER,
8579 // once per row, and it was the only row-returning path that did.
8580 // The aggregate path, `table_access`, and the PK walker all compile
8581 // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
8582 // server's live samples were `eval_expr` 99, `apply_binary` 81,
8583 // `mod_op` 29 — the interpreter, not delivery.
8584 //
8585 // The arithmetic accounted for it exactly. Over the wire, the same
8586 // filter costs 6.375 ms returning rows and 0.679 ms counting them;
8587 // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
8588 // which is what an interpreted predicate costs against the compiled
8589 // lane's 11.7. It was named "delivery after a filter" before this
8590 // profile, and it was never delivery.
8591 if let Some(c) = compiled_where {
8592 if !crate::eval::compiled::eval_compiled_pred(
8593 c,
8594 row,
8595 ctx,
8596 eval_stack,
8597 ctx.mysql_dialect,
8598 )? {
8599 return Ok(false);
8600 }
8601 } else if let Some(w) = where_ {
8602 let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
8603 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8604 return Ok(false);
8605 }
8606 }
8607 values.clear();
8608 for (p, bound) in projection.iter().zip(bound_pos) {
8609 values.push(match bound {
8610 Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
8611 None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
8612 });
8613 }
8614 Ok(true)
8615 }
8616
8617 /// The same filter and projection, then emit. Split from
8618 /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
8619 /// before it can emit them — a sort — runs the identical predicate and
8620 /// projection rather than a second copy of them.
8621 #[allow(clippy::too_many_arguments)]
8622 fn stream_project_row<F>(
8623 row: &spg_storage::Row<'static>,
8624 where_: Option<&Expr>,
8625 compiled_where: Option<&crate::eval::CompiledExpr>,
8626 eval_stack: &mut Vec<Value<'static>>,
8627 projection: &[ProjectedItem],
8628 bound_pos: &[Option<usize>],
8629 ctx: &crate::eval::EvalContext<'_>,
8630 values: &mut Vec<Value<'static>>,
8631 emit: &mut F,
8632 ) -> Result<bool, EngineError>
8633 where
8634 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8635 {
8636 if !Self::stream_filter_project(
8637 row,
8638 where_,
8639 compiled_where,
8640 eval_stack,
8641 projection,
8642 bound_pos,
8643 ctx,
8644 values,
8645 )? {
8646 return Ok(false);
8647 }
8648 emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
8649 Ok(true)
8650 }
8651
8652 fn try_stream_single_table<F>(
8653 &self,
8654 stmt: &SelectStatement,
8655 from: &FromClause,
8656 cancel: CancelToken<'_>,
8657 emit: &mut F,
8658 ) -> Result<Option<usize>, EngineError>
8659 where
8660 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8661 {
8662 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8663 return Ok(None);
8664 };
8665 // Cold-tier rows live outside `rows()`; the materialising fallback
8666 // covers both tiers and this walk would silently drop them.
8667 if table.has_cold_rows_fast() {
8668 return Ok(None);
8669 }
8670 let alias = from
8671 .primary
8672 .alias
8673 .as_deref()
8674 .unwrap_or(from.primary.name.as_str());
8675 let cols = table.schema().columns.clone();
8676 let sess = self.dml_session();
8677 let ctx = EvalContext::new(&cols, Some(alias))
8678 .with_catalog(self.active_catalog())
8679 .with_session(&sess);
8680 let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8681
8682 let columns: Vec<ColumnSchema> = projection
8683 .iter()
8684 .map(|p| {
8685 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8686 c.user_enum_type = p.user_enum_type.clone();
8687 c.mysql_fsp = p.mysql_fsp;
8688 c
8689 })
8690 .collect();
8691 emit(crate::StreamItem::Header(&columns))?;
8692
8693 // v7.37 (round 957) — resolve each bare-column projection ONCE
8694 // instead of once per row. `find_column_pos`-style resolution is a
8695 // linear walk of the schema comparing column-name strings, and the
8696 // row loop below ran it for every cell of every row: measured at
8697 // 400k rows, binding it out of the loop took `SELECT pad` from
8698 // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
8699 //
8700 // ORDER BY has bound its keys this way since round 582
8701 // (`order_by_bound_positions`); the projection never did.
8702 //
8703 // `locate_column` is the same resolution `resolve_column` performs,
8704 // returning the site instead of the value, so the two cannot drift
8705 // apart the way a second hand-written resolver would. Anything it
8706 // declines — an expression, a whole-row reference, a name that does
8707 // not resolve — binds to `None` and takes the general path below,
8708 // errors included, so an empty table still reports nothing rather
8709 // than raising at bind time.
8710 let bound_pos: Vec<Option<usize>> = projection
8711 .iter()
8712 .map(|p| match &p.expr {
8713 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8714 Ok(Some(pos)) => Some(pos),
8715 _ => None,
8716 },
8717 _ => None,
8718 })
8719 .collect();
8720
8721 // One snapshot for the whole scan, as the materialising path takes.
8722 let snapshot = self.current_snapshot();
8723
8724 // v7.39 (round 970) — ask the indices BEFORE walking the table.
8725 //
8726 // This walk had no index step at all, and it is preferred over the
8727 // materialising path, which does have one (`pick_indexed_rows` ->
8728 // `try_index_seek`). So a primary-key point lookup — the commonest
8729 // statement there is — read every row: measured on 500k rows,
8730 // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
8731 // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
8732 // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
8733 //
8734 // The control that named it: `... OFFSET 0` — semantically the same
8735 // query — answered in 0.159 ms, because OFFSET is one of the shape
8736 // gates that declines this walk and sends the statement to the path
8737 // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
8738 // no semantics in common; what they share is making this function
8739 // stand down.
8740 //
8741 // The seek only NARROWS: every candidate still goes through the
8742 // full WHERE below, exactly as the mutation paths use it, so a
8743 // partial index match cannot change an answer. Positions come back
8744 // already visibility-filtered and already capped at a quarter of the
8745 // table (round 490), so a seek can never cost more than the scan it
8746 // replaces, and `None` means "walk the table" as before.
8747 //
8748 // Sorted because the scan would have produced table order and the
8749 // index produces key order. Without an ORDER BY neither is promised,
8750 // but a walk that silently reorders its answer when an index happens
8751 // to exist is a difference nobody asked for.
8752 let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
8753 crate::index_access::try_index_seek_positions(w, &cols, table, alias, &snapshot)
8754 });
8755
8756 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8757 // r1023 — compile the predicate once for the whole scan. Same gate
8758 // every other path uses: `fully_compilable` or keep the interpreter,
8759 // so a shape the VM cannot take answers exactly as it did before.
8760 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8761 .where_
8762 .as_ref()
8763 .filter(|w| crate::eval::fully_compilable(w))
8764 .map(|w| crate::eval::compile_expr(w, &ctx));
8765 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8766 let mut count: usize = 0;
8767 match seek_positions {
8768 Some(mut positions) => {
8769 positions.sort_unstable();
8770 for (n, pos) in positions.into_iter().enumerate() {
8771 if n.is_multiple_of(256) {
8772 cancel.check()?;
8773 }
8774 let Some(row) = table.rows().get(pos) else {
8775 continue;
8776 };
8777 if Self::stream_project_row(
8778 row,
8779 stmt.where_.as_ref(),
8780 compiled_where.as_ref(),
8781 &mut eval_stack,
8782 &projection,
8783 &bound_pos,
8784 &ctx,
8785 &mut values,
8786 emit,
8787 )? {
8788 count += 1;
8789 }
8790 }
8791 }
8792 None => {
8793 // v7.38.11 — the streaming scan is the path a client
8794 // reaches over the wire, so it is the one that has to
8795 // ask the BRIN summary which slots can be skipped. The
8796 // predicate still runs on every row that survives.
8797 let slots = stmt
8798 .where_
8799 .as_ref()
8800 .and_then(|w| crate::brin::candidate_slots(w, table))
8801 .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
8802 for (i, row) in table.scan_visible_slots(slots, &snapshot) {
8803 if i.is_multiple_of(256) {
8804 cancel.check()?;
8805 }
8806 if Self::stream_project_row(
8807 row,
8808 stmt.where_.as_ref(),
8809 compiled_where.as_ref(),
8810 &mut eval_stack,
8811 &projection,
8812 &bound_pos,
8813 &ctx,
8814 &mut values,
8815 emit,
8816 )? {
8817 count += 1;
8818 }
8819 }
8820 }
8821 }
8822 Ok(Some(count))
8823 }
8824
8825 pub(crate) fn try_exec_joined_streaming<F>(
8826 &self,
8827 stmt: &SelectStatement,
8828 cancel: CancelToken<'_>,
8829 emit: &mut F,
8830 ) -> Result<Option<usize>, EngineError>
8831 where
8832 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8833 {
8834 // Shape gates — keep the streamable surface narrow on
8835 // purpose. The fall-back path still handles everything else.
8836 let Some(from) = &stmt.from else {
8837 return Ok(None);
8838 };
8839 // v7.37 (round 830) — decline anything a row-security policy binds
8840 // for this session. Policies are injected in
8841 // `exec_bare_select_cancel`, below this path, so a statement claimed
8842 // here would read the table unfiltered: measured, `SELECT val FROM
8843 // sec` returned all three rows to a session whose policy allows two,
8844 // while `SELECT upper(val) FROM sec` — declined by the shape gates
8845 // and so materialised — returned the correct two.
8846 //
8847 // Declining sends it to the path that enforces. Teaching this one to
8848 // inject the predicate itself would keep the streaming benefit for
8849 // RLS tables and is the better end state; it is not what a
8850 // correctness fix should carry, and the fall-back is exactly as
8851 // correct, only slower.
8852 if self.select_reads_policy_subject_table(stmt) {
8853 return Ok(None);
8854 }
8855 // r1058 — a WITH list this path never materialises: the CTE
8856 // name would be resolved as a physical relation and error
8857 // ("relation \"big\" does not exist" over the extended
8858 // protocol, caught by the perm-runner's wire legs). The
8859 // materialising fallback owns CTE execution.
8860 if !stmt.ctes.is_empty() {
8861 return Ok(None);
8862 }
8863 // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
8864 // tables` and kin) exist only as synth arms on the
8865 // materialising path; claiming one here errored "relation
8866 // does not exist" over the extended protocol for a query the
8867 // simple protocol answered. Prefix test only — a genuinely
8868 // missing relation must keep erroring in-path.
8869 if from.primary.name.starts_with("__spg_")
8870 || from
8871 .joins
8872 .iter()
8873 .any(|j| j.table.name.starts_with("__spg_"))
8874 {
8875 return Ok(None);
8876 }
8877 // r1058 — decline partitioned / inheritance parents, same
8878 // shape of bug as the RLS decline above: this path scans the
8879 // named table's own (empty) heap, so `SELECT id, region FROM
8880 // cust` on a partition parent streamed ZERO rows over the wire
8881 // while COUNT(*) — an aggregate, materialised below — said 3.
8882 // Caught by the perm-runner's server permutations; the
8883 // materialising fallback expands children correctly.
8884 if crate::partition::has_children(self.active_catalog(), &from.primary.name)
8885 || from
8886 .joins
8887 .iter()
8888 .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
8889 {
8890 return Ok(None);
8891 }
8892 // v7.39 (round 790) — single-table SELECTs stream too. This
8893 // gate said "joins only" because the path was written for
8894 // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
8895 // fell to the materialising fallback, which builds the whole
8896 // `Vec<Row<'static>>` and only then iterates it. Measured on
8897 // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
8898 // reached through a one-row JOIN — 2.6x, purely for lacking a
8899 // join. The deferred-join structure handles one source as the
8900 // degenerate stride-1 case, so the walk below is unchanged.
8901 let _single_table = from.joins.is_empty();
8902 // An ORDER BY that the bounded sort can serve streams; everything
8903 // else still falls to the materialising fallback below.
8904 // r1025 — an ordering the index already holds needs no sort at all.
8905 // Tried before the spill sort, which is the path it replaces.
8906 if !stmt.order_by.is_empty()
8907 && from.joins.is_empty()
8908 && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
8909 {
8910 return Ok(Some(n));
8911 }
8912 if !stmt.order_by.is_empty()
8913 && from.joins.is_empty()
8914 && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
8915 {
8916 return Ok(Some(n));
8917 }
8918 // r1031 — integer keys carried inline instead of an `OrderKey`
8919 // vector per row. Tried AFTER the spill sort on purpose: this lane
8920 // buffers the whole answer, so anything the spill path would take
8921 // must keep taking it rather than be turned back into an in-memory
8922 // sort that answers with a budget error.
8923 if !stmt.order_by.is_empty()
8924 && from.joins.is_empty()
8925 && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
8926 {
8927 return Ok(Some(n));
8928 }
8929 if !stmt.order_by.is_empty()
8930 || stmt.limit.is_some()
8931 || stmt.offset.is_some()
8932 || stmt.having.is_some()
8933 || stmt.group_by.is_some()
8934 || stmt.distinct
8935 || !stmt.unions.is_empty()
8936 || stmt.limit_with_ties
8937 {
8938 return Ok(None);
8939 }
8940 if aggregate::uses_aggregate(stmt) {
8941 return Ok(None);
8942 }
8943 // No window / SRF on the streaming path.
8944 if select_has_window(stmt) {
8945 return Ok(None);
8946 }
8947 if stmt
8948 .items
8949 .iter()
8950 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8951 {
8952 return Ok(None);
8953 }
8954 // v7.37 (round 831) — a joinless FROM over a plain stored table
8955 // never needs the deferred structure, and building one costs the
8956 // whole table. `materialise_table_ref_filtered` clones every row
8957 // into a `Vec<Row<'static>>` before anything is filtered or
8958 // projected, so peak cost tracks the TABLE, not the result:
8959 // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
8960 // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
8961 // projection saving nothing, while an arithmetic projection — which
8962 // the shape gates decline, so it materialises through the ordinary
8963 // executor — cost +21 MB.
8964 //
8965 // Scanning in batches and releasing each one is what `cursor_fill`
8966 // already does for a lazy cursor, and it is the same walk: resume
8967 // from a slot, take visible rows, evaluate, hand them over, drop
8968 // them. Round 800's finding stands and is why this reads rows OUT
8969 // rather than seeding the join by index — touching the stored
8970 // `PersistentVec` in place makes the whole table resident, which is
8971 // worse than the copy. Each batch is copied, then freed.
8972 if from.joins.is_empty()
8973 && from.primary.unnest_expr.is_none()
8974 && from.primary.lateral_subquery.is_none()
8975 && from.primary.as_of_segment.is_none()
8976 && from.primary.generate_series_args.is_none()
8977 && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
8978 {
8979 return Ok(Some(n));
8980 }
8981 // Build the deferred join under the regular byte budget.
8982 let mut budget = ByteBudget::new(self.max_query_bytes);
8983 let deferred = {
8984 let mut needed = alloc::collections::BTreeSet::new();
8985 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
8986 self.build_joined_filtered_rows(
8987 from,
8988 stmt.where_.as_ref(),
8989 cancel,
8990 if prunable { Some(&needed) } else { None },
8991 &mut budget,
8992 )?
8993 };
8994 let combined_schema = &deferred.combined_schema;
8995 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
8996 // `::regclass` / enum cast in a joined projection or HAVING needs it.
8997 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
8998 // the same predicate the unjoined shape carries.
8999 let joined_sess = self.dml_session();
9000 let ctx = EvalContext::new(combined_schema, None)
9001 .with_catalog(self.active_catalog())
9002 .with_session(&joined_sess);
9003 let projection =
9004 build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
9005 // Every projection item must be a bound qualified column —
9006 // anything that needs `eval_expr_with_correlated` keeps the
9007 // materialising path.
9008 let bound_pos = |e: &Expr| -> Option<usize> {
9009 match e {
9010 // v7.39 (round 822) — an UNQUALIFIED column resolves here
9011 // too. The `qualifier.is_some()` guard this replaces meant
9012 // `SELECT pad FROM big` — the commonest projection there is
9013 // — never reached the streaming walk: it fell out at this
9014 // gate and re-ran on the materialising path, after the
9015 // deferred join structure had already been built and paid
9016 // for. Measured (round 821, statement_timeout=120 over 400k
9017 // rows): `big.pad` and `b.pad` streamed and cancelled at
9018 // ~65k rows in 0.14 s, while bare `pad` ran to completion in
9019 // 0.80 s with the timeout never consulted. `find_column_pos`
9020 // has always handled the unqualified case (it falls through
9021 // to a by-name match), so the guard narrowed the gate for no
9022 // reason it recorded.
9023 Expr::Column(c) => eval::find_column_pos(c, &ctx),
9024 _ => None,
9025 }
9026 };
9027 let proj_decomposed: Vec<(usize, usize)> = {
9028 let mut out = Vec::with_capacity(projection.len());
9029 for p in &projection {
9030 let Some(abs) = bound_pos(&p.expr) else {
9031 return Ok(None);
9032 };
9033 let Some(k) = deferred
9034 .offsets
9035 .partition_point(|&o| o <= abs)
9036 .checked_sub(1)
9037 else {
9038 return Ok(None);
9039 };
9040 out.push((k, abs - deferred.offsets[k]));
9041 }
9042 out
9043 };
9044 // Emit columns once.
9045 let columns: Vec<ColumnSchema> = projection
9046 .iter()
9047 // v7.39 (read01 round 54) — keep the column's enum identity through
9048 // the projection (it lives outside the DataType lattice), or a
9049 // derived table / UNION / windowed result forgets it and any outer
9050 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
9051 .map(|p| {
9052 let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
9053 c.user_enum_type = p.user_enum_type.clone();
9054 c.mysql_fsp = p.mysql_fsp;
9055 c
9056 })
9057 .collect();
9058 emit(crate::StreamItem::Header(&columns))?;
9059 let sources_ref = &deferred.sources;
9060 let stride = deferred.stride;
9061 let survivors_ref = &deferred.survivors;
9062 let n_surv = if stride == 0 {
9063 0
9064 } else {
9065 survivors_ref.len() / stride
9066 };
9067 // Reused per-row cell-ref scratch — pushes are zero-alloc
9068 // after the first row.
9069 let null_value = Value::Null;
9070 let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
9071 let mut count: usize = 0;
9072 for surv_i in 0..n_surv {
9073 if surv_i.is_multiple_of(256) {
9074 cancel.check()?;
9075 }
9076 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9077 cell_refs.clear();
9078 for &(k, col_in_src) in &proj_decomposed {
9079 let ri = tuple[k];
9080 let v: &Value = if ri == usize::MAX {
9081 &null_value
9082 } else {
9083 sources_ref[k]
9084 .get(ri)
9085 .and_then(|r| r.values.get(col_in_src))
9086 .unwrap_or(&null_value)
9087 };
9088 cell_refs.push(v);
9089 }
9090 emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
9091 count += 1;
9092 }
9093 Ok(Some(count))
9094 }
9095
9096 fn exec_joined_select(
9097 &self,
9098 stmt: &SelectStatement,
9099 from: &FromClause,
9100 cancel: CancelToken<'_>,
9101 ) -> Result<QueryResult, EngineError> {
9102 // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
9103 // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
9104 // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
9105 // FROM B WHERE B.k = A.k)` into
9106 // SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
9107 // WHERE B.k IS NULL
9108 // The general join executor builds a hash, probes every outer
9109 // tuple, materialises (left_padded_with_null) for every miss,
9110 // then runs the aggregate over the result set. For COUNT(*) we
9111 // only need the count — skip the tuple materialisation. Build
9112 // a HashSet of B's unique join values, scan A's PK index, and
9113 // increment the counter on each miss. PG's Merge Anti-Join
9114 // does roughly this; ours becomes a simple HashSet probe.
9115 if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
9116 return Ok(out);
9117 }
9118 // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
9119 // When ORDER BY is on an indexed primary column, walking the
9120 // btree in the requested direction lets the streamer break
9121 // after `LIMIT + OFFSET` survivors without ever materialising
9122 // the rest of the join — the 80 ms `mailrs_prod_not_exists`
9123 // plateau is exactly this shape.
9124 if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
9125 return Ok(out);
9126 }
9127 // v7.30.3 (mailrs round-26) — the bounded single-join path
9128 // first; peak memory scales with LIMIT instead of the table.
9129 if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
9130 return Ok(out);
9131 }
9132 // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
9133 // WHERE materialisation to the shared helper so the LATERAL
9134 // / UNNEST / regular-catalog paths route through one place.
9135 // (`build_joined_filtered_rows` carries LATERAL support as
9136 // of Phase 3.P0-41.) Downstream we still handle aggregate /
9137 // projection / ORDER BY / DISTINCT / LIMIT inline because
9138 // those depend on the SelectStatement's items list.
9139 let mut budget = ByteBudget::new(self.max_query_bytes);
9140 let deferred = {
9141 let mut needed = alloc::collections::BTreeSet::new();
9142 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
9143 self.build_joined_filtered_rows(
9144 from,
9145 stmt.where_.as_ref(),
9146 cancel,
9147 if prunable { Some(&needed) } else { None },
9148 &mut budget,
9149 )?
9150 };
9151 let combined_schema = &deferred.combined_schema;
9152 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
9153 // `::regclass` / enum cast in a joined projection or HAVING needs it.
9154 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
9155 // the same predicate the unjoined shape carries.
9156 let joined_sess = self.dml_session();
9157 let ctx = EvalContext::new(combined_schema, None)
9158 .with_catalog(self.active_catalog())
9159 .with_session(&joined_sess);
9160 // Aggregate path: handle GROUP BY / aggregate calls over the
9161 // joined+filtered rows.
9162 if aggregate::uses_aggregate(stmt) {
9163 // v7.32 (P4 borrow channel, increment 2) — borrow each
9164 // surviving join tuple as a RowRef::Tuple; the aggregate
9165 // engine reads source cells by reference (bound fast path =
9166 // zero clone) instead of consuming materialised combined
9167 // Rows. This is where the +211k materialise_tuple_vals
9168 // clones disappear for the join+aggregate shape.
9169 let refs = deferred.row_refs();
9170 // v7.29 — a per-query memo so correlated scalar
9171 // subqueries batch-evaluate once (group map) instead of
9172 // executing per group.
9173 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
9174 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
9175 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
9176 .map_err(|err| match err {
9177 EngineError::Eval(ev) => ev,
9178 other => eval::EvalError::TypeMismatch {
9179 detail: alloc::format!("{other}"),
9180 },
9181 })
9182 };
9183 let agg = aggregate::run(
9184 stmt,
9185 crate::join::AggRows::Refs(&refs),
9186 combined_schema,
9187 None,
9188 Some(&agg_correlated),
9189 self.parallel_runner.0.as_deref(),
9190 Some(self.active_catalog()),
9191 Some(self),
9192 )?;
9193 return self.finish_agg_result(agg, stmt, cancel);
9194 }
9195
9196 let projection =
9197 build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
9198 // v7.39 (round 734) — a set-returning projection over a JOIN.
9199 // This executor's projection loop treats every item as a scalar,
9200 // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
9201 // "function unnest(integer[]) does not exist" where PG expands
9202 // it. The row-set executor already carries the full SRF pipeline
9203 // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
9204 // sharding): materialise the joined survivors and hand over. The
9205 // WHERE is cleared — the join already applied it, and combined
9206 // columns resolve identically in both executors.
9207 if !self.srf_target_idxs(&projection).is_empty() {
9208 let refs = deferred.row_refs();
9209 let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
9210 let mut s2 = stmt.clone();
9211 s2.where_ = None;
9212 let schema = combined_schema.clone();
9213 return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
9214 }
9215 // v7.33 (P4 borrow channel, increment 3) — project directly off
9216 // the deferred row-index tuples instead of materialising an
9217 // intermediate combined Row per survivor. A bound qualified
9218 // column is read by reference (`RowRef::get` → `tuple_value`) and
9219 // cloned ONCE into the output row; the old `materialise()` (a full
9220 // combined Row plus a source→intermediate clone per referenced
9221 // cell, for every survivor) is gone. A row materialises on demand
9222 // only when a projection or ORDER BY expression needs the eval
9223 // path (subquery / function / arithmetic / unqualified column).
9224 // Same bind-once classification the aggregate input fast path uses
9225 // (`accumulate_groups`), reading the same `tuple_value` mapping the
9226 // differential gate already covers.
9227 let refs = deferred.row_refs();
9228 let bound_pos = |e: &Expr| -> Option<usize> {
9229 match e {
9230 Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
9231 _ => None,
9232 }
9233 };
9234 let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
9235 let all_proj_bound = proj_pos.iter().all(Option::is_some);
9236 // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
9237 // pre-decompose each bound projection position into
9238 // `(source_k, col_in_source)` so the per-row column read
9239 // skips the per-cell `tuple_value` partition_point + slice
9240 // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
9241 // calls) that walk dominated; this version reaches into
9242 // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
9243 let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
9244 .iter()
9245 .map(|p| {
9246 p.and_then(|abs| {
9247 let k = deferred
9248 .offsets
9249 .partition_point(|&o| o <= abs)
9250 .checked_sub(1)?;
9251 Some((k, abs - deferred.offsets[k]))
9252 })
9253 })
9254 .collect();
9255 // v7.39 (round 962) — which projection items are whole-row
9256 // references, and to which join source. The test is
9257 // `locate_column` declining the name, which is the SAME resolver
9258 // the evaluation path uses, so this cannot drift from it: a real
9259 // column carrying an alias's name resolves to a position and is
9260 // not reported here. The source index comes from the alias
9261 // prefix, the way the combined schema names its columns.
9262 let whole_row_src: Vec<Option<usize>> = projection
9263 .iter()
9264 .map(|p| {
9265 let Expr::Column(c) = &p.expr else {
9266 return None;
9267 };
9268 if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
9269 return None;
9270 }
9271 let prefix = alloc::format!("{name}.", name = c.name);
9272 let abs = deferred
9273 .combined_schema
9274 .iter()
9275 .position(|s| s.name.starts_with(&prefix))?;
9276 deferred
9277 .offsets
9278 .partition_point(|&o| o <= abs)
9279 .checked_sub(1)
9280 })
9281 .collect();
9282 // ORDER BY (when present) still evaluates against a materialised
9283 // Row — keep the order-key encoder correct rather than fork it.
9284 let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
9285 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
9286 let mut proj_memo = memoize::MemoizeCache::default();
9287 let sources_ref = &deferred.sources;
9288 let stride = deferred.stride;
9289 let survivors_ref = &deferred.survivors;
9290 let n_surv = survivors_ref.len() / stride.max(1);
9291 // v7.38 (read01 B8) — streaming top-N budget (see the sibling
9292 // single-table path). Bounds this JOIN projection's accumulator
9293 // to O(keep) for `ORDER BY … LIMIT k`.
9294 let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
9295 && !stmt.distinct
9296 && !stmt.limit_with_ties
9297 && !self.env_cfg().disable_topk
9298 {
9299 stmt.limit_literal().and_then(|l| {
9300 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
9301 (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
9302 })
9303 } else {
9304 None
9305 };
9306 // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
9307 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9308 hashbrown::HashMap::new();
9309 let distinct_hb = hashbrown::DefaultHashBuilder::default();
9310 // v7.38.13 — which output positions must NOT fold. Built once per
9311 // scan from the projection, which carries the source column's
9312 // byte-wise-ness; see `FoldSpec`.
9313 let distinct_mask = fold_mask(&projection);
9314 for surv_i in 0..n_surv {
9315 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9316 let row = &refs[surv_i];
9317 let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
9318 Some(row.as_row())
9319 } else {
9320 None
9321 };
9322 let mut values = Vec::with_capacity(projection.len());
9323 for (i, p) in projection.iter().enumerate() {
9324 if let Some((k, col_in_src)) = proj_decomposed[i] {
9325 // v7.36 — direct (source_k, col) lookup, no
9326 // partition_point. tuple[k] is the row index in
9327 // sources[k]; LEFT-NULL slots are `usize::MAX`.
9328 let ri = tuple[k];
9329 let v: Value<'static> = if ri == usize::MAX {
9330 Value::Null
9331 } else {
9332 sources_ref[k]
9333 .get(ri)
9334 .and_then(|r| r.values.get(col_in_src))
9335 .cloned()
9336 .map(Value::into_owned)
9337 .unwrap_or(Value::Null)
9338 };
9339 values.push(v);
9340 } else if let Some(pos) = proj_pos[i] {
9341 // Bound but couldn't decompose (shouldn't normally
9342 // happen — keep as a safe path).
9343 values.push(
9344 row.get(pos)
9345 .cloned()
9346 .map(Value::into_owned)
9347 .unwrap_or(Value::Null),
9348 );
9349 } else if let Some(k) = whole_row_src[i]
9350 && tuple[k] == usize::MAX
9351 {
9352 // v7.39 (round 962) — a whole-row reference to a side
9353 // an OUTER join null-extended is NULL, not a
9354 // composite whose fields are all NULL. PG18.4 answers
9355 // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
9356 // an empty cell; round 961 answered `(,)`.
9357 //
9358 // The evaluator below cannot tell the two apart: it
9359 // reads the MATERIALISED combined row, where a
9360 // null-extended side is indistinguishable from a real
9361 // row whose every column is NULL — and that row is
9362 // `(,)` in PG too, so guessing by "all fields NULL"
9363 // would trade one wrong answer for another. The
9364 // tuple, which is still in hand here, does know:
9365 // `usize::MAX` is the sentinel the join writes for
9366 // exactly this.
9367 values.push(Value::Null);
9368 } else {
9369 // Eval path — `materialised` is Some whenever any
9370 // projection item is non-bound (need_eval_row true).
9371 // v7.24 (round-16 B) — select-list subqueries under a
9372 // JOIN go through the correlated-aware evaluator too.
9373 let mrow = materialised.as_deref().expect("materialised for eval");
9374 values.push(self.eval_expr_with_correlated(
9375 &p.expr,
9376 mrow,
9377 &ctx,
9378 cancel,
9379 Some(&mut proj_memo),
9380 )?);
9381 }
9382 }
9383 let out_row = Row::new(values);
9384 // v7.37.16 — streaming DISTINCT (see the scan-path twin):
9385 // probe on the projected row; duplicates skip the
9386 // build_order_keys eval and never enter `tagged`.
9387 if stmt.distinct {
9388 let bucket = seen_distinct
9389 .entry(norm_hash_row(
9390 &out_row,
9391 &distinct_hb,
9392 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9393 ))
9394 .or_default();
9395 if bucket.iter().any(|i| {
9396 row_eq_norm(
9397 &tagged[i].1,
9398 &out_row,
9399 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9400 )
9401 }) {
9402 continue;
9403 }
9404 bucket.push(tagged.len());
9405 }
9406 let order_keys = if stmt.order_by.is_empty() {
9407 Vec::new()
9408 } else {
9409 let mrow = materialised.as_deref().expect("materialised for order by");
9410 build_order_keys(&stmt.order_by, mrow, &ctx)?
9411 };
9412 budget.charge(approx_row_bytes(&out_row))?;
9413 tagged.push((order_keys, out_row));
9414 if let Some((k, descs)) = &topk_stream {
9415 topk_trim(&mut tagged, *k, descs);
9416 }
9417 }
9418 if !stmt.order_by.is_empty() {
9419 // v7.38 元机制 D acceptor — see other call site above.
9420 let keep = if self.env_cfg().disable_topk {
9421 None
9422 } else {
9423 stmt.limit_literal()
9424 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
9425 };
9426 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
9427 // v7.39 (round 688) — the join's ORDER BY resolves its keys
9428 // against `ctx`, which is built from `build_combined_schema`, so
9429 // this is where a declared collation reaches the sort. There was
9430 // exactly ONE resolver call in the engine before this — the
9431 // single-table scan's — which is why every other shape sorted by
9432 // bytes no matter what the schemas carried.
9433 let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
9434 crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
9435 }
9436 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
9437 apply_offset_and_limit(
9438 &mut output_rows,
9439 stmt.offset_literal(),
9440 stmt.limit_literal(),
9441 );
9442 let columns: Vec<ColumnSchema> = projection
9443 .into_iter()
9444 .map(|p| {
9445 let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
9446 c.user_enum_type = p.user_enum_type;
9447 c.collation_name = p.collation_name;
9448 c.mysql_fsp = p.mysql_fsp;
9449 c
9450 })
9451 .collect();
9452 Ok(QueryResult::Rows {
9453 columns,
9454 rows: output_rows,
9455 })
9456 }
9457}
9458
9459impl Engine {
9460 /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
9461 /// by id, decodes each row body against the table's current
9462 /// schema, applies the SELECT's projection + optional WHERE +
9463 /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
9464 /// / ORDER BY are unsupported on this path (STABILITY carve-
9465 /// out); operators wanting them should restore the segment
9466 /// into a regular table first.
9467 fn exec_select_as_of_segment(
9468 &self,
9469 stmt: &SelectStatement,
9470 from: &spg_sql::ast::FromClause,
9471 segment_id: u32,
9472 ) -> Result<QueryResult, EngineError> {
9473 // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
9474 // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
9475 if !from.joins.is_empty()
9476 || stmt.group_by.is_some()
9477 || stmt.having.is_some()
9478 || !stmt.unions.is_empty()
9479 || !stmt.order_by.is_empty()
9480 || stmt.offset.is_some()
9481 || stmt.distinct
9482 || aggregate::uses_aggregate(stmt)
9483 {
9484 return Err(EngineError::Unsupported(
9485 "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
9486 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
9487 .into(),
9488 ));
9489 }
9490 let table = self
9491 .active_catalog()
9492 .get(&from.primary.name)
9493 .ok_or_else(|| StorageError::TableNotFound {
9494 name: from.primary.name.clone(),
9495 })?;
9496 let schema = table.schema().clone();
9497 let schema_cols = &schema.columns;
9498 let alias = from
9499 .primary
9500 .alias
9501 .as_deref()
9502 .unwrap_or(from.primary.name.as_str());
9503 let ctx = self.ev_ctx(schema_cols, Some(alias));
9504 let seg = self
9505 .active_catalog()
9506 .cold_segment(segment_id)
9507 .ok_or_else(|| {
9508 EngineError::Unsupported(alloc::format!(
9509 "AS OF SEGMENT: cold segment {segment_id} not registered"
9510 ))
9511 })?;
9512 let mut out_rows: Vec<Row<'static>> = Vec::new();
9513 let mut limit_remaining: Option<usize> =
9514 stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
9515 for (_key, body) in seg.scan() {
9516 let (row, _consumed) =
9517 spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
9518 .map_err(EngineError::Storage)?;
9519 if let Some(where_expr) = &stmt.where_ {
9520 let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
9521 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9522 continue;
9523 }
9524 }
9525 // Projection.
9526 let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
9527 out_rows.push(projected);
9528 if let Some(rem) = limit_remaining.as_mut() {
9529 if *rem == 0 {
9530 out_rows.pop();
9531 break;
9532 }
9533 *rem -= 1;
9534 }
9535 }
9536 // Output column schema: derive from SELECT items.
9537 let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
9538 Ok(QueryResult::Rows {
9539 columns,
9540 rows: out_rows,
9541 })
9542 }
9543
9544 /// v6.10.2 — simple-path WHERE eval that doesn't go through
9545 /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
9546 /// scan paths predicate against a snapshot frozen segment, no
9547 /// cross-row state.
9548 fn eval_expr_simple(
9549 &self,
9550 expr: &Expr,
9551 row: &Row<'static>,
9552 ctx: &EvalContext,
9553 ) -> Result<Value<'static>, EngineError> {
9554 let cancel = CancelToken::none();
9555 self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
9556 }
9557}
9558
9559// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
9560
9561/// One row-producing projection: an expression to evaluate, the resulting
9562/// column's user-visible name, its inferred type, and nullability.
9563#[derive(Debug, Clone)]
9564pub(crate) struct ProjectedItem {
9565 pub(crate) expr: Expr,
9566 pub(crate) output_name: String,
9567 pub(crate) ty: DataType,
9568 pub(crate) nullable: bool,
9569 /// v7.39 (read01 round 54) — a projected enum column keeps its enum
9570 /// identity. Enum-ness lives outside the DataType lattice (the value is a
9571 /// Text), so a projection that dropped this made the RESULT schema forget
9572 /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
9573 /// that schema, silently fell back to TEXT order instead of member order.
9574 pub(crate) user_enum_type: Option<String>,
9575 /// v7.39 (round 425) — a projected MySQL temporal column keeps its
9576 /// declared fractional-seconds precision, so the renderer can pad to
9577 /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
9578 /// a whole second). Like `user_enum_type` this lives outside the
9579 /// DataType lattice, so a projection that dropped it made the RESULT
9580 /// schema forget how wide the fraction should print.
9581 pub(crate) mysql_fsp: Option<u8>,
9582 /// v7.39 (round 688) — and its declared collation, the third thing to
9583 /// live outside the DataType lattice and the third to be lost the same
9584 /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
9585 /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
9586 /// projection rebuilt the output column and the ORDER BY resolves
9587 /// against THAT schema.
9588 pub(crate) collation_name: Option<String>,
9589 /// v7.38.13 — and whether this position must NOT fold when DISTINCT
9590 /// de-dups it. The fourth thing to live outside the DataType lattice
9591 /// and the fourth to be lost the same way: a column declared
9592 /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
9593 /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
9594 /// returns two.
9595 ///
9596 /// A BOOL rather than the `Collation` enum on purpose. The enum's
9597 /// storage default is `Binary`, but the FOLD default under MySQL is
9598 /// case-insensitive — carrying the enum would silently mean
9599 /// "exempt" for every projected expression that is not a column.
9600 /// This field states the question it answers.
9601 pub(crate) fold_exempt: bool,
9602}
9603
9604/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
9605/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
9606/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
9607/// the spec's "two NULLs are not distinct"; the second is a tolerated
9608/// quirk for v1 (no NaN literals are reachable from the SQL surface).
9609/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
9610fn expr_is_aggregate_call(e: &Expr) -> bool {
9611 match e {
9612 Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
9613 Expr::AggregateOrdered { .. } => true,
9614 _ => false,
9615 }
9616}
9617
9618/// Collect distinct top-level aggregate call expressions (dedup by value). Does
9619/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
9620/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
9621/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
9622/// than today — never a regression on a working query).
9623fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
9624 if expr_is_aggregate_call(e) {
9625 if !out.iter().any(|x| x == e) {
9626 out.push(e.clone());
9627 }
9628 return;
9629 }
9630 match e {
9631 Expr::Binary { lhs, rhs, .. } => {
9632 collect_agg_exprs(lhs, out);
9633 collect_agg_exprs(rhs, out);
9634 }
9635 Expr::Unary { expr, .. }
9636 | Expr::Cast { expr, .. }
9637 | Expr::IsNull { expr, .. }
9638 | Expr::BoolTest { expr, .. }
9639 | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
9640 Expr::FunctionCall { args, .. } => {
9641 for a in args {
9642 collect_agg_exprs(a, out);
9643 }
9644 }
9645 Expr::Like { expr, pattern, .. } => {
9646 collect_agg_exprs(expr, out);
9647 collect_agg_exprs(pattern, out);
9648 }
9649 Expr::Extract { source, .. } => collect_agg_exprs(source, out),
9650 Expr::WindowFunction {
9651 args,
9652 partition_by,
9653 order_by,
9654 ..
9655 } => {
9656 for a in args {
9657 collect_agg_exprs(a, out);
9658 }
9659 for p in partition_by {
9660 collect_agg_exprs(p, out);
9661 }
9662 for (o, _, _) in order_by {
9663 collect_agg_exprs(o, out);
9664 }
9665 }
9666 _ => {}
9667 }
9668}
9669
9670/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
9671fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
9672 if expr_is_aggregate_call(e) {
9673 if let Some(idx) = aggs.iter().position(|x| x == e) {
9674 *e = Expr::Column(ColumnName {
9675 qualifier: None,
9676 name: alloc::format!("__agg{idx}"),
9677 });
9678 }
9679 return;
9680 }
9681 match e {
9682 Expr::Binary { lhs, rhs, .. } => {
9683 replace_agg_exprs(lhs, aggs);
9684 replace_agg_exprs(rhs, aggs);
9685 }
9686 Expr::Unary { expr, .. }
9687 | Expr::Cast { expr, .. }
9688 | Expr::IsNull { expr, .. }
9689 | Expr::BoolTest { expr, .. }
9690 | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
9691 Expr::FunctionCall { args, .. } => {
9692 for a in args {
9693 replace_agg_exprs(a, aggs);
9694 }
9695 }
9696 Expr::Like { expr, pattern, .. } => {
9697 replace_agg_exprs(expr, aggs);
9698 replace_agg_exprs(pattern, aggs);
9699 }
9700 Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
9701 Expr::WindowFunction {
9702 args,
9703 partition_by,
9704 order_by,
9705 ..
9706 } => {
9707 for a in args {
9708 replace_agg_exprs(a, aggs);
9709 }
9710 for p in partition_by {
9711 replace_agg_exprs(p, aggs);
9712 }
9713 for (o, _, _) in order_by {
9714 replace_agg_exprs(o, aggs);
9715 }
9716 }
9717 _ => {}
9718 }
9719}
9720
9721/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
9722/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
9723/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
9724/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
9725/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
9726/// Returns None outside the bounded subset (leaves current behaviour). Only fires
9727/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
9728/// window-only / aggregate-only queries.
9729fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
9730 if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
9731 return None;
9732 }
9733 // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
9734 if !stmt.unions.is_empty() {
9735 return None;
9736 }
9737 let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
9738 if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
9739 return None;
9740 }
9741 stmt.from.as_ref()?;
9742 // Collect the aggregate calls to hoist from projection + outer ORDER BY.
9743 let mut aggs: Vec<Expr> = Vec::new();
9744 for item in &stmt.items {
9745 if let SelectItem::Expr { expr, .. } = item {
9746 collect_agg_exprs(expr, &mut aggs);
9747 }
9748 }
9749 for ob in &stmt.order_by {
9750 collect_agg_exprs(&ob.expr, &mut aggs);
9751 }
9752 // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
9753 let mut inner_items: Vec<SelectItem> = Vec::new();
9754 for g in &group_cols {
9755 inner_items.push(SelectItem::Expr {
9756 expr: g.clone(),
9757 alias: None,
9758 });
9759 }
9760 for (i, a) in aggs.iter().enumerate() {
9761 inner_items.push(SelectItem::Expr {
9762 expr: a.clone(),
9763 alias: Some(alloc::format!("__agg{i}")),
9764 });
9765 }
9766 let inner = SelectStatement {
9767 items: inner_items,
9768 distinct: false,
9769 distinct_on: Vec::new(),
9770 unions: Vec::new(),
9771 order_by: Vec::new(),
9772 limit: None,
9773 offset: None,
9774 limit_with_ties: false,
9775 window_check_exprs: Vec::new(),
9776 ..stmt.clone()
9777 };
9778 let derived = TableRef {
9779 name: "__aggwin".into(),
9780 alias: Some("__aggwin".into()),
9781 only: false,
9782 as_of_segment: None,
9783 unnest_expr: None,
9784 unnest_column_aliases: Vec::new(),
9785 with_ordinality: false,
9786 generate_series_args: None,
9787 lateral_subquery: Some(alloc::boxed::Box::new(inner)),
9788 jsonb_each_text_arg: None,
9789 table_fn_call: None,
9790 rows_from: None,
9791 json_table: None,
9792 scalar_fn_item: false,
9793 };
9794 // Outer window query over the derived rows: aggregates → __aggN column refs.
9795 let mut outer_items = stmt.items.clone();
9796 for item in &mut outer_items {
9797 if let SelectItem::Expr { expr, alias } = item {
9798 // Preserve PG's column label for a bare aggregate projection.
9799 if alias.is_none()
9800 && let Expr::FunctionCall { name, .. } = expr
9801 && crate::aggregate::is_aggregate_name(name)
9802 {
9803 *alias = Some(name.to_ascii_lowercase());
9804 }
9805 replace_agg_exprs(expr, &aggs);
9806 }
9807 }
9808 let mut outer_order = stmt.order_by.clone();
9809 for ob in &mut outer_order {
9810 replace_agg_exprs(&mut ob.expr, &aggs);
9811 }
9812 let mut outer_distinct_on = stmt.distinct_on.clone();
9813 for e in &mut outer_distinct_on {
9814 replace_agg_exprs(e, &aggs);
9815 }
9816 Some(SelectStatement {
9817 locking: None,
9818 ctes: Vec::new(),
9819 distinct: stmt.distinct,
9820 distinct_on: outer_distinct_on,
9821 items: outer_items,
9822 from: Some(FromClause {
9823 primary: derived,
9824 joins: Vec::new(),
9825 }),
9826 where_: None,
9827 group_by: None,
9828 group_by_all: false,
9829 having: None,
9830 unions: Vec::new(),
9831 order_by: outer_order,
9832 limit: stmt.limit.clone(),
9833 offset: stmt.offset.clone(),
9834 limit_with_ties: stmt.limit_with_ties,
9835 window_check_exprs: Vec::new(),
9836 })
9837}
9838
9839/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
9840/// membership.
9841///
9842/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
9843/// there?", and all four answered by scanning the whole right side once per
9844/// left row. The cost was (left rows x right rows), which is why
9845/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
9846/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
9847/// row that does not pays for all of it. Over 100k left rows, raising the
9848/// right side from 100 to 10,000 took 35 ms to 2848.
9849///
9850/// This is the shape round 485 already solved for DISTINCT, and it reuses
9851/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
9852/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
9853/// every bucket with the exact comparator, so a collision costs time and
9854/// never an answer.
9855struct PeerIndex<'r> {
9856 bh: hashbrown::DefaultHashBuilder,
9857 buckets: hashbrown::HashMap<u64, Vec<usize>>,
9858 rows: &'r [Row<'static>],
9859 fold: FoldSpec<'r>,
9860}
9861
9862impl<'r> PeerIndex<'r> {
9863 fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
9864 // ONE hasher for the whole pass: the default builder is seeded per
9865 // instance, so a fresh one per row would put equal rows in different
9866 // buckets.
9867 let bh = hashbrown::DefaultHashBuilder::default();
9868 let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
9869 hashbrown::HashMap::with_capacity(rows.len());
9870 for (i, r) in rows.iter().enumerate() {
9871 buckets
9872 .entry(norm_hash_row(r, &bh, fold))
9873 .or_default()
9874 .push(i);
9875 }
9876 Self {
9877 bh,
9878 buckets,
9879 rows,
9880 fold,
9881 }
9882 }
9883
9884 fn contains(&self, r: &Row<'static>) -> bool {
9885 let h = norm_hash_row(r, &self.bh, self.fold);
9886 self.buckets
9887 .get(&h)
9888 .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
9889 }
9890
9891 /// Remove ONE occurrence, so the multiset forms cancel row for row the
9892 /// way the pool they replaced did.
9893 fn take_one(&mut self, r: &Row<'static>) -> bool {
9894 let h = norm_hash_row(r, &self.bh, self.fold);
9895 let Some(b) = self.buckets.get_mut(&h) else {
9896 return false;
9897 };
9898 let Some(pos) = b
9899 .iter()
9900 .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
9901 else {
9902 return false;
9903 };
9904 b.swap_remove(pos);
9905 true
9906 }
9907}
9908
9909pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
9910 dedup_by_row(rows, |r| r, fold)
9911}
9912
9913/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
9914/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
9915/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
9916/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
9917/// order is preserved, and correctness needs only the one-way guarantee
9918/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
9919/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
9920fn dedup_by_row<T>(
9921 items: Vec<T>,
9922 row_of: impl Fn(&T) -> &Row<'static>,
9923 fold: FoldSpec<'_>,
9924) -> Vec<T> {
9925 if items.len() <= 32 {
9926 let mut out: Vec<T> = Vec::with_capacity(items.len());
9927 for it in items {
9928 if !out
9929 .iter()
9930 .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
9931 {
9932 out.push(it);
9933 }
9934 }
9935 return out;
9936 }
9937 // ONE BuildHasher instance for the whole pass — the default builder
9938 // is randomly seeded PER INSTANCE, so a fresh one per row would give
9939 // equal rows different hashes and never dedup.
9940 let bh = hashbrown::DefaultHashBuilder::default();
9941 let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
9942 let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9943 hashbrown::HashMap::with_capacity(items.len());
9944 for it in items {
9945 let h = norm_hash_row(row_of(&it), &bh, fold);
9946 let bucket = buckets.entry(h).or_default();
9947 if !bucket
9948 .iter()
9949 .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
9950 {
9951 bucket.push(out.len());
9952 out.push(it);
9953 }
9954 }
9955 out
9956}
9957
9958/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
9959/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
9960/// rows may collide (buckets are re-checked with the exact comparator).
9961///
9962/// Domain design mirrors `value_cmp`'s equivalence classes:
9963/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
9964/// shares one domain: a value that is an integer fitting i64 hashes the
9965/// i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
9966/// anything else hashes the f64 approximation computed by THE SAME
9967/// formula the value_cmp float arms use (`numeric_to_f64`), so
9968/// `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
9969/// hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
9970/// Known un-closable corner: an integer in [2^53, 2^63) can compare
9971/// Equal to a float via value_cmp's lossy f64 arm while hashing in the
9972/// exact-i64 domain — mixed int/float rows at that magnitude may miss a
9973/// dedup (PG itself compares int8↔float8 in the lossy float8 domain).
9974/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
9975/// compares them blank-insensitively; plain Text pairs that differ only
9976/// in trailing blanks merely collide and are separated exactly).
9977/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
9978/// hash their fields under a distinct tag.
9979/// - Everything value_cmp falls back to debug-format ordering for
9980/// (Json, arrays, vectors, geometry, ranges, …) shares one constant
9981/// bucket — degrades to the exact linear scan, never wrong.
9982fn norm_hash_row(
9983 row: &Row<'static>,
9984 bh: &hashbrown::DefaultHashBuilder,
9985 fold: FoldSpec<'_>,
9986) -> u64 {
9987 norm_hash_values(&row.values, bh, fold)
9988}
9989
9990/// v7.39 (round 485) — the same hash over a bare value slice, so the
9991/// DISTINCT probe can run against a reused buffer instead of demanding a
9992/// `Row` that has to be allocated first (see `values_eq_norm`).
9993fn norm_hash_values(
9994 values: &[Value<'static>],
9995 bh: &hashbrown::DefaultHashBuilder,
9996 fold: FoldSpec<'_>,
9997) -> u64 {
9998 use core::hash::{BuildHasher, Hash, Hasher};
9999 let mut h = bh.build_hasher();
10000 for (i, v) in values.iter().enumerate() {
10001 // v7.39 (round 410) — hash the folded key when the MySQL collation
10002 // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
10003 // `'A'` vs `'a '`) share a hash bucket.
10004 //
10005 // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
10006 // byte-wise column that folded here while the comparator did not
10007 // would scatter equal rows across buckets and stop de-duplicating
10008 // at all; the hash and the comparator have to read the same mask.
10009 if fold.folds(i)
10010 && let Some(folded) = mysql_dedup_fold(v)
10011 {
10012 folded.hash(&mut h);
10013 continue;
10014 }
10015 norm_hash_value(v, &mut h);
10016 }
10017 h.finish()
10018}
10019
10020/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
10021///
10022/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
10023const fn pow10_i128(p: u16) -> Option<i128> {
10024 const P: [i128; 39] = {
10025 let mut t = [1i128; 39];
10026 let mut i = 1;
10027 while i < 39 {
10028 t[i] = t[i - 1] * 10;
10029 i += 1;
10030 }
10031 t
10032 };
10033 if (p as usize) < P.len() {
10034 Some(P[p as usize])
10035 } else {
10036 None
10037 }
10038}
10039
10040fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
10041 const TAG_NULL: u8 = 0;
10042 const TAG_BOOL: u8 = 1;
10043 const TAG_NUM_I64: u8 = 2;
10044 const TAG_NUM_F64: u8 = 3;
10045 const TAG_TEXT: u8 = 4;
10046 const TAG_DATE: u8 = 6;
10047 const TAG_TIME: u8 = 7;
10048 const TAG_TIMESTAMP: u8 = 8;
10049 const TAG_TIMETZ: u8 = 10;
10050 const TAG_UUID: u8 = 11;
10051 const TAG_MONEY: u8 = 12;
10052 const TAG_BYTES: u8 = 13;
10053 const TAG_INTERVAL: u8 = 14;
10054 const TAG_CHAR1: u8 = 15;
10055 const TAG_OPAQUE: u8 = 255;
10056 // One shared writer for the numeric family: an integer value
10057 // representable as i64 goes exact (round-trip probe — no_std, so no
10058 // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
10059 // through 0i64, folding it into 0.0 as value_cmp requires.
10060 let num_f64 = |h: &mut H, x: f64| {
10061 if x.is_nan() {
10062 h.write_u8(TAG_NUM_F64);
10063 h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
10064 return;
10065 }
10066 const TWO63: f64 = 9_223_372_036_854_775_808.0;
10067 if (-TWO63..TWO63).contains(&x) {
10068 #[allow(clippy::cast_possible_truncation)]
10069 let n = x as i64;
10070 #[allow(clippy::cast_precision_loss)]
10071 if (n as f64) == x {
10072 h.write_u8(TAG_NUM_I64);
10073 h.write_i64(n);
10074 return;
10075 }
10076 }
10077 h.write_u8(TAG_NUM_F64);
10078 h.write_u64(x.to_bits());
10079 };
10080 match v {
10081 Value::Null => h.write_u8(TAG_NULL),
10082 Value::Bool(b) => {
10083 h.write_u8(TAG_BOOL);
10084 h.write_u8(u8::from(*b));
10085 }
10086 Value::SmallInt(n) => {
10087 h.write_u8(TAG_NUM_I64);
10088 h.write_i64(i64::from(*n));
10089 }
10090 Value::Int(n) => {
10091 h.write_u8(TAG_NUM_I64);
10092 h.write_i64(i64::from(*n));
10093 }
10094 Value::BigInt(n) => {
10095 h.write_u8(TAG_NUM_I64);
10096 h.write_i64(*n);
10097 }
10098 Value::Float(x) => num_f64(h, *x),
10099 Value::Numeric {
10100 scaled,
10101 scale,
10102 kind,
10103 } => match kind {
10104 spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
10105 spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
10106 spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
10107 spg_storage::NumericKind::Finite => {
10108 // Reduce trailing fractional zeros so 1.50 and 1.5 share a
10109 // representation, then: exact integers fitting i64 go to the
10110 // i64 domain; everything else uses numeric_to_f64 — the SAME
10111 // formula value_cmp's Numeric↔Float arm compares with.
10112 // r1044 — the reduction is required (`1.5` and `1.50` are
10113 // one value and must land in one bucket) and it used to
10114 // walk one digit at a time. That is O(scale), and scale
10115 // is not small in practice: `n / 100` on a NUMERIC
10116 // column stores `9.1900000000000000`, scale 16, so the
10117 // loop ran fourteen times PER ROW.
10118 //
10119 // Priced by ablation rather than guessed at — removing
10120 // the loop entirely took `SELECT DISTINCT n FROM t ORDER
10121 // BY n` over 400,000 rows from 52 ms to 14.8, against
10122 // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
10123 // tried first moved it not at all, which is why this one
10124 // was measured before it was written.
10125 //
10126 // Binary search over the same powers finds the whole
10127 // run of trailing zeros in at most six tests and one
10128 // division, instead of one test and one division per
10129 // digit.
10130 let (mut s, mut sc) = (*scaled, *scale);
10131 if sc > 0 && s != 0 {
10132 let mut lo: u16 = 0;
10133 let mut hi: u16 = sc;
10134 while lo < hi {
10135 let mid = (lo + hi).div_ceil(2);
10136 match pow10_i128(mid) {
10137 Some(p) if s % p == 0 => lo = mid,
10138 _ => hi = mid - 1,
10139 }
10140 }
10141 if lo > 0 {
10142 if let Some(p) = pow10_i128(lo) {
10143 s /= p;
10144 sc -= lo;
10145 }
10146 }
10147 }
10148 if sc == 0 {
10149 if let Ok(n) = i64::try_from(s) {
10150 h.write_u8(TAG_NUM_I64);
10151 h.write_i64(n);
10152 } else {
10153 num_f64(h, crate::orderby::numeric_to_f64(s, 0));
10154 }
10155 } else {
10156 num_f64(h, crate::orderby::numeric_to_f64(s, sc));
10157 }
10158 }
10159 },
10160 // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
10161 // value that also fits i128 reuses the Numeric path above so
10162 // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
10163 // any i128-representable value — constant bucket is safe.
10164 Value::NumericBig(b) => match b.to_i128() {
10165 Some(s) => norm_hash_value(
10166 &Value::Numeric {
10167 scaled: s,
10168 scale: b.scale(),
10169 kind: spg_storage::NumericKind::Finite,
10170 },
10171 h,
10172 ),
10173 None => h.write_u8(TAG_OPAQUE),
10174 },
10175 // value_cmp compares Text↔BpChar blank-insensitively (both sides
10176 // trimmed), so both hash the trimmed bytes. Text pairs differing
10177 // only in trailing blanks collide and are split exactly in-bucket.
10178 Value::Text(s) | Value::BpChar(s) => {
10179 h.write_u8(TAG_TEXT);
10180 h.write(s.trim_end_matches(' ').as_bytes());
10181 }
10182 Value::Char1(c) => {
10183 h.write_u8(TAG_CHAR1);
10184 h.write_u8(*c);
10185 }
10186 Value::Date(d) => {
10187 h.write_u8(TAG_DATE);
10188 h.write_i32(*d);
10189 }
10190 Value::Time(t) => {
10191 h.write_u8(TAG_TIME);
10192 h.write_i64(*t);
10193 }
10194 Value::Timestamp(t) => {
10195 h.write_u8(TAG_TIMESTAMP);
10196 h.write_i64(*t);
10197 }
10198 Value::TimeTz { us, offset_secs } => {
10199 h.write_u8(TAG_TIMETZ);
10200 h.write_i64(*us);
10201 h.write_i32(*offset_secs);
10202 }
10203 Value::Uuid(u) => {
10204 h.write_u8(TAG_UUID);
10205 h.write(u);
10206 }
10207 Value::Money(c) => {
10208 h.write_u8(TAG_MONEY);
10209 h.write_i64(*c);
10210 }
10211 Value::Bytes(b) => {
10212 h.write_u8(TAG_BYTES);
10213 h.write(b.as_ref());
10214 }
10215 Value::Interval {
10216 months,
10217 days,
10218 micros,
10219 } => {
10220 h.write_u8(TAG_INTERVAL);
10221 h.write_i32(*months);
10222 h.write_i32(*days);
10223 h.write_i64(*micros);
10224 }
10225 // v7.37.16 — REAL joined the numeric value_cmp family (widened
10226 // to f64, same formulas as the arms), so it hashes in the shared
10227 // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
10228 // f32→f64 is exact, so equal-under-cmp implies equal bits here.
10229 Value::Real(x) => num_f64(h, f64::from(*x)),
10230 // Json (structural equality), vector families (float rendering),
10231 // arrays / geometry / net / ranges / composites (debug-format
10232 // fallback): one constant bucket — exact linear within.
10233 _ => h.write_u8(TAG_OPAQUE),
10234 }
10235}
10236
10237/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
10238/// treats numerically-equal exact values as one regardless of type or scale
10239/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
10240/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
10241/// `Row` `==` would keep them distinct.
10242/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
10243/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
10244/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
10245/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
10246/// the folded comparison key for a text value, None for anything else (which
10247/// keeps the byte-exact `value_cmp` path).
10248fn mysql_dedup_fold(v: &Value) -> Option<String> {
10249 match v {
10250 Value::Text(s) | Value::BpChar(s) => {
10251 Some(spg_storage::mysql_ci_fold(s.trim_end_matches(' ')))
10252 }
10253 _ => None,
10254 }
10255}
10256
10257/// v7.39 (round 485) — how many projected rows the single-table scan
10258/// builds, and how many of those the DISTINCT probe throws away again.
10259///
10260/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
10261/// 21 % of all samples in malloc/free called straight from the scan
10262/// closure. The closure's one per-row allocation is the projected
10263/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
10264/// instructions later — but "most" is a guess until it is a number, so
10265/// these count it. (Round 480 was spent acting on an inference about a
10266/// branch that turned out never to run.)
10267/// v7.39 (round 488) — reachability counters for round 487's projection
10268/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
10269/// and a never-called-function probe rules out code layout — so the
10270/// question is whether that shape reaches this code at all, which is a
10271/// number, not an inference.
10272pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10273pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10274
10275pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10276pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
10277 core::sync::atomic::AtomicU64::new(0);
10278
10279/// v7.38.13 — how DISTINCT must compare one row of output.
10280///
10281/// The MySQL default collation folds case and trailing spaces when it
10282/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
10283/// must not fold — `e2e_mysql_collate_binary_round370` calls the
10284/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
10285/// one when the schema asked to keep them apart", and names DISTINCT as
10286/// one of the sites that has to honour it.
10287///
10288/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
10289/// value in a MySQL session, because a bool cannot see a column. The
10290/// GROUP BY path consults the schema and was right all along; the test
10291/// only ever exercised that spelling, so the DISTINCT hole was never
10292/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
10293///
10294/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
10295/// which is what a caller with no schema to offer gets.
10296#[derive(Clone, Copy)]
10297pub(crate) struct FoldSpec<'c> {
10298 mysql: bool,
10299 binary: &'c [bool],
10300}
10301
10302impl<'c> FoldSpec<'c> {
10303 /// No column information — every Text position folds under MySQL.
10304 pub(crate) const fn dialect(mysql: bool) -> Self {
10305 Self { mysql, binary: &[] }
10306 }
10307
10308 /// The mask read off the output columns.
10309 pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
10310 Self { mysql, binary }
10311 }
10312
10313 /// Does position `i` fold?
10314 #[inline]
10315 fn folds(&self, i: usize) -> bool {
10316 self.mysql && !self.binary.get(i).copied().unwrap_or(false)
10317 }
10318}
10319
10320/// The fold-exempt mask for a projection.
10321///
10322/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
10323/// projection rebuilds that schema through `ColumnSchema::new`, whose
10324/// collation default is `Binary` — a mask built from it would mark
10325/// EVERY column byte-wise and stop DISTINCT folding at all.
10326pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
10327 projection.iter().map(|p| p.fold_exempt).collect()
10328}
10329
10330pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
10331 values_eq_norm(&a.values, &b.values, fold)
10332}
10333
10334/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
10335/// DISTINCT probe can compare a reused projection buffer against a kept
10336/// row without building a `Row` for it.
10337pub(crate) fn values_eq_norm(
10338 a: &[Value<'static>],
10339 b: &[Value<'static>],
10340 fold: FoldSpec<'_>,
10341) -> bool {
10342 a.len() == b.len()
10343 && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
10344 if fold.folds(i)
10345 && let (Some(fx), Some(fy)) = (mysql_dedup_fold(x), mysql_dedup_fold(y))
10346 {
10347 return fx == fy;
10348 }
10349 crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
10350 })
10351}
10352
10353/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
10354/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
10355/// order via the byte values; vectors are not sortable.
10356pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
10357 // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
10358 // so values sharing a ≥6-byte common prefix (`product_001` vs
10359 // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
10360 // order by their exact bytes instead of the old lossy f64 coarse key.
10361 // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
10362 // matches PG's default C / binary text collation. Every other type
10363 // keeps the lossless-enough `f64` fast path below.
10364 if let Value::Text(s) = v {
10365 return Ok(OrderKey::Text(s.as_ref().into()));
10366 }
10367 // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
10368 // byte order (PG bpcharcmp under C collation), so mixed-pad values of
10369 // the same logical string order equal.
10370 if let Value::BpChar(s) = v {
10371 return Ok(OrderKey::Text(s.trim_end_matches(' ').into()));
10372 }
10373 // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
10374 // carry the parsed value and compare it structurally (see
10375 // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
10376 if let Value::Json(s) = v {
10377 return Ok(match crate::json::parse(s) {
10378 Ok(jv) => OrderKey::Json(jv),
10379 Err(_) => OrderKey::Text(s.as_ref().into()),
10380 });
10381 }
10382 // v7.37 — byte-orderable types PG sorts byte-wise but that have no
10383 // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
10384 // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
10385 // matching PG's network ordering.
10386 match v {
10387 Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
10388 // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
10389 Value::NumericBig(b) => {
10390 return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
10391 spg_storage::NumericKey::from_big(b),
10392 )));
10393 }
10394 Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
10395 Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
10396 Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
10397 Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
10398 Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
10399 let mut key = alloc::vec::Vec::with_capacity(18);
10400 key.push(*family);
10401 key.extend_from_slice(addr);
10402 key.push(*bits);
10403 return Ok(OrderKey::Bytes(key));
10404 }
10405 _ => {}
10406 }
10407 // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
10408 // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
10409 // own OrderKey so integer arrays sort numerically; a NULL element rides to
10410 // the end via the +INF sentinel.
10411 let inf = || OrderKey::NullBig;
10412 let arr = match v {
10413 Value::IntArray(a) => Some(
10414 a.iter()
10415 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10416 .collect(),
10417 ),
10418 Value::SmallIntArray(a) => Some(
10419 a.iter()
10420 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10421 .collect(),
10422 ),
10423 Value::BigIntArray(a) => Some(
10424 a.iter()
10425 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10426 .collect(),
10427 ),
10428 Value::BoolArray(a) => Some(
10429 a.iter()
10430 .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
10431 .collect(),
10432 ),
10433 Value::TextArray(a) => Some(
10434 a.iter()
10435 .map(|o| o.as_ref().map_or_else(inf, |s| OrderKey::Text(s.clone())))
10436 .collect(),
10437 ),
10438 #[allow(clippy::cast_precision_loss)]
10439 Value::FloatArray(a) => Some(
10440 a.iter()
10441 .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
10442 .collect(),
10443 ),
10444 // r1040 — array elements take the same exact key their scalar
10445 // form does; an f64 projection here would order `{0.1}` against
10446 // `{0.1000000000000000001}` by luck.
10447 Value::NumericArray(a) => Some(
10448 a.iter()
10449 .map(|o| {
10450 o.map_or_else(inf, |(m, s)| {
10451 OrderKey::Numeric(alloc::boxed::Box::new(
10452 spg_storage::NumericKey::from_numeric(
10453 m,
10454 s,
10455 spg_storage::NumericKind::Finite,
10456 ),
10457 ))
10458 })
10459 })
10460 .collect(),
10461 ),
10462 Value::DateArray(a) => Some(
10463 a.iter()
10464 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10465 .collect(),
10466 ),
10467 _ => None,
10468 };
10469 if let Some(elements) = arr {
10470 return Ok(OrderKey::Array(elements));
10471 }
10472 // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
10473 // right, which is exactly the lexicographic element order an Array key
10474 // already gives: `(2,'b') < (9,'a')` because the leading field decides.
10475 if let Value::Composite(fields) = v {
10476 let elements = fields
10477 .iter()
10478 .map(|(_, fv)| value_to_order_key(fv))
10479 .collect::<Result<alloc::vec::Vec<_>, _>>()?;
10480 return Ok(OrderKey::Array(elements));
10481 }
10482 // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
10483 // Projecting these to f64 (the historic path) silently collapses BigInt /
10484 // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
10485 // the wrong order for large ids and microsecond timestamps.
10486 match v {
10487 Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
10488 Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
10489 Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
10490 // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
10491 // integer (days / micros / cents / calendar year); TIMETZ by the
10492 // UTC-equivalent micros (local wall - offset) so the same physical
10493 // instant in different zones sorts equal.
10494 Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
10495 Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
10496 Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
10497 Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
10498 Value::TimeTz { us, offset_secs } => {
10499 return Ok(OrderKey::Int(
10500 i128::from(*us) - i128::from(*offset_secs) * 1_000_000,
10501 ));
10502 }
10503 Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
10504 _ => {}
10505 }
10506 let num = match v {
10507 // Callers without NULLS FIRST/LAST context (array elements,
10508 // histogram sampling) put NULL last, as before.
10509 Value::Null => return Ok(OrderKey::NullBig),
10510 // v7.17.0 Phase 3.P0-38 — range ordering is not supported
10511 // in v7.17.0 (needs lex-then-inclusivity tiebreak).
10512 Value::Range { .. } => {
10513 return Err(EngineError::Unsupported(
10514 "ORDER BY of a range value is not supported in v7.17.0".into(),
10515 ));
10516 }
10517 // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
10518 Value::Hstore(_) => {
10519 return Err(EngineError::Unsupported(
10520 "ORDER BY of a hstore value is not supported".into(),
10521 ));
10522 }
10523 // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
10524 Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
10525 return Err(EngineError::Unsupported(
10526 "ORDER BY of a 2D array is not supported in v7.17.0".into(),
10527 ));
10528 }
10529 // r1039/r1040 — the exact canonical key, not an f64 projection.
10530 //
10531 // r1039 fixed the three specials, which carry a canonical zero in
10532 // `scaled` and so all sorted as the number 0. The projection
10533 // itself was the rest of the defect: "precision losses here only
10534 // matter for tie-breaks well past 15 significant digits" was the
10535 // comment, and the measurement disagreed — f64 called
10536 // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
10537 // returned them in insertion order. Three of ten values came back
10538 // in the wrong place against PG18.4.
10539 Value::Numeric {
10540 scaled,
10541 scale,
10542 kind,
10543 } => {
10544 return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
10545 spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
10546 )));
10547 }
10548 Value::Float(x) => *x,
10549 // v7.37.16 — REAL sorts by its exact f64 widening (it had no
10550 // arm and fell through to the unsupported error).
10551 Value::Real(x) => f64::from(*x),
10552 Value::Bool(b) => {
10553 if *b {
10554 1.0
10555 } else {
10556 0.0
10557 }
10558 }
10559 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
10560 return Err(EngineError::Unsupported(
10561 "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
10562 ));
10563 }
10564 // v7.37 — PG orders INTERVAL by its total time, treating a month as
10565 // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
10566 // f64 is exact for any interval under ~285 years, and only ORDER BY
10567 // tie-breaks past that magnitude lose precision. Matches the
10568 // min/max(interval) comparator in aggregate.rs.
10569 #[allow(clippy::cast_precision_loss)]
10570 Value::Interval {
10571 months,
10572 days,
10573 micros,
10574 } => {
10575 let total = i128::from(*months) * 30 * 86_400_000_000
10576 + i128::from(*days) * 86_400_000_000
10577 + i128::from(*micros);
10578 total as f64
10579 }
10580 Value::Json(_) => {
10581 return Err(EngineError::Unsupported(
10582 "ORDER BY of a JSON value is not supported — cast the document to text first"
10583 .into(),
10584 ));
10585 }
10586 // v7.5.0 — Value is #[non_exhaustive]; future variants need
10587 // an explicit ORDER BY mapping. Surface as Unsupported until
10588 // engine support is added.
10589 _ => {
10590 return Err(EngineError::Unsupported(
10591 "ORDER BY of this value type is not supported".into(),
10592 ));
10593 }
10594 };
10595 Ok(OrderKey::Num(num))
10596}
10597
10598/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
10599/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
10600/// `EngineError` so the projection-build path keeps `UnknownQualifier`
10601/// vs `ColumnNotFound` distinct.
10602/// PG's name for the physical row identity. It is reserved there — no table
10603/// can have a column called this — which is what lets `*` skip it by name.
10604pub(crate) const CTID_COLUMN: &str = "ctid";
10605
10606/// v7.39 (round 512) — PG's system columns, in the order they are appended.
10607/// All six are reserved names there, which is what lets `*` skip them and
10608/// lets a scan tell them from a user column without a flag.
10609pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
10610
10611/// Is this name one of them?
10612pub(crate) fn is_system_column(name: &str) -> bool {
10613 SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
10614}
10615
10616/// Where the scan's appended system columns begin, if this schema carries
10617/// them: the trailing six, named in order. A catalog view with a column of
10618/// its own called `xmin` does not match, which is the point.
10619fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
10620 let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
10621 cols[start..]
10622 .iter()
10623 .zip(SYSTEM_COLUMNS)
10624 .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
10625 .then_some(start)
10626}
10627
10628/// v7.39 (round 540) — which positions `*` must skip.
10629///
10630/// The rule stays round 512's — the synthetic columns are the trailing
10631/// six of a relation's block, matched by POSITION so a genuine `xmin`
10632/// column is not lost — but a JOINED schema names its columns
10633/// `alias.column` and lays the peers out end to end, so a peer's six sit
10634/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
10635/// "trailing six" test back on the block it was written for.
10636fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
10637 let mut skip = alloc::vec![false; cols.len()];
10638 fn qualifier(n: &str) -> Option<&str> {
10639 n.rsplit_once('.').map(|(q, _)| q)
10640 }
10641 fn bare(n: &str) -> &str {
10642 n.rsplit('.').next().unwrap_or(n)
10643 }
10644 let mut i = 0;
10645 while i < cols.len() {
10646 let q = qualifier(&cols[i].name);
10647 let mut end = i;
10648 while end < cols.len() && qualifier(&cols[end].name) == q {
10649 end += 1;
10650 }
10651 if let Some(start) = (end - i)
10652 .checked_sub(SYSTEM_COLUMNS.len())
10653 .map(|off| i + off)
10654 && cols[start..end]
10655 .iter()
10656 .zip(SYSTEM_COLUMNS)
10657 .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
10658 {
10659 for s in skip.iter_mut().take(end).skip(start) {
10660 *s = true;
10661 }
10662 }
10663 i = end;
10664 }
10665 skip
10666}
10667
10668/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
10669/// read? Only then is the column materialised.
10670pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
10671 let mut found = false;
10672 crate::expr_analysis::visit_expr_columns_and_subqueries(
10673 e,
10674 &mut |c| {
10675 if is_system_column(&c.name) {
10676 found = true;
10677 }
10678 },
10679 &mut |_| {},
10680 );
10681 found
10682}
10683
10684fn references_ctid(stmt: &SelectStatement) -> bool {
10685 let in_expr = expr_references_ctid;
10686 stmt.items.iter().any(|i| match i {
10687 SelectItem::Expr { expr, .. } => in_expr(expr),
10688 _ => false,
10689 }) || stmt.where_.as_ref().is_some_and(in_expr)
10690 || stmt.order_by.iter().any(|o| in_expr(&o.expr))
10691 || stmt
10692 .group_by
10693 .as_ref()
10694 .is_some_and(|g| g.iter().any(in_expr))
10695 || stmt.having.as_ref().is_some_and(in_expr)
10696}
10697
10698/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
10699/// is a name the projection has to TYPE before any row exists.
10700///
10701/// Evaluation has answered this since round T9 (`resolve_column` builds a
10702/// `Value::Composite` of every column), but the typing side below had no
10703/// such branch and raised `column "t" does not exist` first — so the
10704/// feature was unreachable through a projection. Measured against PG18.4:
10705/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
10706///
10707/// The type is `Jsonb` + a composite marker, which is exactly how a
10708/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
10709/// the value travels as a `Value::Composite` and renders in the canonical
10710/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
10711/// so the marker names the alias and no rehydration keys off it — the
10712/// value arrives already built.
10713fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
10714 let mut s = ColumnSchema::new(
10715 alloc::string::String::from(alias),
10716 spg_storage::DataType::Jsonb,
10717 true,
10718 );
10719 s.user_composite_type = Some(alloc::string::String::from(alias));
10720 s
10721}
10722
10723pub(crate) fn resolve_projection_column<'a>(
10724 c: &ColumnName,
10725 schema_cols: &'a [ColumnSchema],
10726 table_alias: &str,
10727) -> Result<Cow<'a, ColumnSchema>, EngineError> {
10728 if let Some(q) = &c.qualifier {
10729 let composite = alloc::format!("{q}.{name}", name = c.name);
10730 if let Some(s) = schema_cols.iter().find(|s| s.name == composite) {
10731 return Ok(Cow::Borrowed(s));
10732 }
10733 // Single-table case: the qualifier may equal the active alias —
10734 // then look for the bare column name.
10735 if q == table_alias
10736 && let Some(s) = schema_cols.iter().find(|s| s.name == c.name)
10737 {
10738 return Ok(Cow::Borrowed(s));
10739 }
10740 // For multi-table schemas the qualifier is unknown only if no
10741 // column bears the "<q>." prefix. For single-table, the alias
10742 // mismatch alone is enough.
10743 let prefix = alloc::format!("{q}.");
10744 let qualifier_known =
10745 q == table_alias || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
10746 if !qualifier_known {
10747 return Err(EngineError::Eval(EvalError::UnknownQualifier {
10748 qualifier: q.clone(),
10749 }));
10750 }
10751 return Err(EngineError::Eval(EvalError::ColumnNotFound {
10752 name: c.name.clone(),
10753 }));
10754 }
10755 if let Some(s) = schema_cols.iter().find(|s| s.name == c.name) {
10756 return Ok(Cow::Borrowed(s));
10757 }
10758 let suffix = alloc::format!(".{name}", name = c.name);
10759 let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
10760 let first = matches.next();
10761 let extra = matches.next();
10762 match (first, extra) {
10763 (Some(s), None) => Ok(Cow::Borrowed(s)),
10764 (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
10765 detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
10766 })),
10767 // The whole-row reference, checked LAST so a real column carrying
10768 // the alias's name still wins — the same precedence
10769 // `resolve_column` applies on the evaluation side.
10770 //
10771 // Two schema shapes reach here. A single-table (or subquery, or
10772 // CTE) scan carries its alias and bare column names, so the name
10773 // has to equal the alias. A JOIN's combined schema carries no
10774 // alias at all and qualifies every column `alias.col`, so the
10775 // alias is identified by the prefix instead — which is exactly
10776 // how `whole_row_composite` picks the fields out on the
10777 // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
10778 // answers `(7,z)` on PG18.4 and errored here until this arm
10779 // covered the joined shape too.
10780 _ if !table_alias.is_empty() && c.name == table_alias => {
10781 Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
10782 }
10783 _ if table_alias.is_empty() && {
10784 let prefix = alloc::format!("{name}.", name = c.name);
10785 schema_cols.iter().any(|s| s.name.starts_with(&prefix))
10786 } =>
10787 {
10788 Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
10789 }
10790 _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
10791 name: c.name.clone(),
10792 })),
10793 }
10794}
10795
10796/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
10797/// parser to carry per-branch GROUPING() masks into a grouping-set query's
10798/// ORDER BY. They must never reach the output. No-op unless such a column is
10799/// present, so the common path is untouched.
10800/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
10801///
10802/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
10803/// a `LIMIT 2` that should have answered two groups answered one.
10804fn apply_deferred_limit(
10805 rows: alloc::vec::Vec<Row<'static>>,
10806 deferred: &(
10807 Option<spg_sql::ast::LimitExpr>,
10808 Option<spg_sql::ast::LimitExpr>,
10809 ),
10810) -> alloc::vec::Vec<Row<'static>> {
10811 let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
10812 Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
10813 _ => None,
10814 };
10815 let mut rows = rows;
10816 if let Some(off) = count(&deferred.1) {
10817 rows = rows.split_off(off.min(rows.len()));
10818 }
10819 if let Some(lim) = count(&deferred.0) {
10820 rows.truncate(lim);
10821 }
10822 rows
10823}
10824
10825fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
10826 let QueryResult::Rows { columns, rows } = result else {
10827 return result;
10828 };
10829 if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
10830 return QueryResult::Rows { columns, rows };
10831 }
10832 let keep: Vec<usize> = columns
10833 .iter()
10834 .enumerate()
10835 .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
10836 .map(|(i, _)| i)
10837 .collect();
10838 let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
10839 let new_rows: Vec<Row<'static>> = rows
10840 .into_iter()
10841 .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
10842 .collect();
10843 QueryResult::Rows {
10844 columns: new_cols,
10845 rows: new_rows,
10846 }
10847}
10848
10849/// v7.39 (round 487) — bind every projection item that is a bare column
10850/// reference to its position, once per query.
10851///
10852/// `#[inline(never)]` and out of line on purpose. Round 486 established
10853/// that adding code inside these scan bodies moves neighbouring hot
10854/// functions around under fat LTO: the first version of this had the loop
10855/// inline in `run_single_table_scan` and four aggregate shapes that never
10856/// touch that function — `full_agg`, `join_agg`, `group_500k`,
10857/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
10858/// the same machine. Keeping it out of line kept them still.
10859#[inline(never)]
10860fn bind_direct_columns(
10861 projection: &[ProjectedItem],
10862 ctx: &eval::EvalContext<'_>,
10863) -> Vec<Option<usize>> {
10864 projection
10865 .iter()
10866 .map(|p| match &p.expr {
10867 Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
10868 // Same exclusion `compile_into` makes: a composite column
10869 // has to be rehydrated from stored JSON, which is not a
10870 // cell read.
10871 ctx.columns
10872 .get(*pos)
10873 .is_none_or(|sc| sc.user_composite_type.is_none())
10874 }),
10875 _ => None,
10876 })
10877 .collect()
10878}
10879
10880/// v7.39 (round 505) — the name an un-aliased projected expression reports.
10881///
10882/// PG18 names a call for its function and everything else `?column?`;
10883/// measured with `\gdesc`. SPG used to print the parsed expression back
10884/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
10885/// name-keyed row access found nothing under `upper`.
10886///
10887/// The MySQL half is NOT this rule and is deliberately left alone here:
10888/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
10889/// which needs the parser to hand over spans the AST does not carry yet.
10890/// Until it does, a MySQL session keeps the printed form — closer to what
10891/// MariaDB answers than `?column?` would be.
10892pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
10893 if mysql {
10894 return expr.to_string();
10895 }
10896 spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
10897}
10898
10899pub(crate) fn build_projection(
10900 items: &[SelectItem],
10901 schema_cols: &[ColumnSchema],
10902 table_alias: &str,
10903 mysql: bool,
10904) -> Result<Vec<ProjectedItem>, EngineError> {
10905 build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0)
10906}
10907
10908/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
10909/// invisible to `*`.
10910///
10911/// The windowed-SELECT path appends a synthetic `__win_N` column per window
10912/// function so the rewritten projection can reference the computed values as
10913/// ordinary columns. `*` then expanded them too, and
10914/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
10915/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
10916/// silent one: the row simply had one more field than the client asked for.
10917///
10918/// Hidden by POSITION rather than by name, for the reason round 512 recorded
10919/// about the system columns: a name test looks safe until a real column
10920/// happens to carry the name. These are appended last, so the count is what
10921/// identifies them.
10922pub(crate) fn build_projection_hiding_tail(
10923 items: &[SelectItem],
10924 schema_cols: &[ColumnSchema],
10925 table_alias: &str,
10926 mysql: bool,
10927 hidden_tail: usize,
10928) -> Result<Vec<ProjectedItem>, EngineError> {
10929 let visible = schema_cols.len().saturating_sub(hidden_tail);
10930 // v7.39 (round 462) — a join's combined schema qualifies every column
10931 // `alias.col` so the deferred-join cell lookups resolve by composite
10932 // name. That is an internal convention, and `*` was handing it to the
10933 // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
10934 // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
10935 // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
10936 // already learned this for `q.*`; plain `*` never got the same rule.
10937 //
10938 // The signal is the schema itself, not the call site: only a combined
10939 // join schema arrives with no table alias AND every column qualified.
10940 // A single-table schema carries its alias, an empty schema has nothing
10941 // to strip, and a synthetic schema's names carry no dot.
10942 let joined_schema = table_alias.is_empty()
10943 && !schema_cols.is_empty()
10944 && schema_cols.iter().all(|c| c.name.contains('.'));
10945 let bare_name = |name: &str| -> String {
10946 if !joined_schema {
10947 return name.to_string();
10948 }
10949 match name.split_once('.') {
10950 Some((_, rest)) if !rest.is_empty() => rest.to_string(),
10951 _ => name.to_string(),
10952 }
10953 };
10954 let mut out = Vec::new();
10955 for item in items {
10956 match item {
10957 SelectItem::Wildcard => {
10958 // v7.39 (round 511) — `*` never expands a system column, as
10959 // PG's does not. They join the schema only when the statement
10960 // asked for them, so this matters for the mixed shape
10961 // `SELECT *, ctid FROM t`.
10962 //
10963 // v7.39 (round 512) — by POSITION, not by name. Matching on
10964 // the name alone looked safe because PG reserves them, and it
10965 // is not: `pg_replication_slots` genuinely has a column called
10966 // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
10967 // Only the trailing six, in the order the scan appends them,
10968 // are the synthetic ones.
10969 let sys_skip = synthetic_system_positions(schema_cols);
10970 for (idx, col) in schema_cols.iter().enumerate() {
10971 if sys_skip[idx] || idx >= visible {
10972 continue;
10973 }
10974 out.push(ProjectedItem {
10975 expr: Expr::Column(ColumnName {
10976 qualifier: None,
10977 name: col.name.clone(),
10978 }),
10979 output_name: bare_name(&col.name),
10980 ty: col.ty,
10981 nullable: col.nullable,
10982 user_enum_type: col.user_enum_type.clone(),
10983 mysql_fsp: col.mysql_fsp,
10984 collation_name: col.collation_name.clone(),
10985 fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
10986 });
10987 }
10988 }
10989 // v7.39 (round 128) — `q.*` expands to every column belonging to
10990 // the qualifier `q`. Single-table schemas carry bare column names
10991 // reachable via `table_alias`; a join's combined schema carries
10992 // `alias.col` names, so a column belongs to `q` when its name has
10993 // the `q.` prefix. PG labels the expanded columns by their bare
10994 // name, so the `alias.` prefix is stripped from the output name.
10995 SelectItem::QualifiedWildcard(q) => {
10996 let prefix = alloc::format!("{q}.");
10997 let single_table = !table_alias.is_empty() && q == table_alias;
10998 let mut matched = 0usize;
10999 for col in &schema_cols[..visible] {
11000 let belongs =
11001 col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
11002 if !belongs {
11003 continue;
11004 }
11005 matched += 1;
11006 let output_name = col
11007 .name
11008 .strip_prefix(&prefix)
11009 .unwrap_or(&col.name)
11010 .to_string();
11011 out.push(ProjectedItem {
11012 expr: Expr::Column(ColumnName {
11013 qualifier: None,
11014 name: col.name.clone(),
11015 }),
11016 output_name,
11017 ty: col.ty,
11018 nullable: col.nullable,
11019 user_enum_type: col.user_enum_type.clone(),
11020 mysql_fsp: col.mysql_fsp,
11021 collation_name: col.collation_name.clone(),
11022 fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
11023 });
11024 }
11025 if matched == 0 {
11026 return Err(EngineError::Eval(EvalError::UnknownQualifier {
11027 qualifier: q.clone(),
11028 }));
11029 }
11030 }
11031 SelectItem::Expr { expr, alias } => {
11032 // Plain column ref keeps full schema info (real type +
11033 // nullability). For compound expressions try the
11034 // describe-side function-return-type table first
11035 // (e.g. `SELECT now()` → Timestamptz, `SELECT
11036 // concat(…)` → Text). Falls back to nullable Text
11037 // for shapes the describe path can't resolve.
11038 if let Expr::Column(c) = expr {
11039 let sch = resolve_projection_column(c, schema_cols, table_alias)?;
11040 let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
11041 out.push(ProjectedItem {
11042 expr: expr.clone(),
11043 output_name,
11044 ty: sch.ty,
11045 nullable: sch.nullable,
11046 // v7.39 (read01 round 54) — a bare enum column keeps
11047 // its enum identity through the projection.
11048 user_enum_type: sch.user_enum_type.clone(),
11049 mysql_fsp: sch.mysql_fsp,
11050 collation_name: sch.collation_name.clone(),
11051 // v7.38.13 — and its byte-wise-ness. This is the
11052 // site `SELECT DISTINCT t FROM t` arrives at.
11053 fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
11054 });
11055 } else if let Some(shape) = describe::describe_expr(expr, schema_cols) {
11056 let output_name = alias
11057 .clone()
11058 .unwrap_or_else(|| default_output_name(expr, mysql));
11059 out.push(ProjectedItem {
11060 expr: expr.clone(),
11061 output_name,
11062 ty: shape.ty,
11063 // v7.39 (round 258) — a projected EXPRESSION keeps its
11064 // enum identity too, not just a bare column. `FROM
11065 // (VALUES ('happy'::mood), …) t(m)` lowers to constant
11066 // SELECTs, so the derived column arrived here as a cast
11067 // and lost the enum — making the outer ORDER BY / min /
11068 // max / array_agg sort by the label's TEXT.
11069 nullable: shape.nullable,
11070 user_enum_type: None,
11071 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11072 // A bare column reference keeps its collation; any
11073 // other expression produces a new value and has none.
11074 collation_name: match expr {
11075 Expr::Column(c) => schema_cols
11076 .iter()
11077 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11078 .and_then(|sc| sc.collation_name.clone()),
11079 _ => None,
11080 },
11081 fold_exempt: match expr {
11082 Expr::Column(c) => schema_cols
11083 .iter()
11084 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11085 .is_some_and(|sc| {
11086 matches!(sc.collation, spg_storage::Collation::Binary)
11087 }),
11088 // Not a column: no declared collation to honour,
11089 // so the session default applies and it folds.
11090 _ => false,
11091 },
11092 });
11093 } else {
11094 let output_name = alias
11095 .clone()
11096 .unwrap_or_else(|| default_output_name(expr, mysql));
11097 out.push(ProjectedItem {
11098 expr: expr.clone(),
11099 output_name,
11100 // A user ENUM has no DataType of its own, so
11101 // `describe_expr` cannot type `'ok'::mood` and the
11102 // item lands HERE, defaulting to text — which is why
11103 // pg_typeof answered `text` and a derived table sorted
11104 // enum values by their label.
11105 ty: DataType::Text,
11106 nullable: true,
11107 user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
11108 .map(alloc::string::String::from),
11109 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11110 collation_name: match expr {
11111 Expr::Column(c) => schema_cols
11112 .iter()
11113 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11114 .and_then(|sc| sc.collation_name.clone()),
11115 _ => None,
11116 },
11117 fold_exempt: match expr {
11118 Expr::Column(c) => schema_cols
11119 .iter()
11120 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11121 .is_some_and(|sc| {
11122 matches!(sc.collation, spg_storage::Collation::Binary)
11123 }),
11124 // Not a column: no declared collation to honour,
11125 // so the session default applies and it folds.
11126 _ => false,
11127 },
11128 });
11129 }
11130 }
11131 }
11132 }
11133 Ok(out)
11134}
11135
11136// ---- v4.12 window-function helpers ----
11137// The (partition-key, order-key, original-index) tuple shape used
11138// across these helpers is intrinsic to the planner. Factoring it
11139// into a typedef adds indirection without making the code clearer,
11140// so several lints are allowed inline on the affected functions
11141// rather than module-wide.
11142
11143/// v4.22: pick more specific column types from observed rows when
11144/// the projection builder defaulted to Text (the v1.x behavior for
11145/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
11146/// land an Int column in the CTE storage table rather than failing
11147/// the insert with "expected TEXT, got INT".
11148pub(crate) fn infer_column_types(
11149 columns: &[ColumnSchema],
11150 rows: &[Row<'static>],
11151) -> Vec<ColumnSchema> {
11152 let mut out = columns.to_vec();
11153 for (col_idx, col) in out.iter_mut().enumerate() {
11154 if col.ty != DataType::Text {
11155 continue;
11156 }
11157 let mut inferred: Option<DataType> = None;
11158 let mut all_null = true;
11159 for row in rows {
11160 let Some(v) = row.values.get(col_idx) else {
11161 continue;
11162 };
11163 let ty = match v {
11164 Value::Null => continue,
11165 Value::SmallInt(_) => DataType::SmallInt,
11166 Value::Int(_) => DataType::Int,
11167 Value::BigInt(_) => DataType::BigInt,
11168 Value::Float(_) => DataType::Float,
11169 Value::Bool(_) => DataType::Bool,
11170 Value::Vector(_) => DataType::Vector {
11171 dim: 0,
11172 encoding: VecEncoding::F32,
11173 },
11174 // v7.38 (read01 U16) — carry array values through with an
11175 // array type so a recursive CTE that projects an array
11176 // (e.g. a SEARCH/CYCLE ord / path column) types the working
11177 // column as an array, not Text.
11178 Value::TextArray(_) => DataType::TextArray,
11179 Value::IntArray(_) => DataType::IntArray,
11180 Value::BigIntArray(_) => DataType::BigIntArray,
11181 Value::SmallIntArray(_) => DataType::SmallIntArray,
11182 Value::FloatArray(_) => DataType::FloatArray,
11183 Value::BoolArray(_) => DataType::BoolArray,
11184 // v7.39 (GUC knife 2) — an interval projection describes
11185 // as INTERVAL (typed drivers read the RowDescription OID).
11186 Value::Interval { .. } => DataType::Interval,
11187 _ => DataType::Text,
11188 };
11189 all_null = false;
11190 inferred = Some(match inferred {
11191 None => ty,
11192 Some(prev) if prev == ty => prev,
11193 Some(_) => DataType::Text,
11194 });
11195 }
11196 if let Some(t) = inferred {
11197 col.ty = t;
11198 col.nullable = true;
11199 } else if all_null {
11200 col.nullable = true;
11201 }
11202 }
11203 out
11204}
11205
11206/// Numeric widening rank for UNION type resolution (higher = wider).
11207fn numeric_rank(t: DataType) -> Option<u8> {
11208 match t {
11209 DataType::SmallInt => Some(1),
11210 DataType::Int => Some(2),
11211 DataType::BigInt => Some(3),
11212 DataType::Numeric { .. } => Some(4),
11213 DataType::Float => Some(5),
11214 _ => None,
11215 }
11216}
11217
11218/// Resolve the common result type for a UNION / VALUES column from the
11219/// set of concrete (non-NULL) branch types, following the safe subset
11220/// of PG's type resolution:
11221/// * all-numeric → the widest numeric (int ∪ bigint → bigint, … ∪
11222/// numeric → numeric, … ∪ float → float);
11223/// * DATE ∪ TIMESTAMP → TIMESTAMP;
11224/// * exactly one concrete non-TEXT type mixed with TEXT literals →
11225/// that concrete type (the TEXT cells get parsed into it).
11226/// Returns `None` for anything ambiguous, so the caller leaves the
11227/// column untouched rather than risk a wrong or failing coercion.
11228fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
11229 // NB: types are collected from RUNTIME values, which are coarser
11230 // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
11231 // a single-concrete-type fast path must NOT overwrite the column
11232 // type — it would downgrade tstz to ts. NULL-only unification (PG:
11233 // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
11234 // row's pg_typeof) needs schema-level resolution — recorded, not
11235 // attempted here.
11236 if types.len() < 2 {
11237 return None;
11238 }
11239 if types.iter().all(|t| numeric_rank(*t).is_some()) {
11240 return types
11241 .iter()
11242 .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
11243 .copied();
11244 }
11245 let non_text: Vec<&DataType> = types
11246 .iter()
11247 .filter(|t| !matches!(t, DataType::Text))
11248 .collect();
11249 // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
11250 // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
11251 // if any is timestamp the result is timestamp (ts ∪ date). All values are
11252 // the same UTC-micros instant, so widening date/ts to tstz is lossless.
11253 if non_text.iter().all(|t| {
11254 matches!(
11255 t,
11256 DataType::Date | DataType::Timestamp | DataType::Timestamptz
11257 )
11258 }) && non_text
11259 .iter()
11260 .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
11261 {
11262 if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
11263 return Some(DataType::Timestamptz);
11264 }
11265 return Some(DataType::Timestamp);
11266 }
11267 // A single concrete non-TEXT type mixed with TEXT literals.
11268 if non_text.len() == 1 {
11269 return Some(*non_text[0]);
11270 }
11271 // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
11272 // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
11273 // text): resolve the concrete set first (PG treats the unknown-
11274 // typed string literals as castable to whatever the knowns
11275 // resolve to), then the TEXT cells parse into that target — the
11276 // caller's coercion dry-run still abandons the column if any
11277 // literal doesn't parse.
11278 if !non_text.is_empty() && non_text.len() < types.len() {
11279 let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
11280 return resolve_union_common_type(&concrete);
11281 }
11282 None
11283}
11284
11285/// Coerce every cell of a UNION / VALUES result column to one common
11286/// type (see [`resolve_union_common_type`]). Conservative: a column
11287/// whose branches already agree, or whose types don't resolve, or where
11288/// any cell fails to coerce, is left exactly as it was — this never
11289/// turns a previously-working query into an error.
11290fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
11291 for col_idx in 0..columns.len() {
11292 let mut seen: Vec<DataType> = Vec::new();
11293 for row in rows.iter() {
11294 if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
11295 if !seen.contains(&dt) {
11296 seen.push(dt);
11297 }
11298 }
11299 }
11300 // v7.37.16 — a single concrete runtime type under a TEXT-typed
11301 // column means the column type came off a NULL (or unknown-text)
11302 // branch: NULL literals describe as TEXT (`L::Null → Text`), so
11303 // `VALUES (NULL),(1.5)` left the column "text" while every
11304 // non-NULL cell is numeric. Adopt the concrete type — schema
11305 // only, no cell changes. tstz-safe by construction: a real
11306 // timestamptz column's schema type is Timestamptz, not Text, so
11307 // the coarser runtime type (Value::Timestamp) can't downgrade it
11308 // through this arm; and a real text column's non-NULL cells are
11309 // Text, which keeps seen == [Text] and skips it.
11310 if seen.len() == 1
11311 && matches!(columns[col_idx].ty, DataType::Text)
11312 && !matches!(seen[0], DataType::Text)
11313 {
11314 columns[col_idx].ty = seen[0];
11315 continue;
11316 }
11317 let Some(target) = resolve_union_common_type(&seen) else {
11318 continue;
11319 };
11320 // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
11321 // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
11322 // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
11323 // existing numeric cell untouched and only promote integers (to scale 0)
11324 // rather than rescaling everything to the widest scale.
11325 let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
11326 // Dry-run the coercion; abandon the whole column if any fails.
11327 let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
11328 let mut ok = true;
11329 for row in rows.iter() {
11330 match row.values.get(col_idx) {
11331 Some(Value::Numeric { .. }) if scale_preserving_numeric => {
11332 coerced.push(Some(row.values[col_idx].clone()));
11333 }
11334 Some(v) => {
11335 let cell_target = if scale_preserving_numeric {
11336 DataType::Numeric {
11337 precision: 0,
11338 scale: 0,
11339 }
11340 } else {
11341 target
11342 };
11343 match crate::conversions::coerce_value(
11344 v.clone(),
11345 cell_target,
11346 &columns[col_idx].name,
11347 col_idx,
11348 ) {
11349 Ok(cv) => coerced.push(Some(cv)),
11350 Err(_) => {
11351 ok = false;
11352 break;
11353 }
11354 }
11355 }
11356 None => coerced.push(None),
11357 }
11358 }
11359 if !ok {
11360 continue;
11361 }
11362 for (row, cv) in rows.iter_mut().zip(coerced) {
11363 if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
11364 *slot = nv;
11365 }
11366 }
11367 columns[col_idx].ty = target;
11368 }
11369}
11370
11371/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
11372/// dedup inside the recursive iteration. Crude but deterministic
11373/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
11374fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
11375 let mut out = Vec::new();
11376 for v in &row.values {
11377 // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
11378 // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
11379 // like PG (and like GROUP BY, which already normalizes). The old
11380 // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
11381 // the exact-decimal family through one scale-stripped canonical form.
11382 match v {
11383 Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11384 Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11385 Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11386 Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
11387 other => {
11388 let s = alloc::format!("{other:?}|");
11389 out.extend_from_slice(s.as_bytes());
11390 }
11391 }
11392 }
11393 out
11394}
11395
11396/// Append a scale-independent canonical key for an exact-decimal value: strip
11397/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
11398/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
11399fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
11400 while scale > 0 && scaled % 10 == 0 {
11401 scaled /= 10;
11402 scale -= 1;
11403 }
11404 let s = alloc::format!("\u{1}{scaled}e-{scale}|");
11405 out.extend_from_slice(s.as_bytes());
11406}
11407
11408/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
11409/// (uncorrelated; outer refs were substituted upstream), then zip
11410/// them in parallel, NULL-padding shorter arrays to the longest
11411/// (PG's ROWS FROM shorthand). Shared by the primary-position
11412/// executor and the join-position materialiser, which both detect
11413/// the parser's `__unnest_zip` marker call.
11414pub(crate) fn unnest_zip_rows(
11415 args: &[Expr],
11416) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
11417 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11418 let ctx = EvalContext::new(&empty_schema, None);
11419 let dummy_row = Row::new(alloc::vec::Vec::new());
11420 let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
11421 let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
11422 alloc::vec::Vec::with_capacity(args.len());
11423 for a in args {
11424 let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
11425 let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = match v {
11426 Value::Null => (DataType::Text, alloc::vec::Vec::new()),
11427 Value::TextArray(xs) => (
11428 DataType::Text,
11429 xs.into_iter()
11430 .map(|x| x.map(Value::text).unwrap_or(Value::Null))
11431 .collect(),
11432 ),
11433 Value::IntArray(xs) => (
11434 DataType::Int,
11435 xs.into_iter()
11436 .map(|x| x.map(Value::Int).unwrap_or(Value::Null))
11437 .collect(),
11438 ),
11439 Value::BigIntArray(xs) => (
11440 DataType::BigInt,
11441 xs.into_iter()
11442 .map(|x| x.map(Value::BigInt).unwrap_or(Value::Null))
11443 .collect(),
11444 ),
11445 other => {
11446 return Err(EngineError::Unsupported(alloc::format!(
11447 "unnest() expects array arguments, got {}",
11448 crate::conversions::pg_type_name_for_error_opt(other.data_type())
11449 )));
11450 }
11451 };
11452 dtypes.push(dt);
11453 columns.push(items);
11454 }
11455 let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
11456 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
11457 for i in 0..max_len {
11458 let vals: alloc::vec::Vec<Value<'static>> = columns
11459 .iter()
11460 .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
11461 .collect();
11462 rows.push(Row::new(vals));
11463 }
11464 Ok((dtypes, rows))
11465}
11466
11467/// Detect the parser's multi-arg unnest marker on an unnest_expr.
11468pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
11469 match expr {
11470 Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
11471 _ => None,
11472 }
11473}
11474
11475/// Evaluate generate_series arguments (uncorrelated — outer refs
11476/// were substituted upstream where applicable) and build the row
11477/// stream. Dispatches on the start value's shape and rejects
11478/// mixed-shape calls early (e.g. start = timestamp, stop =
11479/// integer) so the caller gets a clean error rather than a panic.
11480/// Shared by the primary-position executor and the join-position
11481/// materialiser.
11482pub(crate) fn generate_series_rows(
11483 args: &[Expr],
11484 cancel: &CancelToken<'_>,
11485) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
11486 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11487 let ctx = EvalContext::new(&empty_schema, None);
11488 let dummy_row = Row::new(alloc::vec::Vec::new());
11489 let mut arg_values: alloc::vec::Vec<Value<'static>> =
11490 alloc::vec::Vec::with_capacity(args.len());
11491 for a in args {
11492 arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
11493 }
11494 generate_series_from_values(arg_values, args, cancel)
11495}
11496
11497/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
11498/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
11499/// full integer / numeric / timestamp overload set with the FROM-clause path.
11500/// Before this split the target-list arm reimplemented only the integer case,
11501/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
11502/// NULL for the timestamp column instead of the series. `arg_values` are the
11503/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
11504/// timestamp type resolution (it inspects the argument expressions' types).
11505pub(crate) fn generate_series_from_values(
11506 mut arg_values: alloc::vec::Vec<Value<'static>>,
11507 args: &[Expr],
11508 cancel: &CancelToken<'_>,
11509) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
11510 // PG: a NULL bound or step yields zero rows (also keeps the
11511 // NULL-padded lateral probe alive — schema without data).
11512 if arg_values.iter().any(|v| matches!(v, Value::Null)) {
11513 return Ok((DataType::BigInt, alloc::vec::Vec::new()));
11514 }
11515 // PG resolves `generate_series(date, date, interval)` to the
11516 // timestamp/timestamptz overload by implicitly casting each date
11517 // bound up to a timestamp at midnight (verified vs live PG18.4:
11518 // date args yield rows anchored at 00:00:00). SPG's TZ-naive
11519 // timestamp model renders the same instants, so fold any Date
11520 // bound to its midnight Timestamp (canonical `days *
11521 // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
11522 // the shape match so the existing timestamp arm drives the walk.
11523 // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
11524 // `generate_series(date, date, interval)` has no date overload, and among
11525 // the two candidates PG prefers the timestamptz one (timestamptz is the
11526 // preferred type of the datetime category), so the column comes back
11527 // `timestamp with time zone` — the rows render with a `+00` offset. A
11528 // timestamptz bound obviously lands there too. Only genuinely
11529 // timestamp-typed bounds keep the TZ-naive result type.
11530 let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11531 let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
11532 || args.iter().any(|a| {
11533 crate::describe::describe_expr(a, &empty_cols)
11534 .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
11535 });
11536 for v in &mut arg_values {
11537 if let Value::Date(d) = *v {
11538 *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
11539 }
11540 }
11541 match arg_values.as_slice() {
11542 [Value::Timestamp(start), Value::Timestamp(stop), step] => {
11543 let interval_step = match step {
11544 Value::Interval { .. } => step.clone(),
11545 // v7.38 (read01) — PG resolves an unknown-type string step
11546 // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
11547 // a bare text step by parsing it the same way `::interval` does.
11548 Value::Text(s) => crate::conversions::coerce_value(
11549 Value::text(s.as_ref()),
11550 DataType::Interval,
11551 "",
11552 0,
11553 )
11554 .map_err(|_| {
11555 EngineError::Unsupported(alloc::format!(
11556 "generate_series(timestamp, timestamp, …): \
11557 could not parse step {s:?} as INTERVAL"
11558 ))
11559 })?,
11560 other => {
11561 return Err(EngineError::Unsupported(alloc::format!(
11562 "generate_series(timestamp, timestamp, …): \
11563 step must be INTERVAL, got {}",
11564 crate::conversions::pg_type_name_for_error_opt(other.data_type())
11565 )));
11566 }
11567 };
11568 let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
11569 Ok((
11570 if tz {
11571 DataType::Timestamptz
11572 } else {
11573 DataType::Timestamp
11574 },
11575 rows,
11576 ))
11577 }
11578 [start, stop, step]
11579 if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
11580 {
11581 let s = value_to_i64(start);
11582 let e = value_to_i64(stop);
11583 let st = value_to_i64(step);
11584 // PG types the series by the argument type: int4 args → int4
11585 // elements, int8 (bigint) args → int8. Any BigInt operand widens.
11586 let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
11587 let rows = generate_series_integers(s, e, st, wide, cancel)?;
11588 Ok((
11589 if wide {
11590 DataType::BigInt
11591 } else {
11592 DataType::Int
11593 },
11594 rows,
11595 ))
11596 }
11597 [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
11598 let s = value_to_i64(start);
11599 let e = value_to_i64(stop);
11600 let wide = value_is_bigint(start) || value_is_bigint(stop);
11601 let rows = generate_series_integers(s, e, 1, wide, cancel)?;
11602 Ok((
11603 if wide {
11604 DataType::BigInt
11605 } else {
11606 DataType::Int
11607 },
11608 rows,
11609 ))
11610 }
11611 // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
11612 // series in exact numeric arithmetic; NaN / infinity bounds and a
11613 // zero step get dedicated wordings, and a mixed int/numeric call
11614 // resolves here via the implicit int→numeric cast.
11615 [_, _] | [_, _, _]
11616 if arg_values
11617 .iter()
11618 .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
11619 && arg_values.iter().all(|v| {
11620 matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
11621 }) =>
11622 {
11623 use spg_storage::NumericKind as K;
11624 let words: [(&str, &str); 3] = [
11625 (
11626 "start value cannot be NaN",
11627 "start value cannot be infinity",
11628 ),
11629 ("stop value cannot be NaN", "stop value cannot be infinity"),
11630 ("step size cannot be NaN", "step size cannot be infinity"),
11631 ];
11632 for (i, v) in arg_values.iter().enumerate() {
11633 if let Value::Numeric { kind, .. } = v {
11634 if *kind != K::Finite {
11635 let (nan_w, inf_w) = words[i];
11636 return Err(EngineError::Unsupported(
11637 if *kind == K::NaN { nan_w } else { inf_w }.into(),
11638 ));
11639 }
11640 }
11641 }
11642 let big =
11643 |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
11644 let start = big(&arg_values[0]);
11645 let stop = big(&arg_values[1]);
11646 let step = if arg_values.len() == 3 {
11647 big(&arg_values[2])
11648 } else {
11649 spg_storage::bignum::BigNumeric::from_i128(1, 0)
11650 };
11651 if step.is_zero() {
11652 return Err(EngineError::Unsupported(
11653 "step size cannot equal zero".into(),
11654 ));
11655 }
11656 let descending = step.parts().0;
11657 let mut rows = alloc::vec::Vec::new();
11658 let mut cur = start;
11659 const MAX_ROWS: usize = 10_000_000;
11660 loop {
11661 cancel.check()?;
11662 let c = cur.cmp(&stop);
11663 if descending {
11664 if c == core::cmp::Ordering::Less {
11665 break;
11666 }
11667 } else if c == core::cmp::Ordering::Greater {
11668 break;
11669 }
11670 if rows.len() >= MAX_ROWS {
11671 return Err(EngineError::Unsupported(alloc::format!(
11672 "generate_series() result exceeds {MAX_ROWS} rows"
11673 )));
11674 }
11675 rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
11676 cur.clone()
11677 )]));
11678 cur = cur.add(&step);
11679 }
11680 Ok((
11681 DataType::Numeric {
11682 precision: 0,
11683 scale: 0,
11684 },
11685 rows,
11686 ))
11687 }
11688 _ => Err(EngineError::Unsupported(alloc::format!(
11689 "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
11690 argument shapes; got {}",
11691 arg_values
11692 .iter()
11693 .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
11694 .collect::<alloc::vec::Vec<_>>()
11695 .join(", ")
11696 ))),
11697 }
11698}
11699
11700/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
11701/// Step direction follows the sign: positive step iterates upward
11702/// (stops when current > stop); negative iterates downward; zero
11703/// errors. Caller-facing row stream is `BigInt`-typed so a single
11704/// projection schema covers SmallInt / Int / BigInt callers.
11705fn generate_series_integers(
11706 start: i64,
11707 stop: i64,
11708 step: i64,
11709 wide: bool,
11710 cancel: &CancelToken<'_>,
11711) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11712 if step == 0 {
11713 return Err(EngineError::Unsupported(
11714 "step size cannot equal zero".into(),
11715 ));
11716 }
11717 let mut out = alloc::vec::Vec::new();
11718 let mut cur = start;
11719 // Hard cap to keep a runaway call from eating all memory. PG
11720 // has no such cap but does honour query timeout; SPG's cancel
11721 // token will fire too — this is a defense-in-depth backstop.
11722 const MAX_ROWS: usize = 10_000_000;
11723 loop {
11724 cancel.check()?;
11725 if step > 0 && cur > stop {
11726 break;
11727 }
11728 if step < 0 && cur < stop {
11729 break;
11730 }
11731 out.push(Row::new(alloc::vec![if wide {
11732 Value::BigInt(cur)
11733 } else {
11734 Value::Int(cur as i32)
11735 }]));
11736 if out.len() > MAX_ROWS {
11737 return Err(EngineError::Unsupported(alloc::format!(
11738 "generate_series(): exceeded {MAX_ROWS} rows; \
11739 narrow start/stop or use a larger step"
11740 )));
11741 }
11742 cur = match cur.checked_add(step) {
11743 Some(n) => n,
11744 None => break,
11745 };
11746 }
11747 Ok(out)
11748}
11749
11750/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
11751/// `Value::Interval { months, micros }` per the caller's guard;
11752/// each iteration adds the interval via `apply_binary_interval`
11753/// so month-shifting handles short-month rollover (PG semantics).
11754fn generate_series_timestamps(
11755 start: i64,
11756 stop: i64,
11757 step: Value,
11758 cancel: &CancelToken<'_>,
11759) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11760 let (months, days, micros) = match &step {
11761 Value::Interval {
11762 months,
11763 days,
11764 micros,
11765 } => (*months, *days, *micros),
11766 _ => unreachable!("caller guards step.is_interval"),
11767 };
11768 if months == 0 && days == 0 && micros == 0 {
11769 return Err(EngineError::Unsupported(
11770 "generate_series(): INTERVAL step cannot be zero".into(),
11771 ));
11772 }
11773 let ascending = months > 0 || days > 0 || micros > 0;
11774 let mut out = alloc::vec::Vec::new();
11775 let mut cur = Value::Timestamp(start);
11776 const MAX_ROWS: usize = 10_000_000;
11777 loop {
11778 cancel.check()?;
11779 let cur_t = match cur {
11780 Value::Timestamp(t) => t,
11781 _ => unreachable!("loop invariant: cur is Timestamp"),
11782 };
11783 if ascending && cur_t > stop {
11784 break;
11785 }
11786 if !ascending && cur_t < stop {
11787 break;
11788 }
11789 out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
11790 if out.len() > MAX_ROWS {
11791 return Err(EngineError::Unsupported(alloc::format!(
11792 "generate_series(): exceeded {MAX_ROWS} rows; \
11793 narrow start/stop or use a larger step"
11794 )));
11795 }
11796 let next = eval::apply_binary_interval(
11797 spg_sql::ast::BinOp::Add,
11798 &cur,
11799 &Value::Interval {
11800 months,
11801 days,
11802 micros,
11803 },
11804 )
11805 .map_err(EngineError::Eval)?;
11806 cur = match next {
11807 Some(v) => v,
11808 None => break,
11809 };
11810 }
11811 Ok(out)
11812}
11813
11814/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
11815/// WITH TIES` requires an `ORDER BY`. Without one, there's no
11816/// way to identify "ties" deterministically, so PG errors at
11817/// plan time. SPG mirrors that surface so the same DDL / app
11818/// behaviour holds on cutover.
11819fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
11820 if stmt.limit_with_ties && stmt.order_by.is_empty() {
11821 return Err(EngineError::Unsupported(alloc::string::String::from(
11822 "WITH TIES cannot be specified without ORDER BY clause",
11823 )));
11824 }
11825 Ok(())
11826}
11827
11828/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
11829/// (case-insensitive). Used by `exec_select_cancel`'s
11830/// projection loop to detect Set-Returning-Function rows that
11831/// need per-row expansion. Only the top-level call counts —
11832/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
11833/// projection's perspective; it would surface as an "unknown
11834/// function" mismatch downstream, which is what we want
11835/// (multi-SRF / nested SRF is documented carve-out for v7.19).
11836fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
11837 top_level_srf_kind(expr).is_some()
11838}
11839
11840/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
11841/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
11842/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
11843/// source row.
11844#[derive(Clone, Copy, PartialEq, Eq)]
11845pub(crate) enum SrfKind {
11846 Unnest,
11847 /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
11848 /// list. It used to be handled ONLY by the parser's lift into FROM, so a
11849 /// second one in the same list came back as "unknown function".
11850 GenerateSeries,
11851 GenerateSubscripts,
11852 /// `_text` variants unwrap scalars to their lexeme; the plain forms render
11853 /// every value as compact JSON text.
11854 ArrayElements {
11855 as_text: bool,
11856 },
11857 PathQuery,
11858 RegexpMatches,
11859 Each {
11860 as_text: bool,
11861 },
11862 ObjectKeys,
11863}
11864
11865/// Case-insensitive match against any of `names`.
11866fn name_is(name: &str, names: &[&str]) -> bool {
11867 names.iter().any(|n| name.eq_ignore_ascii_case(n))
11868}
11869
11870pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
11871 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
11872 return None;
11873 };
11874 let n = args.len();
11875 // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
11876 // SELECT list (it returned an array there before) and shares the unnest
11877 // expansion machinery.
11878 if n == 1 && name.eq_ignore_ascii_case("unnest") {
11879 return Some(SrfKind::Unnest);
11880 }
11881 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
11882 return Some(SrfKind::GenerateSeries);
11883 }
11884 if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
11885 return Some(SrfKind::GenerateSubscripts);
11886 }
11887 // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
11888 // per element / match in the SELECT list; they collapsed to a single row
11889 // (a TextArray, or an "unknown function" error for `each`) before.
11890 if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
11891 return Some(SrfKind::ArrayElements { as_text: false });
11892 }
11893 if n == 1
11894 && name_is(
11895 name,
11896 &["jsonb_array_elements_text", "json_array_elements_text"],
11897 )
11898 {
11899 return Some(SrfKind::ArrayElements { as_text: true });
11900 }
11901 // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
11902 if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
11903 return Some(SrfKind::PathQuery);
11904 }
11905 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
11906 return Some(SrfKind::RegexpMatches);
11907 }
11908 if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
11909 return Some(SrfKind::Each { as_text: false });
11910 }
11911 if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
11912 return Some(SrfKind::Each { as_text: true });
11913 }
11914 if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
11915 return Some(SrfKind::ObjectKeys);
11916 }
11917 None
11918}
11919
11920/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
11921/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
11922/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
11923/// rows, as in PG).
11924pub(crate) fn top_level_srf_output(
11925 expr: &spg_sql::ast::Expr,
11926 row: &Row<'static>,
11927 ctx: &EvalContext<'_>,
11928) -> Result<Vec<Value<'static>>, EngineError> {
11929 let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
11930 (top_level_srf_kind(expr), expr)
11931 else {
11932 return Err(EngineError::Unsupported(
11933 "expected a SELECT-list SRF call".into(),
11934 ));
11935 };
11936 match kind {
11937 SrfKind::Unnest => {
11938 // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
11939 // the elements DIRECTLY: the old path built the whole
11940 // Value::Array (one eval + a clone per element) only for
11941 // array_value_to_elements to clone every element back out.
11942 // Any other argument shape (a column, a function result)
11943 // keeps the build-then-split path.
11944 if let spg_sql::ast::Expr::Array(items) = &args[0] {
11945 return items
11946 .iter()
11947 .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
11948 .collect();
11949 }
11950 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11951 array_value_to_elements(&arr)
11952 }
11953 SrfKind::GenerateSeries => {
11954 // v7.39 (read01 round 96) — evaluate the args against the actual
11955 // row, then hand off to the shared core so the numeric and
11956 // timestamp/timestamptz overloads work here too (this arm used to
11957 // handle only integers, silently NULLing a temporal/numeric series
11958 // when it shared a target list with another SRF).
11959 let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
11960 for a in args {
11961 arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
11962 }
11963 let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
11964 Ok(rows
11965 .into_iter()
11966 .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
11967 .collect())
11968 }
11969 SrfKind::GenerateSubscripts => {
11970 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11971 let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
11972 if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
11973 return Ok(Vec::new());
11974 }
11975 let len = array_value_to_elements(&arr)?.len();
11976 Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
11977 }
11978 // One Value per array element (`_text` → text / SQL NULL, plain → the
11979 // element's compact JSON text) — the element list the FROM-clause form
11980 // materialises.
11981 SrfKind::ArrayElements { as_text } => {
11982 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11983 if matches!(arg, Value::Null) {
11984 return Ok(Vec::new());
11985 }
11986 let items =
11987 crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
11988 Ok(items
11989 .into_iter()
11990 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
11991 .collect())
11992 }
11993 // The scalar form already yields a TextArray of the keys (or errors on
11994 // a non-object, like PG); expand it into rows.
11995 SrfKind::ObjectKeys => {
11996 let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
11997 array_value_to_elements(&v)
11998 }
11999 // One row per match, each a text[] of the pattern's capture groups.
12000 SrfKind::RegexpMatches => {
12001 let vals: Vec<Value<'static>> = args
12002 .iter()
12003 .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
12004 .collect::<Result<_, _>>()?;
12005 crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
12006 }
12007 // One composite `(key, value)` row per object member (plain → jsonb
12008 // value, `_text` → text / SQL NULL).
12009 SrfKind::Each { as_text } => {
12010 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12011 if matches!(arg, Value::Null) {
12012 return Ok(Vec::new());
12013 }
12014 let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
12015 Ok(pairs
12016 .into_iter()
12017 .map(|(k, v)| {
12018 let val = if as_text {
12019 v.map(Value::text).unwrap_or(Value::Null)
12020 } else {
12021 v.map(Value::json).unwrap_or(Value::Null)
12022 };
12023 Value::Composite(alloc::vec![
12024 ("key".to_string(), Value::text(k)),
12025 ("value".to_string(), val),
12026 ])
12027 })
12028 .collect())
12029 }
12030 // One Value per matched JSON value.
12031 SrfKind::PathQuery => {
12032 let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12033 let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
12034 // v7.39 — optional vars document (3rd arg).
12035 let vars = match args.get(2) {
12036 Some(a) => {
12037 let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
12038 crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
12039 }
12040 None => None,
12041 };
12042 match crate::json::path_query_vars(&doc, &path, vars.as_ref())
12043 .map_err(EngineError::Eval)?
12044 {
12045 Value::Null => Ok(Vec::new()),
12046 Value::TextArray(items) => Ok(items
12047 .into_iter()
12048 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
12049 .collect()),
12050 other => Ok(alloc::vec![other]),
12051 }
12052 }
12053 }
12054}
12055
12056/// v7.19 P5 — turn an array-typed `Value` into the element list
12057/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
12058/// = (no rows)`). Non-array values fall through to a type-mismatch
12059/// error.
12060pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
12061 // v7.39 (round 236) — PG unnests a multidimensional array into its
12062 // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
12063 // rows). SPG stores 2-D arrays as their own variants, which fell
12064 // through to the type-mismatch arm below.
12065 if let Some(flat) = crate::eval::values::flatten_2d(v) {
12066 return array_value_to_elements(&flat);
12067 }
12068 match v {
12069 Value::Null => Ok(Vec::new()),
12070 Value::TextArray(items) => Ok(items
12071 .iter()
12072 .map(|opt| {
12073 opt.as_ref()
12074 .map(|s| Value::text(s.clone()))
12075 .unwrap_or(Value::Null)
12076 })
12077 .collect()),
12078 Value::IntArray(items) => Ok(items
12079 .iter()
12080 .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
12081 .collect()),
12082 Value::BigIntArray(items) => Ok(items
12083 .iter()
12084 .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
12085 .collect()),
12086 // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
12087 // range per canonical span.
12088 Value::Multirange { kind, ranges } => Ok(ranges
12089 .iter()
12090 .map(|s| Value::Range {
12091 kind: *kind,
12092 lower: s.lower.clone(),
12093 upper: s.upper.clone(),
12094 lower_inc: s.lower_inc,
12095 upper_inc: s.upper_inc,
12096 empty: false,
12097 })
12098 .collect()),
12099 other => Err(EngineError::Eval(EvalError::TypeMismatch {
12100 detail: alloc::format!(
12101 "unnest() expects an array argument, got {}",
12102 crate::conversions::pg_type_name_for_error_opt(other.data_type())
12103 ),
12104 })),
12105 }
12106}
12107
12108impl Engine {
12109 /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
12110 /// the SELECT's FROM / JOIN graph, re-parse each view's body
12111 /// source, and prepend it as a synthetic CTE on the
12112 /// returned SelectStatement. Returns `None` when no view
12113 /// references are found (caller proceeds with the original
12114 /// statement); returns `Some(rewritten)` otherwise (caller
12115 /// re-runs exec_select_cancel on the rewritten form so the
12116 /// regular CTE materialiser handles it).
12117 fn expand_views_in_select(
12118 &self,
12119 stmt: &SelectStatement,
12120 ) -> Result<Option<SelectStatement>, EngineError> {
12121 let cat = self.active_catalog();
12122 let mut referenced: Vec<String> = Vec::new();
12123 if let Some(from) = &stmt.from {
12124 collect_view_refs(&from.primary, cat, &mut referenced);
12125 for j in &from.joins {
12126 collect_view_refs(&j.table, cat, &mut referenced);
12127 }
12128 }
12129 // Don't expand a view name that's already shadowed by a
12130 // CTE on the same SELECT — the CTE wins per PG.
12131 referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
12132 if referenced.is_empty() {
12133 return Ok(None);
12134 }
12135 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
12136 for name in &referenced {
12137 let view = cat.view(name).ok_or_else(|| {
12138 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
12139 "view {name:?} disappeared mid-expansion"
12140 )))
12141 })?;
12142 let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
12143 EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
12144 })?;
12145 let Statement::Select(body) = parsed else {
12146 return Err(EngineError::Unsupported(alloc::format!(
12147 "view {name:?} body is not a SELECT (catalog corruption)"
12148 )));
12149 };
12150 new_ctes.push(spg_sql::ast::Cte {
12151 name: name.clone(),
12152 body: spg_sql::ast::CteBody::Select(body),
12153 recursive: false,
12154 column_overrides: view.columns.clone(),
12155 search: None,
12156 cycle: None,
12157 });
12158 }
12159 let mut out = stmt.clone();
12160 // Prepend so view CTEs are visible to caller-supplied CTEs.
12161 new_ctes.extend(out.ctes);
12162 out.ctes = new_ctes;
12163 Ok(Some(out))
12164 }
12165
12166 /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
12167 /// any partition-parent table, rewrite the SELECT so each parent
12168 /// reference resolves to a CTE whose body is a `UNION ALL` over the
12169 /// children that pass the WHERE-derived partition-key range. Returns
12170 /// `None`(no rewrite needed)when no parent is referenced or all
12171 /// references are shadowed by a same-name CTE.
12172 ///
12173 /// Pruning vocabulary at v7.37.6-B:
12174 /// * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
12175 /// and `<key> BETWEEN literal AND literal`.
12176 /// * Anything outside that(OR / nested IN / function call on the
12177 /// key)defaults to "no pruning" — every child + DEFAULT lands
12178 /// in the UNION. Correctness is preserved; only the plan size
12179 /// widens.
12180 fn expand_partition_parents_in_select(
12181 &self,
12182 stmt: &SelectStatement,
12183 ) -> Result<Option<SelectStatement>, EngineError> {
12184 let cat = self.active_catalog();
12185 let Some(from) = &stmt.from else {
12186 return Ok(None);
12187 };
12188 let mut parent_refs: Vec<String> = Vec::new();
12189 collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
12190 for j in &from.joins {
12191 collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
12192 }
12193 // Drop names shadowed by a CTE on the same SELECT(PG semantics
12194 // — same as view expansion above).
12195 parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
12196 if parent_refs.is_empty() {
12197 return Ok(None);
12198 }
12199 // Synthesise a CTE name per parent so the existing
12200 // "CTE shadows a real table" guard doesn't fire (the parent
12201 // IS a real table in the catalog, unlike VIEW expansion's
12202 // case). The FROM-clause TableRef walker below rewrites
12203 // every parent reference to point at the synthetic CTE.
12204 let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
12205 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
12206 let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
12207 for parent_name in &parent_refs {
12208 // No children = no rewrite. The parent itself is a real
12209 // (empty-rows) table — the regular FROM-resolution path
12210 // will scan it and return 0 rows, matching the
12211 // "partition parent with no children" plan. Skipping the
12212 // CTE here also avoids `SELECT * FROM parent` re-entering
12213 // this rewrite on the synthetic body (infinite recursion).
12214 let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
12215 continue;
12216 };
12217 new_ctes.push(spg_sql::ast::Cte {
12218 name: synth_name(parent_name),
12219 body: spg_sql::ast::CteBody::Select(body),
12220 recursive: false,
12221 column_overrides: Vec::new(),
12222 search: None,
12223 cycle: None,
12224 });
12225 expanded_parents.push(parent_name.clone());
12226 }
12227 if expanded_parents.is_empty() {
12228 return Ok(None);
12229 }
12230 let mut out = stmt.clone();
12231 if let Some(from) = out.from.as_mut() {
12232 rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
12233 for j in &mut from.joins {
12234 rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
12235 }
12236 }
12237 new_ctes.extend(out.ctes);
12238 out.ctes = new_ctes;
12239 Ok(Some(out))
12240 }
12241
12242 /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
12243 /// Children include every overlap-hit `Range` plus(always)the
12244 /// `Default` child(if any). Returns `Ok(None)` when no children
12245 /// would survive — caller skips the CTE injection and lets the
12246 /// parent fall through to the regular(empty-rows)scan path,
12247 /// avoiding the infinite recursion that an empty-body CTE
12248 /// referencing the parent name would trigger.
12249 /// v7.37.16 (16.10) — public helper invoked from explain.rs to
12250 /// surface "which children survive the WHERE-clause prune" in
12251 /// EXPLAIN output. Returns `None` when `parent_name` isn't
12252 /// actually a partition parent; otherwise returns the list of
12253 /// children the planner would scan (same algorithm as
12254 /// [`Self::build_partition_parent_union_body`] but without the
12255 /// SQL re-parse).
12256 /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
12257 /// expression (the PG-shaped EXPLAIN's scan builder has no full
12258 /// SelectStatement in hand). Wraps the original by synthesising a
12259 /// minimal statement carrying just the predicate.
12260 pub(crate) fn explain_partition_kept_children_by_where(
12261 &self,
12262 parent_name: &str,
12263 where_: Option<&spg_sql::ast::Expr>,
12264 ) -> Option<Vec<alloc::string::String>> {
12265 let mut synth = SelectStatement::default();
12266 synth.where_ = where_.cloned();
12267 self.explain_partition_kept_children(parent_name, &synth)
12268 }
12269
12270 pub(crate) fn explain_partition_kept_children(
12271 &self,
12272 parent_name: &str,
12273 outer: &SelectStatement,
12274 ) -> Option<Vec<alloc::string::String>> {
12275 use spg_storage::PartitionRole;
12276 let cat = self.active_catalog();
12277 let parent = cat.get(parent_name)?;
12278 let (key_position, parent_kind) = match &parent.schema().partition_role {
12279 Some(PartitionRole::Parent {
12280 key_column_positions,
12281 kind,
12282 ..
12283 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
12284 _ => return None,
12285 };
12286 let key_col_name = parent.schema().columns[key_position].name.clone();
12287 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
12288 Some(expr) => extract_key_range(expr, &key_col_name),
12289 None => (None, None),
12290 };
12291 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
12292 Some(expr) => extract_key_eq_value(expr, &key_col_name),
12293 None => None,
12294 };
12295 let children = crate::partition::children_of_parent(cat, parent_name);
12296 let mut kept: Vec<alloc::string::String> = Vec::new();
12297 let mut default_child: Option<alloc::string::String> = None;
12298 for child_name in &children {
12299 let Some(child) = cat.get(child_name) else {
12300 continue;
12301 };
12302 match &child.schema().partition_role {
12303 Some(PartitionRole::Range { lower, upper, .. }) => {
12304 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
12305 kept.push(child_name.clone());
12306 }
12307 }
12308 Some(PartitionRole::List { values, .. }) => match &eq_value {
12309 Some(v) => {
12310 if values.iter().any(|b| b.equals_value(v)) {
12311 kept.push(child_name.clone());
12312 }
12313 }
12314 None => kept.push(child_name.clone()),
12315 },
12316 Some(PartitionRole::Hash {
12317 modulus, remainder, ..
12318 }) => match &eq_value {
12319 Some(v) => {
12320 let h = crate::partition::pg_compatible_hash(v);
12321 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
12322 kept.push(child_name.clone());
12323 }
12324 }
12325 None => kept.push(child_name.clone()),
12326 },
12327 Some(PartitionRole::Default { .. }) => {
12328 default_child = Some(child_name.clone());
12329 }
12330 _ => {}
12331 }
12332 }
12333 let _ = parent_kind;
12334 if let Some(d) = default_child {
12335 if kept.is_empty() || eq_value.is_none() {
12336 kept.push(d);
12337 }
12338 }
12339 Some(kept)
12340 }
12341
12342 fn build_partition_parent_union_body(
12343 &self,
12344 parent_name: &str,
12345 outer: &SelectStatement,
12346 ) -> Result<Option<SelectStatement>, EngineError> {
12347 use spg_storage::PartitionRole;
12348 let cat = self.active_catalog();
12349 let parent = cat.get(parent_name).ok_or_else(|| {
12350 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
12351 "partition parent {parent_name:?} disappeared mid-expansion"
12352 )))
12353 })?;
12354 let (key_position, parent_kind) = match &parent.schema().partition_role {
12355 Some(PartitionRole::Parent {
12356 key_column_positions,
12357 kind,
12358 ..
12359 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
12360 // v7.39 (round 645) — an INHERITANCE parent, which has no
12361 // role of its own: the relationship is recorded only in the
12362 // children. Three things differ from a partition parent and
12363 // all three are in this body.
12364 //
12365 // * The parent HOLDS ROWS, so it is a term of the union —
12366 // `FROM ONLY`, or expanding it would recurse.
12367 // * There is no partition key, so there is nothing to
12368 // prune: every child is a term.
12369 // * A child may declare columns of its own, so the terms
12370 // name the PARENT's columns rather than `*`. PG's
12371 // `SELECT * FROM parent` returns the parent's shape.
12372 //
12373 // Answered from this match rather than a branch before it —
12374 // round 644 measured what an extra early return beside an
12375 // existing test costs in this file.
12376 _ if crate::partition::has_inheritance_children(cat, parent_name) => {
12377 let cols = parent
12378 .schema()
12379 .columns
12380 .iter()
12381 .map(|c| quote_ident_for_sql(&c.name))
12382 .collect::<Vec<_>>()
12383 .join(", ");
12384 let carry_sys = references_ctid(outer);
12385 let sys = if carry_sys {
12386 let mut t = alloc::string::String::new();
12387 for s in SYSTEM_COLUMNS {
12388 t.push_str(", ");
12389 t.push_str(s);
12390 }
12391 t
12392 } else {
12393 alloc::string::String::new()
12394 };
12395 let mut body = alloc::format!(
12396 "SELECT {cols}{sys} FROM ONLY {}",
12397 quote_ident_for_sql(parent_name)
12398 );
12399 for child in crate::partition::children_of_parent(cat, parent_name) {
12400 body.push_str(&alloc::format!(
12401 " UNION ALL SELECT {cols}{sys} FROM {}",
12402 quote_ident_for_sql(&child)
12403 ));
12404 }
12405 return parse_select_or_corrupt(&body).map(Some);
12406 }
12407 _ => {
12408 return Err(EngineError::Unsupported(alloc::format!(
12409 "partition expansion: {parent_name:?} is not a parent"
12410 )));
12411 }
12412 };
12413 let key_col_name = parent.schema().columns[key_position].name.clone();
12414 // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
12415 // off the WHERE; for LIST / HASH we extract a single `=`
12416 // literal (and the rest of the planner falls back to "keep
12417 // every child" — same conservative path as 16.1/16.2).
12418 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
12419 Some(expr) => extract_key_range(expr, &key_col_name),
12420 None => (None, None),
12421 };
12422 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
12423 Some(expr) => extract_key_eq_value(expr, &key_col_name),
12424 None => None,
12425 };
12426 let children = crate::partition::children_of_parent(cat, parent_name);
12427 let mut kept: Vec<String> = Vec::new();
12428 let mut default_child: Option<String> = None;
12429 // First pass — apply per-strategy gates, defer DEFAULT until
12430 // we know whether some non-DEFAULT child matched.
12431 for child_name in &children {
12432 let Some(child) = cat.get(child_name) else {
12433 continue;
12434 };
12435 match &child.schema().partition_role {
12436 Some(PartitionRole::Range { lower, upper, .. }) => {
12437 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
12438 kept.push(child_name.clone());
12439 }
12440 }
12441 // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
12442 // = <lit>`, only the child whose values contain that
12443 // literal survives. Otherwise (no equality predicate
12444 // or planner couldn't extract one) keep the child
12445 // conservatively.
12446 Some(PartitionRole::List { values, .. }) => match &eq_value {
12447 Some(v) => {
12448 if values.iter().any(|b| b.equals_value(v)) {
12449 kept.push(child_name.clone());
12450 }
12451 }
12452 None => kept.push(child_name.clone()),
12453 },
12454 // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
12455 // we know the residue class deterministically, so
12456 // only the matching REMAINDER child survives.
12457 Some(PartitionRole::Hash {
12458 modulus, remainder, ..
12459 }) => match &eq_value {
12460 Some(v) => {
12461 let h = crate::partition::pg_compatible_hash(v);
12462 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
12463 kept.push(child_name.clone());
12464 }
12465 }
12466 None => kept.push(child_name.clone()),
12467 },
12468 Some(PartitionRole::Default { .. }) => {
12469 default_child = Some(child_name.clone());
12470 }
12471 _ => {}
12472 }
12473 }
12474 // PG-style DEFAULT semantics: the DEFAULT child must be
12475 // scanned iff some row could fall outside every concrete
12476 // child's bound predicate. We approximate that as "no
12477 // concrete child matched" (== full prune) — strictly
12478 // conservative for LIST / HASH (DEFAULT also catches rows
12479 // outside the union of value-sets / residues), and matches
12480 // PG for the equality case where we *do* know the routing
12481 // outcome.
12482 let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
12483 if let Some(d) = default_child {
12484 if kept.is_empty() {
12485 kept.push(d);
12486 } else if eq_value.is_none() {
12487 // Without an equality literal, the DEFAULT child may
12488 // still hold matching rows (e.g. LIKE on TEXT keys
12489 // for which a LIST partition exists). Keep it.
12490 kept.push(d);
12491 }
12492 }
12493 // Build the UNION ALL body text and re-parse — keeps the
12494 // rewrite expressible in surface SQL so the engine's existing
12495 // parser path handles the AST shape uniformly.
12496 if kept.is_empty() {
12497 // No children survive — caller falls back to scanning the
12498 // (empty) parent table. Returning None here is what
12499 // prevents the synthetic CTE from referring back to the
12500 // parent name and re-entering this rewrite pass.
12501 let _ = parent_name;
12502 return Ok(None);
12503 }
12504 // v7.39 (round 622, S05a) — the system columns of the CHILD the row
12505 // actually lives in.
12506 //
12507 // The parent is read through a synthetic CTE, so a `tableoid` on it
12508 // resolved against that CTE: every row of every child reported
12509 // `__spg_partition_pm`, an internal name no user ever typed, where
12510 // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
12511 // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
12512 // one asks "which partition is this row in", answering 0 rows where
12513 // PG answers 1. `ctid` had the same shape: it numbered the CTE's
12514 // output, so rows in different children got distinct ctids instead
12515 // of each child's own physical position.
12516 //
12517 // Naming them in the term is what carries them: the child scan
12518 // materialises its own six because the statement now references
12519 // them, and they land in SYSTEM_COLUMNS order right after the user
12520 // columns — the exact layout the positional `*` skip already
12521 // expects. Only done when the outer statement asks for one, so a
12522 // plain `SELECT * FROM parent` scans exactly what it scanned.
12523 let carry_sys = references_ctid(outer);
12524 let mut body = alloc::string::String::new();
12525 for (i, child_name) in kept.iter().enumerate() {
12526 if i > 0 {
12527 body.push_str(" UNION ALL ");
12528 }
12529 body.push_str("SELECT *");
12530 if carry_sys {
12531 for sys in SYSTEM_COLUMNS {
12532 body.push_str(", ");
12533 body.push_str(sys);
12534 }
12535 }
12536 body.push_str(" FROM ");
12537 body.push_str("e_ident_for_sql(child_name));
12538 }
12539 parse_select_or_corrupt(&body).map(Some)
12540 }
12541}
12542
12543/// Rewrite a `TableRef` pointing at a partition parent so it
12544/// references the synthetic CTE created by the expansion. If the
12545/// original ref had no alias, preserve the parent name as an alias
12546/// so column references like `events_partitioned.received_at`
12547/// keep resolving.
12548fn rewrite_partition_parent_table_ref(
12549 t: &mut spg_sql::ast::TableRef,
12550 parents: &[alloc::string::String],
12551 synth_name: &impl Fn(&str) -> alloc::string::String,
12552) {
12553 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
12554 return;
12555 }
12556 // v7.39 (round 644) — an ONLY reference stays pointed at the parent
12557 // itself. The rewrite is keyed on the NAME, so in
12558 // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
12559 // parent list and this then rewrote BOTH — including the one that
12560 // asked not to descend. PG answers 0 for that join; SPG answered 2.
12561 // Folded into the existing test — see the note in
12562 // `collect_partition_parent_refs` for what a separate one cost.
12563 if t.only || !parents.iter().any(|p| p == &t.name) {
12564 return;
12565 }
12566 if t.alias.is_none() {
12567 t.alias = Some(t.name.clone());
12568 }
12569 t.name = synth_name(&t.name);
12570}
12571
12572/// Walk a `TableRef` and push its `name` if it resolves to a partition
12573/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
12574/// `generate_series_args` references — those aren't catalog tables.
12575fn collect_partition_parent_refs(
12576 t: &spg_sql::ast::TableRef,
12577 cat: &spg_storage::Catalog,
12578 out: &mut Vec<alloc::string::String>,
12579) {
12580 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
12581 return;
12582 }
12583 // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
12584 // The keyword used to be absorbed at parse time, so this fanned out
12585 // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
12586 // answered 2 where PG answers 0.
12587 //
12588 // Folded into the existing test rather than given an early return of
12589 // its own: as two extra lines in this function's body it cost
12590 // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
12591 // outside the panel. Rounds 641 and 643 met the same wall from the
12592 // other two directions — adding to a hot function and taking away
12593 // from a cold one. What goes in a body near the row loop is a
12594 // codegen decision whatever its shape.
12595 if !t.only && crate::partition::has_children(cat, &t.name) {
12596 out.push(t.name.clone());
12597 }
12598}
12599
12600/// v7.37.6-B partition-key range derived from a WHERE expression.
12601/// `i64` microseconds since epoch with the same sign convention as
12602/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
12603/// / `=`),`false` ⇒ exclusive(`>` / `<`).
12604#[derive(Debug, Clone, Copy)]
12605pub(crate) struct PartitionFilterBound {
12606 pub micros: i64,
12607 pub inclusive: bool,
12608}
12609
12610/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
12611/// shapes; tighten the running lo / hi as we go. Anything outside that
12612/// (OR / nested calls / non-key columns)is ignored — caller treats
12613/// `None` as "no constraint on that side."
12614fn extract_key_range(
12615 expr: &spg_sql::ast::Expr,
12616 key_col: &str,
12617) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
12618 let mut lo: Option<PartitionFilterBound> = None;
12619 let mut hi: Option<PartitionFilterBound> = None;
12620 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12621 while let Some(e) = stack.pop() {
12622 match e {
12623 spg_sql::ast::Expr::Binary {
12624 lhs,
12625 op: spg_sql::ast::BinOp::And,
12626 rhs,
12627 } => {
12628 stack.push(lhs);
12629 stack.push(rhs);
12630 }
12631 // BETWEEN is desugared at parse time into `lhs >= low AND
12632 // lhs <= high`, so it lands here as two regular Binary
12633 // arms via the AND walker above.
12634 spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
12635 let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
12636 (Some(lhs.as_ref()), rhs.as_ref(), false)
12637 } else if is_column_ref(rhs, key_col) {
12638 (Some(rhs.as_ref()), lhs.as_ref(), true)
12639 } else {
12640 (None, lhs.as_ref(), false)
12641 };
12642 if col_ref.is_none() {
12643 continue;
12644 }
12645 let Some(lit) = literal_to_micros(lit_side) else {
12646 continue;
12647 };
12648 use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
12649 let effective_op = if swapped {
12650 match op {
12651 Lt => Gt,
12652 LtEq => GtEq,
12653 Gt => Lt,
12654 GtEq => LtEq,
12655 other => *other,
12656 }
12657 } else {
12658 *op
12659 };
12660 match effective_op {
12661 Eq => {
12662 tighten_lo(
12663 &mut lo,
12664 PartitionFilterBound {
12665 micros: lit,
12666 inclusive: true,
12667 },
12668 );
12669 tighten_hi(
12670 &mut hi,
12671 PartitionFilterBound {
12672 micros: lit,
12673 inclusive: true,
12674 },
12675 );
12676 }
12677 GtEq => {
12678 tighten_lo(
12679 &mut lo,
12680 PartitionFilterBound {
12681 micros: lit,
12682 inclusive: true,
12683 },
12684 );
12685 }
12686 Gt => {
12687 tighten_lo(
12688 &mut lo,
12689 PartitionFilterBound {
12690 micros: lit,
12691 inclusive: false,
12692 },
12693 );
12694 }
12695 LtEq => {
12696 tighten_hi(
12697 &mut hi,
12698 PartitionFilterBound {
12699 micros: lit,
12700 inclusive: true,
12701 },
12702 );
12703 }
12704 Lt => {
12705 tighten_hi(
12706 &mut hi,
12707 PartitionFilterBound {
12708 micros: lit,
12709 inclusive: false,
12710 },
12711 );
12712 }
12713 _ => {}
12714 }
12715 }
12716 _ => {}
12717 }
12718 }
12719 (lo, hi)
12720}
12721
12722fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12723 match slot {
12724 None => *slot = Some(new),
12725 Some(cur) => {
12726 if new.micros > cur.micros
12727 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12728 {
12729 *slot = Some(new);
12730 }
12731 }
12732 }
12733}
12734
12735fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12736 match slot {
12737 None => *slot = Some(new),
12738 Some(cur) => {
12739 if new.micros < cur.micros
12740 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12741 {
12742 *slot = Some(new);
12743 }
12744 }
12745 }
12746}
12747
12748fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
12749 if let spg_sql::ast::Expr::Column(c) = e {
12750 c.name.eq_ignore_ascii_case(key_col)
12751 } else {
12752 false
12753 }
12754}
12755
12756/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
12757/// `key_col = <literal>` predicate out for LIST/HASH partition
12758/// pruning. Returns `None` when no equality literal can be lifted
12759/// (planner then keeps every child — correctness preserved). The
12760/// returned `Value<'static>` is an owned coercion so the caller can
12761/// outlive any AST node it was extracted from.
12762pub(crate) fn extract_key_eq_value(
12763 expr: &spg_sql::ast::Expr,
12764 key_col: &str,
12765) -> Option<spg_storage::Value<'static>> {
12766 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12767 while let Some(e) = stack.pop() {
12768 match e {
12769 spg_sql::ast::Expr::Binary {
12770 lhs,
12771 op: spg_sql::ast::BinOp::And,
12772 rhs,
12773 } => {
12774 stack.push(lhs);
12775 stack.push(rhs);
12776 }
12777 spg_sql::ast::Expr::Binary {
12778 lhs,
12779 op: spg_sql::ast::BinOp::Eq,
12780 rhs,
12781 } => {
12782 let lit_side = if is_column_ref(lhs, key_col) {
12783 rhs.as_ref()
12784 } else if is_column_ref(rhs, key_col) {
12785 lhs.as_ref()
12786 } else {
12787 continue;
12788 };
12789 let cloned = lit_side.clone();
12790 let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
12791 continue;
12792 };
12793 // Coerce to an owned Value<'static> so the caller
12794 // can hold it past the WHERE expression's lifetime.
12795 let owned: spg_storage::Value<'static> = match v {
12796 spg_storage::Value::Text(s) => {
12797 spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
12798 }
12799 spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
12800 spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
12801 spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
12802 spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
12803 spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
12804 spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
12805 spg_storage::Value::Null => spg_storage::Value::Null,
12806 // Anything else (Vector / Json / Bytes / Numeric /
12807 // arrays / interval / …) isn't a current partition
12808 // key type; skip without pruning.
12809 _ => continue,
12810 };
12811 return Some(owned);
12812 }
12813 _ => {}
12814 }
12815 }
12816 None
12817}
12818
12819/// Coerce a literal Expr(after the parser folded sequence calls etc.)
12820/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
12821/// pruning and routing agree on the literal vocabulary. Returns
12822/// `None` when the literal isn't recognised(planner then skips
12823/// pruning on that branch — correctness preserved).
12824fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
12825 let cloned = e.clone();
12826 let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
12827 match value {
12828 spg_storage::Value::Timestamp(m) => Some(m),
12829 spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
12830 spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
12831 _ => None,
12832 }
12833}
12834
12835/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
12836/// satisfying the WHERE-derived filter range. PG-style half-open:
12837/// child upper exclusive. Filter inclusivity is honoured per-bound.
12838fn range_satisfies_filter(
12839 range_lo: &spg_storage::PartitionBound,
12840 range_hi: &spg_storage::PartitionBound,
12841 filter_lo: Option<&PartitionFilterBound>,
12842 filter_hi: Option<&PartitionFilterBound>,
12843) -> bool {
12844 use spg_storage::PartitionBound;
12845 // For each filter side, reject children that can't host any row
12846 // matching the predicate.
12847 if let Some(lo) = filter_lo {
12848 // child upper bound vs filter lower:
12849 // if filter is x >= L, child rejects iff child.hi <= L
12850 // if filter is x > L, child rejects iff child.hi <= L
12851 // (child.hi exclusive, so equality with L still rejects)
12852 match range_hi {
12853 PartitionBound::MinValue => return false,
12854 PartitionBound::MaxValue => {}
12855 PartitionBound::TimestampTz(hi) => {
12856 if *hi <= lo.micros {
12857 return false;
12858 }
12859 }
12860 // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
12861 // matched against TIMESTAMPTZ filters here; keep child
12862 // (conservative: don't prune).
12863 PartitionBound::BigInt(_)
12864 | PartitionBound::Int(_)
12865 | PartitionBound::SmallInt(_)
12866 | PartitionBound::Date(_)
12867 | PartitionBound::Text(_) => {}
12868 }
12869 }
12870 if let Some(hi) = filter_hi {
12871 // child lower bound vs filter upper:
12872 // if filter is x <= U, child rejects iff child.lo > U
12873 // if filter is x < U, child rejects iff child.lo >= U
12874 match range_lo {
12875 PartitionBound::MaxValue => return false,
12876 PartitionBound::MinValue => {}
12877 PartitionBound::TimestampTz(lo) => {
12878 let rejects = if hi.inclusive {
12879 *lo > hi.micros
12880 } else {
12881 *lo >= hi.micros
12882 };
12883 if rejects {
12884 return false;
12885 }
12886 }
12887 PartitionBound::BigInt(_)
12888 | PartitionBound::Int(_)
12889 | PartitionBound::SmallInt(_)
12890 | PartitionBound::Date(_)
12891 | PartitionBound::Text(_) => {}
12892 }
12893 }
12894 true
12895}
12896
12897fn quote_ident_for_sql(name: &str) -> alloc::string::String {
12898 // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
12899 // identifier, otherwise quoted). Conservative: always quote so
12900 // children with reserved names round-trip safely through the
12901 // CTE-body parse.
12902 let mut out = alloc::string::String::with_capacity(name.len() + 2);
12903 out.push('"');
12904 for c in name.chars() {
12905 if c == '"' {
12906 out.push('"');
12907 }
12908 out.push(c);
12909 }
12910 out.push('"');
12911 out
12912}
12913
12914fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
12915 let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
12916 EngineError::Unsupported(alloc::format!(
12917 "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
12918 ))
12919 })?;
12920 let Statement::Select(body) = parsed else {
12921 return Err(EngineError::Unsupported(alloc::format!(
12922 "partition expansion: generated SQL {sql:?} is not a SELECT"
12923 )));
12924 };
12925 Ok(body)
12926}
12927
12928/// v7.39 (read01 round 65/66) — the column shape a set-returning function
12929/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
12930/// yields ONE column named after the call's alias when there is one (`FROM
12931/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
12932/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
12933fn setof_column_shape_from(
12934 declared: &str,
12935 name: &str,
12936 alias: Option<&str>,
12937 got: &[ColumnSchema],
12938) -> alloc::vec::Vec<ColumnSchema> {
12939 let upper = declared.to_ascii_uppercase();
12940 if upper.starts_with("TABLE(") {
12941 let raw = &declared["TABLE(".len()..declared.len() - 1];
12942 return raw
12943 .split(',')
12944 .zip(got.iter())
12945 .map(|(decl, g)| {
12946 let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
12947 ColumnSchema::new(cname.to_string(), g.ty, true)
12948 })
12949 .collect();
12950 }
12951 let cname = alias.unwrap_or(name);
12952 got.first()
12953 .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
12954 .unwrap_or_default()
12955}
12956
12957/// The plpgsql twin: the interpreter hands back raw value rows, so the types
12958/// come off the first row.
12959fn setof_column_shape(
12960 declared: &str,
12961 name: &str,
12962 alias: Option<&str>,
12963 first_row: Option<&alloc::vec::Vec<Value<'static>>>,
12964) -> alloc::vec::Vec<ColumnSchema> {
12965 let got: alloc::vec::Vec<ColumnSchema> = first_row
12966 .map(|r| {
12967 r.iter()
12968 .enumerate()
12969 .map(|(i, v)| {
12970 ColumnSchema::new(
12971 alloc::format!("col{i}"),
12972 v.data_type().unwrap_or(DataType::Text),
12973 true,
12974 )
12975 })
12976 .collect()
12977 })
12978 .unwrap_or_default();
12979 setof_column_shape_from(declared, name, alias, &got)
12980}
12981
12982/// v7.39 (read01 round 67) — expand every set-returning call in a target list
12983/// for ONE input row, PG's ProjectSet semantics.
12984///
12985/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
12986/// output has as many rows as the LONGEST of them, and a shorter one is padded
12987/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
12988/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
12989/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
12990/// is zero rows, not one NULL row.
12991///
12992/// Non-SRF items repeat, evaluated once per output row from the same input row.
12993/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
12994/// used to reach the scalar function dispatcher, which reported the aggregate as
12995/// an *unknown function* — the same "symptom two layers above the cause" shape
12996/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
12997/// sees a call, not the clause it came from. The statement knows.
12998/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
12999/// clause may appear.
13000///
13001/// PG rejects `FOR UPDATE` on exactly the shapes that have no
13002/// identifiable base row to lock, each with its own wording. SPG
13003/// accepted all of them and locked nothing, so a query that PG refuses
13004/// outright came back looking like it had taken locks.
13005///
13006/// Every wording read off live PG 18.4.
13007fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
13008 let Some(lock) = &stmt.locking else {
13009 return Ok(());
13010 };
13011 let verb = lock_clause_verb(lock.strength);
13012 let refuse = |what: &str| {
13013 Err(EngineError::Unsupported(alloc::format!(
13014 "{verb} is not allowed with {what}"
13015 )))
13016 };
13017 if !stmt.unions.is_empty() {
13018 return refuse("UNION/INTERSECT/EXCEPT");
13019 }
13020 if stmt.distinct || !stmt.distinct_on.is_empty() {
13021 return refuse("DISTINCT clause");
13022 }
13023 if stmt.group_by.is_some() || stmt.group_by_all {
13024 return refuse("GROUP BY clause");
13025 }
13026 let has_agg = stmt.items.iter().any(|it| match it {
13027 spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
13028 _ => false,
13029 });
13030 if has_agg {
13031 return refuse("aggregate functions");
13032 }
13033 // `FOR UPDATE OF t` must name a relation that is actually in FROM.
13034 for want in &lock.of_tables {
13035 if !locking_from_names(stmt)
13036 .iter()
13037 .any(|n| n.eq_ignore_ascii_case(want))
13038 {
13039 return Err(EngineError::Unsupported(alloc::format!(
13040 "relation \"{want}\" in {verb} clause not found in FROM clause"
13041 )));
13042 }
13043 }
13044 Ok(())
13045}
13046
13047/// How PG names the clause in its diagnostics.
13048const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
13049 use spg_sql::ast::LockStrength as LS;
13050 match s {
13051 LS::Update => "FOR UPDATE",
13052 LS::NoKeyUpdate => "FOR NO KEY UPDATE",
13053 LS::Share => "FOR SHARE",
13054 LS::KeyShare => "FOR KEY SHARE",
13055 }
13056}
13057
13058/// Every relation name (or alias) the FROM clause exposes.
13059fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
13060 let mut out = alloc::vec::Vec::new();
13061 if let Some(f) = &stmt.from {
13062 let mut push = |t: &spg_sql::ast::TableRef| {
13063 if let Some(a) = &t.alias {
13064 out.push(a.clone());
13065 }
13066 out.push(t.name.clone());
13067 };
13068 push(&f.primary);
13069 for j in &f.joins {
13070 push(&j.table);
13071 }
13072 }
13073 out
13074}
13075
13076fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
13077 use spg_sql::ast::Expr;
13078 if let Some(w) = &stmt.where_
13079 && aggregate::contains_aggregate(w)
13080 {
13081 return Err(EngineError::Unsupported(
13082 "aggregate functions are not allowed in WHERE".into(),
13083 ));
13084 }
13085 let mut nested = false;
13086 let mut check = |e: &Expr| {
13087 let mut probe = e.clone();
13088 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13089 let args = match n {
13090 Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
13091 _ => return false,
13092 };
13093 if args.iter().any(aggregate::contains_aggregate) {
13094 nested = true;
13095 }
13096 false
13097 });
13098 };
13099 for it in &stmt.items {
13100 if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
13101 check(expr);
13102 }
13103 }
13104 if let Some(h) = &stmt.having {
13105 check(h);
13106 }
13107 for o in &stmt.order_by {
13108 check(&o.expr);
13109 }
13110 if nested {
13111 return Err(EngineError::Unsupported(
13112 "aggregate function calls cannot be nested".into(),
13113 ));
13114 }
13115 Ok(())
13116}
13117
13118/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
13119/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
13120/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
13121/// to a set and then applies the enclosing expression once per element. SPG only
13122/// ever recognised an SRF that WAS the item, so everything above died on
13123/// "unknown function unnest" — the set-returning call, wrapped in anything at
13124/// all, fell through to the scalar function dispatcher which has no such name.
13125///
13126/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
13127/// rewritten to read that column, and the rewritten expression is evaluated once
13128/// per output row against the input row extended with the lifted values. The
13129/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
13130/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
13131/// executors (the single-table scan, the synthetic-table pipeline, and the
13132/// unnest FROM path) each evaluated the key as an ordinary expression, where the
13133/// literal `n` is just the constant n — the same sort key for every row. The
13134/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
13135/// back in input order, not in a wrong order. Statement prep resolves the common
13136/// case, but only when the SELECT item is an expression — a `*` is not one, and
13137/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
13138/// spelling landed on exactly the shape prep could not resolve.
13139///
13140/// A set-returning item is left alone: copying it into ORDER BY would make the
13141/// key "the whole set", evaluated once per INPUT row.
13142fn resolve_positional_order_by(
13143 order_by: &[spg_sql::ast::OrderBy],
13144 projection: &[ProjectedItem],
13145) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
13146 order_by
13147 .iter()
13148 .filter_map(|o| {
13149 let mut o = o.clone();
13150 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
13151 && *n >= 1
13152 && let Ok(idx) = usize::try_from(*n - 1)
13153 && let Some(item) = projection.get(idx)
13154 && !expr_contains_builtin_srf(&item.expr)
13155 {
13156 // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
13157 // item is itself an integer LITERAL must not be
13158 // substituted textually: the literal would read as an
13159 // ordinal again downstream, and `SELECT 10 … ORDER BY
13160 // 1` died with "position 10 is not in select list"
13161 // where PG happily returns the rows. Ordering by a
13162 // constant orders nothing, so the key drops.
13163 if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
13164 return None;
13165 }
13166 o.expr = item.expr.clone();
13167 }
13168 Some(o)
13169 })
13170 .collect()
13171}
13172
13173/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
13174/// this expression? Statement preparation (`resolve_order_by_position`) runs
13175/// before any catalog is in hand, and it only needs to know "is this item's value
13176/// a set", which the builtin SRFs answer syntactically.
13177pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
13178 let mut found = false;
13179 let mut probe = e.clone();
13180 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13181 if is_top_level_unnest(n) {
13182 found = true;
13183 return true;
13184 }
13185 false
13186 });
13187 found
13188}
13189
13190/// v7.39 (round 599) — everything about a target-list SRF that does not
13191/// depend on the row.
13192///
13193/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
13194/// each SRF-bearing projection expression, walked and rewrote the tree,
13195/// formatted a `__srf_N` name per node, and copied the whole column schema.
13196/// A counting allocator put the path at 24 allocations per input row for a
13197/// single-element `unnest`, against 0 for the same scan without one — 211 MB
13198/// where the plain scan took 4.3 — and the shape held whatever the array
13199/// contained, which is what invariant work looks like.
13200struct SrfPlan {
13201 /// The lifted SRF calls, in slot order.
13202 nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
13203 /// Per projection position, the expression with its SRF calls replaced
13204 /// by `__srf_N` column references. `None` means the item has none.
13205 rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
13206 /// The input schema followed by one column per slot. Only the slots'
13207 /// TYPES vary per row, and they are patched in place.
13208 ext_cols: alloc::vec::Vec<ColumnSchema>,
13209 /// v7.39 (round 743) — the rewritten projection COMPILED against the
13210 /// extended schema, once per plan. The per-output-row evaluation ran
13211 /// the interpreter (~560 ns/row on the unnest panel cell); the Step
13212 /// VM reads the `__srf_N` slots as plain columns. `None` = that item
13213 /// is not fully compilable and keeps the interpreter.
13214 compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
13215 base_cols: usize,
13216}
13217
13218fn build_srf_plan(
13219 engine: &Engine,
13220 projection: &[ProjectedItem],
13221 srf_idxs: &[usize],
13222 ctx: &EvalContext<'_>,
13223) -> Result<SrfPlan, EngineError> {
13224 // Lift every SRF node out of every item that contains one.
13225 let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
13226 let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
13227 let mut reject: Option<EngineError> = None;
13228 for &i in srf_idxs {
13229 let mut e = projection[i].expr.clone();
13230 crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
13231 if reject.is_some() {
13232 return true;
13233 }
13234 // PG refuses a set-returning function inside a conditional: the set
13235 // would have to be produced before anyone knows whether the branch
13236 // is even taken.
13237 let conditional = match n {
13238 spg_sql::ast::Expr::Case { .. } => Some("CASE"),
13239 spg_sql::ast::Expr::FunctionCall { name, .. }
13240 if name.eq_ignore_ascii_case("coalesce") =>
13241 {
13242 Some("COALESCE")
13243 }
13244 _ => None,
13245 };
13246 if let Some(kind) = conditional
13247 && engine.expr_contains_srf(n)
13248 {
13249 reject = Some(EngineError::Unsupported(alloc::format!(
13250 "set-returning functions are not allowed in {kind}"
13251 )));
13252 return true;
13253 }
13254 if !engine.is_srf_node(n) {
13255 return false;
13256 }
13257 let slot = nodes.len();
13258 nodes.push(n.clone());
13259 *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
13260 qualifier: None,
13261 name: alloc::format!("__srf_{slot}"),
13262 });
13263 true
13264 });
13265 rewritten[i] = Some(e);
13266 }
13267 if let Some(err) = reject {
13268 return Err(err);
13269 }
13270 let base_cols = ctx.columns.len();
13271 let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
13272 for slot in 0..nodes.len() {
13273 ext_cols.push(ColumnSchema::new(
13274 alloc::format!("__srf_{slot}"),
13275 DataType::Text,
13276 true,
13277 ));
13278 }
13279 // v7.39 (round 743) — compile the rewritten items against the
13280 // EXTENDED schema. The slot columns' declared type is a per-row
13281 // patched detail the compiled column read does not consult.
13282 let compiled: Vec<Option<eval::CompiledExpr>> = {
13283 let mut ext_ctx = ctx.clone();
13284 ext_ctx.columns = &ext_cols;
13285 projection
13286 .iter()
13287 .enumerate()
13288 .map(|(i, p)| {
13289 let e = rewritten[i].as_ref().unwrap_or(&p.expr);
13290 if eval::fully_compilable(e) {
13291 Some(eval::compile_expr(e, &ext_ctx))
13292 } else {
13293 None
13294 }
13295 })
13296 .collect()
13297 };
13298 Ok(SrfPlan {
13299 nodes,
13300 rewritten,
13301 ext_cols,
13302 compiled,
13303 base_cols,
13304 })
13305}
13306
13307/// One input row expanded through a plan built once for the whole scan.
13308/// v7.39 (round 621) — expand a projection whose target list contains
13309/// set-returning items, remembering which INPUT row each output row came from.
13310///
13311/// The three materialised-source tails — `FROM unnest(…)`, `FROM
13312/// generate_series(…)`, and the one that serves VALUES / a derived table /
13313/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
13314/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
13315/// v(x)` answered `function unnest(integer[]) does not exist` on all the
13316/// others, for a query PG answers. Sharing the expansion is the point: a
13317/// fourth copy would have been the fourth place to forget.
13318fn expand_projection_srfs(
13319 engine: &Engine,
13320 projection: &[ProjectedItem],
13321 srf_idxs: &[usize],
13322 filtered: &[Row<'static>],
13323 ctx: &EvalContext<'_>,
13324) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
13325 let mut out = alloc::vec::Vec::with_capacity(filtered.len());
13326 let mut src = alloc::vec::Vec::with_capacity(filtered.len());
13327 // v7.39 (round 726) — ONE plan for the whole scan. The per-row
13328 // spelling rebuilt it for every input row: a full clone of the
13329 // rewritten projection trees and the extended schema, 50k times on
13330 // the panel's unnest cell.
13331 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
13332 // v7.39 (round 733) — shard the expansion. Each shard clones the
13333 // plan (its ext_cols slot types are per-row mutable) and builds a
13334 // MINIMAL context — EvalContext is not Sync — which is sound only
13335 // when every expression involved is pure: the whole projection and
13336 // every SRF argument must be fully_compilable, or the row loop
13337 // stays serial with the full session context.
13338 // The projection is judged in its REWRITTEN form — the SRF call
13339 // itself is never compilable, but after the lift it is a plain
13340 // `__srf_N` column reference.
13341 let all_pure = projection
13342 .iter()
13343 .enumerate()
13344 .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
13345 && plan.nodes.iter().all(|n| match n {
13346 Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
13347 other => eval::fully_compilable(other),
13348 });
13349 if all_pure
13350 && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
13351 && let Some(r) = engine.parallel_runner.0.as_deref()
13352 {
13353 let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
13354 let chunk = filtered.len().div_ceil(n_shards);
13355 type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
13356 let schema_cols = ctx.columns;
13357 let alias = ctx.table_alias;
13358 let mysql = ctx.mysql_dialect;
13359 let style = ctx.render_style;
13360 let plan_ref = &plan;
13361 let results = r.run_shards(n_shards, &|si| {
13362 let lo = si * chunk;
13363 let hi = ((si + 1) * chunk).min(filtered.len());
13364 let mut sctx = eval::EvalContext::new(schema_cols, alias);
13365 sctx.mysql_dialect = mysql;
13366 sctx.render_style = style;
13367 // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
13368 // compiled programs); each shard rebuilds it, which also
13369 // recompiles against the shard's own context. Build errors
13370 // were already surfaced by the outer build above.
13371 let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
13372 Ok(p) => p,
13373 Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
13374 };
13375 let mut run = || -> ShardOut {
13376 let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
13377 let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
13378 for (i, row) in filtered[lo..hi].iter().enumerate() {
13379 let expanded =
13380 expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
13381 sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
13382 o.extend(expanded);
13383 }
13384 Ok((o, sidx))
13385 };
13386 alloc::boxed::Box::new(run())
13387 });
13388 for boxed in results {
13389 let shard = boxed
13390 .downcast::<ShardOut>()
13391 .expect("runner echoes the closure's box");
13392 let (o, sidx) = (*shard)?;
13393 out.extend(o);
13394 src.extend(sidx);
13395 }
13396 return Ok((out, src));
13397 }
13398 for (i, row) in filtered.iter().enumerate() {
13399 let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
13400 src.extend(core::iter::repeat_n(i, expanded.len()));
13401 out.extend(expanded);
13402 }
13403 Ok((out, src))
13404}
13405
13406/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
13407///
13408/// A key that names a select-list item reads it out of the EXPANDED row,
13409/// because PG sorts after the expansion. A key that names a source column the
13410/// query does not project is evaluated against the input row that output row
13411/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
13412fn srf_order_key(
13413 ob: &spg_sql::ast::OrderBy,
13414 out_col: Option<usize>,
13415 out: &Row<'static>,
13416 src: &Row<'static>,
13417 ctx: &EvalContext<'_>,
13418) -> Result<Value<'static>, EngineError> {
13419 match out_col {
13420 Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
13421 None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
13422 }
13423}
13424
13425fn expand_srf_row_with(
13426 engine: &Engine,
13427 plan: &mut SrfPlan,
13428 projection: &[ProjectedItem],
13429 row: &Row<'static>,
13430 ctx: &EvalContext<'_>,
13431) -> Result<Vec<Row<'static>>, EngineError> {
13432 let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
13433 for n in &plan.nodes {
13434 lists.push(engine.srf_values(n, row, ctx)?);
13435 }
13436 let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
13437 // Only the slots' element types depend on the row; the names and the
13438 // input schema around them do not.
13439 for (slot, list) in lists.iter().enumerate() {
13440 plan.ext_cols[plan.base_cols + slot].ty = list
13441 .iter()
13442 .find_map(|v| v.data_type())
13443 .unwrap_or(DataType::Text);
13444 }
13445 let mut ext_ctx = ctx.clone();
13446 ext_ctx.columns = &plan.ext_cols;
13447 let mut out = Vec::with_capacity(n_rows);
13448 // v7.39 (round 726) — the base columns are the SAME for every
13449 // expanded row; clone them once and rewrite only the SRF slots per
13450 // k. The old form cloned the whole input row per OUTPUT row — for
13451 // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
13452 // TEXT column the projection never reads.
13453 let base_len = row.values.len();
13454 let mut ext_vals = row.values.clone();
13455 ext_vals.resize(base_len + lists.len(), Value::Null);
13456 let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
13457 for k in 0..n_rows {
13458 for (slot, list) in lists.iter().enumerate() {
13459 // Past the end of THIS srf's rows → NULL (PG pads).
13460 ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
13461 }
13462 let ext_row = Row::new(core::mem::take(&mut ext_vals));
13463 let mut vals = Vec::with_capacity(projection.len());
13464 for (i, p) in projection.iter().enumerate() {
13465 // v7.39 (round 743) — compiled when possible; the
13466 // interpreter for the rest, with its exact wording.
13467 vals.push(match &plan.compiled[i] {
13468 Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
13469 .map_err(EngineError::Eval)?,
13470 None => {
13471 let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
13472 eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
13473 }
13474 });
13475 }
13476 ext_vals = ext_row.values;
13477 out.push(Row::new(vals));
13478 }
13479 Ok(out)
13480}
13481
13482/// The one-shot spelling, for the callers that expand a single row.
13483/// v7.39 (round 600) — which output column each ORDER BY key names, for a
13484/// query whose target list contains a set-returning function.
13485///
13486/// The keys used to be built from the INPUT row, before the SRF expanded, so
13487/// anything that named the SRF's own output was evaluated as a scalar call:
13488/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
13489/// "function unnest(integer[]) does not exist", and so did the spellings that
13490/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
13491/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
13492/// back in input order. PG sorts AFTER the expansion, so a key that names a
13493/// select-list item reads that item's value out of the expanded row.
13494///
13495/// `None` keeps the key on the input row, which is where an ORDER BY naming
13496/// a column the query does not project has to be evaluated.
13497fn srf_order_output_cols(
13498 order_by: &[spg_sql::ast::OrderBy],
13499 projection: &[ProjectedItem],
13500) -> Vec<Option<usize>> {
13501 order_by
13502 .iter()
13503 .map(|ob| {
13504 // A positive ordinal is the Nth output column, directly.
13505 // `resolve_positional_order_by` deliberately leaves an ordinal
13506 // pointing at a set-returning item alone — copying the call into
13507 // ORDER BY would have made the key "the whole set" back when keys
13508 // came from the input row. Reading the expanded row's column is
13509 // what it should have meant, and is what this does.
13510 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
13511 && *n >= 1
13512 && let Ok(idx) = usize::try_from(*n - 1)
13513 && idx < projection.len()
13514 {
13515 return Some(idx);
13516 }
13517 // An unqualified name matching exactly one output name. SQL
13518 // resolves ORDER BY against the select list first, so this wins
13519 // over an input column of the same name — which is the whole
13520 // point of `SELECT g AS id … ORDER BY id`.
13521 if let Expr::Column(c) = &ob.expr
13522 && c.qualifier.is_none()
13523 {
13524 let mut hit = None;
13525 for (i, p) in projection.iter().enumerate() {
13526 if p.output_name.eq_ignore_ascii_case(&c.name) {
13527 if hit.is_some() {
13528 hit = None;
13529 break;
13530 }
13531 hit = Some(i);
13532 }
13533 }
13534 if hit.is_some() {
13535 return hit;
13536 }
13537 }
13538 // Or the same expression as a select-list item — which is what
13539 // `ORDER BY 1` becomes once `resolve_positional_order_by` has
13540 // run, and what a repeated `ORDER BY unnest(…)` is.
13541 projection.iter().position(|p| p.expr == ob.expr)
13542 })
13543 .collect()
13544}
13545
13546fn expand_srf_row(
13547 engine: &Engine,
13548 projection: &[ProjectedItem],
13549 srf_idxs: &[usize],
13550 row: &Row<'static>,
13551 ctx: &EvalContext<'_>,
13552) -> Result<Vec<Row<'static>>, EngineError> {
13553 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
13554 expand_srf_row_with(engine, &mut plan, projection, row, ctx)
13555}
13556
13557impl Engine {
13558 /// The rows one target-list SRF yields for an input row. `None` from
13559 /// `srf_target_idxs` means the expression is not set-returning at all.
13560 fn srf_values(
13561 &self,
13562 expr: &spg_sql::ast::Expr,
13563 row: &Row<'static>,
13564 ctx: &EvalContext<'_>,
13565 ) -> Result<Vec<Value<'static>>, EngineError> {
13566 if top_level_srf_kind(expr).is_some() {
13567 return top_level_srf_output(expr, row, ctx);
13568 }
13569 // A user set-returning function. Its body runs through the real
13570 // executor, like every function body since round 63.
13571 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13572 return Err(EngineError::Unsupported(
13573 "expected a SELECT-list SRF call".into(),
13574 ));
13575 };
13576 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
13577 for a in args {
13578 vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13579 }
13580 let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
13581 // v7.39 (read01 round 68) — in a target list a multi-column function is
13582 // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
13583 // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
13584 // what it is for. A single-column function contributes its bare value.
13585 Ok(rows
13586 .into_iter()
13587 .map(|r| {
13588 if r.values.len() == 1 {
13589 r.values.into_iter().next().unwrap_or(Value::Null)
13590 } else {
13591 Value::Composite(
13592 cols.iter()
13593 .map(|c| c.name.clone())
13594 .zip(r.values)
13595 .collect::<alloc::vec::Vec<_>>(),
13596 )
13597 }
13598 })
13599 .collect())
13600 }
13601
13602 /// Is THIS node a set-returning call: one of the builtin kinds, or a user
13603 /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
13604 fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
13605 if is_top_level_unnest(e) {
13606 return true;
13607 }
13608 let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
13609 return false;
13610 };
13611 self.active_catalog().functions_named(name).iter().any(|f| {
13612 let r = f.returns.trim().to_ascii_uppercase();
13613 r.starts_with("SETOF") || r.starts_with("TABLE(")
13614 })
13615 }
13616
13617 /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
13618 fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
13619 let mut found = false;
13620 let mut probe = e.clone();
13621 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13622 if self.is_srf_node(n) {
13623 found = true;
13624 return true;
13625 }
13626 false
13627 });
13628 found
13629 }
13630
13631 /// Which projection items CONTAIN a set-returning call. Before round 78 this
13632 /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
13633 /// ordinary scalar call all the way down to the function dispatcher, which
13634 /// then reported `unnest` as an unknown function.
13635 fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
13636 projection
13637 .iter()
13638 .enumerate()
13639 .filter(|(_, p)| self.expr_contains_srf(&p.expr))
13640 .map(|(i, _)| i)
13641 .collect()
13642 }
13643}
13644
13645impl Engine {
13646 /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
13647 /// no `(f(args)).*` item.
13648 fn lower_record_expansion(
13649 &self,
13650 stmt: &SelectStatement,
13651 ) -> Result<Option<SelectStatement>, EngineError> {
13652 use spg_sql::ast::{Expr, SelectItem};
13653 let is_marker = |it: &SelectItem| {
13654 matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
13655 if name == "__record_expand")
13656 };
13657 if !stmt.items.iter().any(is_marker) {
13658 return Ok(None);
13659 }
13660 let mut out = stmt.clone();
13661 let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
13662 let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
13663 for (n, item) in stmt.items.iter().enumerate() {
13664 if !is_marker(item) {
13665 items.push(item.clone());
13666 continue;
13667 }
13668 let SelectItem::Expr {
13669 expr: Expr::FunctionCall { args, .. },
13670 ..
13671 } = item
13672 else {
13673 unreachable!("checked by is_marker");
13674 };
13675 let Some(Expr::FunctionCall {
13676 name: fname,
13677 args: fargs,
13678 }) = args.first()
13679 else {
13680 return Err(EngineError::Unsupported(
13681 "(<expr>).* expands a function's record — it needs a function call".into(),
13682 ));
13683 };
13684 let cols = self.setof_declared_columns(fname)?;
13685 let alias = alloc::format!("__rec{n}");
13686 let mut tref = bare_table_ref_named(&alias);
13687 tref.table_fn_call = Some(alloc::boxed::Box::new((
13688 fname.to_ascii_lowercase(),
13689 fargs.clone(),
13690 )));
13691 tref.alias = Some(alias.clone());
13692 lateral_refs.push(tref);
13693 for c in cols {
13694 items.push(SelectItem::Expr {
13695 expr: Expr::Column(spg_sql::ast::ColumnName {
13696 qualifier: Some(alias.clone()),
13697 name: c,
13698 }),
13699 alias: None,
13700 });
13701 }
13702 }
13703 out.items = items;
13704 // The function joins the FROM. With no FROM it BECOMES the FROM; with one
13705 // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
13706 // (the arguments may reference the outer row — the round-69 correlation).
13707 for tref in lateral_refs {
13708 match &mut out.from {
13709 None => {
13710 out.from = Some(spg_sql::ast::FromClause {
13711 primary: tref,
13712 joins: alloc::vec::Vec::new(),
13713 });
13714 }
13715 Some(from) => from.joins.push(spg_sql::ast::FromJoin {
13716 kind: spg_sql::ast::JoinKind::Cross,
13717 table: tref,
13718 on: None,
13719 using_cols: None,
13720 natural: false,
13721 }),
13722 }
13723 }
13724 Ok(Some(out))
13725 }
13726
13727 /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
13728 /// v text)` names them; a `SETOF <scalar>` is one column named after the
13729 /// function.
13730 fn setof_declared_columns(
13731 &self,
13732 name: &str,
13733 ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
13734 let cat = self.active_catalog();
13735 let overloads = cat.functions_named(name);
13736 let def = overloads.first().ok_or_else(|| {
13737 EngineError::Unsupported(alloc::format!("function {name} does not exist"))
13738 })?;
13739 let declared = def.returns.trim();
13740 let upper = declared.to_ascii_uppercase();
13741 if upper.starts_with("TABLE(") {
13742 let raw = &declared["TABLE(".len()..declared.len() - 1];
13743 return Ok(raw
13744 .split(',')
13745 .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
13746 .collect());
13747 }
13748 Ok(alloc::vec![name.to_string()])
13749 }
13750}
13751
13752/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
13753/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
13754/// COLUMNS list (data-independent), NESTED children inlined in
13755/// declaration order (PG's flattened output shape).
13756/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
13757/// correlated JSON_TABLE's static schema without evaluating its doc.
13758pub(crate) fn json_table_schema_pub(
13759 cols: &[spg_sql::ast::JsonTableColumn],
13760) -> alloc::vec::Vec<ColumnSchema> {
13761 json_table_schema(cols)
13762}
13763
13764fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
13765 use spg_sql::ast::JsonTableColumn as C;
13766 let mut out = alloc::vec::Vec::new();
13767 for c in cols {
13768 match c {
13769 C::Ordinality { name } => {
13770 out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
13771 }
13772 C::Regular {
13773 name, ty, exists, ..
13774 } => {
13775 let dt = if *exists {
13776 DataType::Bool
13777 } else {
13778 crate::conversions::column_type_to_data_type(*ty)
13779 };
13780 out.push(ColumnSchema::new(name.clone(), dt, true));
13781 }
13782 C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
13783 }
13784 }
13785 out
13786}
13787
13788/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
13789/// JSON_TABLE column's declared type (the DEFAULT expr may be a
13790/// string literal like `'none'` that must land as the column type).
13791fn coerce_json_table_default(
13792 v: Value<'static>,
13793 ty: spg_sql::ast::ColumnTypeName,
13794 name: &str,
13795) -> Result<Value<'static>, EngineError> {
13796 if v.is_null() {
13797 return Ok(Value::Null);
13798 }
13799 let dt = crate::conversions::column_type_to_data_type(ty);
13800 crate::conversions::coerce_value(v, dt, name, 0)
13801}
13802
13803/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
13804fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
13805 use crate::json::JsonValue as J;
13806 match v {
13807 Value::Null => J::Null,
13808 Value::Bool(b) => J::Bool(*b),
13809 Value::SmallInt(n) => J::Number(f64::from(*n)),
13810 Value::Int(n) => J::Number(f64::from(*n)),
13811 Value::BigInt(n) => J::Number(*n as f64),
13812 Value::Float(x) => J::Number(*x),
13813 Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
13814 other => J::String(crate::eval::value_to_text(other)),
13815 }
13816}
13817
13818fn bare_table_ref_named(name: &str) -> TableRef {
13819 TableRef {
13820 name: name.to_string(),
13821 alias: None,
13822 only: false,
13823 as_of_segment: None,
13824 unnest_expr: None,
13825 unnest_column_aliases: alloc::vec::Vec::new(),
13826 with_ordinality: false,
13827 generate_series_args: None,
13828 lateral_subquery: None,
13829 jsonb_each_text_arg: None,
13830 table_fn_call: None,
13831 rows_from: None,
13832 json_table: None,
13833 scalar_fn_item: false,
13834 }
13835}
13836
13837impl Engine {
13838 /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
13839 /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
13840 /// entries are the array-able SRFs, already lowered by the parser into their
13841 /// scalar array form.
13842 fn rows_from_rows(
13843 &self,
13844 primary: &TableRef,
13845 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
13846 let entries = primary
13847 .rows_from
13848 .as_ref()
13849 .expect("caller guards rows_from.is_some()");
13850 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13851 let ctx = self.ev_ctx(&empty, None);
13852 let dummy = Row::new(alloc::vec::Vec::new());
13853 let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
13854 let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13855 for (name, args) in entries {
13856 let (vals, colname) = if name == "__array" {
13857 // The parser lowered this one to `<array expr>`; its rows are the
13858 // array's elements.
13859 let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
13860 (
13861 array_value_to_elements(&arr)?,
13862 alloc::string::String::from("unnest"),
13863 )
13864 } else {
13865 let call = spg_sql::ast::Expr::FunctionCall {
13866 name: name.clone(),
13867 args: args.clone(),
13868 };
13869 (self.srf_values(&call, &dummy, &ctx)?, name.clone())
13870 };
13871 let ty = vals
13872 .first()
13873 .and_then(spg_storage::Value::data_type)
13874 .unwrap_or(DataType::Text);
13875 cols.push(ColumnSchema::new(colname, ty, true));
13876 lists.push(vals);
13877 }
13878 let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
13879 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
13880 for k in 0..n {
13881 let mut vals: alloc::vec::Vec<Value<'static>> =
13882 alloc::vec::Vec::with_capacity(lists.len() + 1);
13883 for l in &lists {
13884 vals.push(l.get(k).cloned().unwrap_or(Value::Null));
13885 }
13886 rows.push(Row::new(vals));
13887 }
13888 if primary.with_ordinality {
13889 cols.push(ColumnSchema::new(
13890 "ordinality".to_string(),
13891 DataType::BigInt,
13892 false,
13893 ));
13894 rows = rows
13895 .into_iter()
13896 .enumerate()
13897 .map(|(i, r)| {
13898 let mut v = r.values;
13899 v.push(Value::BigInt(i as i64 + 1));
13900 Row::new(v)
13901 })
13902 .collect();
13903 }
13904 Ok((rows, cols))
13905 }
13906}
13907
13908/// v7.39 (round 232) — PG names the offending set operation in its
13909/// arity / type-mismatch messages ("each UNION query must have the same
13910/// number of columns"). `UNION ALL` is still spelled UNION there.
13911fn set_op_name(kind: UnionKind) -> &'static str {
13912 match kind {
13913 UnionKind::All | UnionKind::Distinct => "UNION",
13914 UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
13915 UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
13916 }
13917}
13918
13919/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
13920/// type: a bare string or NULL literal that no context has typed yet. SPG
13921/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
13922/// be the syntax. A wildcard or a non-literal expression is never unknown.
13923/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
13924/// LABELS as text (the wire render) but the value is an oid-carrying
13925/// dual, so a UNION with a numeric column must not be refused on the
13926/// label (pg_dump: `SELECT classid … UNION ALL SELECT
13927/// 'pg_opfamily'::regclass …`).
13928fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
13929 fn is_regcast(e: &Expr) -> bool {
13930 matches!(
13931 e,
13932 Expr::Cast {
13933 target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
13934 ..
13935 }
13936 )
13937 }
13938 stmt.items
13939 .iter()
13940 .map(|item| match item {
13941 SelectItem::Expr { expr, .. } => is_regcast(expr),
13942 _ => false,
13943 })
13944 .collect()
13945}
13946
13947fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
13948 stmt.items
13949 .iter()
13950 .map(|item| match item {
13951 SelectItem::Expr { expr, .. } => matches!(
13952 expr,
13953 Expr::Literal(spg_sql::ast::Literal::String(_))
13954 | Expr::Literal(spg_sql::ast::Literal::Null)
13955 ),
13956 _ => false,
13957 })
13958 .collect()
13959}
13960
13961/// v7.39 (round 233) — retype one branch column's cells, reporting the
13962/// conversion failure the way PG does rather than leaving the column
13963/// half-converted. Used when the other branch typed an untyped literal.
13964fn coerce_branch_column(
13965 rows: &mut [Row<'static>],
13966 col_idx: usize,
13967 target: DataType,
13968 col_name: &str,
13969) -> Result<(), EngineError> {
13970 for row in rows.iter_mut() {
13971 let Some(slot) = row.values.get_mut(col_idx) else {
13972 continue;
13973 };
13974 if matches!(slot, Value::Null) {
13975 continue;
13976 }
13977 *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
13978 }
13979 Ok(())
13980}
13981
13982/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
13983/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
13984/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
13985/// reference to q's output columns substituted by the underlying column.
13986///
13987/// Admission is deliberately narrow — anything that changes cardinality,
13988/// order, or scope stays on the materialising path:
13989/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
13990/// FROM with no ordinality or positional column aliases, and no
13991/// subquery anywhere its expressions (an inner scope could reference
13992/// q too — descending is a later knife);
13993/// * inner: one stored table, bare-column projection only, no
13994/// CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
13995/// * every outer column reference must resolve inside q's output list —
13996/// a name that does not is an ERROR today, and flattening would
13997/// silently legalise it against the base table.
13998fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
13999 use spg_sql::ast::SelectItem;
14000 let inner = primary.lateral_subquery.as_deref()?;
14001 // Outer shape.
14002 if !stmt.ctes.is_empty()
14003 || !stmt.unions.is_empty()
14004 || stmt.distinct
14005 || !stmt.distinct_on.is_empty()
14006 || !stmt.window_check_exprs.is_empty()
14007 || stmt.locking.is_some()
14008 || primary.with_ordinality
14009 || !primary.unnest_column_aliases.is_empty()
14010 {
14011 return None;
14012 }
14013 // Inner shape.
14014 if !inner.ctes.is_empty()
14015 || !inner.unions.is_empty()
14016 || inner.distinct
14017 || !inner.distinct_on.is_empty()
14018 || inner.group_by.is_some()
14019 || inner.group_by_all
14020 || inner.having.is_some()
14021 || !inner.order_by.is_empty()
14022 || inner.limit.is_some()
14023 || inner.offset.is_some()
14024 || !inner.window_check_exprs.is_empty()
14025 || inner.locking.is_some()
14026 {
14027 return None;
14028 }
14029 let ifrom = inner.from.as_ref()?;
14030 let it = &ifrom.primary;
14031 if !ifrom.joins.is_empty()
14032 || it.name.is_empty()
14033 || it.lateral_subquery.is_some()
14034 || it.unnest_expr.is_some()
14035 || it.generate_series_args.is_some()
14036 || it.as_of_segment.is_some()
14037 || it.jsonb_each_text_arg.is_some()
14038 || it.table_fn_call.is_some()
14039 || it.rows_from.is_some()
14040 || it.json_table.is_some()
14041 || it.with_ordinality
14042 || !it.unnest_column_aliases.is_empty()
14043 {
14044 return None;
14045 }
14046 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
14047 return None;
14048 }
14049 // The output map: q's visible name -> the underlying column.
14050 let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
14051 let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
14052 alloc::collections::BTreeMap::new();
14053 for item in &inner.items {
14054 let SelectItem::Expr { expr, alias } = item else {
14055 return None;
14056 };
14057 let Expr::Column(c) = expr else {
14058 return None;
14059 };
14060 if let Some(q) = c.qualifier.as_deref()
14061 && !q.eq_ignore_ascii_case(&inner_alias)
14062 {
14063 return None;
14064 }
14065 let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
14066 // A duplicated output name would make substitution ambiguous.
14067 if map
14068 .insert(out_name.to_ascii_lowercase(), c.clone())
14069 .is_some()
14070 {
14071 return None;
14072 }
14073 }
14074 if map.is_empty() {
14075 return None;
14076 }
14077 let derived_alias = primary
14078 .alias
14079 .clone()
14080 .unwrap_or_else(|| primary.name.clone())
14081 .to_ascii_lowercase();
14082 // Substitute in a clone; bail (None) on the first reference the map
14083 // cannot answer.
14084 let mut out = stmt.clone();
14085 let ok = core::cell::Cell::new(true);
14086 let mut subst = |e: &mut Expr| -> bool {
14087 match e {
14088 Expr::Column(c) => {
14089 match c.qualifier.as_deref() {
14090 Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
14091 None => {}
14092 Some(_) => {
14093 ok.set(false);
14094 return true;
14095 }
14096 }
14097 match map.get(&c.name.to_ascii_lowercase()) {
14098 Some(target) => *c = target.clone(),
14099 None => ok.set(false),
14100 }
14101 true
14102 }
14103 // Any subquery could reference q from its own scope;
14104 // descending is a later knife — bail for now.
14105 Expr::ScalarSubquery(_)
14106 | Expr::Exists { .. }
14107 | Expr::InSubquery { .. }
14108 | Expr::RowInSubquery { .. }
14109 | Expr::RowCmpSubquery { .. } => {
14110 ok.set(false);
14111 true
14112 }
14113 _ => false,
14114 }
14115 };
14116 for item in &mut out.items {
14117 match item {
14118 SelectItem::Expr { expr, .. } => {
14119 crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
14120 }
14121 // `SELECT * FROM (…) q` means q's columns, in q's order.
14122 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
14123 }
14124 }
14125 if let Some(w) = &mut out.where_ {
14126 crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
14127 }
14128 if let Some(gs) = &mut out.group_by {
14129 for g in gs {
14130 crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
14131 }
14132 }
14133 if let Some(h) = &mut out.having {
14134 crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
14135 }
14136 for o in &mut out.order_by {
14137 crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
14138 }
14139 for d in &mut out.distinct_on {
14140 crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
14141 }
14142 if !ok.get() {
14143 return None;
14144 }
14145 // FROM becomes the stored table; the filters conjoin.
14146 out.from = Some(spg_sql::ast::FromClause {
14147 primary: it.clone(),
14148 joins: Vec::new(),
14149 });
14150 out.where_ = match (inner.where_.clone(), out.where_.take()) {
14151 (Some(a), Some(b)) => Some(Expr::Binary {
14152 lhs: alloc::boxed::Box::new(a),
14153 op: spg_sql::ast::BinOp::And,
14154 rhs: alloc::boxed::Box::new(b),
14155 }),
14156 (Some(a), None) => Some(a),
14157 (None, b) => b,
14158 };
14159 Some(out)
14160}
14161
14162/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
14163/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
14164/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
14165/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
14166/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
14167/// DISTINCT, an SRF, or an unprovable inner shape stays put.
14168fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
14169 use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
14170 let inner = primary.lateral_subquery.as_deref()?;
14171 // Outer: exactly `SELECT count(*)`, nothing else.
14172 if !stmt.ctes.is_empty()
14173 || !stmt.unions.is_empty()
14174 || stmt.distinct
14175 || !stmt.distinct_on.is_empty()
14176 || stmt.where_.is_some()
14177 || stmt.group_by.is_some()
14178 || stmt.having.is_some()
14179 || !stmt.order_by.is_empty()
14180 || stmt.limit.is_some()
14181 || stmt.offset.is_some()
14182 || stmt.items.len() != 1
14183 {
14184 return None;
14185 }
14186 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
14187 return None;
14188 };
14189 let E::FunctionCall { name, args } = expr else {
14190 return None;
14191 };
14192 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
14193 return None;
14194 }
14195 // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
14196 let Some(LimitExpr::Literal(k)) = &inner.offset else {
14197 return None;
14198 };
14199 let k = i64::from(*k);
14200 if inner.limit.is_some() || inner.order_by.is_empty() {
14201 return None;
14202 }
14203 let mut counted = inner.clone();
14204 counted.order_by = Vec::new();
14205 counted.offset = None;
14206 // The stripped inner must now be a provable simple shape (its
14207 // items become irrelevant — count(*) reads none of them — but an
14208 // SRF item would change the row count, so the flatten predicate's
14209 // scrutiny still applies).
14210 let base = matview_flatten_probe(&counted)?;
14211 let mut out = stmt.clone();
14212 out.items = alloc::vec![SelectItem::Expr {
14213 expr: E::FunctionCall {
14214 name: String::from("greatest"),
14215 args: alloc::vec![
14216 E::Binary {
14217 lhs: alloc::boxed::Box::new(E::FunctionCall {
14218 name: String::from("count_star"),
14219 args: alloc::vec![],
14220 }),
14221 op: spg_sql::ast::BinOp::Sub,
14222 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
14223 },
14224 E::Literal(spg_sql::ast::Literal::Integer(0)),
14225 ],
14226 },
14227 alias: Some(String::from("count")),
14228 }];
14229 out.from = Some(spg_sql::ast::FromClause {
14230 primary: base,
14231 joins: Vec::new(),
14232 });
14233 out.where_ = counted.where_.clone();
14234 Some(out)
14235}
14236
14237/// The inner-shape probe `try_count_over_offset` shares with the
14238/// flatten: single stored table, no modifiers, no subqueries, no SRF
14239/// items. Returns the base TableRef.
14240fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
14241 use spg_sql::ast::SelectItem;
14242 if !inner.ctes.is_empty()
14243 || !inner.unions.is_empty()
14244 || inner.distinct
14245 || !inner.distinct_on.is_empty()
14246 || inner.group_by.is_some()
14247 || inner.group_by_all
14248 || inner.having.is_some()
14249 || !inner.order_by.is_empty()
14250 || inner.limit.is_some()
14251 || inner.offset.is_some()
14252 || !inner.window_check_exprs.is_empty()
14253 || inner.locking.is_some()
14254 {
14255 return None;
14256 }
14257 let ifrom = inner.from.as_ref()?;
14258 let it = &ifrom.primary;
14259 if !ifrom.joins.is_empty()
14260 || it.name.is_empty()
14261 || it.lateral_subquery.is_some()
14262 || it.unnest_expr.is_some()
14263 || it.generate_series_args.is_some()
14264 || it.as_of_segment.is_some()
14265 || it.jsonb_each_text_arg.is_some()
14266 || it.table_fn_call.is_some()
14267 || it.rows_from.is_some()
14268 || it.json_table.is_some()
14269 || it.with_ordinality
14270 {
14271 return None;
14272 }
14273 for item in &inner.items {
14274 match item {
14275 SelectItem::Expr { expr, .. } => {
14276 if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
14277 return None;
14278 }
14279 }
14280 SelectItem::Wildcard => {}
14281 SelectItem::QualifiedWildcard(_) => return None,
14282 }
14283 }
14284 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
14285 return None;
14286 }
14287 Some(it.clone())
14288}
14289
14290/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
14291/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
14292/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
14293/// constant-LENGTH array literal unnests to exactly k rows per input
14294/// row (NULL elements are rows too). One SRF item only, elements
14295/// subquery-free, and the stripped inner must pass the same probe the
14296/// count-over-offset rewrite uses.
14297fn try_count_over_const_unnest(
14298 stmt: &SelectStatement,
14299 primary: &TableRef,
14300) -> Option<SelectStatement> {
14301 use spg_sql::ast::{Expr as E, SelectItem};
14302 let inner = primary.lateral_subquery.as_deref()?;
14303 if !stmt.ctes.is_empty()
14304 || !stmt.unions.is_empty()
14305 || stmt.distinct
14306 || !stmt.distinct_on.is_empty()
14307 || stmt.where_.is_some()
14308 || stmt.group_by.is_some()
14309 || stmt.having.is_some()
14310 || !stmt.order_by.is_empty()
14311 || stmt.limit.is_some()
14312 || stmt.offset.is_some()
14313 || stmt.items.len() != 1
14314 {
14315 return None;
14316 }
14317 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
14318 return None;
14319 };
14320 let E::FunctionCall { name, args } = expr else {
14321 return None;
14322 };
14323 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
14324 return None;
14325 }
14326 // Inner: exactly one item, and it is unnest(ARRAY[...]).
14327 if inner.items.len() != 1
14328 || !inner.order_by.is_empty()
14329 || inner.limit.is_some()
14330 || inner.offset.is_some()
14331 {
14332 return None;
14333 }
14334 let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
14335 return None;
14336 };
14337 let E::FunctionCall {
14338 name: fname,
14339 args: fargs,
14340 } = item
14341 else {
14342 return None;
14343 };
14344 if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
14345 return None;
14346 }
14347 let E::Array(elems) = &fargs[0] else {
14348 return None;
14349 };
14350 if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
14351 return None;
14352 }
14353 let k = elems.len() as i64;
14354 // The stripped inner (the SRF item replaced by a plain constant)
14355 // must be the provable simple shape.
14356 let mut counted = inner.clone();
14357 counted.items = alloc::vec![SelectItem::Expr {
14358 expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
14359 alias: None,
14360 }];
14361 let base = matview_flatten_probe(&counted)?;
14362 let mut out = stmt.clone();
14363 out.items = alloc::vec![SelectItem::Expr {
14364 expr: E::Binary {
14365 lhs: alloc::boxed::Box::new(E::FunctionCall {
14366 name: String::from("count_star"),
14367 args: alloc::vec![],
14368 }),
14369 op: spg_sql::ast::BinOp::Mul,
14370 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
14371 },
14372 alias: Some(String::from("count")),
14373 }];
14374 out.from = Some(spg_sql::ast::FromClause {
14375 primary: base,
14376 joins: Vec::new(),
14377 });
14378 out.where_ = counted.where_.clone();
14379 Some(out)
14380}