spg_engine/select.rs
1//! SELECT execution — the window / meta-view / CTE variants and the
2//! subquery-resolution pre-pass. Lifted out of `lib.rs` (v7.32 engine
3//! modularisation). These `impl Engine` methods are dispatched from the
4//! bare-SELECT entry points and drive the non-trivial SELECT shapes.
5
6use alloc::borrow::Cow;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9
10use spg_sql::ast::{
11 ColumnName, Expr, FromClause, SelectItem, SelectStatement, Statement, TableRef, UnionKind,
12};
13use spg_storage::{
14 Catalog, ColumnSchema, DataType, Row, StorageError, TableSchema, Value, VecEncoding,
15};
16
17use crate::describe;
18use crate::eval::{EvalContext, EvalError};
19use crate::join::RowRef;
20use crate::system_catalog::collect_view_refs;
21use crate::{
22 ByteBudget, CancelToken, Engine, EngineError, OrderKey, QueryResult, aggregate,
23 apply_offset_and_limit, apply_offset_and_limit_tagged, approx_row_bytes, build_order_keys,
24 collect_meta_view_names, collect_qualified_refs, collect_scalar_subqueries,
25 collect_window_nodes, compute_window_partition, eval, expr_tree_has_subquery,
26 materialise_in_order, materialise_meta_view, memoize, order_by_value_cmp_in, partition_key_cmp,
27 rewrite_window_to_columns, select_has_window, select_references_meta_view, select_refers_to,
28 sort_by_keys, synth_info_key_column_usage, synth_info_referential_constraints,
29 synth_info_routines, synth_info_statistics, synth_information_schema_columns,
30 synth_information_schema_tables, synth_mysql_db, synth_mysql_user, synth_pg_attribute,
31 synth_pg_class, synth_pg_constraint, synth_pg_database, synth_pg_extension, synth_pg_index_raw,
32 synth_pg_indexes, synth_pg_namespace, synth_pg_operator, synth_pg_proc, synth_pg_roles,
33 synth_pg_sequence, synth_pg_settings, synth_pg_timezone_abbrevs, synth_pg_timezone_names,
34 synth_pg_trigger, synth_pg_type, synth_pg_views, topk_trim, try_gin_jsonb_seek, try_gin_seek,
35 try_index_seek, try_nsw_knn, try_pk_walk_top_n, try_trgm_seek, value_is_bigint,
36 value_is_integer, value_to_i64,
37};
38
39/// v7.39 (round 618) — a recursive term that can be run over the working set
40/// directly, instead of through a whole query execution per round.
41///
42/// PG plans the recursive term ONCE and re-scans a worktable each iteration.
43/// SPG emptied and refilled a real table and then called `exec_select_cancel`
44/// — FROM resolution, schema build, predicate compilation, projection build
45/// and result materialisation — for every round. Measured with the counting
46/// allocator on `WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM r
47/// WHERE n < N)`: about 40 allocations and 99 kB PER ROUND while the working
48/// set is one row, or 1.98 GB at N = 20000.
49///
50/// This is the shape that covers the ordinary recursive term: read the CTE,
51/// filter it, project it. Anything else — a join, an aggregate, a window, a
52/// subquery, DISTINCT, GROUP BY, ORDER BY, LIMIT, a locking clause, a
53/// non-table source — returns `None` and keeps the general path, so the
54/// answers it gives are the ones that path gave.
55struct RecursiveTermPlan<'t> {
56 items: Vec<&'t Expr>,
57 where_: Option<&'t Expr>,
58 alias: String,
59}
60
61fn plan_recursive_term<'t>(
62 t: &'t SelectStatement,
63 cte_name: &str,
64 ncols: usize,
65) -> Option<RecursiveTermPlan<'t>> {
66 if !t.unions.is_empty()
67 || !t.ctes.is_empty()
68 || t.distinct
69 || !t.distinct_on.is_empty()
70 || t.group_by.is_some()
71 || t.group_by_all
72 || t.having.is_some()
73 || !t.order_by.is_empty()
74 || t.limit.is_some()
75 || t.offset.is_some()
76 || t.limit_with_ties
77 || t.locking.is_some()
78 {
79 return None;
80 }
81 let from = t.from.as_ref()?;
82 if !from.joins.is_empty() {
83 return None;
84 }
85 let p = &from.primary;
86 if !p.name.eq_ignore_ascii_case(cte_name)
87 || p.as_of_segment.is_some()
88 || p.unnest_expr.is_some()
89 || !p.unnest_column_aliases.is_empty()
90 || p.with_ordinality
91 || p.generate_series_args.is_some()
92 || p.lateral_subquery.is_some()
93 || p.jsonb_each_text_arg.is_some()
94 || p.table_fn_call.is_some()
95 {
96 return None;
97 }
98 let unsupported = |e: &Expr| {
99 crate::aggregate::contains_aggregate(e)
100 || crate::subquery::expr_has_subquery(e)
101 || crate::window::expr_has_window_pub(e)
102 };
103 let mut items: Vec<&Expr> = Vec::with_capacity(t.items.len());
104 for it in &t.items {
105 match it {
106 SelectItem::Expr { expr, .. } => {
107 if unsupported(expr) {
108 return None;
109 }
110 items.push(expr);
111 }
112 // `*` would have to be expanded against the CTE's own schema;
113 // the general path already does that, so leave it there.
114 _ => return None,
115 }
116 }
117 if items.len() != ncols {
118 return None;
119 }
120 if let Some(w) = &t.where_
121 && unsupported(w)
122 {
123 return None;
124 }
125 Some(RecursiveTermPlan {
126 items,
127 where_: t.where_.as_ref(),
128 alias: p.alias.clone().unwrap_or_else(|| p.name.clone()),
129 })
130}
131
132impl Engine {
133 /// v4.12 window executor. Implements `ROW_NUMBER` / `RANK` /
134 /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
135 /// `AVG` / `COUNT` / `MIN` / `MAX`. The plan is:
136 /// 1. Apply the WHERE filter.
137 /// 2. For each unique `WindowFunction` node in the projection,
138 /// partition + sort, compute the per-row value.
139 /// 3. Append the window values as synthetic columns (`__win_N`)
140 /// to the row schema.
141 /// 4. Rewrite the projection to read those columns.
142 /// 5. Hand off to the regular project / ORDER BY / LIMIT pipe.
143 #[allow(
144 clippy::too_many_lines,
145 clippy::type_complexity,
146 clippy::needless_range_loop
147 )] // window-eval is one cohesive pipe; splitting fragments
148 pub(crate) fn exec_select_with_window(
149 &self,
150 stmt: &SelectStatement,
151 cancel: CancelToken<'_>,
152 ) -> Result<QueryResult, EngineError> {
153 let from = stmt.from.as_ref().ok_or_else(|| {
154 EngineError::Unsupported("window functions require a FROM clause".into())
155 })?;
156 // v7.17.0 Phase 3.P0-43 — JOIN + window functions. Phase
157 // 3.6 rejected this combination outright ("queued for
158 // v5.x"); P0-43 materialises the join + WHERE through the
159 // existing nested-loop helper and runs the window pipeline
160 // on the joined row set with the combined `alias.col`
161 // schema. The window expressions resolve through the
162 // qualifier-aware column resolver same as the aggregate /
163 // projection paths on JOIN.
164 let (schema_cols_owned, alias_opt): (Vec<ColumnSchema>, Option<&str>);
165 // v7.39 (round 976) — rows this walk OWNS. A derived FROM item and
166 // a JOIN both produce rows that exist nowhere else, so they land
167 // here; a plain stored table does not, and borrows instead.
168 //
169 // It used to clone every row out of the table, on the reasoning
170 // that "the clone is cheap relative to the window computation that
171 // follows". Measured on 400k rows, `row_number() OVER ()` cost
172 // 31.881 ms against 46.520 with a 200-byte column added — so the
173 // clone tracks row width at about 36 ns per row per 200 bytes, and
174 // the window computation it was being compared against is a
175 // counter increment per row. Nothing downstream needs the rows
176 // owned: the very next statement used to be
177 // `filtered.iter().collect()` into the `&Row` slice the window
178 // pipeline actually reads.
179 let mut owned_rows: Vec<Row<'static>> = Vec::new();
180 // What the pipeline reads. Borrows `owned_rows` or the table.
181 let mut filtered: Vec<&Row<'static>> = Vec::new();
182 // Set by the branches that fill `owned_rows`, because "empty" is
183 // an answer a query can legitimately have and so cannot be the
184 // signal for which of the two holds the rows.
185 let mut rows_are_owned = false;
186 if from.joins.is_empty() {
187 let primary = &from.primary;
188 // v7.37 D.13 — window functions over a derived table (subquery /
189 // VALUES / unnest / generate_series). The catalog-by-name lookup
190 // below only finds real tables, so a derived primary threw
191 // TableNotFound. Materialise the derived rows + schema through the
192 // same helper the non-window FROM-primary path uses, then WHERE-
193 // filter and feed the identical window pipeline.
194 let is_derived = primary.lateral_subquery.is_some()
195 || primary.unnest_expr.is_some()
196 || primary.generate_series_args.is_some()
197 || primary.jsonb_each_text_arg.is_some()
198 || primary.table_fn_call.is_some();
199 if is_derived {
200 let (drows, dcols) = self.materialise_table_ref(primary)?;
201 schema_cols_owned = dcols;
202 alias_opt = primary.alias.as_deref();
203 let ctx = self.ev_ctx(&schema_cols_owned, alias_opt);
204 let mut owned: Vec<Row<'static>> = Vec::new();
205 for (i, row) in drows.into_iter().enumerate() {
206 if i.is_multiple_of(256) {
207 cancel.check()?;
208 }
209 if let Some(w) = &stmt.where_ {
210 let cond = eval::eval_expr(w, &row, &ctx)?;
211 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
212 continue;
213 }
214 }
215 owned.push(row);
216 }
217 owned_rows = owned;
218 rows_are_owned = true;
219 } else {
220 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
221 StorageError::TableNotFound {
222 name: primary.name.clone(),
223 }
224 })?;
225 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
226 schema_cols_owned = table.schema().columns.clone();
227 alias_opt = Some(alias);
228 let ctx = self.ev_ctx(&schema_cols_owned, alias_opt);
229 // The WHERE test, in ONE place, for all four ways a row can
230 // reach this walk. It deliberately does not touch the row
231 // collections: a closure that pushed into them would tie
232 // its argument to the closure body and no borrowed row
233 // could escape it, which is what forced the clone-shaped
234 // version of this loop in the first place.
235 let passes = |row: &Row<'static>| -> Result<bool, EngineError> {
236 if let Some(w) = &stmt.where_ {
237 let cond = eval::eval_expr(w, row, &ctx)?;
238 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
239 return Ok(false);
240 }
241 }
242 Ok(true)
243 };
244 // v7.37.15 Phase B — scan_visible filters rows by the
245 // engine's current snapshot. Phase B's `current_snapshot()`
246 // returns `Snapshot::unbounded()` so every row is visible,
247 // matching pre-v7.37.15 byte-for-byte. Phase C will wire
248 // real per-tx snapshots through this same callsite — no
249 // code change needed here when that lands.
250 let snap = self.current_snapshot();
251 if table.has_cold_rows_fast() {
252 // v7.36 (cold-tier coverage) — a cold segment's rows
253 // are produced on demand and live in a temporary this
254 // walk cannot borrow from, so a table carrying any owns
255 // its rows. Hot iter then cold iter, both through the
256 // same WHERE, as before.
257 let mut owned: Vec<Row<'static>> = Vec::new();
258 for (i, row) in table.scan_visible(&snap) {
259 if i.is_multiple_of(256) {
260 cancel.check()?;
261 }
262 if passes(row)? {
263 owned.push(row.clone());
264 }
265 }
266 let hot_len = table.row_count();
267 for (offset, row) in self.iter_cold_rows_of_table(table).iter().enumerate() {
268 let i = hot_len + offset;
269 if i.is_multiple_of(256) {
270 cancel.check()?;
271 }
272 if passes(row)? {
273 owned.push(row.clone());
274 }
275 }
276 owned_rows = owned;
277 rows_are_owned = true;
278 } else {
279 // v7.39 (round 975) — ask the indices first, the way
280 // the streaming walk has since round 970. This walk had
281 // the same hole and it is reached by any statement
282 // carrying a window function, so a WHERE that names an
283 // indexed column read the whole table: measured on 400k
284 // rows, `row_number() OVER () … WHERE id = 500` — a
285 // ONE-row answer on a primary key — took 13.762 ms
286 // against PG18.4's 0.151, while the same predicate
287 // without the window took 0.091. The cost was
288 // independent of how many rows survived (999 survivors
289 // cost 13.312 ms) and of row width (13.312 narrow vs
290 // 13.327 wide), which is what a full table walk looks
291 // like and what a result-shaped cost does not.
292 //
293 // The seek only NARROWS — `passes` still applies the
294 // whole WHERE — so no answer can change. Positions
295 // arrive visibility-filtered by the same predicate the
296 // scan applies and capped at a quarter of the table,
297 // and `None` walks the table exactly as before.
298 let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
299 crate::index_access::try_index_seek_positions(
300 w,
301 &schema_cols_owned,
302 table,
303 alias,
304 &snap,
305 self.speaks_mysql,
306 )
307 });
308 match seek_positions {
309 Some(mut positions) => {
310 // Table order, which is the order the scan
311 // would have produced.
312 positions.sort_unstable();
313 for (n, pos) in positions.into_iter().enumerate() {
314 if n.is_multiple_of(256) {
315 cancel.check()?;
316 }
317 let Some(row) = table.rows().get(pos) else {
318 continue;
319 };
320 if passes(row)? {
321 filtered.push(row);
322 }
323 }
324 }
325 None => {
326 for (i, row) in table.scan_visible(&snap) {
327 if i.is_multiple_of(256) {
328 cancel.check()?;
329 }
330 if passes(row)? {
331 filtered.push(row);
332 }
333 }
334 }
335 }
336 }
337 }
338 } else {
339 let deferred = self.build_joined_filtered_rows(
340 from,
341 stmt.where_.as_ref(),
342 cancel,
343 None,
344 &mut ByteBudget::new(self.max_query_bytes),
345 )?;
346 // A join's survivors are row-index tuples over its sources, so
347 // there is no single row to borrow — this branch owns them.
348 owned_rows = deferred.materialise();
349 rows_are_owned = true;
350 schema_cols_owned = deferred.combined_schema;
351 alias_opt = None;
352 }
353 if rows_are_owned {
354 filtered = owned_rows.iter().collect();
355 }
356 let schema_cols = &schema_cols_owned;
357 let ctx = self.ev_ctx(schema_cols, alias_opt);
358 let alias = alias_opt.unwrap_or("");
359 let n_rows = filtered.len();
360 // The window pipeline reads `&[&Row<'static>]`, and `filtered`
361 // already is one whichever branch produced it — the separate
362 // `filtered_refs` this used to build was the collect that made
363 // owning the rows look necessary.
364
365 // 2) Collect unique window function nodes from projection.
366 let mut window_nodes: Vec<Expr> = Vec::new();
367 for item in &stmt.items {
368 if let SelectItem::Expr { expr, .. } = item {
369 collect_window_nodes(expr, &mut window_nodes);
370 }
371 }
372 // v7.39 (round 592) — and from ORDER BY, which may name a window the
373 // select list never mentions. The order-key builder below rewrites
374 // window calls to `__win_N` columns, and a call that was never
375 // collected has no column to become.
376 for o in &stmt.order_by {
377 collect_window_nodes(&o.expr, &mut window_nodes);
378 }
379
380 // 3) For each window, compute per-row value.
381 // Index: same order as window_nodes; for row i, win_vals[w][i].
382 let mut win_vals: Vec<Vec<Value<'static>>> = Vec::with_capacity(window_nodes.len());
383 for wnode in &window_nodes {
384 let Expr::WindowFunction {
385 name,
386 args,
387 partition_by,
388 order_by,
389 frame,
390 null_treatment,
391 filter,
392 } = wnode
393 else {
394 unreachable!("collect_window_nodes pushes only WindowFunction");
395 };
396 // Compute (partition_key, order_key, original_index) for each row.
397 // v7.39 (round 593) — a key that is a plain column sits at the same
398 // position in every row, but was resolved BY NAME for each one. A
399 // per-library profile of `lag(id) OVER (ORDER BY id)` put
400 // `resolve_column` at 5.8% of the query on its own, with
401 // `rehydrate_cell` and the `eval_expr` dispatch behind it. Resolve
402 // once; anything that is not a plain column keeps the resolver.
403 let p_bound: Vec<Option<usize>> = partition_by
404 .iter()
405 .map(|e| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
406 .collect();
407 let o_bound: Vec<Option<usize>> = order_by
408 .iter()
409 .map(|(e, _, _)| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
410 .collect();
411 let arg_bound = args
412 .first()
413 .and_then(|a| crate::orderby::bound_column_position(a, schema_cols, alias_opt));
414 // v7.39 (round 690) — a window's ORDER BY over a column that
415 // declares a collation sorts by it, the same as a top-level
416 // ORDER BY. Resolved from the bound position, so only a bare
417 // column gets one; an expression produces a new value and the
418 // derivation that would give IT a collation is unbuilt.
419 let o_colls: Vec<Option<alloc::string::String>> = o_bound
420 .iter()
421 .map(|p| {
422 p.and_then(|pos| schema_cols.get(pos))
423 .and_then(|sc| sc.collation_name.clone())
424 .filter(|n| crate::collate::is_supported(n))
425 })
426 .collect();
427 let mut indexed: Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)> =
428 Vec::with_capacity(n_rows);
429 // v7.39 (round 731) — single bound INT partition key, no window
430 // ORDER BY: group on the i64 directly. The generic build paid
431 // two heap Vecs per row (pkey + empty okey) plus a canonical
432 // string encode per row just to bucket 500k rows into 100
433 // groups; the whole per-row key apparatus disappears here.
434 // Neither key Vec is read downstream on this path: the hash
435 // grouping replaces partition_key_cmp, and okey is empty by
436 // construction.
437 let int_pkey_fast = order_by.is_empty()
438 && partition_by.len() == 1
439 && p_bound[0].is_some_and(|pos| {
440 matches!(
441 schema_cols.get(pos).map(|c| c.ty),
442 Some(
443 spg_storage::DataType::Int
444 | spg_storage::DataType::BigInt
445 | spg_storage::DataType::SmallInt
446 )
447 )
448 });
449 // v7.39 (round 979) — the same idea for a single bound INT
450 // window ORDER BY: sort on the i64 instead of on a heap vector
451 // per row.
452 //
453 // Measured at 400k rows (round 978, ablation, answer checked
454 // byte-for-byte against the general path on a key column that
455 // is a permutation): `row_number() OVER (ORDER BY k)` went
456 // 157.057-157.868 ms to 31.253-31.679, which is 79.8% and puts
457 // it on top of the `OVER ()` baseline — the sort essentially
458 // disappears. Round 977 had already shown the cost was
459 // key-shaped rather than row-shaped: the sort's share was
460 // 132.0 ms on a three-integer table and 132.5 with a 200-byte
461 // column added, and a per-row COPY does scale with width
462 // (round 976 measured that at +36 ns/row/200 bytes).
463 //
464 // Gated to ROW_NUMBER, which is the one function that reads
465 // neither key vector — it numbers the order it is handed.
466 // `rank` and `dense_rank` compare adjacent entries' order keys
467 // in `compute_window_partition`, so leaving those vectors
468 // empty would silently give every row rank 1. A wider version
469 // would carry the i64 in the entry and teach those two to use
470 // it; this one is the part that can be shown correct by
471 // construction.
472 let int_okey_fast = partition_by.is_empty()
473 && order_by.len() == 1
474 && frame.is_none()
475 && filter.is_none()
476 && matches!(null_treatment, spg_sql::ast::NullTreatment::Respect)
477 && name.eq_ignore_ascii_case("row_number")
478 && o_bound[0].is_some_and(|pos| {
479 matches!(
480 schema_cols.get(pos).map(|c| c.ty),
481 Some(
482 spg_storage::DataType::Int
483 | spg_storage::DataType::BigInt
484 | spg_storage::DataType::SmallInt
485 )
486 )
487 });
488 // Set when a cell in that column turns out not to be an
489 // integer after all. The declared type says it should be, but
490 // "should" is not a thing to sort 400k rows on, so the general
491 // path takes over and this build is discarded.
492 let mut int_okey_bailed = false;
493 if int_okey_fast {
494 let pos = o_bound[0].expect("gated bound");
495 let desc = order_by[0].1;
496 // PG orders NULLs last ascending and first descending
497 // unless the query says otherwise.
498 let nulls_first = order_by[0].2.unwrap_or(desc);
499 let mut keyed: Vec<(bool, i64, usize)> = Vec::with_capacity(n_rows);
500 for (i, row) in filtered.iter().enumerate() {
501 match row.values.get(pos) {
502 Some(Value::Int(n)) => keyed.push((false, i64::from(*n), i)),
503 Some(Value::BigInt(n)) => keyed.push((false, *n, i)),
504 Some(Value::SmallInt(n)) => keyed.push((false, i64::from(*n), i)),
505 Some(Value::Null) | None => keyed.push((true, 0, i)),
506 Some(_) => {
507 int_okey_bailed = true;
508 break;
509 }
510 }
511 }
512 if !int_okey_bailed {
513 // `null_rank` puts NULLs on the side the query asked
514 // for; the row's original index breaks every tie, so
515 // equal keys keep the order the scan produced — what
516 // the stable sort below would have given them.
517 let null_rank = |is_null: bool| -> u8 { u8::from(is_null != nulls_first) };
518 keyed.sort_unstable_by(|a, b| {
519 null_rank(a.0)
520 .cmp(&null_rank(b.0))
521 .then_with(|| {
522 if a.0 {
523 core::cmp::Ordering::Equal
524 } else if desc {
525 b.1.cmp(&a.1)
526 } else {
527 a.1.cmp(&b.1)
528 }
529 })
530 .then_with(|| a.2.cmp(&b.2))
531 });
532 for (_, _, i) in keyed {
533 indexed.push((Vec::new(), Vec::new(), i));
534 }
535 } else {
536 indexed.clear();
537 }
538 }
539 if int_okey_fast && !int_okey_bailed {
540 // Ordered above; nothing else to build.
541 } else if int_pkey_fast {
542 let pos = p_bound[0].expect("gated bound");
543 let mut slot: hashbrown::HashMap<Option<i64>, usize> = hashbrown::HashMap::new();
544 let mut groups: Vec<Vec<usize>> = Vec::new();
545 for (i, row) in filtered.iter().enumerate() {
546 let k: Option<i64> = match row.values.get(pos) {
547 Some(Value::BigInt(n)) => Some(*n),
548 Some(Value::Int(n)) => Some(i64::from(*n)),
549 Some(Value::SmallInt(n)) => Some(i64::from(*n)),
550 _ => None,
551 };
552 match slot.get(&k) {
553 Some(&gi) => groups[gi].push(i),
554 None => {
555 slot.insert(k, groups.len());
556 groups.push(alloc::vec![i]);
557 }
558 }
559 }
560 // The downstream partition-boundary scan compares pkeys
561 // of ADJACENT entries, so the key must ride along — one
562 // single-element Vec per row (half the generic build's
563 // allocations, no string encode).
564 for g in groups {
565 for i in g {
566 let k: Value<'static> = match filtered[i].values.get(pos) {
567 Some(v) => v.clone(),
568 None => Value::Null,
569 };
570 indexed.push((alloc::vec![k], Vec::new(), i));
571 }
572 }
573 } else {
574 for (i, row) in filtered.iter().enumerate() {
575 let pkey: Vec<Value<'static>> = partition_by
576 .iter()
577 .enumerate()
578 .map(
579 |(k, p)| match p_bound[k].and_then(|pos| row.values.get(pos)) {
580 Some(v) => Ok(v.clone()),
581 None => eval::eval_expr(p, row, &ctx),
582 },
583 )
584 .collect::<Result<_, _>>()?;
585 // v7.39 (read01 round 54) — a window's ORDER BY over an enum
586 // column must sort by MEMBER order (enumsortorder), not the
587 // label's text. Enum values are Text at runtime, so the raw
588 // value key sorted alphabetically — `row_number() OVER (ORDER
589 // BY mood)` numbered the rows happy,ok,sad. Substitute the
590 // member ordinal, the same key the top-level ORDER BY uses.
591 // (Closes the enum-order knife's recorded window residual.)
592 let okey: Vec<(Value, bool, Option<bool>)> = order_by
593 .iter()
594 .enumerate()
595 .map(|(k, (e, desc, nf))| -> Result<_, EngineError> {
596 let v = match o_bound[k].and_then(|pos| row.values.get(pos)) {
597 Some(v) => v.clone(),
598 None => eval::eval_expr(e, row, &ctx)?,
599 };
600 let v = match crate::orderby::enum_order_ordinal(e, &v, &ctx) {
601 Some(ord) => Value::Float(ord),
602 None => v,
603 };
604 Ok((v, *desc, *nf))
605 })
606 .collect::<Result<_, _>>()?;
607 indexed.push((pkey, okey, i));
608 }
609 }
610 // Sort by (partition_key, order_key). Partition key uses
611 // a stable encoded form; order key respects ASC/DESC.
612 // v7.39 (round 731) — with NO window ORDER BY the sort's only
613 // job was putting same-partition rows next to each other, and a
614 // 500k-row comparison sort is a spectacular way to hash-group:
615 // the panel's `sum(id) OVER (PARTITION BY g)` spent ~100 ms
616 // here. Group by encoded key instead, preserving row order
617 // inside each group — exactly what the stable sort preserved,
618 // so every function (row_number included) answers the same.
619 if int_okey_fast && !int_okey_bailed {
620 // Already ordered by the i64 key above.
621 } else if int_pkey_fast {
622 // Already grouped above; same-partition rows are adjacent
623 // in original row order.
624 } else if order_by.is_empty() && !partition_by.is_empty() {
625 let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
626 let mut groups: Vec<
627 Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)>,
628 > = Vec::new();
629 let mut keybuf = String::new();
630 for entry in indexed.drain(..) {
631 keybuf.clear();
632 for v in &entry.0 {
633 crate::aggregate::push_canonical_key(&mut keybuf, v);
634 }
635 match slot.get(keybuf.as_str()) {
636 Some(&gi) => groups[gi].push(entry),
637 None => {
638 slot.insert(keybuf.clone(), groups.len());
639 groups.push(alloc::vec![entry]);
640 }
641 }
642 }
643 for g in groups {
644 indexed.extend(g);
645 }
646 } else {
647 indexed.sort_by(|a, b| {
648 let p_cmp = partition_key_cmp(&a.0, &b.0);
649 if p_cmp != core::cmp::Ordering::Equal {
650 return p_cmp;
651 }
652 crate::window::order_key_cmp_in(&a.1, &b.1, &o_colls)
653 });
654 }
655 // Per-partition compute.
656 let mut out_vals: Vec<Value<'static>> = alloc::vec![Value::Null; n_rows];
657 let mut p_start = 0;
658 while p_start < indexed.len() {
659 let mut p_end = p_start + 1;
660 while p_end < indexed.len()
661 && partition_key_cmp(&indexed[p_start].0, &indexed[p_end].0)
662 == core::cmp::Ordering::Equal
663 {
664 p_end += 1;
665 }
666 // Compute the function within this partition slice.
667 compute_window_partition(
668 name,
669 args,
670 arg_bound,
671 !order_by.is_empty(),
672 frame.as_ref(),
673 *null_treatment,
674 filter.as_deref(),
675 &indexed[p_start..p_end],
676 &filtered,
677 &ctx,
678 &mut out_vals,
679 )?;
680 p_start = p_end;
681 }
682 win_vals.push(out_vals);
683 }
684
685 // 4) Build extended schema: original columns + synthetic.
686 let mut ext_cols = schema_cols.clone();
687 for i in 0..window_nodes.len() {
688 ext_cols.push(ColumnSchema::new(
689 alloc::format!("__win_{i}"),
690 DataType::Text, // type doesn't matter for projection eval
691 true,
692 ));
693 }
694 // 6) Rewrite the projection: WindowFunction nodes → Column(__win_N).
695 let mut rewritten_items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
696 for item in &stmt.items {
697 let new_item = match item {
698 SelectItem::Wildcard => SelectItem::Wildcard,
699 SelectItem::QualifiedWildcard(q) => SelectItem::QualifiedWildcard(q.clone()),
700 SelectItem::Expr { expr, alias } => {
701 let mut e = expr.clone();
702 rewrite_window_to_columns(&mut e, &window_nodes);
703 // The rewrite swaps the window call for a synthetic
704 // `__win_N` column, and the projection then reported
705 // THAT as the column name — `SELECT count(*) OVER ()`
706 // answered `__win_0`, an internal name, where PG18
707 // answers `count`. Pin the name while the call the
708 // column is named for is still in hand.
709 let alias = if alias.is_none() && e != *expr {
710 Some(default_output_name(expr, self.speaks_mysql))
711 } else {
712 alias.clone()
713 };
714 SelectItem::Expr { expr: e, alias }
715 }
716 };
717 rewritten_items.push(new_item);
718 }
719
720 // 7) Project into final rows. JOIN case uses None so the
721 // qualifier check in `resolve_column` falls through to the
722 // composite `alias.col` schema lookup; single-table case
723 // keeps the bare alias so `bare_col` resolution still
724 // works for the projection's per-row column references.
725 // v7.39 (read01 round 54) — build through `ev_ctx`, the canonical
726 // constructor: it threads the catalog (plus render style / tz / GUCs)
727 // that a bare `EvalContext::new` drops. Without the catalog the OUTER
728 // `ORDER BY <enum col>` of a windowed query sorted by TEXT — the
729 // window values were right, the row order silently was not.
730 let ext_ctx = self.ev_ctx(&ext_cols, alias_opt);
731 let projection = build_projection_hiding_tail(
732 &rewritten_items,
733 &ext_cols,
734 alias,
735 self.speaks_mysql,
736 window_nodes.len(),
737 Some(self.active_catalog()),
738 )?;
739 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(n_rows);
740 // v7.39 (round 592) — the extended row (input columns plus the window
741 // values) used to be materialised for EVERY input row and kept until
742 // the projection had run: the input values cloned into a fresh Vec,
743 // then grown once to take the window columns. A counting allocator put
744 // the window path at 4 allocations a row where a plain derived table
745 // takes 1, and named all four — the input row, the clone, the growth,
746 // and the projected row. Only the last has to exist afterwards, so the
747 // extended row is one buffer refilled per row.
748 let mut ext_row: Row<'static> =
749 Row::new(Vec::with_capacity(schema_cols.len() + window_nodes.len()));
750 for i in 0..n_rows {
751 if i.is_multiple_of(256) {
752 cancel.check()?;
753 }
754 ext_row.values.clear();
755 ext_row.values.extend(filtered[i].values.iter().cloned());
756 for w in 0..window_nodes.len() {
757 ext_row.values.push(win_vals[w][i].clone());
758 }
759 let row = &ext_row;
760 let mut values = Vec::with_capacity(projection.len());
761 for p in &projection {
762 values.push(eval::eval_expr(&p.expr, row, &ext_ctx)?);
763 }
764 let order_keys = if stmt.order_by.is_empty() {
765 Vec::new()
766 } else {
767 let mut keys = Vec::with_capacity(stmt.order_by.len());
768 for o in &stmt.order_by {
769 let mut e = o.expr.clone();
770 rewrite_window_to_columns(&mut e, &window_nodes);
771 let key = eval::eval_expr(&e, row, &ext_ctx)?;
772 // v7.39 (read01 round 54) — this path builds its order keys
773 // itself instead of going through `build_order_keys`, so it
774 // skipped the enum-ordinal substitution: the OUTER
775 // `ORDER BY <enum col>` of a windowed query sorted by the
776 // label's TEXT, not by member order. The window values were
777 // right and only the row order was wrong — silently.
778 match crate::orderby::enum_order_ordinal(&e, &key, &ext_ctx) {
779 Some(ord) => keys.push(value_to_order_key(&Value::Float(ord))?),
780 None => keys.push(value_to_order_key(&key)?),
781 }
782 }
783 keys
784 };
785 tagged.push((order_keys, Row::new(values)));
786 }
787 // ORDER BY + LIMIT/OFFSET on the projected rows.
788 if !stmt.order_by.is_empty() {
789 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
790 sort_by_keys(&mut tagged, &descs);
791 }
792 let mut out_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
793 // v7.37 D.41 — `SELECT DISTINCT` over a window projection: the window
794 // pipeline builds one output row per input row, so DISTINCT must dedup the
795 // projected rows (PG evaluates window functions before DISTINCT). Applied
796 // after ORDER BY (duplicate rows share sort keys, so order is preserved)
797 // and before LIMIT.
798 if stmt.distinct {
799 // v7.38.14 — see the synthetic-source sites below: the mask was
800 // always available here, from the same projection this function
801 // already built.
802 out_rows = dedup_rows(
803 out_rows,
804 FoldSpec::of_masks(
805 self.speaks_mysql,
806 &fold_mask(&projection),
807 &pad_mask(&projection),
808 ),
809 );
810 }
811 apply_offset_and_limit(&mut out_rows, stmt.offset_literal(), stmt.limit_literal());
812 let final_cols: Vec<ColumnSchema> = projection
813 .into_iter()
814 .map(|p| p.to_column_schema())
815 .collect();
816 Ok(QueryResult::Rows {
817 columns: final_cols,
818 rows: out_rows,
819 })
820 }
821
822 /// v4.11: materialise each CTE into a temp table inside a
823 /// cloned catalog, then run the body SELECT against a fresh
824 /// engine instance that owns the enriched catalog. The clone
825 /// is moderately expensive — only paid by CTE-bearing queries.
826 /// Subqueries inside CTE bodies / the main body resolve as
827 /// usual; `clock_fn` is propagated so `NOW()` lines up.
828 /// v7.16.2 — mailrs round-10 A.3. Materialise the
829 /// `information_schema.*` / `pg_catalog.*` virtual views
830 /// the SELECT references, then re-execute the SELECT
831 /// against an enriched catalog where those views are real
832 /// tables. Same pattern as `exec_with_ctes`. The temp
833 /// engine carries `meta_views_materialised = true` so its
834 /// own meta-dispatch short-circuits — without that we'd
835 /// infinite-recurse since the temp catalog's view name
836 /// still starts with `__spg_info_` and re-triggers the
837 /// check.
838 pub(crate) fn exec_select_with_meta_views(
839 &self,
840 stmt: &SelectStatement,
841 cancel: CancelToken<'_>,
842 ) -> Result<QueryResult, EngineError> {
843 let catalog = self.meta_view_catalog(stmt)?;
844 let mut temp = Engine::restore(catalog);
845 if let Some(c) = self.clock {
846 temp = temp.with_clock(c);
847 }
848 if let Some(f) = self.salt_fn {
849 temp = temp.with_salt_fn(f);
850 }
851 // v7.39 (round 522) — the temp engine holds the materialised
852 // catalog and, until now, nothing of the SESSION. So every
853 // session-scoped answer changed the moment a system view
854 // appeared in the FROM clause: `SELECT current_user` said
855 // `unmei` and `SELECT current_user FROM pg_class` said `admin`;
856 // `current_setting('work_mem')` fell back to the boot default
857 // after a SET; `application_name` read empty. A privilege check
858 // written against a catalog join was reading a different
859 // identity than the same check written without one.
860 //
861 // Carry what a session can be observed through — its parameters
862 // (which is also where the session user lives), the role store
863 // the privilege builtins read, the dialect, and the rendering
864 // settings a timestamp is spelled with.
865 temp.session_params.clone_from(&self.session_params);
866 temp.users.clone_from(&self.users);
867 temp.backslash_escapes = self.backslash_escapes;
868 temp.speaks_mysql = self.speaks_mysql;
869 temp.mysql_strict = self.mysql_strict;
870 temp.render_style = self.render_style;
871 temp.tz_offset_fn = self.tz_offset_fn;
872 temp.tz_localize_fn = self.tz_localize_fn;
873 temp.tz_abbrev_fn = self.tz_abbrev_fn;
874 temp.meta_views_materialised = true;
875 temp.exec_select_cancel(stmt, cancel)
876 }
877
878 /// v7.39 (round 462) — the catalog a meta-view SELECT resolves
879 /// against: this engine's catalog with every `__spg_*` view the
880 /// statement references materialised into it.
881 ///
882 /// Split out of `exec_select_with_meta_views` so Describe can reach
883 /// the same shapes execution reaches. Describe used to look the FROM
884 /// relation up in the plain catalog, where a system view does not
885 /// exist, and reported "no columns" for every one of them — so an
886 /// extended-protocol client reading `pg_stat_user_tables` got rows
887 /// with no column metadata. Sharing the materialisation means a
888 /// view added here is described correctly the day it is added.
889 pub(crate) fn meta_view_catalog(&self, stmt: &SelectStatement) -> Result<Catalog, EngineError> {
890 let mut needed: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
891 collect_meta_view_names(stmt, &mut needed);
892 let mut catalog = self.active_catalog().clone();
893 for view in &needed {
894 if catalog.get(view).is_some() {
895 continue;
896 }
897 match view.as_str() {
898 "__spg_info_columns" => {
899 let (schema, rows) = synth_information_schema_columns(
900 self.active_catalog(),
901 self.speaks_mysql,
902 &self.mysql_schema_name(),
903 );
904 materialise_meta_view(&mut catalog, view, schema, rows)?;
905 }
906 "__spg_info_tables" => {
907 let (schema, rows) = synth_information_schema_tables(
908 self.active_catalog(),
909 self.speaks_mysql,
910 &self.mysql_schema_name(),
911 );
912 materialise_meta_view(&mut catalog, view, schema, rows)?;
913 }
914 "__spg_pg_class" => {
915 let (schema, rows) = synth_pg_class(
916 self.active_catalog(),
917 i64::try_from(self.vacuum_oldest_active()).unwrap_or(i64::MAX),
918 );
919 materialise_meta_view(&mut catalog, view, schema, rows)?;
920 }
921 "__spg_pg_attribute" => {
922 let (schema, rows) = synth_pg_attribute(self.active_catalog());
923 materialise_meta_view(&mut catalog, view, schema, rows)?;
924 }
925 // v7.17.0 Phase 3.P0-50 — pg_catalog.pg_type for
926 // sqlx / SQLAlchemy / Diesel / pgAdmin lookups.
927 "__spg_pg_type" => {
928 let (schema, rows) = synth_pg_type(self.active_catalog());
929 materialise_meta_view(&mut catalog, view, schema, rows)?;
930 }
931 // v7.39 (round 621) — pg_catalog.pg_operator, which did not
932 // exist at all.
933 "__spg_pg_operator" => {
934 let (schema, rows) = synth_pg_operator(self.active_catalog());
935 materialise_meta_view(&mut catalog, view, schema, rows)?;
936 }
937 // v7.17.0 Phase 3.P0-51 — pg_catalog.pg_proc for
938 // function-name introspection (ORM / pgAdmin).
939 "__spg_pg_proc" => {
940 let (schema, rows) = synth_pg_proc(self.active_catalog());
941 materialise_meta_view(&mut catalog, view, schema, rows)?;
942 }
943 // v7.24 (round-16 D) — pg_catalog.pg_trigger. The
944 // round-16 "why doesn't prod fire the trigger"
945 // question was unanswerable because triggers had NO
946 // introspection surface; tgname/tgenabled plus the
947 // pragmatic relname/timing/events/function columns
948 // make "is it registered and enabled" a one-liner.
949 "__spg_pg_trigger" => {
950 let (schema, rows) = synth_pg_trigger(self.active_catalog());
951 materialise_meta_view(&mut catalog, view, schema, rows)?;
952 }
953 // v7.17.0 Phase 3.P0-52 — pg_catalog.pg_namespace
954 // (schema list for admin tools' tree views).
955 "__spg_pg_namespace" => {
956 let (schema, rows) = synth_pg_namespace(self.active_catalog());
957 materialise_meta_view(&mut catalog, view, schema, rows)?;
958 }
959 // v7.39 — pg_tables convenience view (was a pgwire
960 // canned response that ignored projections).
961 "__spg_pg_tables" => {
962 let (schema, rows) =
963 crate::system_catalog::synth_pg_tables(self.active_catalog());
964 materialise_meta_view(&mut catalog, view, schema, rows)?;
965 }
966 // v7.37.24 (24.1) — pg_catalog.pg_enum (label list
967 // for ENUM types; sqlx / ORM enum codecs read this).
968 "__spg_pg_enum" => {
969 let (schema, rows) =
970 crate::system_catalog::synth_pg_enum(self.active_catalog());
971 materialise_meta_view(&mut catalog, view, schema, rows)?;
972 }
973 // v7.37.21 (21.13) — pg_catalog.pg_replication_slots
974 // (shape-stable empty until 21.12 persists slot state).
975 // v7.39 (round 277) — session-scoped prepared statements.
976 "__spg_pg_prepared_statements" => {
977 let (schema, rows) = crate::system_catalog::synth_pg_prepared_statements(
978 &self.prepared_statements,
979 );
980 materialise_meta_view(&mut catalog, view, schema, rows)?;
981 }
982 "__spg_pg_replication_slots" => {
983 let (schema, rows) =
984 crate::system_catalog::synth_pg_replication_slots(self.active_catalog());
985 materialise_meta_view(&mut catalog, view, schema, rows)?;
986 }
987 // v7.37.21 (21.13-b) — pg_catalog.pg_publication
988 // (one row per CREATE PUBLICATION).
989 "__spg_pg_publication" => {
990 let (schema, rows) = crate::system_catalog::synth_pg_publication(self);
991 materialise_meta_view(&mut catalog, view, schema, rows)?;
992 }
993 // v7.37.21 (21.13-c) — pg_catalog.pg_subscription
994 // (one row per CREATE SUBSCRIPTION; subconninfo
995 // redacted so dashboards can't leak credentials).
996 "__spg_pg_subscription" => {
997 let (schema, rows) = crate::system_catalog::synth_pg_subscription(self);
998 materialise_meta_view(&mut catalog, view, schema, rows)?;
999 }
1000 // v7.37.22 (22.x-stat-db) — pg_catalog.pg_stat_database
1001 // (one row for SPG's single database; counters are
1002 // shape-stable 0 until wiring lands).
1003 "__spg_pg_stat_database" => {
1004 let (schema, rows) = crate::system_catalog::synth_pg_stat_database(
1005 self,
1006 self.stat_tup_inserted,
1007 self.stat_tup_updated,
1008 self.stat_tup_deleted,
1009 );
1010 materialise_meta_view(&mut catalog, view, schema, rows)?;
1011 }
1012 // v7.37.22 (22.14) — pg_catalog.pg_stat_user_tables
1013 // (per-table churn counters; live_tup = row count).
1014 "__spg_pg_stat_user_tables" => {
1015 // r192 — DML counters come from the engine-side
1016 // non-transactional map, not the (tx-shadowed)
1017 // catalog tables.
1018 let (schema, rows) = crate::system_catalog::synth_pg_stat_user_tables(
1019 self.active_catalog(),
1020 &self.table_write_stats,
1021 );
1022 materialise_meta_view(&mut catalog, view, schema, rows)?;
1023 }
1024 // v7.37.22 (22.15) — pg_catalog.pg_stat_user_indexes
1025 // (per-index usage counters; flag unused indexes).
1026 "__spg_pg_stat_user_indexes" => {
1027 let (schema, rows) =
1028 crate::system_catalog::synth_pg_stat_user_indexes(self.active_catalog());
1029 materialise_meta_view(&mut catalog, view, schema, rows)?;
1030 }
1031 // v7.37.22 (22.16) — pg_catalog.pg_stat_bgwriter.
1032 "__spg_pg_stat_bgwriter" => {
1033 let (schema, rows) =
1034 crate::system_catalog::synth_pg_stat_bgwriter(self.active_catalog());
1035 materialise_meta_view(&mut catalog, view, schema, rows)?;
1036 }
1037 // v7.38 (read01 P3.14) — pg_catalog.pg_stat_checkpointer /
1038 // pg_stat_wal shell views (shape-stable, counters pending).
1039 "__spg_pg_stat_checkpointer" => {
1040 let (schema, rows) =
1041 crate::system_catalog::synth_pg_stat_checkpointer(self.active_catalog());
1042 materialise_meta_view(&mut catalog, view, schema, rows)?;
1043 }
1044 "__spg_pg_stat_wal" => {
1045 let (schema, rows) =
1046 crate::system_catalog::synth_pg_stat_wal(self.active_catalog());
1047 materialise_meta_view(&mut catalog, view, schema, rows)?;
1048 }
1049 // v7.38 (read01 P3.15) — pg_catalog.pg_stat_slru /
1050 // pg_stat_subscription_stats shell views.
1051 "__spg_pg_stat_slru" => {
1052 let (schema, rows) =
1053 crate::system_catalog::synth_pg_stat_slru(self.active_catalog());
1054 materialise_meta_view(&mut catalog, view, schema, rows)?;
1055 }
1056 "__spg_pg_stat_subscription_stats" => {
1057 let (schema, rows) = crate::system_catalog::synth_pg_stat_subscription_stats(
1058 self.active_catalog(),
1059 );
1060 materialise_meta_view(&mut catalog, view, schema, rows)?;
1061 }
1062 // v7.37.22 (22.17) — pg_catalog.pg_stat_archiver.
1063 "__spg_pg_stat_archiver" => {
1064 let (schema, rows) =
1065 crate::system_catalog::synth_pg_stat_archiver(self.active_catalog());
1066 materialise_meta_view(&mut catalog, view, schema, rows)?;
1067 }
1068 // v7.37.21 (21.13-d) — pg_catalog.pg_stat_replication.
1069 "__spg_pg_stat_replication" => {
1070 let (schema, rows) =
1071 crate::system_catalog::synth_pg_stat_replication(self.active_catalog());
1072 materialise_meta_view(&mut catalog, view, schema, rows)?;
1073 }
1074 // v7.37.24 (24.13) — pg_catalog.pg_am.
1075 "__spg_pg_am" => {
1076 let (schema, rows) = crate::system_catalog::synth_pg_am(self.active_catalog());
1077 materialise_meta_view(&mut catalog, view, schema, rows)?;
1078 }
1079 // v7.37.22 (22.18) — pg_catalog.pg_stat_io (PG 16+).
1080 "__spg_pg_stat_io" => {
1081 let (schema, rows) =
1082 crate::system_catalog::synth_pg_stat_io(self.active_catalog());
1083 materialise_meta_view(&mut catalog, view, schema, rows)?;
1084 }
1085 // v7.37.22 (22.19) — pg_catalog.pg_stat_user_functions.
1086 "__spg_pg_stat_user_functions" => {
1087 let (schema, rows) =
1088 crate::system_catalog::synth_pg_stat_user_functions(self.active_catalog());
1089 materialise_meta_view(&mut catalog, view, schema, rows)?;
1090 }
1091 // v7.39 (round 287) — pg_catalog.pg_largeobject{,_metadata}.
1092 "__spg_pg_largeobject" => {
1093 let (schema, rows) =
1094 crate::system_catalog::synth_pg_largeobject(self.active_catalog());
1095 materialise_meta_view(&mut catalog, view, schema, rows)?;
1096 }
1097 "__spg_pg_largeobject_metadata" => {
1098 let (schema, rows) =
1099 crate::system_catalog::synth_pg_largeobject_metadata(self.active_catalog());
1100 materialise_meta_view(&mut catalog, view, schema, rows)?;
1101 }
1102 // v7.37.23 (23.7-a) — pg_catalog.pg_statistic_ext.
1103 "__spg_pg_statistic_ext" => {
1104 let (schema, rows) =
1105 crate::system_catalog::synth_pg_statistic_ext(self.active_catalog());
1106 materialise_meta_view(&mut catalog, view, schema, rows)?;
1107 }
1108 // v7.38.18 — pg_catalog.pg_stats, the readable view.
1109 "__spg_pg_stats" => {
1110 let (schema, rows) = crate::system_catalog::synth_pg_stats(
1111 self.active_catalog(),
1112 &self.statistics,
1113 );
1114 materialise_meta_view(&mut catalog, view, schema, rows)?;
1115 }
1116 // v7.37.24 (24.15) — pg_catalog.pg_statistic.
1117 "__spg_pg_statistic" => {
1118 let (schema, rows) = crate::system_catalog::synth_pg_statistic(
1119 self.active_catalog(),
1120 &self.statistics,
1121 );
1122 materialise_meta_view(&mut catalog, view, schema, rows)?;
1123 }
1124 // v7.37.22 (22.20) — pg_catalog.pg_stat_progress_vacuum.
1125 "__spg_pg_stat_progress_vacuum" => {
1126 let (schema, rows) =
1127 crate::system_catalog::synth_pg_stat_progress_vacuum(self.active_catalog());
1128 materialise_meta_view(&mut catalog, view, schema, rows)?;
1129 }
1130 // v7.37.22 (22.21) — pg_catalog.pg_stat_progress_create_index.
1131 "__spg_pg_stat_progress_create_index" => {
1132 let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_create_index(
1133 self.active_catalog(),
1134 );
1135 materialise_meta_view(&mut catalog, view, schema, rows)?;
1136 }
1137 // v7.37.22 (22.22) — pg_catalog.pg_stat_progress_analyze.
1138 "__spg_pg_stat_progress_analyze" => {
1139 let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_analyze(
1140 self.active_catalog(),
1141 );
1142 materialise_meta_view(&mut catalog, view, schema, rows)?;
1143 }
1144 // v7.37.24 (24.16) — pg_catalog.pg_inherits
1145 // (partition parent → child OID mapping).
1146 "__spg_pg_inherits" => {
1147 let (schema, rows) =
1148 crate::system_catalog::synth_pg_inherits(self.active_catalog());
1149 materialise_meta_view(&mut catalog, view, schema, rows)?;
1150 }
1151 // v7.39 (round 650) — the text-search catalogs, filled
1152 // with what SPG actually has rather than PG's thirty.
1153 "__spg_pg_ts_config_map" => {
1154 let (schema, rows) =
1155 crate::system_catalog::synth_pg_ts_config_map(self.active_catalog());
1156 materialise_meta_view(&mut catalog, view, schema, rows)?;
1157 }
1158 "__spg_pg_ts_config" => {
1159 let (schema, rows) =
1160 crate::system_catalog::synth_pg_ts_config(self.active_catalog());
1161 materialise_meta_view(&mut catalog, view, schema, rows)?;
1162 }
1163 "__spg_pg_ts_dict" => {
1164 let (schema, rows) =
1165 crate::system_catalog::synth_pg_ts_dict(self.active_catalog());
1166 materialise_meta_view(&mut catalog, view, schema, rows)?;
1167 }
1168 "__spg_pg_ts_parser" => {
1169 let (schema, rows) =
1170 crate::system_catalog::synth_pg_ts_parser(self.active_catalog());
1171 materialise_meta_view(&mut catalog, view, schema, rows)?;
1172 }
1173 "__spg_pg_ts_template" => {
1174 let (schema, rows) =
1175 crate::system_catalog::synth_pg_ts_template(self.active_catalog());
1176 materialise_meta_view(&mut catalog, view, schema, rows)?;
1177 }
1178 // v7.37.24 (24.17) — pg_catalog.pg_depend
1179 // (dependency graph; shape-stable empty since
1180 // SPG's drop enforcement is per-kind, not per-object).
1181 "__spg_pg_depend" => {
1182 let (schema, rows) =
1183 crate::system_catalog::synth_pg_depend(self.active_catalog());
1184 materialise_meta_view(&mut catalog, view, schema, rows)?;
1185 }
1186 // 7.38.1 S5.1 — pg_catalog.pg_opclass (pg_dump wall #1).
1187 "__spg_pg_opclass" => {
1188 let (schema, rows) =
1189 crate::system_catalog::synth_pg_opclass(self.active_catalog());
1190 materialise_meta_view(&mut catalog, view, schema, rows)?;
1191 }
1192 "__spg_pg_opfamily" => {
1193 let (schema, rows) =
1194 crate::system_catalog::synth_pg_opfamily(self.active_catalog());
1195 materialise_meta_view(&mut catalog, view, schema, rows)?;
1196 }
1197 "__spg_pg_amop" => {
1198 let (schema, rows) =
1199 crate::system_catalog::synth_pg_amop(self.active_catalog());
1200 materialise_meta_view(&mut catalog, view, schema, rows)?;
1201 }
1202 "__spg_pg_amproc" => {
1203 let (schema, rows) =
1204 crate::system_catalog::synth_pg_amproc(self.active_catalog());
1205 materialise_meta_view(&mut catalog, view, schema, rows)?;
1206 }
1207 // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1208 // ORM reflection + pg_dump read the deparsed default text).
1209 "__spg_pg_attrdef" => {
1210 let (schema, rows) =
1211 crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1212 materialise_meta_view(&mut catalog, view, schema, rows)?;
1213 }
1214 // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1215 "__spg_pg_policy" => {
1216 let (schema, rows) =
1217 crate::system_catalog::synth_pg_policy(self.active_catalog());
1218 materialise_meta_view(&mut catalog, view, schema, rows)?;
1219 }
1220 "__spg_pg_policies" => {
1221 let (schema, rows) =
1222 crate::system_catalog::synth_pg_policies(self.active_catalog());
1223 materialise_meta_view(&mut catalog, view, schema, rows)?;
1224 }
1225 // v7.37.24 (24.14) — pg_catalog.pg_collation.
1226 "__spg_pg_collation" => {
1227 let (schema, rows) =
1228 crate::system_catalog::synth_pg_collation(self.active_catalog());
1229 materialise_meta_view(&mut catalog, view, schema, rows)?;
1230 }
1231 // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1232 "__spg_pg_tablespace" => {
1233 let (schema, rows) =
1234 crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1235 materialise_meta_view(&mut catalog, view, schema, rows)?;
1236 }
1237 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1238 // for pgAdmin / DataGrip "indexes per table" listings.
1239 "__spg_pg_indexes" => {
1240 let (schema, rows) = synth_pg_indexes(self.active_catalog());
1241 materialise_meta_view(&mut catalog, view, schema, rows)?;
1242 }
1243 // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1244 // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1245 "__spg_pg_description" => {
1246 let (schema, rows) =
1247 crate::system_catalog::synth_pg_description(self.active_catalog());
1248 materialise_meta_view(&mut catalog, view, schema, rows)?;
1249 }
1250 // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1251 // for index introspection by ORM compilers.
1252 "__spg_pg_index" => {
1253 let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1254 materialise_meta_view(&mut catalog, view, schema, rows)?;
1255 }
1256 // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1257 // for FK / UNIQUE / PK / CHECK introspection.
1258 "__spg_pg_constraint" => {
1259 let (schema, rows) = synth_pg_constraint(self.active_catalog());
1260 materialise_meta_view(&mut catalog, view, schema, rows)?;
1261 }
1262 // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1263 // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1264 "__spg_pg_sequence" => {
1265 let (schema, rows) = synth_pg_sequence(self.active_catalog());
1266 materialise_meta_view(&mut catalog, view, schema, rows)?;
1267 }
1268 // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1269 // pg_roles / pg_user. SPG is single-database so
1270 // pg_database surfaces just `postgres`; pg_roles
1271 // / pg_user walk the engine's UserStore.
1272 "__spg_pg_database" => {
1273 let (schema, rows) = synth_pg_database(self);
1274 materialise_meta_view(&mut catalog, view, schema, rows)?;
1275 }
1276 "__spg_pg_roles" => {
1277 let (schema, rows) = synth_pg_roles(self);
1278 materialise_meta_view(&mut catalog, view, schema, rows)?;
1279 }
1280 // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1281 // same roles, with PG's own `use*` column names. It used to
1282 // publish pg_roles' columns under this name.
1283 "__spg_pg_user" => {
1284 let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1285 materialise_meta_view(&mut catalog, view, schema, rows)?;
1286 }
1287 // v7.39 (read01 round 58) — role membership.
1288 "__spg_pg_auth_members" => {
1289 let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1290 materialise_meta_view(&mut catalog, view, schema, rows)?;
1291 }
1292 // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1293 // pg_views surfaces every CREATE VIEW result; SPG
1294 // ships one row per declared view from the catalog.
1295 "__spg_pg_views" => {
1296 let (schema, rows) = synth_pg_views(self.active_catalog());
1297 materialise_meta_view(&mut catalog, view, schema, rows)?;
1298 }
1299 // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1300 // catalogued query-rewrite RULE.
1301 "__spg_pg_rules" => {
1302 let (schema, rows) =
1303 crate::system_catalog::synth_pg_rules(self.active_catalog());
1304 materialise_meta_view(&mut catalog, view, schema, rows)?;
1305 }
1306 // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1307 // catalogue `pg_get_ruledef(oid)` resolves against.
1308 "__spg_pg_rewrite" => {
1309 let (schema, rows) =
1310 crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1311 materialise_meta_view(&mut catalog, view, schema, rows)?;
1312 }
1313 // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1314 // and PG's own column names.
1315 "__spg_pg_matviews" => {
1316 let (schema, rows) =
1317 crate::system_catalog::synth_pg_matviews(self.active_catalog());
1318 materialise_meta_view(&mut catalog, view, schema, rows)?;
1319 }
1320 // pg_catalog.pg_extension — native capability list
1321 // (mailrs embed round-12).
1322 // v7.39 (round 546) — the catalogs SPG has real content
1323 // for, from the facts it already holds.
1324 "__spg_pg_db_role_setting" => {
1325 let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1326 materialise_meta_view(&mut catalog, view, schema, rows)?;
1327 }
1328 "__spg_pg_language" => {
1329 let (schema, rows) = crate::system_catalog::synth_pg_language();
1330 materialise_meta_view(&mut catalog, view, schema, rows)?;
1331 }
1332 "__spg_pg_sequences" => {
1333 let (schema, rows) =
1334 crate::system_catalog::synth_pg_sequences(self.active_catalog());
1335 materialise_meta_view(&mut catalog, view, schema, rows)?;
1336 }
1337 "__spg_pg_range" => {
1338 let (schema, rows) = crate::system_catalog::synth_pg_range();
1339 materialise_meta_view(&mut catalog, view, schema, rows)?;
1340 }
1341 "__spg_pg_partitioned_table" => {
1342 let (schema, rows) =
1343 crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1344 materialise_meta_view(&mut catalog, view, schema, rows)?;
1345 }
1346 "__spg_pg_authid" => {
1347 let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1348 materialise_meta_view(&mut catalog, view, schema, rows)?;
1349 }
1350 "__spg_pg_group" => {
1351 let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1352 materialise_meta_view(&mut catalog, view, schema, rows)?;
1353 }
1354 "__spg_pg_shadow" => {
1355 let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1356 materialise_meta_view(&mut catalog, view, schema, rows)?;
1357 }
1358 // v7.39 (round 544) — pg_cast, probed from the real
1359 // cast implementation.
1360 "__spg_pg_cast" => {
1361 let (schema, rows) = crate::system_catalog::synth_pg_cast();
1362 materialise_meta_view(&mut catalog, view, schema, rows)?;
1363 }
1364 // v7.39 (round 541) — an empty catalog that exists.
1365 "__spg_pg_foreign_table" => {
1366 let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1367 materialise_meta_view(&mut catalog, view, schema, rows)?;
1368 }
1369 "__spg_pg_extension" => {
1370 let (schema, rows) = synth_pg_extension();
1371 materialise_meta_view(&mut catalog, view, schema, rows)?;
1372 }
1373 // v7.39 (round 502) — the timezone catalogues.
1374 "__spg_pg_timezone_names" => {
1375 let (schema, rows) = synth_pg_timezone_names(self);
1376 materialise_meta_view(&mut catalog, view, schema, rows)?;
1377 }
1378 "__spg_pg_timezone_abbrevs" => {
1379 let (schema, rows) = synth_pg_timezone_abbrevs(self);
1380 materialise_meta_view(&mut catalog, view, schema, rows)?;
1381 }
1382 // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1383 "__spg_pg_settings" => {
1384 let (schema, rows) = synth_pg_settings(self);
1385 materialise_meta_view(&mut catalog, view, schema, rows)?;
1386 }
1387 // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1388 // v7.39 (read01 round 51) — information_schema.role_table_grants
1389 // and .table_privileges. Both report the owner's seven implicit
1390 // table privileges; SPG's single role owns everything.
1391 // v7.39 (read01 round 59) — information_schema.column_privileges.
1392 "__spg_info_column_privileges" => {
1393 let (schema, rows) =
1394 crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1395 materialise_meta_view(&mut catalog, view, schema, rows)?;
1396 }
1397 "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1398 let grantee = self.current_role().to_string();
1399 let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1400 self.active_catalog(),
1401 &grantee,
1402 );
1403 materialise_meta_view(&mut catalog, view, schema, rows)?;
1404 }
1405 "__spg_info_key_column_usage" => {
1406 let (schema, rows) = synth_info_key_column_usage(self.active_catalog());
1407 materialise_meta_view(&mut catalog, view, schema, rows)?;
1408 }
1409 // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1410 "__spg_info_referential_constraints" => {
1411 let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1412 materialise_meta_view(&mut catalog, view, schema, rows)?;
1413 }
1414 // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1415 "__spg_info_statistics" => {
1416 let (schema, rows) = synth_info_statistics(self.active_catalog());
1417 materialise_meta_view(&mut catalog, view, schema, rows)?;
1418 }
1419 // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1420 "__spg_info_routines" => {
1421 let (schema, rows) = synth_info_routines();
1422 materialise_meta_view(&mut catalog, view, schema, rows)?;
1423 }
1424 // v7.37.24 (24.3) — information_schema.attributes.
1425 "__spg_info_attributes" => {
1426 let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1427 self.active_catalog(),
1428 );
1429 materialise_meta_view(&mut catalog, view, schema, rows)?;
1430 }
1431 // v7.37.24 (24.2) — information_schema.domains.
1432 "__spg_info_domains" => {
1433 let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1434 self.active_catalog(),
1435 );
1436 materialise_meta_view(&mut catalog, view, schema, rows)?;
1437 }
1438 // v7.37.24 (24.9) — information_schema.schemata.
1439 "__spg_info_schemata" => {
1440 let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1441 self.active_catalog(),
1442 self.speaks_mysql,
1443 &self.listed_database_names(),
1444 );
1445 materialise_meta_view(&mut catalog, view, schema, rows)?;
1446 }
1447 // v7.37.24 (24.9) — information_schema.views.
1448 "__spg_info_views" => {
1449 let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1450 self.active_catalog(),
1451 );
1452 materialise_meta_view(&mut catalog, view, schema, rows)?;
1453 }
1454 // v7.37.24 (24.9) — information_schema.table_constraints.
1455 "__spg_info_table_constraints" => {
1456 let (schema, rows) =
1457 crate::system_catalog::synth_information_schema_table_constraints(
1458 self.active_catalog(),
1459 );
1460 materialise_meta_view(&mut catalog, view, schema, rows)?;
1461 }
1462 // v7.37.17 — information_schema.constraint_column_usage.
1463 "__spg_info_constraint_column_usage" => {
1464 let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1465 self.active_catalog(),
1466 );
1467 materialise_meta_view(&mut catalog, view, schema, rows)?;
1468 }
1469 // v7.37.17 — information_schema.triggers.
1470 "__spg_info_triggers" => {
1471 let (schema, rows) =
1472 crate::system_catalog::synth_info_triggers(self.active_catalog());
1473 materialise_meta_view(&mut catalog, view, schema, rows)?;
1474 }
1475 // v7.37.17 — information_schema.check_constraints.
1476 "__spg_info_check_constraints" => {
1477 let (schema, rows) =
1478 crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1479 materialise_meta_view(&mut catalog, view, schema, rows)?;
1480 }
1481 // v7.37.17 — information_schema.sequences.
1482 "__spg_info_sequences" => {
1483 let (schema, rows) =
1484 crate::system_catalog::synth_info_sequences(self.active_catalog());
1485 materialise_meta_view(&mut catalog, view, schema, rows)?;
1486 }
1487 // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1488 "__spg_mysql_user" => {
1489 let (schema, rows) = synth_mysql_user(self);
1490 materialise_meta_view(&mut catalog, view, schema, rows)?;
1491 }
1492 "__spg_mysql_db" => {
1493 let (schema, rows) = synth_mysql_db();
1494 materialise_meta_view(&mut catalog, view, schema, rows)?;
1495 }
1496 // v7.39 (round 541) — the catalogs PG has that SPG is
1497 // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1498 other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1499 let (schema, rows) =
1500 crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1501 materialise_meta_view(&mut catalog, view, schema, rows)?;
1502 }
1503 _ => {
1504 return Err(EngineError::Unsupported(alloc::format!(
1505 "meta view {view:?} is not yet materialisable; \
1506 v7.16.2 covers information_schema.columns / .tables \
1507 and pg_catalog.pg_class / pg_attribute; \
1508 v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1509 pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1510 pg_user / pg_views / pg_matviews / pg_settings"
1511 )));
1512 }
1513 }
1514 }
1515 Ok(catalog)
1516 }
1517
1518 pub(crate) fn exec_with_ctes(
1519 &self,
1520 stmt: &SelectStatement,
1521 cancel: CancelToken<'_>,
1522 ) -> Result<QueryResult, EngineError> {
1523 cancel.check()?;
1524 // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1525 // bodies are supported here. Writable CTEs on a SELECT
1526 // outer require `&mut self` and route through the
1527 // top-level `exec_select_cancel_mut` entry; sentori
1528 // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1529 // INSERT, not a SELECT, so this restriction is harmless
1530 // in practice.
1531 if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1532 // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1533 // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1534 // of a statement, not nested inside a subquery; this path is
1535 // reached exactly when one is nested. The old text described SPG's
1536 // own executor plumbing ("the top-level mutable entry"), which
1537 // means nothing to a client.
1538 return Err(EngineError::Unsupported(
1539 "WITH clause containing a data-modifying statement must be at the top level".into(),
1540 ));
1541 }
1542 let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1543 // Strip CTEs from the body before running on the temp engine
1544 // so we don't recurse forever.
1545 let mut body = stmt.clone();
1546 body.ctes = Vec::new();
1547 let mut temp = Engine::restore(catalog);
1548 if let Some(c) = self.clock {
1549 temp = temp.with_clock(c);
1550 }
1551 if let Some(f) = self.salt_fn {
1552 temp = temp.with_salt_fn(f);
1553 }
1554 temp.exec_select_cancel(&body, cancel)
1555 }
1556
1557 /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1558 /// `&self` SELECT path. Caller guarantees no modifying CTE
1559 /// bodies are present.
1560 pub(crate) fn materialise_ctes_readonly(
1561 &self,
1562 ctes: &[spg_sql::ast::Cte],
1563 cancel: CancelToken<'_>,
1564 ) -> Result<crate::Catalog, EngineError> {
1565 cancel.check()?;
1566 let mut catalog = self.active_catalog().clone();
1567 for cte in ctes {
1568 let body_select = cte.body.as_select().ok_or_else(|| {
1569 EngineError::Unsupported(alloc::format!(
1570 "data-modifying CTE not supported on this SELECT entry"
1571 ))
1572 })?;
1573 // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1574 // (PG scoping: the WITH name wins for the outer query and later
1575 // CTEs, while THIS body still sees the real table — a
1576 // non-recursive body's self-name is the table, probe P2). This
1577 // materialiser works on a CLONE, so the shadow is simply: run
1578 // the body against the untouched clone, then drop the real
1579 // table from the clone before installing the CTE's temp. A
1580 // RECURSIVE self-reference is the CTE itself (P6), so there the
1581 // drop happens before the iterating materialiser runs.
1582 let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1583 let synthetic = spg_sql::ast::Cte {
1584 name: cte.name.clone(),
1585 body: spg_sql::ast::CteBody::Select(body_select.clone()),
1586 recursive: true,
1587 column_overrides: cte.column_overrides.clone(),
1588 search: None,
1589 cycle: None,
1590 };
1591 if catalog.get(&cte.name).is_some() {
1592 let _ = catalog.drop_table(&cte.name);
1593 }
1594 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1595 } else {
1596 let mut cte_engine = Engine::restore(catalog.clone());
1597 if let Some(c) = self.clock {
1598 cte_engine = cte_engine.with_clock(c);
1599 }
1600 if let Some(f) = self.salt_fn {
1601 cte_engine = cte_engine.with_salt_fn(f);
1602 }
1603 let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1604 let QueryResult::Rows { columns, rows } = body_result else {
1605 return Err(EngineError::Unsupported(alloc::format!(
1606 "CTE {:?} body did not return rows",
1607 cte.name
1608 )));
1609 };
1610 (columns, rows)
1611 };
1612 let inferred = infer_column_types(&columns, &rows);
1613 let mut columns = inferred;
1614 if !cte.column_overrides.is_empty() {
1615 if cte.column_overrides.len() != columns.len() {
1616 return Err(EngineError::Unsupported(alloc::format!(
1617 "CTE {:?} column list has {} names but body returns {} columns",
1618 cte.name,
1619 cte.column_overrides.len(),
1620 columns.len()
1621 )));
1622 }
1623 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1624 col.name.clone_from(name);
1625 }
1626 }
1627 let schema = TableSchema::new(cte.name.clone(), columns);
1628 // v7.39 (round 156) — the body ran against the untouched clone;
1629 // from here on the CTE name resolves to the temp (PG scoping).
1630 if catalog.get(&cte.name).is_some() {
1631 let _ = catalog.drop_table(&cte.name);
1632 }
1633 catalog.create_table(schema).map_err(EngineError::Storage)?;
1634 let table = catalog
1635 .get_mut(&cte.name)
1636 .expect("just-created CTE table must exist");
1637 for row in rows {
1638 table.insert(row).map_err(EngineError::Storage)?;
1639 }
1640 }
1641 Ok(catalog)
1642 }
1643
1644 /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1645 /// Retained for non-DML callers; the DML path (writable CTE on
1646 /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1647 /// `dml.rs` which installs the CTE temps directly on the
1648 /// active catalog so the outer statement's writes hit real
1649 /// tables.
1650 #[allow(dead_code)]
1651 pub(crate) fn materialise_ctes(
1652 &mut self,
1653 ctes: &[spg_sql::ast::Cte],
1654 cancel: CancelToken<'_>,
1655 ) -> Result<crate::Catalog, EngineError> {
1656 cancel.check()?;
1657 // v7.37.43-T4.4 — modifying CTEs need to write through the
1658 // SAME catalog as the outer statement, not a clone (PG's
1659 // writable CTE puts all modifications in one transaction).
1660 // For the read-only case the original logic cloned, but
1661 // since the outer statement also goes through the cloned
1662 // engine and ALL writes must converge, we now drive the
1663 // accumulator off `self.active_catalog().clone()` and
1664 // commit the modifying writes directly to `self`'s active
1665 // catalog so the surface is consistent.
1666 let mut catalog = self.active_catalog().clone();
1667 // v7.39 (round 149) — a modifying CTE body's target must be a
1668 // real relation, never a sibling CTE (PG: relation does not
1669 // exist); checked before any alias lands in the accumulator.
1670 for cte in ctes {
1671 let body_target = match &cte.body {
1672 spg_sql::ast::CteBody::Select(_) => None,
1673 spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1674 spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1675 spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1676 spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1677 };
1678 if let Some(t) = body_target
1679 && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1680 && catalog.get(t).is_none()
1681 {
1682 return Err(EngineError::Storage(
1683 spg_storage::StorageError::TableNotFound { name: t.into() },
1684 ));
1685 }
1686 }
1687 for cte in ctes {
1688 if catalog.get(&cte.name).is_some() {
1689 return Err(EngineError::Unsupported(alloc::format!(
1690 "CTE name {:?} shadows an existing table; rename the CTE",
1691 cte.name
1692 )));
1693 }
1694 let (columns, rows) = match &cte.body {
1695 // v7.39 (round 145) — see the sibling site: only a body that
1696 // truly self-references takes the iterating materialiser.
1697 spg_sql::ast::CteBody::Select(body)
1698 if cte.recursive && select_refers_to(body, &cte.name) =>
1699 {
1700 // Recursive CTE — the existing helper takes a
1701 // SELECT body and the snapshot catalog.
1702 let synthetic = spg_sql::ast::Cte {
1703 name: cte.name.clone(),
1704 body: spg_sql::ast::CteBody::Select(body.clone()),
1705 recursive: true,
1706 column_overrides: cte.column_overrides.clone(),
1707 search: None,
1708 cycle: None,
1709 };
1710 self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1711 }
1712 spg_sql::ast::CteBody::Select(body) => {
1713 // v7.25 (round-17) — run against the accumulated
1714 // catalog so later CTEs can reference earlier
1715 // ones in the same WITH clause.
1716 let mut cte_engine = Engine::restore(catalog.clone());
1717 if let Some(c) = self.clock {
1718 cte_engine = cte_engine.with_clock(c);
1719 }
1720 if let Some(f) = self.salt_fn {
1721 cte_engine = cte_engine.with_salt_fn(f);
1722 }
1723 let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1724 let QueryResult::Rows { columns, rows } = body_result else {
1725 return Err(EngineError::Unsupported(alloc::format!(
1726 "CTE {:?} body did not return rows",
1727 cte.name
1728 )));
1729 };
1730 (columns, rows)
1731 }
1732 spg_sql::ast::CteBody::Insert(body) => {
1733 self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1734 }
1735 spg_sql::ast::CteBody::Update(body) => {
1736 self.exec_modifying_cte_update(&cte.name, body, cancel)?
1737 }
1738 spg_sql::ast::CteBody::Delete(body) => {
1739 self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1740 }
1741 spg_sql::ast::CteBody::Merge(body) => {
1742 self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1743 }
1744 };
1745 // v4.22: the projection builder labels any non-column
1746 // expression as Text — including literal SELECT 1.
1747 // Promote each column's type to whatever the rows
1748 // actually carry so the CTE storage table accepts them.
1749 let inferred = infer_column_types(&columns, &rows);
1750 let mut columns = inferred;
1751 if !cte.column_overrides.is_empty() {
1752 if cte.column_overrides.len() != columns.len() {
1753 return Err(EngineError::Unsupported(alloc::format!(
1754 "CTE {:?} column list has {} names but body returns {} columns",
1755 cte.name,
1756 cte.column_overrides.len(),
1757 columns.len()
1758 )));
1759 }
1760 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1761 col.name.clone_from(name);
1762 }
1763 }
1764 let schema = TableSchema::new(cte.name.clone(), columns);
1765 catalog.create_table(schema).map_err(EngineError::Storage)?;
1766 let table = catalog
1767 .get_mut(&cte.name)
1768 .expect("just-created CTE table must exist");
1769 for row in rows {
1770 table.insert(row).map_err(EngineError::Storage)?;
1771 }
1772 }
1773 Ok(catalog)
1774 }
1775
1776 /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1777 /// against `self` (so the mutation lands in the active catalog
1778 /// inside the current transaction) and captures the RETURNING
1779 /// projection — column schema + rows — to materialise as the
1780 /// CTE alias's table. An INSERT without RETURNING produces a
1781 /// 0-row table with a synthetic single-column placeholder
1782 /// (matches PG: the CTE alias is still defined, but referencing
1783 /// it from the outer query without RETURNING raises a
1784 /// column-resolution error at scan time).
1785 fn exec_modifying_cte_insert(
1786 &mut self,
1787 cte_name: &str,
1788 body: &spg_sql::ast::InsertStatement,
1789 _cancel: CancelToken<'_>,
1790 ) -> Result<
1791 (
1792 Vec<spg_storage::ColumnSchema>,
1793 Vec<spg_storage::Row<'static>>,
1794 ),
1795 EngineError,
1796 > {
1797 // round 151 — a WITH-headed body keeps its own ctes; the body
1798 // statement routes through its writable-CTE entry (outer CTEs
1799 // are never copied into bodies, so no recursion risk).
1800 let body = body.clone();
1801 let result = self.exec_insert(body)?;
1802 match result {
1803 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1804 QueryResult::CommandOk { .. } => {
1805 // No RETURNING — emit a sentinel single-column
1806 // schema with zero rows so the alias is defined.
1807 let placeholder = spg_storage::ColumnSchema::new(
1808 alloc::format!("{cte_name}_returning_absent"),
1809 spg_storage::DataType::Text,
1810 true,
1811 );
1812 Ok((alloc::vec![placeholder], Vec::new()))
1813 }
1814 }
1815 }
1816
1817 /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1818 /// as INSERT above.
1819 fn exec_modifying_cte_update(
1820 &mut self,
1821 cte_name: &str,
1822 body: &spg_sql::ast::UpdateStatement,
1823 cancel: CancelToken<'_>,
1824 ) -> Result<
1825 (
1826 Vec<spg_storage::ColumnSchema>,
1827 Vec<spg_storage::Row<'static>>,
1828 ),
1829 EngineError,
1830 > {
1831 let body = body.clone();
1832 let result = self.exec_update_cancel(&body, cancel)?;
1833 match result {
1834 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1835 QueryResult::CommandOk { .. } => {
1836 let placeholder = spg_storage::ColumnSchema::new(
1837 alloc::format!("{cte_name}_returning_absent"),
1838 spg_storage::DataType::Text,
1839 true,
1840 );
1841 Ok((alloc::vec![placeholder], Vec::new()))
1842 }
1843 }
1844 }
1845
1846 /// v7.37.43-T4.4 — execute a DELETE CTE body.
1847 fn exec_modifying_cte_delete(
1848 &mut self,
1849 cte_name: &str,
1850 body: &spg_sql::ast::DeleteStatement,
1851 cancel: CancelToken<'_>,
1852 ) -> Result<
1853 (
1854 Vec<spg_storage::ColumnSchema>,
1855 Vec<spg_storage::Row<'static>>,
1856 ),
1857 EngineError,
1858 > {
1859 let body = body.clone();
1860 let result = self.exec_delete_cancel(&body, cancel)?;
1861 match result {
1862 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1863 QueryResult::CommandOk { .. } => {
1864 let placeholder = spg_storage::ColumnSchema::new(
1865 alloc::format!("{cte_name}_returning_absent"),
1866 spg_storage::DataType::Text,
1867 true,
1868 );
1869 Ok((alloc::vec![placeholder], Vec::new()))
1870 }
1871 }
1872 }
1873
1874 /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1875 fn exec_modifying_cte_merge(
1876 &mut self,
1877 cte_name: &str,
1878 body: &spg_sql::ast::MergeStatement,
1879 cancel: CancelToken<'_>,
1880 ) -> Result<
1881 (
1882 Vec<spg_storage::ColumnSchema>,
1883 Vec<spg_storage::Row<'static>>,
1884 ),
1885 EngineError,
1886 > {
1887 let body = body.clone();
1888 let result = self.exec_merge_cancel(&body, cancel)?;
1889 match result {
1890 QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1891 QueryResult::CommandOk { .. } => {
1892 let placeholder = spg_storage::ColumnSchema::new(
1893 alloc::format!("{cte_name}_returning_absent"),
1894 spg_storage::DataType::Text,
1895 true,
1896 );
1897 Ok((alloc::vec![placeholder], Vec::new()))
1898 }
1899 }
1900 }
1901
1902 /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1903 /// UNION (or UNION ALL) of an anchor that does not reference
1904 /// the CTE name, and one or more recursive terms that do. The
1905 /// anchor runs first; each subsequent iteration runs the
1906 /// recursive term against a temp catalog where the CTE name is
1907 /// bound to the *previous* iteration's output. Iteration stops
1908 /// when the recursive term yields no rows; UNION (DISTINCT)
1909 /// deduplicates against the accumulated result, UNION ALL does
1910 /// not. A hard cap on total rows prevents runaway queries.
1911 #[allow(clippy::too_many_lines)]
1912 pub(crate) fn materialise_recursive_cte(
1913 &self,
1914 cte: &spg_sql::ast::Cte,
1915 base_catalog: &Catalog,
1916 cancel: CancelToken<'_>,
1917 ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1918 const MAX_TOTAL_ROWS: usize = 1_000_000;
1919 const MAX_ITERATIONS: usize = 100_000;
1920 cancel.check()?;
1921 // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1922 // a modifying recursive CTE is parser-rejectable but we
1923 // guard here defensively.
1924 let body_select = cte.body.as_select().ok_or_else(|| {
1925 EngineError::Unsupported(alloc::format!(
1926 "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1927 cte.name
1928 ))
1929 })?;
1930 if body_select.unions.is_empty() {
1931 return Err(EngineError::Unsupported(alloc::format!(
1932 "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1933 cte.name
1934 )));
1935 }
1936 // Anchor: the body's leading SELECT, with unions stripped.
1937 let mut anchor = body_select.clone();
1938 let all_union_terms = core::mem::take(&mut anchor.unions);
1939 anchor.ctes = Vec::new();
1940 // v7.37 D.42 — split the UNION members: those that do NOT reference the
1941 // CTE are additional ANCHOR terms, only the ones that do recurse. A
1942 // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1943 // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1944 // treating the non-recursive `SELECT r2` as a recursive term made it
1945 // re-emit its constant row every iteration → runaway loop.
1946 let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1947 .into_iter()
1948 .partition(|(_, t)| !select_refers_to(t, &cte.name));
1949 let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1950 let QueryResult::Rows {
1951 columns: anchor_cols,
1952 rows: mut anchor_rows,
1953 } = anchor_result
1954 else {
1955 return Err(EngineError::Unsupported(alloc::format!(
1956 "WITH RECURSIVE {:?}: anchor did not return rows",
1957 cte.name
1958 )));
1959 };
1960 // Append every non-recursive UNION member's rows to the anchor set.
1961 for (_, term) in &anchor_terms {
1962 let mut term = term.clone();
1963 term.ctes = Vec::new();
1964 if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
1965 anchor_rows.extend(rows);
1966 }
1967 }
1968 // The projection builder labels non-column expressions Text;
1969 // refine column types from the anchor's actual values so the
1970 // intermediate iter-catalog tables accept them.
1971 let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
1972 if !cte.column_overrides.is_empty() {
1973 if cte.column_overrides.len() != columns.len() {
1974 return Err(EngineError::Unsupported(alloc::format!(
1975 "CTE {:?} column list has {} names but anchor returns {} columns",
1976 cte.name,
1977 cte.column_overrides.len(),
1978 columns.len()
1979 )));
1980 }
1981 for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1982 col.name.clone_from(name);
1983 }
1984 }
1985 let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
1986 let mut working_set: Vec<Row<'static>> = anchor_rows;
1987 let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
1988 // Track at least one "all UNION ALL" flag — if every union
1989 // kind is ALL we skip the dedup step (faster + matches PG).
1990 let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
1991 if !all_union_all {
1992 for r in &all_rows {
1993 seen.insert(encode_row_key(r));
1994 }
1995 }
1996 // v7.39 (round 598) — the engine and its catalog are built ONCE.
1997 // Each iteration used to clone the catalog, create the CTE table,
1998 // and construct a whole `Engine` — which initialises 82 fields — to
1999 // hold that round's working set. A counting allocator put the loop
2000 // at 63 allocations and 104 kB per iteration, or 1 GB for a
2001 // 10,000-row recursive CTE, and none of it varied with how much
2002 // else was in the catalog: the per-round rebuild WAS the cost. The
2003 // table is emptied and refilled instead.
2004 let mut iter_catalog = base_catalog.clone();
2005 let schema = TableSchema::new(cte.name.clone(), columns.clone());
2006 iter_catalog
2007 .create_table(schema)
2008 .map_err(EngineError::Storage)?;
2009 let mut iter_engine = Engine::restore(iter_catalog);
2010 if let Some(c) = self.clock {
2011 iter_engine = iter_engine.with_clock(c);
2012 }
2013 if let Some(f) = self.salt_fn {
2014 iter_engine = iter_engine.with_salt_fn(f);
2015 }
2016 // The recursive terms are cloned once too — the clone stripped the
2017 // CTE list off each of them, per term per iteration.
2018 let recursive_terms: Vec<SelectStatement> = union_terms
2019 .iter()
2020 .map(|(_, t)| {
2021 let mut t = t.clone();
2022 t.ctes = Vec::new();
2023 t
2024 })
2025 .collect();
2026 // v7.39 (round 618) — plan every recursive term once. Taken only if
2027 // ALL of them plan, so a query never runs half on each path.
2028 let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2029 .iter()
2030 .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2031 .collect();
2032 let fast_ctx = term_plans.as_ref().map(|plans| {
2033 let alias = plans[0].alias.clone();
2034 (alias, ())
2035 });
2036 for iter in 0..MAX_ITERATIONS {
2037 cancel.check()?;
2038 if working_set.is_empty() {
2039 break;
2040 }
2041 if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2042 // The worktable IS the working set: no table to empty and
2043 // refill, and no query execution per round.
2044 let mut next_set: Vec<Row<'static>> = Vec::new();
2045 for plan in plans {
2046 let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2047 for row in &working_set {
2048 cancel.check()?;
2049 if let Some(w) = plan.where_ {
2050 let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2051 if !matches!(v, Value::Bool(true)) {
2052 continue;
2053 }
2054 }
2055 let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2056 for it in &plan.items {
2057 vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2058 }
2059 let out = Row::new(vals);
2060 if !all_union_all {
2061 let key = encode_row_key(&out);
2062 if !seen.insert(key) {
2063 continue;
2064 }
2065 }
2066 next_set.push(out);
2067 }
2068 }
2069 if next_set.is_empty() {
2070 break;
2071 }
2072 all_rows.extend(next_set.iter().cloned());
2073 working_set = next_set;
2074 if all_rows.len() > MAX_TOTAL_ROWS {
2075 return Err(EngineError::Unsupported(alloc::format!(
2076 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2077 cte.name
2078 )));
2079 }
2080 if iter + 1 == MAX_ITERATIONS {
2081 return Err(EngineError::Unsupported(alloc::format!(
2082 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2083 cte.name
2084 )));
2085 }
2086 continue;
2087 }
2088 {
2089 // Truncated rather than dropped and recreated: the table's
2090 // own structure is what dropping it throws away, and it is
2091 // identical every round.
2092 let cat = iter_engine.base_catalog_mut();
2093 let table = cat.get_mut(&cte.name).expect("created above");
2094 table.truncate();
2095 for row in &working_set {
2096 table.insert(row.clone()).map_err(EngineError::Storage)?;
2097 }
2098 }
2099 // Run each recursive term in sequence and collect new rows.
2100 let mut next_set: Vec<Row<'static>> = Vec::new();
2101 for term in &recursive_terms {
2102 let r = iter_engine.exec_select_cancel(term, cancel)?;
2103 let QueryResult::Rows {
2104 columns: rc,
2105 rows: rs,
2106 } = r
2107 else {
2108 return Err(EngineError::Unsupported(alloc::format!(
2109 "WITH RECURSIVE {:?}: recursive term did not return rows",
2110 cte.name
2111 )));
2112 };
2113 if rc.len() != columns.len() {
2114 return Err(EngineError::Unsupported(alloc::format!(
2115 "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2116 cte.name,
2117 rc.len(),
2118 columns.len()
2119 )));
2120 }
2121 for row in rs {
2122 if !all_union_all {
2123 let key = encode_row_key(&row);
2124 if !seen.insert(key) {
2125 continue;
2126 }
2127 }
2128 next_set.push(row);
2129 }
2130 }
2131 if next_set.is_empty() {
2132 break;
2133 }
2134 all_rows.extend(next_set.iter().cloned());
2135 working_set = next_set;
2136 if all_rows.len() > MAX_TOTAL_ROWS {
2137 return Err(EngineError::Unsupported(alloc::format!(
2138 "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2139 cte.name
2140 )));
2141 }
2142 if iter + 1 == MAX_ITERATIONS {
2143 return Err(EngineError::Unsupported(alloc::format!(
2144 "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2145 cte.name
2146 )));
2147 }
2148 }
2149 Ok((columns, all_rows))
2150 }
2151
2152 pub(crate) fn resolve_select_subqueries(
2153 &self,
2154 stmt: &mut SelectStatement,
2155 cancel: CancelToken<'_>,
2156 ) -> Result<(), EngineError> {
2157 for item in &mut stmt.items {
2158 if let SelectItem::Expr { expr, alias } = item {
2159 // An UNCORRELATED subquery is replaced by its value right
2160 // here, and the shape the column was named for goes with
2161 // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2162 // boolean literal, so SPG answered `?column?` where PG18
2163 // answers `exists`. Only a subquery at the TOP of the item
2164 // loses its name this way — one nested inside a call still
2165 // reports the call.
2166 if alias.is_none()
2167 && matches!(
2168 expr,
2169 Expr::ScalarSubquery(_)
2170 | Expr::Exists { .. }
2171 | Expr::InSubquery { .. }
2172 | Expr::RowInSubquery { .. }
2173 | Expr::RowCmpSubquery { .. }
2174 )
2175 {
2176 *alias = Some(default_output_name(expr, self.speaks_mysql));
2177 }
2178 self.resolve_expr_subqueries(expr, cancel)?;
2179 }
2180 }
2181 if let Some(w) = &mut stmt.where_ {
2182 self.resolve_expr_subqueries(w, cancel)?;
2183 }
2184 // v7.24.1 — JOIN ON conditions can carry subqueries too;
2185 // they were never walked, so even an UNCORRELATED subquery
2186 // in ON hit "subquery reached row eval".
2187 if let Some(from) = &mut stmt.from {
2188 for j in &mut from.joins {
2189 if let Some(on) = &mut j.on {
2190 self.resolve_expr_subqueries(on, cancel)?;
2191 }
2192 }
2193 }
2194 if let Some(gs) = &mut stmt.group_by {
2195 for g in gs {
2196 self.resolve_expr_subqueries(g, cancel)?;
2197 }
2198 }
2199 if let Some(h) = &mut stmt.having {
2200 self.resolve_expr_subqueries(h, cancel)?;
2201 }
2202 for o in &mut stmt.order_by {
2203 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2204 }
2205 for (_, peer) in &mut stmt.unions {
2206 self.resolve_select_subqueries(peer, cancel)?;
2207 }
2208 Ok(())
2209 }
2210
2211 #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2212 pub(crate) fn resolve_expr_subqueries(
2213 &self,
2214 e: &mut Expr,
2215 cancel: CancelToken<'_>,
2216 ) -> Result<(), EngineError> {
2217 // Replace-on-this-node cases first.
2218 if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2219 *e = replacement;
2220 return Ok(());
2221 }
2222 match e {
2223 Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } => {
2224 self.resolve_expr_subqueries(expr, cancel)?
2225 }
2226 Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2227 Expr::AggregateOrdered { call, order_by, .. } => {
2228 self.resolve_expr_subqueries(call, cancel)?;
2229 for o in order_by.iter_mut() {
2230 self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2231 }
2232 }
2233 Expr::Binary { lhs, rhs, .. } => {
2234 self.resolve_expr_subqueries(lhs, cancel)?;
2235 self.resolve_expr_subqueries(rhs, cancel)?;
2236 }
2237 Expr::Unary { expr, .. }
2238 | Expr::Cast { expr, .. }
2239 | Expr::IsNull { expr, .. }
2240 | Expr::BoolTest { expr, .. }
2241 | Expr::FieldAccess { base: expr, .. } => {
2242 self.resolve_expr_subqueries(expr, cancel)?;
2243 }
2244 Expr::FunctionCall { args, .. } => {
2245 for a in args {
2246 self.resolve_expr_subqueries(a, cancel)?;
2247 }
2248 }
2249 Expr::Like { expr, pattern, .. } => {
2250 self.resolve_expr_subqueries(expr, cancel)?;
2251 self.resolve_expr_subqueries(pattern, cancel)?;
2252 }
2253 Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2254 // v4.12 window functions — recurse into args + ORDER BY
2255 // + PARTITION BY in case they carry inner subqueries.
2256 Expr::WindowFunction {
2257 args,
2258 partition_by,
2259 order_by,
2260 ..
2261 } => {
2262 for a in args {
2263 self.resolve_expr_subqueries(a, cancel)?;
2264 }
2265 for p in partition_by {
2266 self.resolve_expr_subqueries(p, cancel)?;
2267 }
2268 for (e, _, _) in order_by {
2269 self.resolve_expr_subqueries(e, cancel)?;
2270 }
2271 }
2272 // Subquery nodes are handled in subquery_replacement
2273 // (which returned None — defensive no-op); Literal /
2274 // Column are leaves.
2275 Expr::ScalarSubquery(_)
2276 | Expr::Exists { .. }
2277 | Expr::InSubquery { .. }
2278 | Expr::RowInSubquery { .. }
2279 | Expr::RowCmpSubquery { .. }
2280 | Expr::Literal(_)
2281 | Expr::Placeholder(_)
2282 | Expr::Column(_) => {}
2283 // v7.30.2 — list elements can carry scalar subqueries
2284 // (`x IN (1, (SELECT …))`).
2285 Expr::InList { expr, list, .. } => {
2286 self.resolve_expr_subqueries(expr, cancel)?;
2287 for item in list {
2288 self.resolve_expr_subqueries(item, cancel)?;
2289 }
2290 }
2291 // v7.10.10 — recurse children.
2292 Expr::Array(items) => {
2293 for elem in items {
2294 self.resolve_expr_subqueries(elem, cancel)?;
2295 }
2296 }
2297 Expr::ArraySubscript { target, index } => {
2298 self.resolve_expr_subqueries(target, cancel)?;
2299 self.resolve_expr_subqueries(index, cancel)?;
2300 }
2301 Expr::ArraySlice { target, lo, hi } => {
2302 self.resolve_expr_subqueries(target, cancel)?;
2303 if let Some(l) = lo {
2304 self.resolve_expr_subqueries(l, cancel)?;
2305 }
2306 if let Some(h) = hi {
2307 self.resolve_expr_subqueries(h, cancel)?;
2308 }
2309 }
2310 Expr::AnyAll { expr, array, .. } => {
2311 self.resolve_expr_subqueries(expr, cancel)?;
2312 // Quantified subquery — an uncorrelated one
2313 // materialises up front; a correlated one stays for
2314 // the per-row resolver.
2315 if let Expr::ScalarSubquery(inner) = array.as_mut() {
2316 if !crate::subquery::select_is_correlated(inner) {
2317 let s = (**inner).clone();
2318 **array = self.materialize_quantified_rows(&s, cancel)?;
2319 }
2320 } else {
2321 self.resolve_expr_subqueries(array, cancel)?;
2322 }
2323 }
2324 Expr::Case {
2325 operand,
2326 branches,
2327 else_branch,
2328 } => {
2329 if let Some(o) = operand {
2330 self.resolve_expr_subqueries(o, cancel)?;
2331 }
2332 for (w, t) in branches {
2333 self.resolve_expr_subqueries(w, cancel)?;
2334 self.resolve_expr_subqueries(t, cancel)?;
2335 }
2336 if let Some(e) = else_branch {
2337 self.resolve_expr_subqueries(e, cancel)?;
2338 }
2339 }
2340 }
2341 Ok(())
2342 }
2343}
2344
2345impl Engine {
2346 /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2347 /// `SelectItem::Wildcard` to all schema columns and
2348 /// `SelectItem::Expr` via the regular eval path.
2349 pub(crate) fn project_row_simple(
2350 &self,
2351 row: &Row<'static>,
2352 items: &[SelectItem],
2353 schema_cols: &[ColumnSchema],
2354 alias: &str,
2355 ) -> Result<Row<'static>, EngineError> {
2356 let ctx = self.ev_ctx(schema_cols, Some(alias));
2357 let cancel = CancelToken::none();
2358 let mut out_vals = Vec::new();
2359 for item in items {
2360 match item {
2361 // In a single-table projection (AS OF SEGMENT / RETURNING) a
2362 // qualified `t.*` covers exactly the same columns as a bare `*`.
2363 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2364 out_vals.extend(row.values.iter().cloned());
2365 }
2366 SelectItem::Expr { expr, .. } => {
2367 let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2368 out_vals.push(v);
2369 }
2370 }
2371 }
2372 Ok(Row::new(out_vals))
2373 }
2374
2375 /// v6.10.2 — derive the output `ColumnSchema` list for an
2376 /// AS OF SEGMENT projection. Wildcards take the full schema;
2377 /// expressions take the alias if present or a synthetic
2378 /// `?column?` (PG convention) otherwise.
2379 pub(crate) fn derive_output_columns(
2380 &self,
2381 items: &[SelectItem],
2382 schema_cols: &[ColumnSchema],
2383 table_alias: &str,
2384 ) -> Vec<ColumnSchema> {
2385 let mut out = Vec::new();
2386 for item in items {
2387 match item {
2388 // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2389 // a single-table projection.
2390 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2391 out.extend(schema_cols.iter().cloned());
2392 }
2393 SelectItem::Expr { expr, alias } => {
2394 // Bare column references inherit the schema
2395 // column's name + type — PG names `RETURNING id`
2396 // "id" and types it BIGINT, and the sqlx embed
2397 // path type-checks RowDescription against the
2398 // Rust target (mailrs embed round-12).
2399 if let Expr::Column(col) = expr
2400 && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2401 {
2402 let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2403 let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2404 // v7.39 (read01 round 54) — carry the enum identity:
2405 // it lives outside the DataType lattice, so a derived
2406 // table built from this schema otherwise forgets it and
2407 // the OUTER `ORDER BY <enum col>` silently sorts by the
2408 // label's TEXT instead of member order.
2409 c.user_enum_type = sc.user_enum_type.clone();
2410 out.push(c);
2411 continue;
2412 }
2413 let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2414 // v7.30.4 (mailrs round-27, P0) — type the
2415 // expression with the same inference the SELECT
2416 // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2417 // The old Text default broke every typed decode
2418 // of `RETURNING uidnext - 1 AS uid`: four days
2419 // of inbound mail indexed nowhere. Inference
2420 // failure keeps the old Text fallback rather
2421 // than inventing new error paths here.
2422 // v7.39 (round 258) — take the enum identity from the
2423 // same projection build, not just the type: a constant
2424 // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2425 // VALUES row lowers to) is an EXPRESSION, so it landed
2426 // here and the derived table forgot the enum.
2427 let (ty, nullable) = build_projection(
2428 core::slice::from_ref(item),
2429 schema_cols,
2430 table_alias,
2431 self.speaks_mysql,
2432 Some(self.active_catalog()),
2433 )
2434 .ok()
2435 .and_then(|p| p.into_iter().next())
2436 .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2437 out.push(ColumnSchema::new(name, ty, nullable));
2438 }
2439 }
2440 }
2441 out
2442 }
2443
2444 /// v4.5: SELECT with cooperative cancellation. The token is
2445 /// honoured between UNION peers and inside the bare-SELECT row
2446 /// loop; HNSW kNN graph walks and the aggregate executor don't
2447 /// honour it yet (deferred — those paths bound their work
2448 /// internally by `LIMIT k` and `GROUP BY` cardinality).
2449 /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2450 /// its (lowercased) name, or None if the name isn't a virtual view.
2451 /// Callers decide whether to return it directly (`SELECT *`) or stage
2452 /// it as a temp table for the full query pipeline.
2453 fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2454 Some(match name {
2455 "spg_statistic" => self.exec_spg_statistic(),
2456 "spg_stat_replication" => self.exec_spg_stat_replication(),
2457 "spg_stat_segment" => self.exec_spg_stat_segment(),
2458 "spg_memory_stats" => self.exec_spg_memory_stats(),
2459 "spg_stat_query" => self.exec_spg_stat_query(),
2460 "pg_stat_statements" => self.exec_pg_stat_statements(),
2461 "spg_stat_activity" => self.exec_spg_stat_activity(),
2462 "pg_stat_activity" => self.exec_pg_stat_activity(),
2463 "pg_locks" => self.exec_pg_locks(),
2464 "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2465 "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2466 "spg_partition_health" => self.exec_spg_partition_health(),
2467 "spg_audit_chain" => self.exec_spg_audit_chain(),
2468 "spg_audit_verify" => self.exec_spg_audit_verify(),
2469 "spg_table_ddl" => self.exec_spg_table_ddl(),
2470 "spg_role_ddl" => self.exec_spg_role_ddl(),
2471 "spg_database_ddl" => self.exec_spg_database_ddl(),
2472 _ => return None,
2473 })
2474 }
2475
2476 /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2477 /// describes against: this engine's catalog with the view staged as a
2478 /// table, exactly as `exec_select_cancel_as` stages it for a
2479 /// non-bare query.
2480 ///
2481 /// These views never reach the catalog — each is a fixed row set built
2482 /// inside its own `exec_*` — so Describe reported no columns for all
2483 /// seventeen of them. Rows are deliberately not inserted: Describe
2484 /// only needs the shape, and `infer_column_types` reads the rows we
2485 /// already have in hand.
2486 pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2487 let from = stmt.from.as_ref()?;
2488 if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2489 return None;
2490 }
2491 let lower = from.primary.name.to_ascii_lowercase();
2492 let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2493 return None;
2494 };
2495 let mut catalog = self.active_catalog().clone();
2496 let cols = infer_column_types(&columns, &rows);
2497 catalog
2498 .create_table(TableSchema::new(from.primary.name.clone(), cols))
2499 .ok()?;
2500 Some(catalog)
2501 }
2502
2503 pub(crate) fn exec_select_cancel(
2504 &self,
2505 stmt: &SelectStatement,
2506 cancel: CancelToken<'_>,
2507 ) -> Result<QueryResult, EngineError> {
2508 self.exec_select_cancel_as(stmt, cancel, None)
2509 }
2510
2511 /// v7.39 (round 334, V55) — the same read core, authorised as
2512 /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2513 /// function's OWNER: that is the entire point of the form, and without
2514 /// it every definer function failed with "permission denied" on the
2515 /// very table it exists to expose.
2516 /// v7.39 (round 559) — see the call site. `None` for anything but
2517 /// the bare shape, so every other query keeps its old path.
2518 fn try_bare_count_star(
2519 &self,
2520 stmt: &SelectStatement,
2521 as_role: Option<&str>,
2522 ) -> Result<Option<QueryResult>, EngineError> {
2523 use spg_sql::ast::SelectItem;
2524 if as_role.is_some()
2525 || !stmt.ctes.is_empty()
2526 || !stmt.unions.is_empty()
2527 || stmt.where_.is_some()
2528 || stmt.group_by.is_some()
2529 || stmt.having.is_some()
2530 || stmt.distinct
2531 || !stmt.order_by.is_empty()
2532 || stmt.limit.is_some()
2533 || stmt.offset.is_some()
2534 || stmt.items.len() != 1
2535 {
2536 return Ok(None);
2537 }
2538 let Some(from) = &stmt.from else {
2539 return Ok(None);
2540 };
2541 if !from.joins.is_empty()
2542 || stmt.locking.is_some()
2543 || from.primary.lateral_subquery.is_some()
2544 || from.primary.unnest_expr.is_some()
2545 || from.primary.generate_series_args.is_some()
2546 || from.primary.name.is_empty()
2547 || from.primary.name.starts_with("__spg_")
2548 {
2549 return Ok(None);
2550 }
2551 // A partition PARENT holds no rows of its own — they live in the
2552 // children — so its header count is 0 and the ordinary path has
2553 // to fan out. Caught by the partition conformance cases.
2554 //
2555 // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2556 // of them, which is worse: its header count is a real number,
2557 // just not the answer. `SELECT count(*) FROM par` returned 1
2558 // where PG returns 2, because this shortcut fired before the
2559 // fan-out could. The question is "does anything descend from
2560 // this", not "was it declared a partition parent".
2561 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2562 return Ok(None);
2563 }
2564 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2565 return Ok(None);
2566 };
2567 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2568 return Ok(None);
2569 };
2570 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2571 return Ok(None);
2572 }
2573 // A row-security policy filters rows, so the header count is not
2574 // the answer; the ordinary path applies the policy.
2575 let Some(table) = self.active_catalog().get(&from.primary.name) else {
2576 return Ok(None);
2577 };
2578 if table.schema().row_security {
2579 return Ok(None);
2580 }
2581 // Rows frozen to the cold tier are not in `headers`, so the
2582 // header count would miss them. Caught by the cold-tier e2e.
2583 if table.has_cold_rows_fast() {
2584 return Ok(None);
2585 }
2586 let n = table.count_visible(&self.current_snapshot());
2587 let col = alias.clone().unwrap_or_else(|| String::from("count"));
2588 Ok(Some(QueryResult::Rows {
2589 columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2590 rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2591 i64::try_from(n).unwrap_or(i64::MAX)
2592 )])],
2593 }))
2594 }
2595
2596 /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2597 /// that col>` served from the index, never reading a row.
2598 ///
2599 /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2600 /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2601 /// count (2x at 1k). PG needs its visibility map for this — a heap
2602 /// tuple carries its own visibility, so an index entry alone cannot
2603 /// say whether the row is live, and PG reads the heap for any page
2604 /// the map does not mark all-visible. SPG keeps a header array
2605 /// beside the rows, so the locator answers it directly and there is
2606 /// no map to be stale.
2607 /// v7.39 (round 564) — the shape test, once, for both the
2608 /// materialising scan and the streaming one.
2609 ///
2610 /// Two callers asking the same question in two places is how a fact
2611 /// starts drifting; the answer here is the single copy. Returns the
2612 /// table, the alias the predicate is written against, the projected
2613 /// column's position, and the name the single output column takes.
2614 pub(crate) fn index_only_shape<'s>(
2615 &'s self,
2616 stmt: &'s SelectStatement,
2617 ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2618 use spg_sql::ast::SelectItem;
2619 if !stmt.ctes.is_empty()
2620 || !stmt.unions.is_empty()
2621 || stmt.group_by.is_some()
2622 || stmt.having.is_some()
2623 || stmt.distinct
2624 || stmt.locking.is_some()
2625 || !stmt.order_by.is_empty()
2626 || stmt.limit.is_some()
2627 || stmt.offset.is_some()
2628 || stmt.items.len() != 1
2629 {
2630 return None;
2631 }
2632 let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2633 return None;
2634 };
2635 if !from.joins.is_empty()
2636 || from.primary.lateral_subquery.is_some()
2637 || from.primary.unnest_expr.is_some()
2638 || from.primary.generate_series_args.is_some()
2639 || from.primary.name.is_empty()
2640 || from.primary.name.starts_with("__spg_")
2641 {
2642 return None;
2643 }
2644 // v7.39 (round 645) — see the note on the sibling shortcut above:
2645 // an inheritance parent's own header count is not the answer.
2646 if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2647 return None;
2648 }
2649 let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2650 return None;
2651 };
2652 let spg_sql::ast::Expr::Column(c) = expr else {
2653 return None;
2654 };
2655 let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2656 if let Some(q) = c.qualifier.as_deref()
2657 && !q.eq_ignore_ascii_case(alias_name)
2658 {
2659 return None;
2660 }
2661 let table = self.active_catalog().get(&from.primary.name)?;
2662 if table.schema().row_security {
2663 return None;
2664 }
2665 let cols = &table.schema().columns;
2666 let pos = cols
2667 .iter()
2668 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2669 let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2670 Some((table, alias_name, pos, out))
2671 }
2672
2673 /// v7.39 (round 565) — would this statement be answered out of the
2674 /// index alone?
2675 ///
2676 /// EXPLAIN has to name the node the executor will actually run, and
2677 /// the only honest way to know is to ask the same two questions the
2678 /// executor asks: the statement's shape, and everything decidable
2679 /// about the scan before it walks. Neither is re-stated here.
2680 pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2681 let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2682 return false;
2683 };
2684 let Some(where_) = stmt.where_.as_ref() else {
2685 return false;
2686 };
2687 crate::index_access::index_only_precheck(
2688 where_,
2689 &table.schema().columns,
2690 table,
2691 alias_name,
2692 pos,
2693 self.speaks_mysql,
2694 )
2695 .is_some()
2696 }
2697
2698 fn try_index_only_scan(
2699 &self,
2700 stmt: &SelectStatement,
2701 ) -> Result<Option<QueryResult>, EngineError> {
2702 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2703 return Ok(None);
2704 };
2705 // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2706 // are not materialised here, and a partition parent's own
2707 // heap/indexes are empty (its rows live in the children).
2708 if !stmt.ctes.is_empty() {
2709 return Ok(None);
2710 }
2711 if let Some(from) = &stmt.from
2712 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2713 {
2714 return Ok(None);
2715 }
2716 let where_ = stmt.where_.as_ref().expect("shape checked it");
2717 let cols = &table.schema().columns;
2718 let Some(values) = crate::index_access::try_index_only_range(
2719 where_,
2720 cols,
2721 table,
2722 alias_name,
2723 &self.current_snapshot(),
2724 pos,
2725 self.speaks_mysql,
2726 ) else {
2727 return Ok(None);
2728 };
2729 let schema = alloc::vec![ColumnSchema::new(
2730 out_name,
2731 cols[pos].ty,
2732 cols[pos].nullable
2733 )];
2734 Ok(Some(QueryResult::Rows {
2735 columns: schema,
2736 rows: values
2737 .into_iter()
2738 .map(|v| Row::new(alloc::vec![v]))
2739 .collect(),
2740 }))
2741 }
2742
2743 /// v7.39 (round 564) — the same scan, emitting each value instead of
2744 /// building a `Vec<Row>` for the encoder to walk once and drop.
2745 ///
2746 /// A profile of the server serving a 50k-row range put 10.2% of the
2747 /// connection thread's CPU on BUILDING that vector and another 9.7%
2748 /// on dropping it — a fifth of the query, spent allocating and
2749 /// freeing one single-element `Vec` per output row so that the wire
2750 /// encoder could borrow each value for a few nanoseconds. The
2751 /// streaming interface it then hands them to takes `&[Value]`
2752 /// already.
2753 ///
2754 /// Returns `None` when the shape does not apply, so the caller falls
2755 /// back before anything has been emitted.
2756 pub(crate) fn try_index_only_stream<F>(
2757 &self,
2758 stmt: &SelectStatement,
2759 emit: &mut F,
2760 ) -> Result<Option<usize>, EngineError>
2761 where
2762 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2763 {
2764 let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2765 return Ok(None);
2766 };
2767 // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2768 // are not materialised here, and a partition parent's own
2769 // heap/indexes are empty (its rows live in the children).
2770 if !stmt.ctes.is_empty() {
2771 return Ok(None);
2772 }
2773 if let Some(from) = &stmt.from
2774 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2775 {
2776 return Ok(None);
2777 }
2778 let where_ = stmt.where_.as_ref().expect("shape checked it");
2779 let cols = &table.schema().columns;
2780 let schema = alloc::vec![ColumnSchema::new(
2781 out_name,
2782 cols[pos].ty,
2783 cols[pos].nullable
2784 )];
2785 let snapshot = self.current_snapshot();
2786 // The header goes out only once the walk has agreed to run — a
2787 // shape rejection after it would leave the client with a
2788 // RowDescription for a result that never comes.
2789 let mut wrote_header = false;
2790 let counted = crate::index_access::index_only_range_each(
2791 where_,
2792 cols,
2793 table,
2794 alias_name,
2795 &snapshot,
2796 pos,
2797 self.speaks_mysql,
2798 &mut |v: spg_storage::Value<'_>| {
2799 if !wrote_header {
2800 emit(crate::StreamItem::Header(&schema))?;
2801 wrote_header = true;
2802 }
2803 emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2804 },
2805 );
2806 match counted {
2807 None => Ok(None),
2808 Some(Err(e)) => Err(e),
2809 Some(Ok(n)) => {
2810 if !wrote_header {
2811 emit(crate::StreamItem::Header(&schema))?;
2812 }
2813 Ok(Some(n))
2814 }
2815 }
2816 }
2817
2818 /// `DISTINCT ON`'s de-duplication, which runs after the inner
2819 /// SELECT has produced its rows.
2820 ///
2821 /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2822 /// reason round 848 established: a debug build gives every branch's
2823 /// locals a slot in the frame whichever branch runs, and this one is
2824 /// eighty lines of hashing, key slicing and survivor sorting that a
2825 /// statement without `DISTINCT ON` never touches. Round 867
2826 /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2827 /// reaches none of it — the segment that had been blamed on
2828 /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2829 #[inline(never)]
2830 fn apply_distinct_on(
2831 &self,
2832 result: QueryResult,
2833 don_hidden: usize,
2834 don_limit: &(
2835 Option<spg_sql::ast::LimitExpr>,
2836 Option<spg_sql::ast::LimitExpr>,
2837 ),
2838 don_top1: usize,
2839 orig_order_by: &[spg_sql::ast::OrderBy],
2840 ) -> Result<QueryResult, EngineError> {
2841 let QueryResult::Rows { columns, rows } = result else {
2842 return Ok(result);
2843 };
2844 // The keys are the hidden trailing columns appended above.
2845 // v7.39 (round 729) — top-1 mode: the trailing columns are the
2846 // DON keys plus the ORDER tail; keep each group's best in one
2847 // hash pass, then sort the SURVIVORS with the original spec.
2848 let mut kept: alloc::vec::Vec<Row<'static>>;
2849 let key_start;
2850 if don_top1 > 0 {
2851 let tail = don_top1 - 1;
2852 key_start = columns.len().saturating_sub(don_hidden + tail);
2853 let ord_start = key_start + don_hidden;
2854 let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2855 .iter()
2856 .map(|o| (o.desc, o.nulls_first))
2857 .collect();
2858 let mysql = self.speaks_mysql;
2859 let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2860 for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2861 let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2862 let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2863 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2864 core::cmp::Ordering::Less => return true,
2865 core::cmp::Ordering::Greater => return false,
2866 core::cmp::Ordering::Equal => {}
2867 }
2868 }
2869 false
2870 };
2871 let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2872 let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2873 let mut keybuf = String::new();
2874 for row in rows {
2875 keybuf.clear();
2876 for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2877 aggregate::push_canonical_key(&mut keybuf, v);
2878 }
2879 match slot.get(keybuf.as_str()) {
2880 Some(&i) => {
2881 if better(&row, &best[i]) {
2882 best[i] = row;
2883 }
2884 }
2885 None => {
2886 slot.insert(keybuf.clone(), best.len());
2887 best.push(row);
2888 }
2889 }
2890 }
2891 // Survivors sort with the FULL original spec (keys are still
2892 // aboard as hidden columns).
2893 let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2894 .iter()
2895 .map(|o| (o.desc, o.nulls_first))
2896 .collect();
2897 best.sort_by(|a, b| {
2898 for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2899 let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2900 let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2901 match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2902 core::cmp::Ordering::Equal => {}
2903 o => return o,
2904 }
2905 }
2906 core::cmp::Ordering::Equal
2907 });
2908 for r in &mut best {
2909 r.values.truncate(key_start);
2910 }
2911 kept = best;
2912 } else {
2913 key_start = columns.len().saturating_sub(don_hidden);
2914 let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2915 kept = alloc::vec::Vec::new();
2916 for mut row in rows {
2917 let key: alloc::vec::Vec<Value<'static>> =
2918 row.values.get(key_start..).unwrap_or(&[]).to_vec();
2919 if seen.iter().any(|k| k == &key) {
2920 continue;
2921 }
2922 seen.push(key);
2923 row.values.truncate(key_start);
2924 kept.push(row);
2925 }
2926 }
2927 let mut columns = columns;
2928 columns.truncate(key_start);
2929 // PG limits what DISTINCT ON left, not what fed it.
2930 let kept = apply_deferred_limit(kept, don_limit);
2931 Ok(QueryResult::Rows {
2932 columns,
2933 rows: kept,
2934 })
2935 }
2936
2937 pub(crate) fn exec_select_cancel_as(
2938 &self,
2939 stmt: &SelectStatement,
2940 cancel: CancelToken<'_>,
2941 as_role: Option<&str>,
2942 ) -> Result<QueryResult, EngineError> {
2943 // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2944 // <all columns>` is legal PG (the wildcard expands to grouped
2945 // columns); SPG refused the whole shape. Expand the wildcard
2946 // into explicit column refs up front — the aggregate layer's
2947 // existing "must appear in the GROUP BY clause" validation
2948 // then answers PG's sentence for any non-grouped column.
2949 if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2950 return self.exec_select_cancel_as(&expanded, cancel, as_role);
2951 }
2952 // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2953 // a row.
2954 //
2955 // The aggregate layer already short-circuits this to
2956 // `rows.len()`, so the O(1) part was never the problem — the
2957 // cost is UPSTREAM, materialising every visible row so that
2958 // layer can take its length. Measured over pgwire on 500k rows:
2959 // PG18 8.2 ms with two parallel workers, 10.3 ms with
2960 // parallelism off, SPG 16.5 ms — 1.6x slower than a
2961 // single-threaded PG on the commonest aggregate there is, and no
2962 // ledger entry recorded it.
2963 //
2964 // Counting visible HEADERS needs no row at all. PG cannot do
2965 // this: its visibility lives in the heap tuples themselves, so
2966 // it has to read them (that is why its own count(*) is a full
2967 // scan, parallel or not).
2968 // v7.39 (read01 round 57) — the table-privilege gate on the common
2969 // read core. A superuser session returns from it immediately.
2970 // v7.39 (round 529) — resolve an ORDER BY that names an output
2971 // ALIAS. The statement-level pass never reached a SELECT nested in
2972 // a FROM clause, a CTE or a scalar subquery, so the same query
2973 // worked on its own and failed the moment anything wrapped it —
2974 // which is what generated SQL does constantly.
2975 let aliased;
2976 let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
2977 let mut s = stmt.clone();
2978 crate::orderby::resolve_order_by_position(&mut s);
2979 aliased = s;
2980 &aliased
2981 } else {
2982 stmt
2983 };
2984 // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
2985 //
2986 // Its keys were evaluated against the PROJECTED row, so a key that
2987 // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
2988 // ORDER BY g, v DESC`, the canonical "latest row per group" — could
2989 // not be read at all and the query failed. PG evaluates them on the
2990 // input. They are projected as hidden columns here and stripped
2991 // again below, the same way the grouping-set ordering columns
2992 // already travel.
2993 //
2994 // And the dedup ran AFTER the inner statement's LIMIT, so
2995 // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
2996 // PG answers two: the limit had already taken two rows of the same
2997 // group before anything deduplicated them. A paginated DISTINCT ON
2998 // returned short pages, with no error. The limit is deferred to
2999 // after the dedup, which is PG's order.
3000 let don_stmt;
3001 // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
3002 // order spec (the rewritten stmt's is emptied).
3003 let orig_order_by = stmt.order_by.clone();
3004 let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
3005 (stmt, 0, (None, None), 0usize)
3006 } else {
3007 let mut s = stmt.clone();
3008 let hidden = s.distinct_on.len();
3009 for (i, e) in stmt.distinct_on.iter().enumerate() {
3010 s.items.push(SelectItem::Expr {
3011 expr: e.clone(),
3012 alias: Some(alloc::format!("__distinct_on_{i}")),
3013 });
3014 }
3015 // v7.39 (round 729) — group-top-1 short circuit. When the
3016 // DISTINCT ON keys are exactly the ORDER BY's leading keys,
3017 // the answer is "per group, the row that wins the remaining
3018 // order" — a single O(n) hash pass. The old path sorted the
3019 // ENTIRE input first (500k rows, ~180 ms on the panel cell)
3020 // to keep 100. The inner query runs UNSORTED with every
3021 // order key appended as a hidden column; the dedup below
3022 // keeps each group's best, then sorts the SURVIVORS.
3023 // Declared-collation order keys stay on the sorting path
3024 // (the value comparator here is collation-blind).
3025 let prefix_matches = s.order_by.len() >= hidden
3026 && stmt
3027 .distinct_on
3028 .iter()
3029 .zip(s.order_by.iter())
3030 .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3031 let colls_plain =
3032 crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3033 .map(|cs| cs.iter().all(Option::is_none))
3034 .unwrap_or(false);
3035 let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3036 let tail = s.order_by.len() - hidden;
3037 for (j, o) in s.order_by[hidden..].iter().enumerate() {
3038 s.items.push(SelectItem::Expr {
3039 expr: o.expr.clone(),
3040 alias: Some(alloc::format!("__don_ord_{j}")),
3041 });
3042 }
3043 // Carry the tail's direction flags through the aliases'
3044 // ORDER; the survivors re-sort below with the full spec.
3045 s.order_by = Vec::new();
3046 tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3047 } else {
3048 0
3049 };
3050 // Only a folded literal is deferred; a placeholder or an
3051 // expression keeps the path it has today rather than being
3052 // resolved a second way here.
3053 let deferrable = matches!(
3054 (&s.limit, &s.offset),
3055 (
3056 None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3057 None | Some(spg_sql::ast::LimitExpr::Literal(_))
3058 )
3059 );
3060 let deferred = if deferrable {
3061 (s.limit.take(), s.offset.take())
3062 } else {
3063 (None, None)
3064 };
3065 don_stmt = s;
3066 (&don_stmt, hidden, deferred, top1_tail)
3067 };
3068 self.acl_check_select_as(stmt, as_role)?;
3069 validate_aggregate_placement(stmt)?;
3070 // BEFORE the fast paths below, not after: a name that resolves to
3071 // nothing is not a question the count fast path or the index-only
3072 // scan should get to answer first. Placed after them at first,
3073 // and the two of them swallowed `WHERE` and `ORDER BY` while
3074 // `GROUP BY` and `HAVING`, which cannot take those routes, raised
3075 // — the same statement answering two ways depending on the plan.
3076 self.validate_clause_columns(stmt)?;
3077 self.validate_function_arity(stmt)?;
3078 // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3079 // privilege gate above. Placed before it at first, and the
3080 // security-definer e2e caught it immediately: a SECURITY INVOKER
3081 // function whose body is `SELECT count(*) FROM t` answered
3082 // instead of being refused, because the fast path never reached
3083 // the check.
3084 if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3085 return Ok(r);
3086 }
3087 // v7.39 (round 560) — an index-only range scan. Same placement
3088 // reasoning as the count above: after the privilege gate.
3089 if let Some(r) = self.try_index_only_scan(stmt)? {
3090 return Ok(r);
3091 }
3092 validate_locking_clause(stmt)?;
3093 let result = self.exec_select_cancel_inner(stmt, cancel)?;
3094 // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3095 // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3096 // They carry the per-branch mask through the UNION-ALL sort and must not
3097 // appear in the output. Stripped per SELECT level (grouping-set queries
3098 // are often wrapped in a derived subquery), before DISTINCT ON.
3099 let result = strip_synthetic_order_cols(result);
3100 // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3101 // rows arrive here already ORDER BY'd; keep the FIRST row of
3102 // each group the expressions define (PG semantics). The
3103 // expressions evaluate against the projected schema — an
3104 // expression that isn't in the select list errors honestly.
3105 if stmt.distinct_on.is_empty() {
3106 return Ok(result);
3107 }
3108 self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3109 }
3110
3111 /// The UNION chain: execute the head as a bare block, then fold each
3112 /// peer in with left-associative dedup.
3113 ///
3114 /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3115 /// reason round 848 established. A statement with no unions returns
3116 /// one line above the call — and every nested subquery on a deep
3117 /// path is such a statement, so each level of the recursion carried
3118 /// 170 lines of locals it could not reach. Round 867 measured that
3119 /// frame at 34,800 bytes, the largest single one on the descent,
3120 /// after two earlier attributions had blamed its caller and then its
3121 /// callee: the gap between two marks is the frame of everything
3122 /// BETWEEN them, and this function had no mark of its own.
3123 #[inline(never)]
3124 fn exec_union_chain(
3125 &self,
3126 stmt_ref: &SelectStatement,
3127 stmt: &SelectStatement,
3128 cancel: CancelToken<'_>,
3129 ) -> Result<QueryResult, EngineError> {
3130 // UNION path: clone-strip the head into a bare block (its own
3131 // DISTINCT and any inner ORDER BY are dropped by parser rule —
3132 // the wrapper SelectStatement carries them), execute, then chain
3133 // peers with left-associative dedup semantics.
3134 // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3135 // output columns; a position past their count is PG's 42P10.
3136 crate::orderby::check_order_by_positions(stmt_ref)?;
3137 let mut head_unknown = branch_unknown_mask(stmt_ref);
3138 let head_regcast = branch_regcast_mask(stmt_ref);
3139 let mut head = stmt_ref.clone();
3140 head.unions = Vec::new();
3141 head.order_by = Vec::new();
3142 head.limit = None;
3143 let QueryResult::Rows {
3144 mut columns,
3145 mut rows,
3146 } = self.exec_bare_select_cancel(&head, cancel)?
3147 else {
3148 unreachable!("bare SELECT cannot return CommandOk")
3149 };
3150 for (kind, peer) in &stmt_ref.unions {
3151 // v7.37.17 (17.6 siblings) — a peer carrying its own
3152 // unions is a nested INTERSECT group (the parser's
3153 // precedence regrouping); recurse through the
3154 // union-aware wrapper for it.
3155 let peer_result = if peer.unions.is_empty() {
3156 self.exec_bare_select_cancel(peer, cancel)?
3157 } else {
3158 self.exec_select_cancel(peer, cancel)?
3159 };
3160 let QueryResult::Rows {
3161 columns: peer_cols,
3162 rows: mut peer_rows,
3163 } = peer_result
3164 else {
3165 unreachable!("bare SELECT cannot return CommandOk")
3166 };
3167 if peer_cols.len() != columns.len() {
3168 // v7.39 (round 232) — PG's wording, which clients match on.
3169 return Err(EngineError::Unsupported(alloc::format!(
3170 "each {} query must have the same number of columns",
3171 set_op_name(*kind)
3172 )));
3173 }
3174 // v7.39 (round 232+233) — PG resolves each result column to one
3175 // type before it merges anything, and refuses the query when the
3176 // two branches have no common type. SPG's unifier
3177 // (`unify_union_columns`) is value-driven and deliberately
3178 // conservative — "a column where any cell fails to coerce is left
3179 // exactly as it was" — so a mismatch produced a column holding
3180 // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3181 // back with integers and text interleaved) instead of an error.
3182 //
3183 // The check has to read the branch ASTs, not just their schemas:
3184 // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3185 // as TEXT and is indistinguishable from a real text column by
3186 // schema alone — yet PG treats the two completely differently
3187 // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3188 // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3189 let peer_unknown = branch_unknown_mask(peer);
3190 let peer_regcast = branch_regcast_mask(peer);
3191 for i in 0..columns.len() {
3192 let hu = head_unknown.get(i).copied().unwrap_or(false);
3193 let pu = peer_unknown.get(i).copied().unwrap_or(false);
3194 let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3195 let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3196 || head_regcast.get(i).copied().unwrap_or(false);
3197 match (hu, pu) {
3198 // Both sides carry a real type: they must share a category.
3199 (false, false) => {
3200 if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3201 return Err(EngineError::Unsupported(alloc::format!(
3202 "{} types {} and {} cannot be matched",
3203 set_op_name(*kind),
3204 crate::conversions::pg_type_name_for_error(ht),
3205 crate::conversions::pg_type_name_for_error(pt),
3206 )));
3207 }
3208 }
3209 // One side is an untyped literal: it takes the other's
3210 // type, and failing to convert is the error PG reports.
3211 (true, false) => {
3212 coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3213 columns[i].ty = pt;
3214 head_unknown[i] = false;
3215 }
3216 (false, true) => {
3217 coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3218 }
3219 // Both untyped — nothing to resolve against yet.
3220 (true, true) => {}
3221 }
3222 }
3223 // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3224 // nullable (PG semantics). Previously the result kept only the head's
3225 // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3226 // non-null `1`) wrongly reported the column NOT NULL, which let
3227 // `count(col)`'s NOT-NULL fast-path count the NULL row.
3228 for (i, pc) in peer_cols.iter().enumerate() {
3229 if pc.nullable {
3230 columns[i].nullable = true;
3231 }
3232 }
3233 // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3234 // text by the session collation (CI + accent + PAD SPACE), like
3235 // GROUP BY. PG stays byte-exact.
3236 let mysql = self.speaks_mysql;
3237 // v7.38.14 — the mask, which 7.38.13 recorded as impossible here
3238 // and was wrong about. `columns` and `peer_cols` are both in
3239 // scope; what was actually missing is that the branches' output
3240 // schemas did not CARRY the collation, so a mask built from them
3241 // would have marked every column byte-wise. Unifying the
3242 // projection-to-schema conversion fixed the supply side, and the
3243 // mask is now buildable from what was always there.
3244 //
3245 // Either side byte-wise keeps the position byte-wise, mirroring
3246 // `eval::resolve::mysql_text_fold_applies`: a set operation
3247 // between a folding column and a declared-binary one must not
3248 // quietly fold the binary one's values away.
3249 let set_mask: alloc::vec::Vec<bool> = columns
3250 .iter()
3251 .zip(peer_cols.iter())
3252 .map(|(l, r)| {
3253 matches!(l.collation, spg_storage::Collation::Binary)
3254 || matches!(r.collation, spg_storage::Collation::Binary)
3255 })
3256 .collect();
3257 let fold = FoldSpec::of(mysql, &set_mask);
3258 match kind {
3259 UnionKind::All => rows.extend(peer_rows),
3260 UnionKind::Distinct => {
3261 rows.extend(peer_rows);
3262 rows = dedup_rows(rows, fold);
3263 }
3264 // v7.37.17 (17.6 siblings) — PG set semantics.
3265 // v7.39 (round 591) — all four ask the same question of the
3266 // right side, and all four used to answer it by scanning it
3267 // once per left row. `PeerIndex` buckets it by the hash
3268 // DISTINCT already uses, so the answer is a lookup.
3269 // INTERSECT: distinct rows present on both sides.
3270 UnionKind::Intersect => {
3271 let idx = PeerIndex::build(&peer_rows, fold);
3272 rows = dedup_rows(rows, fold)
3273 .into_iter()
3274 .filter(|r| idx.contains(r))
3275 .collect();
3276 }
3277 // INTERSECT ALL: multiset intersection — each row
3278 // keeps min(left count, right count) occurrences.
3279 UnionKind::IntersectAll => {
3280 let mut idx = PeerIndex::build(&peer_rows, fold);
3281 let mut kept: Vec<Row<'static>> = Vec::new();
3282 for r in rows {
3283 if idx.take_one(&r) {
3284 kept.push(r);
3285 }
3286 }
3287 rows = kept;
3288 }
3289 // EXCEPT: distinct left rows absent from the right.
3290 UnionKind::Except => {
3291 let idx = PeerIndex::build(&peer_rows, fold);
3292 rows = dedup_rows(rows, fold)
3293 .into_iter()
3294 .filter(|r| !idx.contains(r))
3295 .collect();
3296 }
3297 // EXCEPT ALL: multiset subtraction — each right
3298 // occurrence cancels one left occurrence.
3299 UnionKind::ExceptAll => {
3300 let mut idx = PeerIndex::build(&peer_rows, fold);
3301 let mut kept: Vec<Row<'static>> = Vec::new();
3302 for r in rows {
3303 if !idx.take_one(&r) {
3304 kept.push(r);
3305 }
3306 }
3307 rows = kept;
3308 }
3309 }
3310 }
3311 // PG resolves a UNION / VALUES result column to one common type
3312 // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3313 // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3314 // built each branch independently, leaving mixed-type columns
3315 // that broke ORDER BY, comparisons, and value-based window
3316 // frames. Unify + coerce before the combined ORDER BY sees them.
3317 unify_union_columns(&mut columns, &mut rows);
3318 // ORDER BY at the top of a UNION applies to the combined result.
3319 // Eval against the projected schema (NOT the source table).
3320 if !stmt.order_by.is_empty() {
3321 // v7.39 (read01 round 54) — the combined-result ctx must carry the
3322 // catalog, and the projected columns must keep their enum identity
3323 // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3324 // by TEXT instead of member order — silently wrong rows, not an
3325 // error. (Same shape as the enum-order knife's GROUP BY fix.)
3326 let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3327 // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3328 // survive to here when the head projects a Wildcard (the
3329 // group-tail wrapper shape): map them onto the Nth
3330 // projected column so the combined sort works.
3331 let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3332 .order_by
3333 .iter()
3334 .map(|o| {
3335 let mut o = o.clone();
3336 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3337 && *n >= 1
3338 && let Ok(idx) = usize::try_from(*n - 1)
3339 && idx < columns.len()
3340 {
3341 o.expr = Expr::Column(spg_sql::ast::ColumnName {
3342 qualifier: None,
3343 name: columns[idx].name.clone(),
3344 });
3345 }
3346 o
3347 })
3348 .collect();
3349 let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3350 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3351 for r in rows {
3352 let keys = build_order_keys(&resolved_order, &r, &synth_ctx)?;
3353 tagged.push((keys, r));
3354 }
3355 sort_by_keys(&mut tagged, &descs);
3356 rows = tagged.into_iter().map(|(_, r)| r).collect();
3357 }
3358 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3359 Ok(QueryResult::Rows { columns, rows })
3360 }
3361
3362 fn exec_select_cancel_inner(
3363 &self,
3364 stmt: &SelectStatement,
3365 cancel: CancelToken<'_>,
3366 ) -> Result<QueryResult, EngineError> {
3367 cancel.check()?;
3368 // v7.38 P0 元机制 A — first observable point inside the
3369 // planner / executor. Tests use this to inject a delay or
3370 // a cancellation race before any row is produced. Release
3371 // build expands to `let _ = (...);` — zero cost.
3372 crate::injection_point!("planner_first_row_fetch", &stmt.from);
3373 // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3374 // PG analyses every definition, referenced or not, so `SELECT i FROM
3375 // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3376 // succeeded here (the parser used to drop the unreferenced defs
3377 // whole). The check is the CREATE VIEW check's shape (round 700): a
3378 // LIMIT-0 run of the same FROM with the definitions' key
3379 // expressions as the projection — it cannot disagree with what a
3380 // referencing window would have done, because it resolves the same
3381 // names the same way. Zero cost for the ordinary statement: the
3382 // list is empty unless a WINDOW clause left unreferenced defs.
3383 if !stmt.window_check_exprs.is_empty() {
3384 let mut probe = stmt.clone();
3385 probe.items = stmt
3386 .window_check_exprs
3387 .iter()
3388 .map(|e| spg_sql::ast::SelectItem::Expr {
3389 expr: e.clone(),
3390 alias: None,
3391 })
3392 .collect();
3393 probe.window_check_exprs = Vec::new();
3394 probe.distinct = false;
3395 probe.distinct_on = Vec::new();
3396 probe.group_by = None;
3397 probe.group_by_all = false;
3398 probe.having = None;
3399 probe.unions = Vec::new();
3400 probe.order_by = Vec::new();
3401 probe.locking = None;
3402 probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3403 probe.offset = None;
3404 probe.limit_with_ties = false;
3405 self.exec_select_cancel_inner(&probe, cancel)?;
3406 }
3407 // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3408 // takes the catalog, so the parser leaves a marker and the rewrite lands
3409 // here: the call moves into a LATERAL FROM item and the item becomes one
3410 // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3411 // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3412 // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3413 // second one.
3414 if let Some(lowered) = self.lower_record_expansion(stmt)? {
3415 return self.exec_select_cancel_inner(&lowered, cancel);
3416 }
3417 // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3418 // FROM / JOIN graph references any catalogued view name,
3419 // re-parse the view body and prepend it as a synthetic
3420 // CTE. Recurses on views-in-views via the regular CTE
3421 // dispatch below. Fast-path: skip the walker entirely when
3422 // the catalog has no views (the typical OLTP load).
3423 if !self.active_catalog().views_all().is_empty() {
3424 if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3425 return self.exec_select_cancel(&rewritten, cancel);
3426 }
3427 }
3428 // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3429 // gets rewritten to a UNION-ALL over the children that overlap
3430 // the WHERE-derived key range. Uses the same CTE-injection
3431 // trick as VIEW expansion above so downstream resolution
3432 // doesn't need a partition-aware code path.
3433 if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3434 return self.exec_select_cancel(&rewritten, cancel);
3435 }
3436 // v7.16.2 — information_schema / pg_catalog virtual
3437 // views (mailrs round-10 A.3). If the SELECT touches a
3438 // synthetic meta-table name (`__spg_info_*` /
3439 // `__spg_pg_*` — produced by the parser for
3440 // `information_schema.X` / `pg_catalog.X`), clone the
3441 // catalog, materialise the requested view as a real
3442 // temporary table, and re-execute against an enriched
3443 // engine. Same pattern as `exec_with_ctes` for CTEs.
3444 if !self.meta_views_materialised && select_references_meta_view(stmt) {
3445 return self.exec_select_with_meta_views(stmt, cancel);
3446 }
3447 // v6.10.2 — cold-tier time-travel short-circuit. When the
3448 // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3449 // dedicated cold-segment scan instead of the regular
3450 // hot+index path. The scope is intentionally narrow for
3451 // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3452 // optionally with a single-column-equality WHERE. JOINs /
3453 // aggregates / ORDER BY / subqueries on top of a time-
3454 // travelled scan are STABILITY § "Out of v6.10".
3455 if let Some(from) = &stmt.from
3456 && let Some(seg_id) = from.primary.as_of_segment
3457 {
3458 return self.exec_select_as_of_segment(stmt, from, seg_id);
3459 }
3460 // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3461 // pre-CTE because they don't read from the catalog and
3462 // shouldn't participate in regular FROM resolution.
3463 // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3464 // short-circuits. A meta-view FROM materialises to a fixed row
3465 // set. For a bare `SELECT *` we return it directly; otherwise we
3466 // stage it as a temp table and run the normal pipeline, so
3467 // projection / WHERE / ORDER BY / aggregates work over these views
3468 // (they were `SELECT *`-only before). A real table shadowing the
3469 // name wins (checked first), which also stops the staged re-run
3470 // from recursing back into meta-view detection.
3471 if let Some(from) = &stmt.from
3472 && from.joins.is_empty()
3473 && self.active_catalog().get(&from.primary.name).is_none()
3474 {
3475 let lower = from.primary.name.to_ascii_lowercase();
3476 if let Some(result) = self.meta_view_result(&lower) {
3477 let bare = stmt.where_.is_none()
3478 && stmt.group_by.is_none()
3479 && stmt.having.is_none()
3480 && stmt.unions.is_empty()
3481 && stmt.order_by.is_empty()
3482 && stmt.limit.is_none()
3483 && stmt.offset.is_none()
3484 && !stmt.distinct
3485 && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3486 if bare {
3487 return Ok(result);
3488 }
3489 if let QueryResult::Rows { columns, rows } = result {
3490 let mut catalog = self.active_catalog().clone();
3491 let cols = infer_column_types(&columns, &rows);
3492 let schema = TableSchema::new(from.primary.name.clone(), cols);
3493 catalog.create_table(schema).map_err(EngineError::Storage)?;
3494 let t = catalog
3495 .get_mut(&from.primary.name)
3496 .expect("just-created meta-view table must exist");
3497 for row in rows {
3498 t.insert(row).map_err(EngineError::Storage)?;
3499 }
3500 let mut eng = Engine::restore(catalog);
3501 if let Some(c) = self.clock {
3502 eng = eng.with_clock(c);
3503 }
3504 if let Some(f) = self.salt_fn {
3505 eng = eng.with_salt_fn(f);
3506 }
3507 // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3508 // connection identity so `WHERE pid = pg_backend_pid()`
3509 // matches inside the staged meta-view run.
3510 if let Some(f) = self.backend_pid_fn {
3511 eng.set_backend_pid_fn(f);
3512 }
3513 return eng.exec_select_cancel(stmt, cancel);
3514 }
3515 return Ok(result);
3516 }
3517 }
3518 // v4.11: CTEs materialise into a temporary enriched catalog
3519 // *before* anything else — the body SELECT can then refer
3520 // to CTE names via the regular FROM-clause resolution.
3521 // Uncorrelated only: each CTE body runs once against the
3522 // current catalog, not against later CTEs' results (left-
3523 // to-right materialisation would relax this, but we keep
3524 // it simple for v4.11 MVP).
3525 if !stmt.ctes.is_empty() {
3526 return self.exec_with_ctes(stmt, cancel);
3527 }
3528 // v4.10: subqueries (uncorrelated) are resolved here, before
3529 // the executor sees the row loop. We clone the statement so
3530 // we can mutate without disturbing the caller's AST — most
3531 // queries pass through with no subquery nodes and the clone
3532 // is cheap; with subqueries the materialisation cost
3533 // dominates anyway.
3534 let mut stmt_owned;
3535 let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3536 stmt_owned = stmt.clone();
3537 // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3538 // aggregate-wrapped correlated scalar subquery whose
3539 // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3540 // executor streams one join instead of splicing a per-row
3541 // subplan. Runs before the per-row/batch resolver, which then
3542 // only sees the subqueries the pull-up left behind.
3543 self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3544 // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3545 // the "per-key latest" scalar subquery shape (inbox / feed
3546 // / timeline applications) becomes a CTE + LEFT JOIN
3547 // against a GROUP BY pre-aggregation that reuses the v7.33
3548 // first_ordered argmax executor. Runs AFTER unique-key
3549 // pull-up (so the unique-key fast path still wins for
3550 // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3551 // Phase 1 (this commit) is skeleton only — no-op pass.
3552 self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3553 // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3554 // sublink pull-up to semi/anti-join, before the resolver gets
3555 // a chance to walk per-row.
3556 self.pull_up_exists_sublinks(&mut stmt_owned);
3557 // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3558 // exec_with_ctes so they materialise once before the body
3559 // SELECT runs. exec_with_ctes strips ctes from the body
3560 // clone, then re-enters select.
3561 if !stmt_owned.ctes.is_empty() {
3562 return self.exec_with_ctes(&stmt_owned, cancel);
3563 }
3564 // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3565 // SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3566 // BEFORE `resolve_select_subqueries` materialises the inner
3567 // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3568 // INSUBQ benchmark). Run the inner once, collect the result
3569 // values into a `HashSet<i64>` directly, then probe A.pk per
3570 // value and tally. Returns `Some` when the shape matches.
3571 if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3572 return Ok(out);
3573 }
3574 self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3575 &stmt_owned
3576 } else {
3577 stmt
3578 };
3579 if stmt_ref.unions.is_empty() {
3580 return self.exec_bare_select_cancel(stmt_ref, cancel);
3581 }
3582 self.exec_union_chain(stmt_ref, stmt, cancel)
3583 }
3584
3585 #[allow(clippy::too_many_lines)]
3586 #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3587 /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3588 /// Synthesises a single-column virtual table whose column type
3589 /// is TEXT and whose rows are the array elements. Routes
3590 /// through the regular projection / WHERE / ORDER BY / LIMIT
3591 /// machinery so set-returning UNNEST composes naturally with
3592 /// the rest of the SELECT surface.
3593 fn exec_select_unnest(
3594 &self,
3595 stmt: &SelectStatement,
3596 primary: &TableRef,
3597 cancel: CancelToken<'_>,
3598 ) -> Result<QueryResult, EngineError> {
3599 let expr = primary
3600 .unnest_expr
3601 .as_deref()
3602 .expect("caller guards unnest_expr.is_some()");
3603 // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3604 // N value columns instead of one; the shared builder does
3605 // the work and the tail below (WHERE / agg / projection)
3606 // runs against the wider schema.
3607 let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3608 match unnest_zip_args(expr) {
3609 Some(args) => Some(unnest_zip_rows(args)?),
3610 None => None,
3611 };
3612 // Evaluate the array expression once. Empty schema / empty
3613 // row — uncorrelated UNNEST cannot reference outer columns.
3614 // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3615 // introspection family (enum_range / enum_first / enum_last) resolves
3616 // its labels from the argument's STATIC enum type against the
3617 // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3618 // fell through to the generic arm, got NULL, and expanded to zero rows
3619 // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3620 // carry the catalog) worked.
3621 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3622 let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3623 let dummy_row = Row::new(alloc::vec::Vec::new());
3624 // v7.11.13 — unnest dispatches per array element type so
3625 // INT[] / BIGINT[] surface their PG types in projection.
3626 // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3627 // columns (PG: lexeme | positions | weights); everything else
3628 // keeps the alias / "unnest" defaults below.
3629 let mut composite_names: Option<&[&str]> = None;
3630 let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3631 if let Some(m) = multi {
3632 m
3633 } else {
3634 // v7.39 (round 236) — flatten a multidimensional array into
3635 // its row-major elements (PG) before the 1-D-only match.
3636 let unnest_src = {
3637 let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3638 crate::eval::values::flatten_2d(&v).unwrap_or(v)
3639 };
3640 let mut return_multi: Option<(
3641 alloc::vec::Vec<DataType>,
3642 alloc::vec::Vec<Row<'static>>,
3643 )> = None;
3644 let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3645 {
3646 Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3647 Value::TextArray(items) => {
3648 let rows = items
3649 .into_iter()
3650 .map(|item| {
3651 Row::new(alloc::vec![match item {
3652 Some(s) => Value::text(s),
3653 None => Value::Null,
3654 }])
3655 })
3656 .collect();
3657 (DataType::Text, rows)
3658 }
3659 Value::IntArray(items) => {
3660 let rows = items
3661 .into_iter()
3662 .map(|item| {
3663 Row::new(alloc::vec![match item {
3664 Some(n) => Value::Int(n),
3665 None => Value::Null,
3666 }])
3667 })
3668 .collect();
3669 (DataType::Int, rows)
3670 }
3671 Value::BigIntArray(items) => {
3672 let rows = items
3673 .into_iter()
3674 .map(|item| {
3675 Row::new(alloc::vec![match item {
3676 Some(n) => Value::BigInt(n),
3677 None => Value::Null,
3678 }])
3679 })
3680 .collect();
3681 (DataType::BigInt, rows)
3682 }
3683 Value::Multirange { kind, ranges } => {
3684 let rows = ranges
3685 .iter()
3686 .map(|sp| {
3687 Row::new(alloc::vec![Value::Range {
3688 kind,
3689 lower: sp.lower.clone(),
3690 upper: sp.upper.clone(),
3691 lower_inc: sp.lower_inc,
3692 upper_inc: sp.upper_inc,
3693 empty: false,
3694 }])
3695 })
3696 .collect();
3697 (DataType::Range(kind), rows)
3698 }
3699 // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3700 // one row per lexeme, PG18-measured columns
3701 // lexeme | positions | weights (`a | {1,3} |
3702 // {D,D}`); a position-less lexeme (a stripped
3703 // vector) reads NULL in both array columns.
3704 Value::TsVector(lexemes) => {
3705 composite_names = Some(&["lexeme", "positions", "weights"]);
3706 let rows = lexemes
3707 .iter()
3708 .map(|l| {
3709 let (pos, wts) = if l.positions.is_empty() {
3710 (Value::Null, Value::Null)
3711 } else {
3712 let letter = match l.weight {
3713 3 => "A",
3714 2 => "B",
3715 1 => "C",
3716 _ => "D",
3717 };
3718 (
3719 Value::SmallIntArray(
3720 l.positions
3721 .iter()
3722 .map(|p| {
3723 Some(i16::try_from(*p).unwrap_or(i16::MAX))
3724 })
3725 .collect(),
3726 ),
3727 Value::TextArray(
3728 l.positions
3729 .iter()
3730 .map(|_| Some(letter.into()))
3731 .collect(),
3732 ),
3733 )
3734 };
3735 Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3736 })
3737 .collect();
3738 return_multi = Some((
3739 alloc::vec![
3740 DataType::Text,
3741 DataType::SmallIntArray,
3742 DataType::TextArray
3743 ],
3744 rows,
3745 ));
3746 (DataType::Text, alloc::vec::Vec::new())
3747 }
3748 other => {
3749 // v7.39 (round 622, S05a) — see table_access.rs:
3750 // the same sentence, and it is a type mismatch.
3751 return Err(EngineError::Eval(EvalError::TypeMismatch {
3752 detail: alloc::format!(
3753 "unnest() expects an array argument, got {}",
3754 crate::conversions::pg_type_name_for_error_opt(other.data_type())
3755 ),
3756 }));
3757 }
3758 };
3759 if let Some(m) = return_multi {
3760 m
3761 } else {
3762 (alloc::vec![elem_dtype], rows)
3763 }
3764 };
3765 let alias = primary
3766 .alias
3767 .clone()
3768 .unwrap_or_else(|| "unnest".to_string());
3769 // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3770 // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3771 // entries map positionally over the value columns. Without
3772 // the column list, a single column falls back to the table
3773 // alias (pre-v7.13.2 behaviour); multi-arg columns default
3774 // to PG's `unnest`.
3775 let n_vals = dtypes.len();
3776 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3777 .iter()
3778 .enumerate()
3779 .map(|(i, dt)| {
3780 let name = primary
3781 .unnest_column_aliases
3782 .get(i)
3783 .cloned()
3784 .unwrap_or_else(|| {
3785 if let Some(names) = composite_names {
3786 names
3787 .get(i)
3788 .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3789 } else if n_vals == 1 {
3790 alias.clone()
3791 } else {
3792 "unnest".to_string()
3793 }
3794 });
3795 ColumnSchema::new(name, *dt, true)
3796 })
3797 .collect();
3798 // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3799 // parser desugared a base-type-returning function here (see
3800 // TableRef::scalar_fn_item); the marker rides the column so it survives
3801 // every EvalContext an inner stage rebuilds.
3802 if primary.scalar_fn_item && schema_cols.len() == 1 {
3803 schema_cols[0].scalar_row_source = true;
3804 }
3805 // WITH ORDINALITY — trailing BIGINT counting rows from 1
3806 // in element order. The alias entry after the value
3807 // columns renames it (PG default: `ordinality`).
3808 let rows = if primary.with_ordinality {
3809 let ord_name = primary
3810 .unnest_column_aliases
3811 .get(n_vals)
3812 .cloned()
3813 .unwrap_or_else(|| "ordinality".to_string());
3814 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3815 rows.into_iter()
3816 .enumerate()
3817 .map(|(i, row)| {
3818 let mut vals = row.values.clone();
3819 vals.push(Value::BigInt(i as i64 + 1));
3820 Row::new(vals)
3821 })
3822 .collect()
3823 } else {
3824 rows
3825 };
3826 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3827 // `EvalContext::new` drops it and every catalog-dependent cast
3828 // (regclass / enum / composite / domain) silently degrades.
3829 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3830 // Apply WHERE.
3831 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3832 let mut out = alloc::vec::Vec::with_capacity(rows.len());
3833 for row in rows {
3834 cancel.check()?;
3835 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3836 if matches!(v, Value::Bool(true)) {
3837 out.push(row);
3838 }
3839 }
3840 out
3841 } else {
3842 rows
3843 };
3844 // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3845 // unnest source. Same routing the relational scan path
3846 // already takes — without it `SELECT COUNT(*) FROM
3847 // unnest(ARRAY[…])` either errored at projection time or
3848 // returned the wrong shape.
3849 if aggregate::uses_aggregate(stmt) {
3850 // v7.29 — a per-query memo so correlated scalar
3851 // subqueries batch-evaluate once (group map) instead of
3852 // executing per group.
3853 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3854 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3855 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3856 .map_err(|err| match err {
3857 EngineError::Eval(ev) => ev,
3858 other => eval::EvalError::TypeMismatch {
3859 detail: alloc::format!("{other}"),
3860 },
3861 })
3862 };
3863 // v7.39 (round 656) — hand the rows over as they are rather than
3864 // collecting a second vector of `RowRef` wrappers. Note this is
3865 // a set-returning-function path, NOT the relational scan: the
3866 // measured O(rows) cost lived in `run_single_table_aggregate`,
3867 // and converting these four first was a miss that cost a full
3868 // round — every test stayed green and the number did not move.
3869 let agg = aggregate::run(
3870 stmt,
3871 crate::join::AggRows::Owned(&filtered),
3872 &schema_cols,
3873 Some(&alias),
3874 Some(&agg_correlated),
3875 self.parallel_runner.0.as_deref(),
3876 Some(self.active_catalog()),
3877 Some(self),
3878 )?;
3879 return self.finish_agg_result(agg, stmt, cancel);
3880 }
3881 // Projection.
3882 let projection = build_projection(
3883 &stmt.items,
3884 &schema_cols,
3885 &alias,
3886 self.speaks_mysql,
3887 Some(self.active_catalog()),
3888 )?;
3889 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3890 alloc::vec::Vec::with_capacity(filtered.len());
3891 // v7.19 P5 — Set-Returning-Function in projection
3892 // position (PG `SELECT unnest(arr) FROM t` shape). When a
3893 // SELECT item evaluates to a top-level unnest(arr) call,
3894 // expand it: for each input row, evaluate the array, emit
3895 // one output row per element, broadcasting non-SRF
3896 // projections from the same input row. Multi-SRF + LCM
3897 // padding stays a documented carve-out; mailrs uses
3898 // single-SRF for redirect_uris.
3899 // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3900 // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3901 let srf_idxs = self.srf_target_idxs(&projection);
3902 // v7.39 (round 621) — which input row each output row came from. An
3903 // SRF turns one input row into many, and the ORDER BY below used to
3904 // index the EXPANDED rows by the INPUT row's position: the result was
3905 // silently truncated to the input row count and left unsorted, so
3906 // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3907 // answered three of its six rows, in no order. Without the ORDER BY
3908 // the same query was already right.
3909 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3910 if !srf_idxs.is_empty() {
3911 let (rows, src) =
3912 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3913 projected_rows = rows;
3914 src_of_row = src;
3915 } else {
3916 // v7.24 (round-16 B) — select-list subqueries resolve
3917 // per row (correlated-aware; plain exprs take the fast
3918 // path inside).
3919 let mut proj_memo = memoize::MemoizeCache::default();
3920 for row in &filtered {
3921 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3922 for p in &projection {
3923 vals.push(self.eval_expr_with_correlated(
3924 &p.expr,
3925 row,
3926 &scan_ctx,
3927 cancel,
3928 Some(&mut proj_memo),
3929 )?);
3930 }
3931 projected_rows.push(Row::new(vals));
3932 }
3933 }
3934 // ORDER BY / LIMIT — apply on the projected rows (cheap;
3935 // unnest result sets are small by design).
3936 let columns: alloc::vec::Vec<ColumnSchema> = projection
3937 .iter()
3938 // v7.39 (read01 round 54) — keep the column's enum identity through
3939 // the projection (it lives outside the DataType lattice), or a
3940 // derived table / UNION / windowed result forgets it and any outer
3941 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
3942 .map(|p| p.to_column_schema())
3943 .collect();
3944 // Re-evaluate ORDER BY against the source schema (pre-projection
3945 // so col refs by name still resolve through `scan_ctx`).
3946 // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
3947 // column. Evaluated as an expression it is just the constant N: the same
3948 // key for every row, so the sort ran and changed nothing.
3949 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
3950 if !order_by.is_empty() {
3951 // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
3952 // A key that names a select-list item reads it out of the expanded
3953 // row (PG sorts AFTER the expansion); one that names a source
3954 // column the query does not project is evaluated on the input row
3955 // it came from, which is what `srf_order_output_cols` decides.
3956 let out_cols = if srf_idxs.is_empty() {
3957 alloc::vec![None; order_by.len()]
3958 } else {
3959 srf_order_output_cols(&order_by, &projection)
3960 };
3961 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
3962 .iter()
3963 .enumerate()
3964 .map(|(k, out)| -> Result<_, EngineError> {
3965 let src = src_of_row.get(k).copied().unwrap_or(k);
3966 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
3967 .iter()
3968 .zip(out_cols.iter())
3969 .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
3970 .collect();
3971 Ok((k, keys?))
3972 })
3973 .collect::<Result<_, _>>()?;
3974 indexed.sort_by(|a, b| {
3975 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
3976 let o = &order_by[idx];
3977 let cmp = order_by_value_cmp_in(
3978 o.desc,
3979 o.nulls_first,
3980 ka,
3981 kb,
3982 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
3983 );
3984 if cmp != core::cmp::Ordering::Equal {
3985 return cmp;
3986 }
3987 }
3988 core::cmp::Ordering::Equal
3989 });
3990 projected_rows = indexed
3991 .into_iter()
3992 .map(|(i, _)| projected_rows[i].clone())
3993 .collect();
3994 }
3995 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
3996 if stmt.distinct {
3997 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
3998 // spec folds EVERY text position, so a column declared
3999 // `COLLATE utf8mb4_bin` had its values merged here exactly the
4000 // way 3b494b6e fixed on the main scan path. The projection is
4001 // already in scope at each of these sites, so the mask needs no
4002 // new plumbing -- it was simply never asked for.
4003 projected_rows = dedup_rows(
4004 projected_rows,
4005 FoldSpec::of_masks(
4006 scan_ctx.mysql_dialect,
4007 &fold_mask(&projection),
4008 &pad_mask(&projection),
4009 ),
4010 );
4011 }
4012 // LIMIT / OFFSET — apply at the tail.
4013 if let Some(offset) = stmt.offset_literal() {
4014 let off = (offset as usize).min(projected_rows.len());
4015 projected_rows.drain(..off);
4016 }
4017 if let Some(limit) = stmt.limit_literal() {
4018 projected_rows.truncate(limit as usize);
4019 }
4020 Ok(QueryResult::Rows {
4021 columns,
4022 rows: projected_rows,
4023 })
4024 }
4025
4026 /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
4027 /// step])` set-returning source. Mirrors `exec_select_unnest`'s
4028 /// shape: evaluate the arg list once against an empty row,
4029 /// materialise the row stream by stepping start → stop, then
4030 /// route through the standard WHERE / projection / ORDER BY /
4031 /// LIMIT pipeline. Two arg-type combos in v7.17:
4032 /// * integer / integer [/ integer] — SmallInt, Int, BigInt
4033 /// (widened to BigInt internally; step defaults to 1)
4034 /// * timestamp / timestamp / interval — date-range
4035 /// iteration (mailrs's daily-report pattern)
4036 fn exec_select_generate_series(
4037 &self,
4038 stmt: &SelectStatement,
4039 primary: &TableRef,
4040 cancel: CancelToken<'_>,
4041 ) -> Result<QueryResult, EngineError> {
4042 let args = primary
4043 .generate_series_args
4044 .as_ref()
4045 .expect("caller guards generate_series_args.is_some()");
4046 let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
4047 let alias = primary
4048 .alias
4049 .clone()
4050 .unwrap_or_else(|| "generate_series".to_string());
4051 // `AS t(n)` — the first column-alias entry renames the
4052 // series column (PG semantics); bare alias keeps the
4053 // pre-existing behaviour of naming the column after it.
4054 let col_name = primary
4055 .unnest_column_aliases
4056 .first()
4057 .cloned()
4058 .unwrap_or_else(|| alias.clone());
4059 let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
4060 let mut schema_cols = alloc::vec![col_schema.clone()];
4061 // WITH ORDINALITY — trailing BIGINT counting rows from 1;
4062 // the second column-alias entry renames it.
4063 let rows = if primary.with_ordinality {
4064 let ord_name = primary
4065 .unnest_column_aliases
4066 .get(1)
4067 .cloned()
4068 .unwrap_or_else(|| "ordinality".to_string());
4069 schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4070 rows.into_iter()
4071 .enumerate()
4072 .map(|(i, row)| {
4073 let mut vals = row.values.clone();
4074 vals.push(Value::BigInt(i as i64 + 1));
4075 Row::new(vals)
4076 })
4077 .collect()
4078 } else {
4079 rows
4080 };
4081 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4082 // `EvalContext::new` drops it and every catalog-dependent cast
4083 // (regclass / enum / composite / domain) silently degrades.
4084 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4085 // WHERE.
4086 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4087 let mut out = alloc::vec::Vec::with_capacity(rows.len());
4088 for row in rows {
4089 cancel.check()?;
4090 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4091 if matches!(v, Value::Bool(true)) {
4092 out.push(row);
4093 }
4094 }
4095 out
4096 } else {
4097 rows
4098 };
4099 // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4100 // returning sources. When the SELECT projection contains
4101 // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4102 // …) we route the filtered row stream through the same
4103 // aggregate executor the relational scan path uses, so
4104 // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4105 // a single 100 row instead of erroring at projection
4106 // time. GROUP BY / HAVING / ORDER BY over the aggregate
4107 // output all ride through `aggregate::run`.
4108 if aggregate::uses_aggregate(stmt) {
4109 // v7.29 — a per-query memo so correlated scalar
4110 // subqueries batch-evaluate once (group map) instead of
4111 // executing per group.
4112 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4113 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4114 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4115 .map_err(|err| match err {
4116 EngineError::Eval(ev) => ev,
4117 other => eval::EvalError::TypeMismatch {
4118 detail: alloc::format!("{other}"),
4119 },
4120 })
4121 };
4122 // v7.39 (round 656) — hand the rows over as they are rather than
4123 // collecting a second vector of `RowRef` wrappers. Note this is
4124 // a set-returning-function path, NOT the relational scan: the
4125 // measured O(rows) cost lived in `run_single_table_aggregate`,
4126 // and converting these four first was a miss that cost a full
4127 // round — every test stayed green and the number did not move.
4128 let agg = aggregate::run(
4129 stmt,
4130 crate::join::AggRows::Owned(&filtered),
4131 &schema_cols,
4132 Some(&alias),
4133 Some(&agg_correlated),
4134 self.parallel_runner.0.as_deref(),
4135 Some(self.active_catalog()),
4136 Some(self),
4137 )?;
4138 return self.finish_agg_result(agg, stmt, cancel);
4139 }
4140 // Projection.
4141 let projection = build_projection(
4142 &stmt.items,
4143 &schema_cols,
4144 &alias,
4145 self.speaks_mysql,
4146 Some(self.active_catalog()),
4147 )?;
4148 // v7.39 (round 621) — and here, for the same reason.
4149 let srf_idxs = self.srf_target_idxs(&projection);
4150 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4151 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4152 alloc::vec::Vec::with_capacity(filtered.len());
4153 let mut proj_memo = memoize::MemoizeCache::default();
4154 if !srf_idxs.is_empty() {
4155 let (rows, src) =
4156 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4157 projected_rows = rows;
4158 src_of_row = src;
4159 } else {
4160 for row in &filtered {
4161 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4162 for p in &projection {
4163 // v7.24 (round-16 B) — correlated-aware.
4164 vals.push(self.eval_expr_with_correlated(
4165 &p.expr,
4166 row,
4167 &scan_ctx,
4168 cancel,
4169 Some(&mut proj_memo),
4170 )?);
4171 }
4172 projected_rows.push(Row::new(vals));
4173 }
4174 }
4175 let columns: alloc::vec::Vec<ColumnSchema> = projection
4176 .iter()
4177 // v7.39 (read01 round 54) — keep the column's enum identity through
4178 // the projection (it lives outside the DataType lattice), or a
4179 // derived table / UNION / windowed result forgets it and any outer
4180 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4181 .map(|p| p.to_column_schema())
4182 .collect();
4183 // ORDER BY against the source schema.
4184 // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4185 // more of them than there were inputs), and a positional key means the
4186 // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4187 // and what the other two synthetic-source tails already did.
4188 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4189 if !order_by.is_empty() {
4190 let out_cols = if srf_idxs.is_empty() {
4191 alloc::vec![None; order_by.len()]
4192 } else {
4193 srf_order_output_cols(&order_by, &projection)
4194 };
4195 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4196 .iter()
4197 .enumerate()
4198 .map(|(k, out)| -> Result<_, EngineError> {
4199 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4200 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4201 .iter()
4202 .zip(out_cols.iter())
4203 .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4204 .collect();
4205 Ok((k, keys?))
4206 })
4207 .collect::<Result<_, _>>()?;
4208 indexed.sort_by(|a, b| {
4209 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4210 let o = &stmt.order_by[idx];
4211 let cmp = order_by_value_cmp_in(
4212 o.desc,
4213 o.nulls_first,
4214 ka,
4215 kb,
4216 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4217 );
4218 if cmp != core::cmp::Ordering::Equal {
4219 return cmp;
4220 }
4221 }
4222 core::cmp::Ordering::Equal
4223 });
4224 projected_rows = indexed
4225 .into_iter()
4226 .map(|(i, _)| projected_rows[i].clone())
4227 .collect();
4228 }
4229 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4230 if stmt.distinct {
4231 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4232 // spec folds EVERY text position, so a column declared
4233 // `COLLATE utf8mb4_bin` had its values merged here exactly the
4234 // way 3b494b6e fixed on the main scan path. The projection is
4235 // already in scope at each of these sites, so the mask needs no
4236 // new plumbing -- it was simply never asked for.
4237 projected_rows = dedup_rows(
4238 projected_rows,
4239 FoldSpec::of_masks(
4240 scan_ctx.mysql_dialect,
4241 &fold_mask(&projection),
4242 &pad_mask(&projection),
4243 ),
4244 );
4245 }
4246 if let Some(offset) = stmt.offset_literal() {
4247 let off = (offset as usize).min(projected_rows.len());
4248 projected_rows.drain(..off);
4249 }
4250 if let Some(limit) = stmt.limit_literal() {
4251 projected_rows.truncate(limit as usize);
4252 }
4253 Ok(QueryResult::Rows {
4254 columns,
4255 rows: projected_rows,
4256 })
4257 }
4258
4259 /// The FROM shapes that are not an ordinary table scan — joins, the
4260 /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4261 ///
4262 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4263 /// reason round 848 established in the parser: a debug build gives
4264 /// EVERY branch's locals a slot in the frame, whichever branch runs.
4265 /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4266 /// stacks several of them; a plain scan reaches none of these
4267 /// branches. Moving them out took the frame to 52,336.
4268 ///
4269 /// `Ok(None)` means "not one of these shapes, carry on".
4270 #[inline(never)]
4271 fn try_from_shape_paths(
4272 &self,
4273 stmt: &SelectStatement,
4274 from: &spg_sql::ast::FromClause,
4275 cancel: CancelToken<'_>,
4276 ) -> Result<Option<QueryResult>, EngineError> {
4277 if !from.joins.is_empty() {
4278 // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4279 // elimination: when a LEFT JOIN's right side is referenced
4280 // ONLY in the ON equality and the right-side join key is
4281 // UNIQUE/PK, the join preserves outer cardinality exactly
4282 // and contributes no values used downstream. Drop the
4283 // entire join. PG does this on the
4284 // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4285 // — A's row count is what survives, B never has to be
4286 // touched.
4287 if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4288 return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4289 }
4290 // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4291 // the v7.32 joinfold rewrite that turns inner JOINs into a
4292 // single-table scan when the catalogue can prove key-only
4293 // dependency. Tests use this to assert "without joinfold,
4294 // the join still executes correctly" (joinfold is a
4295 // semantically-equivalent rewrite, not a correctness fix).
4296 if !self.env_cfg().disable_joinfold {
4297 if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4298 return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4299 }
4300 }
4301 return self.exec_joined_select(stmt, from, cancel).map(Some);
4302 }
4303 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4304 // single-column table at SELECT entry by evaluating the
4305 // expression once against the empty row (UNNEST is
4306 // uncorrelated in v7.11; correlated / LATERAL unnest is a
4307 // v7.12 carve-out). Build a virtual `Table` in a heap-only
4308 // catalog, then route to the regular scan path.
4309 if from.primary.unnest_expr.is_some() {
4310 return self
4311 .exec_select_unnest(stmt, &from.primary, cancel)
4312 .map(Some);
4313 }
4314 // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4315 // returning function. Same dispatch shape as unnest but
4316 // emits a two-column (key TEXT, value TEXT) row stream.
4317 if from.primary.jsonb_each_text_arg.is_some() {
4318 return self
4319 .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4320 .map(Some);
4321 }
4322 // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4323 // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4324 // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4325 // array form. Each function runs; the results zip in LOCKSTEP with the
4326 // shorter padded to NULL — the SAME rule the target-list SRFs follow
4327 // (round 67), which is why `srf_values` is what evaluates each entry.
4328 if from.primary.rows_from.is_some() {
4329 let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4330 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4331 if let Some(col) = schema_cols.get_mut(i) {
4332 col.name = new_name.clone();
4333 }
4334 }
4335 let alias = from
4336 .primary
4337 .alias
4338 .clone()
4339 .unwrap_or_else(|| from.primary.name.clone());
4340 return self
4341 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4342 .map(Some);
4343 }
4344 // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4345 // COLUMNS (...))`. Materialise the row stream + schema by
4346 // walking the row path, then run the regular pipeline over it.
4347 if let Some(jt) = &from.primary.json_table {
4348 let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4349 let alias = from
4350 .primary
4351 .alias
4352 .clone()
4353 .unwrap_or_else(|| from.primary.name.clone());
4354 return self
4355 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4356 .map(Some);
4357 }
4358 if from.primary.table_fn_call.is_some() {
4359 let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4360 // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4361 // (from 1, in output order) AFTER the function's own columns. The
4362 // alias list names it like any other, which is why it is appended
4363 // BEFORE the renaming pass below.
4364 let rows = if from.primary.with_ordinality {
4365 schema_cols.push(ColumnSchema::new(
4366 "ordinality".to_string(),
4367 DataType::BigInt,
4368 false,
4369 ));
4370 rows.into_iter()
4371 .enumerate()
4372 .map(|(i, r)| {
4373 let mut vals = r.values;
4374 vals.push(Value::BigInt(i as i64 + 1));
4375 Row::new(vals)
4376 })
4377 .collect()
4378 } else {
4379 rows
4380 };
4381 for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4382 if let Some(col) = schema_cols.get_mut(i) {
4383 col.name = new_name.clone();
4384 }
4385 }
4386 let alias = from
4387 .primary
4388 .alias
4389 .clone()
4390 .unwrap_or_else(|| from.primary.name.clone());
4391 return self
4392 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4393 .map(Some);
4394 }
4395 // v7.37.17 (17.6 siblings) — plain derived table in primary
4396 // position: `FROM ( SELECT … ) alias` (no joins). The inner
4397 // SELECT materialises once (it is uncorrelated by
4398 // construction), then the outer projection / WHERE /
4399 // aggregate / ORDER BY pipeline runs over the synthetic
4400 // table. Joined derived tables keep riding the LATERAL
4401 // machinery in join.rs.
4402 if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4403 // v7.39 (round 727) — flatten first. A simple derived table
4404 // (bare-column projection over one stored table, nothing that
4405 // changes cardinality or order) used to force the inner
4406 // SELECT through the SERIAL row-at-a-time projection pipeline
4407 // just to materialise a synthetic table the outer query then
4408 // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4409 // measured 18.6 ms against PG's 5 — and bare count over the
4410 // same filter WITHOUT the wrapper is 2 ms here, because it
4411 // rides the fused parallel lane. Rewriting to the unwrapped
4412 // form is PG's subquery pull-up; the whole tree gets the
4413 // fast lanes back.
4414 if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4415 return self.exec_select_cancel(&flat, cancel).map(Some);
4416 }
4417 // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4418 // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4419 // ORDER BY never changes the row count, and OFFSET drops
4420 // exactly k. The materialising path sorted 500k rows to
4421 // count 10k (57 ms); PG runs its parallel sort anyway
4422 // (28 ms). The rewrite skips the sort entirely on both
4423 // counts — a plan PG itself does not have.
4424 if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4425 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4426 }
4427 // v7.39 (round 743) — `count(*) OVER a derived whose only
4428 // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4429 // a constant-length array unnests to exactly k rows per
4430 // input row, NULL elements included. PG expands the set to
4431 // count it (6.6 ms on the panel cell); the identity doesn't.
4432 if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4433 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4434 }
4435 return self
4436 .exec_select_derived(stmt, &from.primary, cancel)
4437 .map(Some);
4438 }
4439 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4440 // [, step])` set-returning source. Dispatch mirrors UNNEST:
4441 // materialise the row stream from a single eval pass, then
4442 // run the regular projection / WHERE / ORDER BY / LIMIT
4443 // pipeline over the synthetic single-column table.
4444 if from.primary.generate_series_args.is_some() {
4445 return self
4446 .exec_select_generate_series(stmt, &from.primary, cancel)
4447 .map(Some);
4448 }
4449 Ok(None)
4450 }
4451
4452 /// Pick an index seek for this WHERE, if any of the four apply:
4453 /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4454 ///
4455 /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4456 /// frame reason on `try_from_shape_paths`: in a debug build a
4457 /// closure's locals belong to the enclosing frame, and this one is
4458 /// four seek attempts wide on a function that nests.
4459 #[inline(never)]
4460 fn pick_indexed_rows<'r>(
4461 &'r self,
4462 stmt: &SelectStatement,
4463 table: &'r spg_storage::Table,
4464 schema_cols: &[spg_storage::ColumnSchema],
4465 alias: &str,
4466 ctx: &crate::eval::EvalContext<'_>,
4467 seek_snapshot: &crate::Snapshot,
4468 ) -> Option<crate::index_access::Seeked<'r>> {
4469 stmt.where_.as_ref().and_then(|w| {
4470 // BTree / col=literal seek first — covers the v7.11.3 multi-
4471 // column AND case and the leading-column equality lookup.
4472 try_index_seek(
4473 w,
4474 schema_cols,
4475 self.active_catalog(),
4476 table,
4477 alias,
4478 seek_snapshot,
4479 ctx.mysql_dialect,
4480 )
4481 .or_else(|| {
4482 // v7.12.3 — GIN-accelerated `WHERE col @@
4483 // tsquery` when the column has a `USING gin`
4484 // index. Returns an over-approximate candidate
4485 // set; the WHERE re-eval loop below verifies
4486 // the full `@@` predicate per row.
4487 try_gin_seek(
4488 w,
4489 schema_cols,
4490 self.active_catalog(),
4491 table,
4492 alias,
4493 ctx,
4494 seek_snapshot,
4495 )
4496 .map(crate::index_access::Seeked::over_approximate)
4497 })
4498 .or_else(|| {
4499 // v7.15.0 — trigram-GIN-accelerated
4500 // `WHERE col LIKE / ILIKE '<pat>'` when the
4501 // column has a `gin_trgm_ops` GIN index.
4502 // Over-approximate candidate set; the WHERE
4503 // re-eval verifies the LIKE per row.
4504 try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4505 .map(crate::index_access::Seeked::over_approximate)
4506 })
4507 .or_else(|| {
4508 // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4509 // accelerated `WHERE col @> <jsonb_literal>`
4510 // when the column has a `USING gin` index. The
4511 // posting-list intersection returns an over-
4512 // approximate candidate set; the WHERE re-eval
4513 // verifies the full `@>` predicate per row.
4514 try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4515 .map(crate::index_access::Seeked::over_approximate)
4516 })
4517 })
4518 }
4519
4520 /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4521 /// the two `count(*)` short-circuits. Out-of-line for the frame
4522 /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4523 /// of them, and in a debug build their locals sit in the frame
4524 /// regardless.
4525 #[inline(never)]
4526 fn try_seek_fast_paths(
4527 &self,
4528 stmt: &SelectStatement,
4529 table: &spg_storage::Table,
4530 schema_cols: &[spg_storage::ColumnSchema],
4531 alias: &str,
4532 seek_snapshot: &crate::Snapshot,
4533 cancel: CancelToken<'_>,
4534 ) -> Result<Option<QueryResult>, EngineError> {
4535 if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4536 // NSW kNN dispatches against the hot-tier vector index only
4537 // (vector cells aren't promoted to cold segments), so wrap
4538 // the returned row indices as `Cow::Borrowed` for the
4539 // unified `materialise_in_order` shape.
4540 let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4541 .into_iter()
4542 .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4543 .collect();
4544 return materialise_in_order(stmt, schema_cols, alias, &ordered, self.speaks_mysql)
4545 .map(Some);
4546 }
4547
4548 // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4549 // the scan via the BTree iterator in the requested direction
4550 // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4551 // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4552 // the load-bearing consumer; this skips the materialise-every-
4553 // row + partial-sort tail entirely. Walker output is already
4554 // in ORDER BY order so `materialise_in_order` (no extra sort)
4555 // is the natural sink.
4556 if let Some(walked) = try_pk_walk_top_n(
4557 stmt,
4558 self.active_catalog(),
4559 table,
4560 schema_cols,
4561 alias,
4562 self,
4563 cancel,
4564 self.speaks_mysql,
4565 ) {
4566 return materialise_in_order(stmt, schema_cols, alias, &walked, self.speaks_mysql)
4567 .map(Some);
4568 }
4569
4570 // Index seek: if WHERE is `col = literal` (or commuted) and the
4571 // referenced column has an index, dispatch each locator through
4572 // the catalog (hot tier → borrow, cold tier → page-read +
4573 // decode) and iterate just those rows. Otherwise fall back to a
4574 // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4575 // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4576 // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4577 // we don't pay the row materialisation cost twice. Returns
4578 // a bare `Rows{count}` if the shape matches.
4579 if aggregate::uses_aggregate(stmt)
4580 && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4581 {
4582 return Ok(Some(out));
4583 }
4584 // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4585 // locators directly, skipping row materialisation + WHERE re-eval.
4586 if aggregate::uses_aggregate(stmt)
4587 && let Some(out) = self.try_count_star_indexed_range_fast(
4588 stmt,
4589 table,
4590 schema_cols,
4591 alias,
4592 seek_snapshot,
4593 )
4594 {
4595 return Ok(Some(out));
4596 }
4597 Ok(None)
4598 }
4599
4600 /// The two rewrites that must happen before the FROM clause is even
4601 /// looked at: a meta-view reference needs the catalog views
4602 /// materialised, and a windowed projection belongs to the window
4603 /// executor. Out-of-line for the frame reason on
4604 /// `try_from_shape_paths`.
4605 #[inline(never)]
4606 fn try_pre_from_paths(
4607 &self,
4608 stmt: &SelectStatement,
4609 cancel: CancelToken<'_>,
4610 ) -> Result<Option<QueryResult>, EngineError> {
4611 if !self.meta_views_materialised && select_references_meta_view(stmt) {
4612 return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4613 }
4614 // v4.12: window-function path. When the projection contains
4615 // any `name(args) OVER (...)` we route to the dedicated
4616 // executor — partition + sort + per-row window value before
4617 // the regular projection.
4618 if select_has_window(stmt) {
4619 // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4620 // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4621 // needs the aggregation done first, then windows over the grouped
4622 // rows. Rewrite to an aggregate derived subquery + outer window query
4623 // (which the window-over-derived path, D.13, executes). Only fires on
4624 // the currently-erroring agg+window+GROUP BY shape, so it can't
4625 // regress working window-only or aggregate-only queries.
4626 if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4627 return self.exec_select_cancel(&rewritten, cancel).map(Some);
4628 }
4629 return self.exec_select_with_window(stmt, cancel).map(Some);
4630 }
4631 Ok(None)
4632 }
4633
4634 /// A projection naming `ctid` or another system column: the schema
4635 /// has to be widened with them before the scan. Out-of-line for the
4636 /// frame reason on `try_from_shape_paths`.
4637 #[inline(never)]
4638 fn try_ctid_projection(
4639 &self,
4640 stmt: &SelectStatement,
4641 primary: &spg_sql::ast::TableRef,
4642 table: &spg_storage::Table,
4643 schema_cols: &[spg_storage::ColumnSchema],
4644 alias: &str,
4645 cancel: CancelToken<'_>,
4646 ) -> Result<Option<QueryResult>, EngineError> {
4647 if references_ctid(stmt) {
4648 let snapshot = self.current_snapshot();
4649 let mut ext_cols = schema_cols.to_vec();
4650 for name in SYSTEM_COLUMNS {
4651 ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4652 }
4653 let table_oid =
4654 crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4655 .unwrap_or(0);
4656 let headers = table.headers();
4657 let rows: Vec<Row<'static>> = table
4658 .scan_visible(&snapshot)
4659 .map(|(i, r)| {
4660 let mut vals = r.values.clone();
4661 // One block, offsets from 1, as PG numbers them.
4662 vals.push(Value::Tid(0, i as u32 + 1));
4663 let h = headers.get(i);
4664 vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4665 vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4666 // SPG keeps no per-statement command ids; PG shows 0 for
4667 // every row a reader can see, which is every row here.
4668 vals.push(Value::Cid(0));
4669 vals.push(Value::Cid(0));
4670 vals.push(Value::BigInt(table_oid));
4671 Row::new(vals)
4672 })
4673 .collect();
4674 return self
4675 .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4676 .map(Some);
4677 }
4678 Ok(None)
4679 }
4680
4681 /// A sequence read as a one-row relation (`SELECT last_value FROM
4682 /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4683 /// the frame reason on `try_from_shape_paths`.
4684 #[inline(never)]
4685 fn try_sequence_relation(
4686 &self,
4687 stmt: &SelectStatement,
4688 primary: &spg_sql::ast::TableRef,
4689 cancel: CancelToken<'_>,
4690 ) -> Result<Option<QueryResult>, EngineError> {
4691 if self.active_catalog().get(&primary.name).is_none()
4692 && let Some(seq) = self.active_catalog().sequence(&primary.name)
4693 {
4694 let rows = alloc::vec![Row::new(alloc::vec![
4695 Value::BigInt(seq.last_value),
4696 Value::BigInt(0),
4697 Value::Bool(seq.is_called),
4698 ])];
4699 let schema_cols = alloc::vec![
4700 ColumnSchema::new("last_value", DataType::BigInt, false),
4701 ColumnSchema::new("log_cnt", DataType::BigInt, false),
4702 ColumnSchema::new("is_called", DataType::Bool, false),
4703 ];
4704 let alias = primary
4705 .alias
4706 .clone()
4707 .unwrap_or_else(|| primary.name.clone());
4708 return self
4709 .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4710 .map(Some);
4711 }
4712 Ok(None)
4713 }
4714
4715 pub(crate) fn exec_bare_select_cancel(
4716 &self,
4717 stmt: &SelectStatement,
4718 cancel: CancelToken<'_>,
4719 ) -> Result<QueryResult, EngineError> {
4720 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4721 // is meaningless without an ORDER BY; PG raises a hard
4722 // error and SPG mirrors the surface so the same DDL/app
4723 // path behaves identically on cutover.
4724 check_with_ties_requires_order_by(stmt)?;
4725 // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4726 // PG rejects window calls there outright. Checked here rather than
4727 // on the window path: `HAVING row_number() OVER () = 1` has no
4728 // window in its projection at all.
4729 crate::window::reject_window_in_row_clauses(stmt)?;
4730 // v7.39 (round 232) — the ORDER BY legality rules (positional
4731 // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4732 // check: before anything scans.
4733 crate::orderby::check_order_by_legality(stmt)?;
4734 // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4735 // equivalent statement the regular executor handles (merged join
4736 // columns collapse to a single unqualified output column; NATURAL
4737 // gets its common-column ON synthesised). The rewrite clears the
4738 // flags, so this re-entrant call is a no-op on the second pass.
4739 if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4740 return self.exec_bare_select_cancel(&rewritten, cancel);
4741 }
4742 // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4743 // exactly the group keys, IS a DISTINCT and was paying for the
4744 // aggregate executor to find that out. Same placement and shape
4745 // as the desugar above; the rewrite clears `group_by`, so the
4746 // re-entry is a no-op on the second pass. See `baregroup` for
4747 // what the gate rules out.
4748 if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4749 return self.exec_bare_select_cancel(&rewritten, cancel);
4750 }
4751 // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4752 // operand in a security-barrier subquery, then re-enter (the wrapped
4753 // operands are no longer bare RLS tables, so this is a no-op on the
4754 // second pass).
4755 if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4756 return self.exec_bare_select_cancel(&rewritten, cancel);
4757 }
4758 // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4759 // AND the RLS USING predicate into a single-table SELECT's WHERE.
4760 // Superuser sessions and non-RLS tables get `None` (no clone, no
4761 // change). Applied inline (shadowing `stmt`) rather than via re-entry
4762 // so it can't re-inject on a recursive pass.
4763 let rls_stmt;
4764 let stmt = match self.rls_select_predicate(stmt)? {
4765 Some(pred) => {
4766 let mut s = stmt.clone();
4767 s.where_ = Some(match s.where_.take() {
4768 Some(existing) => spg_sql::ast::Expr::Binary {
4769 lhs: alloc::boxed::Box::new(existing),
4770 op: spg_sql::ast::BinOp::And,
4771 rhs: alloc::boxed::Box::new(pred),
4772 },
4773 None => pred,
4774 });
4775 rls_stmt = s;
4776 &rls_stmt
4777 }
4778 None => stmt,
4779 };
4780 // v7.16.2 — same meta-view dispatch as
4781 // `exec_select_cancel`, applied here too because
4782 // `subquery_replacement` enters this function directly
4783 // for Exists / ScalarSubquery / InSubquery resolution
4784 // (bypassing the top-level entry to avoid double
4785 // subquery walking). Without this dispatch the subquery
4786 // hits `__spg_info_columns` and reports TableNotFound.
4787 if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4788 return Ok(done);
4789 }
4790 // Constant SELECT (no FROM) — evaluate each item once against an
4791 // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4792 // `SELECT '7'::INT`. Column references will surface as
4793 // ColumnNotFound on eval since the schema is empty.
4794 let Some(from) = &stmt.from else {
4795 return self.exec_constant_select(stmt);
4796 };
4797 // Multi-table FROM (one or more joined peers) goes through the
4798 // nested-loop join executor. Single-table FROM stays on the
4799 // existing scan + index-seek path.
4800 if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4801 return Ok(done);
4802 }
4803 // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4804 // tested — eight ORDER BY shapes byte-identical spilled against
4805 // in-memory, with 103 runs opened to prove the spill ran — and it
4806 // loses on wall clock, which is a hard stop whatever the memory
4807 // buys. Measured round 865, same psql client both sides, same
4808 // machine, row counts verified, and both sides confirmed to be
4809 // doing an external merge rather than an indexed walk:
4810 //
4811 // PG18 178.7 - 187.0 ms Sort Method: external merge, 85 MB
4812 // SPG spilled 269.7 - 299.6 ms 33 spill files at peak
4813 //
4814 // Non-overlapping, about 1.55x. Re-enable by restoring the call
4815 // below once that closes; nothing else has to change, which is
4816 // the point of it being a separate path.
4817 //
4818 // if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4819 // return Ok(done);
4820 // }
4821 //
4822 // v7.37 (round 882) — this walk stays unhooked, but its streaming
4823 // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4824 // bail in `try_exec_joined_streaming`. Collecting the answer was
4825 // most of what this one cost: handing rows over as the merge
4826 // produces them holds peak to the budget plus one row, and the
4827 // wall clock lands inside PG18's range rather than 1.55x outside
4828 // it. Numbers in `extsort.rs`'s header.
4829 let primary = &from.primary;
4830 // v7.39 (round 244) — a sequence is selectable as a one-row relation
4831 // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4832 // read it). Synthesize PG's three columns.
4833 if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4834 return Ok(done);
4835 }
4836 let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4837 StorageError::TableNotFound {
4838 name: primary.name.clone(),
4839 }
4840 })?;
4841 let schema_cols = &table.schema().columns;
4842 // The qualifier accepted on column refs is the alias (if any) else the
4843 // bare table name.
4844 let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4845 // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4846 // system columns at all: `SELECT ctid FROM t` answered "column
4847 // \"ctid\" does not exist", which takes out the dedup idiom every
4848 // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4849 // GROUP BY key)`.
4850 //
4851 // The value comes from the row's position, which the scan already
4852 // yields; the column is appended to the schema and the rows only
4853 // when the statement asks for it, so nothing else pays for it. That
4854 // also routes the query down the general path, past the index fast
4855 // paths below — they hand back rows without positions, and a ctid
4856 // that was sometimes right would be worse than none.
4857 if let Some(done) =
4858 self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4859 {
4860 return Ok(done);
4861 }
4862 let ctx = self.ev_ctx(schema_cols, Some(alias));
4863
4864 // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4865 // WHERE and an NSW index on `col` skips the full scan. The
4866 // walk returns rows already in ascending-distance order, so
4867 // ORDER BY / LIMIT are honoured implicitly.
4868 // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4869 // and thread it into every index-seek fast path below. No-op
4870 // today (every hot header is committed-alive).
4871 let seek_snapshot = self.current_snapshot();
4872 if let Some(done) =
4873 self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4874 {
4875 return Ok(done);
4876 }
4877 // full scan over the hot tier (cold-tier rows are only reached
4878 // via index seek in v5.1 — full table scans against cold-tier
4879 // data ship in v5.2 with the freezer's per-segment scan API).
4880 let indexed_rows =
4881 self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4882
4883 // Aggregate path: filter rows first, then hand off to the
4884 // aggregate executor which does its own projection + ORDER BY.
4885 if aggregate::uses_aggregate(stmt) {
4886 return self.run_single_table_aggregate(
4887 stmt,
4888 table,
4889 schema_cols,
4890 alias,
4891 indexed_rows,
4892 cancel,
4893 );
4894 }
4895 self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4896 }
4897
4898 /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4899 /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4900 /// uncorrelated FROM-primary case is the simpler shape, used by
4901 /// e2e pins. Materialises the (key, value) pair stream into a
4902 /// synthetic two-column TEXT table, then routes through the
4903 /// regular projection / WHERE / ORDER BY pipeline.
4904 /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4905 /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4906 /// item into (rows, schema). `outer_doc` is `Some` only when this
4907 /// is a NESTED level being expanded against a parent row item's
4908 /// already-parsed sub-document; the top-level call parses the doc
4909 /// expr itself. Row/column paths reuse the existing jsonpath
4910 /// evaluator (`json::json_table_path`); coercion reuses
4911 /// `coerce_value` on the JSON scalar text, so a json string
4912 /// coerces to DATE by its content, matching PG.
4913 #[allow(clippy::type_complexity)]
4914 pub(crate) fn json_table_rows(
4915 &self,
4916 jt: &spg_sql::ast::JsonTable,
4917 outer_doc: Option<&crate::json::JsonValue>,
4918 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4919 // Column schema is static (independent of data): flatten the
4920 // COLUMNS tree in declaration order (NESTED contributes its
4921 // children inline, the PG output shape).
4922 let schema = json_table_schema(&jt.columns);
4923
4924 // PASSING variables → a single JsonValue object the jsonpath
4925 // engine reads `$name` from.
4926 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4927 let ctx = EvalContext::new(&empty_schema, None);
4928 let dummy = Row::new(alloc::vec::Vec::new());
4929 let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4930 None
4931 } else {
4932 let mut entries = alloc::vec::Vec::new();
4933 for (name, e) in &jt.passing {
4934 let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
4935 entries.push((name.clone(), value_to_json_value(&v)));
4936 }
4937 Some(crate::json::JsonValue::Object(entries))
4938 };
4939
4940 // The document root: a NESTED level gets it from the parent;
4941 // the top level parses its doc expr.
4942 let root_owned;
4943 let root: &crate::json::JsonValue = match outer_doc {
4944 Some(d) => d,
4945 None => {
4946 let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
4947 let src = match &doc_val {
4948 Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
4949 Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
4950 other => {
4951 return Err(EngineError::Unsupported(alloc::format!(
4952 "JSON_TABLE document must be json/text, got {}",
4953 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4954 )));
4955 }
4956 };
4957 root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
4958 &root_owned
4959 }
4960 };
4961
4962 let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
4963 .map_err(EngineError::Eval)?;
4964 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
4965 for (idx, item) in items.iter().enumerate() {
4966 self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
4967 }
4968 Ok((rows, schema))
4969 }
4970
4971 /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
4972 /// Regular columns produce one value each; a NESTED column expands
4973 /// as an outer join (each nested match → one row sharing the
4974 /// parent cells; no nested match → one row with the nested cells
4975 /// NULL). Sibling NESTED at one level cross by concatenation of
4976 /// their independent expansions (PG's UNION-of-outer shape).
4977 fn json_table_emit_item(
4978 &self,
4979 jt: &spg_sql::ast::JsonTable,
4980 item: &crate::json::JsonValue,
4981 ordinality: usize,
4982 vars: Option<&crate::json::JsonValue>,
4983 out: &mut alloc::vec::Vec<Row<'static>>,
4984 ) -> Result<(), EngineError> {
4985 use spg_sql::ast::JsonTableColumn as C;
4986 // Parent cells (regular + ordinality), left-to-right; NESTED
4987 // columns contribute a run of child cells appended after.
4988 let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
4989 let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
4990 alloc::vec::Vec::new();
4991 let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4992 for col in &jt.columns {
4993 match col {
4994 C::Ordinality { .. } => {
4995 parent_cells.push(Value::BigInt(ordinality as i64 + 1));
4996 }
4997 C::Regular { .. } => {
4998 parent_cells.push(self.json_table_column_value(col, item, vars)?);
4999 }
5000 C::Nested { path, columns } => {
5001 // Recurse: a nested JSON_TABLE over `item` filtered
5002 // by `path`, with the same PASSING vars.
5003 let sub = spg_sql::ast::JsonTable {
5004 doc: jt.doc.clone(), // unused (outer_doc provided)
5005 row_path: path.clone(),
5006 columns: columns.clone(),
5007 passing: alloc::vec::Vec::new(),
5008 };
5009 let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
5010 nested_widths.push(nschema.len());
5011 nested_runs.push(nrows);
5012 }
5013 }
5014 }
5015 if nested_runs.is_empty() {
5016 out.push(Row::new(parent_cells));
5017 return Ok(());
5018 }
5019 // PG sibling-NESTED semantics: each sibling expands
5020 // INDEPENDENTLY and the results CONCATENATE — a row from
5021 // sibling s fills only s's cells, every other sibling's cells
5022 // NULL. An empty sibling contributes ZERO rows (not a NULL
5023 // row). Only when EVERY sibling is empty does the parent still
5024 // emit one all-NULL row (the outer-join guarantee that a parent
5025 // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
5026 // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
5027 let before = out.len();
5028 for (s_idx, run) in nested_runs.iter().enumerate() {
5029 for nrow in run {
5030 let mut cells = parent_cells.clone();
5031 for (o_idx, w) in nested_widths.iter().enumerate() {
5032 if o_idx == s_idx {
5033 cells.extend(nrow.values.iter().cloned());
5034 } else {
5035 for _ in 0..*w {
5036 cells.push(Value::Null);
5037 }
5038 }
5039 }
5040 out.push(Row::new(cells));
5041 }
5042 }
5043 if out.len() == before {
5044 // Every sibling empty → one all-NULL nested row.
5045 let mut cells = parent_cells.clone();
5046 for w in &nested_widths {
5047 for _ in 0..*w {
5048 cells.push(Value::Null);
5049 }
5050 }
5051 out.push(Row::new(cells));
5052 }
5053 Ok(())
5054 }
5055
5056 /// v7.39 (round 205) — evaluate one Regular column against a row
5057 /// item: EXISTS → bool; else path → at most one value, coerced to
5058 /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
5059 fn json_table_column_value(
5060 &self,
5061 col: &spg_sql::ast::JsonTableColumn,
5062 item: &crate::json::JsonValue,
5063 vars: Option<&crate::json::JsonValue>,
5064 ) -> Result<Value<'static>, EngineError> {
5065 use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
5066 let C::Regular {
5067 name,
5068 ty,
5069 path,
5070 exists,
5071 format_json,
5072 wrapper,
5073 on_empty,
5074 on_error,
5075 } = col
5076 else {
5077 unreachable!("caller guards Regular");
5078 };
5079 let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5080 if *exists {
5081 return Ok(Value::Bool(!matches.is_empty()));
5082 }
5083 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5084 let ctx = EvalContext::new(&empty_schema, None);
5085 let dummy = Row::new(alloc::vec::Vec::new());
5086 let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5087 match b {
5088 B::Null => Ok(Some(Value::Null)),
5089 B::Error => Ok(None),
5090 B::Default(e) => Ok(Some(
5091 eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5092 )),
5093 }
5094 };
5095 // Empty match set → ON EMPTY.
5096 if matches.is_empty() {
5097 return match default_of(on_empty)? {
5098 Some(v) => coerce_json_table_default(v, *ty, name),
5099 None => Err(EngineError::Unsupported(alloc::format!(
5100 "no SQL/JSON item found for JSON_TABLE column {name:?}"
5101 ))),
5102 };
5103 }
5104 let first = &matches[0];
5105 // FORMAT JSON: return the PG-canonical json representation.
5106 // WITH WRAPPER wraps the whole match SET in an array (even a
5107 // single scalar → `[5]`); without it, the single match's json.
5108 if *format_json {
5109 let text = if *wrapper {
5110 crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5111 } else {
5112 first.canonical_json_text()
5113 };
5114 return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5115 }
5116 if first.is_json_null() {
5117 return Ok(Value::Null);
5118 }
5119 // Coerce the scalar text to the declared type; on failure → ON
5120 // ERROR (default NULL, DEFAULT expr, or raise).
5121 let dt = crate::conversions::column_type_to_data_type(*ty);
5122 let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5123 match crate::conversions::coerce_value(scalar, dt, name, 0) {
5124 Ok(v) => Ok(v),
5125 Err(e) => match default_of(on_error)? {
5126 Some(v) => coerce_json_table_default(v, *ty, name),
5127 None => Err(e),
5128 },
5129 }
5130 }
5131
5132 /// table function into (rows, default schema). Dispatch by name.
5133 pub(crate) fn table_fn_rows(
5134 &self,
5135 primary: &TableRef,
5136 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5137 let (fn_name, args) = primary
5138 .table_fn_call
5139 .as_deref()
5140 .expect("caller guards table_fn_call.is_some()");
5141 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5142 let ctx = EvalContext::new(&empty_schema, None);
5143 let dummy_row = Row::new(alloc::vec::Vec::new());
5144 let arg0: Option<Value<'static>> = match args.first() {
5145 Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5146 None => None,
5147 };
5148 match fn_name.as_str() {
5149 // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5150 // `…_recordset` (+ json_ variants). The row shape is the BASE
5151 // argument's declared type — a table's or a composite type's
5152 // column list — which only the catalog knows, so the parser hands
5153 // the raw arguments here rather than desugaring blind.
5154 "jsonb_populate_record"
5155 | "json_populate_record"
5156 | "jsonb_populate_recordset"
5157 | "json_populate_recordset" => {
5158 let type_name = match args.first() {
5159 Some(Expr::Cast {
5160 target: spg_sql::ast::CastTarget::Named(n),
5161 ..
5162 }) => n.clone(),
5163 _ => {
5164 return Err(EngineError::Unsupported(alloc::format!(
5165 "{fn_name}(): first argument must name a row type, \
5166 e.g. NULL::mytable"
5167 )));
5168 }
5169 };
5170 let cat = self.active_catalog();
5171 let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5172 t.schema().columns.clone()
5173 } else if let Some(c) = cat.composite_types().get(&type_name) {
5174 c.fields
5175 .iter()
5176 .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5177 .collect()
5178 } else {
5179 return Err(EngineError::Unsupported(alloc::format!(
5180 "type \"{type_name}\" does not exist"
5181 )));
5182 };
5183 let json_arg = match args.get(1) {
5184 Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5185 None => Value::Null,
5186 };
5187 // The set form iterates the JSON array; the scalar form is
5188 // the one-element case of the same walk.
5189 let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5190 crate::json::array_element_rows(&json_arg, false, fn_name)
5191 .map_err(EngineError::Eval)?
5192 .into_iter()
5193 .map(|s| s.map_or(Value::Null, Value::json))
5194 .collect()
5195 } else if matches!(json_arg, Value::Null) {
5196 alloc::vec::Vec::new()
5197 } else {
5198 alloc::vec![json_arg]
5199 };
5200 let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5201 for doc in &docs {
5202 let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5203 for c in &cols {
5204 // `->>` semantics: a missing key is NULL, present keys
5205 // arrive as text and cast to the declared column type.
5206 let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5207 .map_err(EngineError::Eval)?;
5208 let v = if matches!(raw, Value::Null) {
5209 Value::Null
5210 } else {
5211 crate::conversions::coerce_value(raw, c.ty, "", 0)
5212 .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5213 };
5214 vals.push(v);
5215 }
5216 rows.push(Row::new(vals));
5217 }
5218 Ok((rows, cols))
5219 }
5220 // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5221 // a text[] of 'name=value' reloptions/fdw options → one
5222 // (option_name, option_value) row per element. NULL or an
5223 // empty array yields zero rows (PG); an element without
5224 // '=' carries a NULL option_value, matching PG's split.
5225 "pg_options_to_table" => {
5226 let schema = alloc::vec![
5227 ColumnSchema::new("option_name", DataType::Text, true),
5228 ColumnSchema::new("option_value", DataType::Text, true),
5229 ];
5230 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5231 if let Some(Value::TextArray(items)) = arg0 {
5232 for item in items.into_iter().flatten() {
5233 let (name, value) = match item.split_once('=') {
5234 Some((n, v)) => (Value::text(n), Value::text(v)),
5235 None => (Value::text(item.as_str()), Value::Null),
5236 };
5237 rows.push(Row::new(alloc::vec![name, value]));
5238 }
5239 }
5240 Ok((rows, schema))
5241 }
5242 // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5243 // PG18's per-sequence state SRF, (last_value, is_called).
5244 // pg_dump reads it joined to pg_sequence for every dumped
5245 // sequence's setval line. The oid resolves through the
5246 // same relation_oid mapping seqrelid publishes.
5247 "pg_get_sequence_data" => {
5248 let schema = alloc::vec![
5249 ColumnSchema::new("last_value", DataType::BigInt, false),
5250 ColumnSchema::new("is_called", DataType::Bool, false),
5251 ];
5252 let want = match arg0 {
5253 Some(Value::Int(n)) => i64::from(n),
5254 Some(Value::BigInt(n)) => n,
5255 _ => {
5256 return Err(EngineError::Unsupported(
5257 "pg_get_sequence_data(): argument must be a sequence oid".into(),
5258 ));
5259 }
5260 };
5261 let cat = self.active_catalog();
5262 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5263 for (name, def) in cat.sequences_all() {
5264 if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5265 rows.push(Row::new(alloc::vec![
5266 Value::BigInt(def.last_value),
5267 Value::Bool(def.is_called),
5268 ]));
5269 break;
5270 }
5271 }
5272 Ok((rows, schema))
5273 }
5274 "pg_partition_tree" => {
5275 let cols = alloc::vec![
5276 ColumnSchema::new("relid".to_string(), DataType::Text, true),
5277 ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5278 ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5279 ColumnSchema::new("level".to_string(), DataType::Int, true),
5280 ];
5281 let Some(Value::Text(name)) = &arg0 else {
5282 // NULL (or missing) argument → zero rows (PG).
5283 return Ok((alloc::vec::Vec::new(), cols));
5284 };
5285 let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5286 if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5287 return Err(EngineError::Unsupported(alloc::format!(
5288 "relation \"{name}\" does not exist"
5289 )));
5290 }
5291 let rows = entries
5292 .into_iter()
5293 .map(|(relid, parent, isleaf, level)| {
5294 Row::new(alloc::vec![
5295 Value::text(relid),
5296 parent.map_or(Value::Null, Value::text),
5297 Value::Bool(isleaf),
5298 #[allow(clippy::cast_possible_truncation)]
5299 Value::Int(level as i32),
5300 ])
5301 })
5302 .collect();
5303 Ok((rows, cols))
5304 }
5305 "pg_partition_ancestors" => {
5306 let cols =
5307 alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5308 let Some(Value::Text(name)) = &arg0 else {
5309 return Ok((alloc::vec::Vec::new(), cols));
5310 };
5311 let cat = self.active_catalog();
5312 if cat.get(name.as_ref()).is_none() {
5313 return Err(EngineError::Unsupported(alloc::format!(
5314 "relation \"{name}\" does not exist"
5315 )));
5316 }
5317 // A relation outside any partition tree yields no rows (PG).
5318 let in_tree = cat
5319 .get(name.as_ref())
5320 .is_some_and(|t| t.schema().partition_role.is_some());
5321 let rows = if in_tree {
5322 crate::partition_walks::ancestors_of(cat, name.as_ref())
5323 .into_iter()
5324 .map(|n| Row::new(alloc::vec![Value::text(n)]))
5325 .collect()
5326 } else {
5327 alloc::vec::Vec::new()
5328 };
5329 Ok((rows, cols))
5330 }
5331 // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5332 // saw, what each token was called, which dictionary took it
5333 // and what came out. It is a projection of the same tokenizer
5334 // and the same map the indexer uses, so it cannot describe a
5335 // pipeline other than the one that runs.
5336 "ts_debug" => {
5337 use crate::fts::{TokenType, TsDict};
5338 let cols = alloc::vec![
5339 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5340 ColumnSchema::new("description".to_string(), DataType::Text, false),
5341 ColumnSchema::new("token".to_string(), DataType::Text, false),
5342 ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5343 ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5344 ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5345 ];
5346 // PG's one-arg form uses the session configuration; the
5347 // two-arg form names one.
5348 let (cfg_name, text) = match (&arg0, args.get(1)) {
5349 (Some(Value::Text(c)), Some(t)) => {
5350 let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5351 (c.to_string(), crate::eval::value_to_text(&v))
5352 }
5353 (Some(v), None) => (
5354 alloc::string::String::from("english"),
5355 crate::eval::value_to_text(v),
5356 ),
5357 _ => return Ok((alloc::vec::Vec::new(), cols)),
5358 };
5359 let english = match cfg_name
5360 .trim()
5361 .trim_start_matches("pg_catalog.")
5362 .to_ascii_lowercase()
5363 .as_str()
5364 {
5365 "english" => true,
5366 "simple" => false,
5367 other => {
5368 return Err(EngineError::Unsupported(alloc::format!(
5369 "text search configuration \"{other}\" does not exist"
5370 )));
5371 }
5372 };
5373 let rows = crate::fts::tokenize_typed(&text)
5374 .into_iter()
5375 .map(|tok| {
5376 let dict = tok.ty.dictionary(english);
5377 let dname = dict.map(|d| match d {
5378 TsDict::Simple => "simple",
5379 TsDict::EnglishStem => "english_stem",
5380 });
5381 let folded = tok.text.to_lowercase();
5382 let lexemes = dict.map(|d| match d {
5383 TsDict::Simple => alloc::vec![Some(folded.clone())],
5384 TsDict::EnglishStem => {
5385 if crate::fts::is_english_stopword(&folded) {
5386 alloc::vec::Vec::new()
5387 } else {
5388 alloc::vec![Some(crate::fts::porter_stem(&folded))]
5389 }
5390 }
5391 });
5392 Row::new(alloc::vec![
5393 Value::text(tok.ty.alias()),
5394 Value::text(tok.ty.description()),
5395 Value::text(tok.text),
5396 Value::TextArray(
5397 dname
5398 .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5399 .unwrap_or_default(),
5400 ),
5401 dname.map_or(Value::Null, Value::text),
5402 lexemes.map_or(Value::Null, Value::TextArray),
5403 ])
5404 })
5405 .collect();
5406 let _ = TokenType::AsciiWord;
5407 Ok((rows, cols))
5408 }
5409 // v7.39 (round 651) — `ts_token_type('default')`, the list the
5410 // parser actually produces. It is a projection of the
5411 // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5412 // read, so the three cannot disagree about what a token is.
5413 "ts_token_type" => {
5414 use crate::fts::TokenType as T;
5415 let cols = alloc::vec![
5416 ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5417 ColumnSchema::new("alias".to_string(), DataType::Text, false),
5418 ColumnSchema::new("description".to_string(), DataType::Text, false),
5419 ];
5420 // PG takes the parser by name or oid; SPG has the one.
5421 if let Some(Value::Text(p)) = &arg0
5422 && !p.eq_ignore_ascii_case("default")
5423 && !p.eq_ignore_ascii_case("pg_catalog.default")
5424 {
5425 return Err(EngineError::Unsupported(alloc::format!(
5426 "text search parser \"{p}\" does not exist"
5427 )));
5428 }
5429 const TYPES: &[T] = &[
5430 T::AsciiWord,
5431 T::Word,
5432 T::NumWord,
5433 T::Email,
5434 T::Url,
5435 T::Host,
5436 T::SFloat,
5437 T::Version,
5438 T::HwordNumPart,
5439 T::HwordPart,
5440 T::HwordAsciiPart,
5441 T::Blank,
5442 T::Tag,
5443 T::Protocol,
5444 T::NumHword,
5445 T::AsciiHword,
5446 T::Hword,
5447 T::UrlPath,
5448 T::File,
5449 T::Float,
5450 T::Int,
5451 T::Uint,
5452 T::Entity,
5453 ];
5454 let rows = TYPES
5455 .iter()
5456 .map(|t| {
5457 Row::new(alloc::vec![
5458 Value::Int(*t as i32),
5459 Value::text(t.alias()),
5460 Value::text(t.description()),
5461 ])
5462 })
5463 .collect();
5464 Ok((rows, cols))
5465 }
5466 // v7.39 (read01 round 65) — a set-returning USER function in FROM
5467 // (`FROM rows_of(2)`). Its body runs through the real executor, like
5468 // every other function body since round 63.
5469 other => {
5470 if !self.active_catalog().functions_named(other).is_empty() {
5471 return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5472 }
5473 Err(EngineError::Unsupported(alloc::format!(
5474 "table function {other}() is not supported in FROM"
5475 )))
5476 }
5477 }
5478 }
5479
5480 /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5481 /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5482 /// are bound into it as literals and it goes through the read path, so the
5483 /// rows it yields are exactly the rows a hand-written query would see.
5484 ///
5485 /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5486 /// text)` names them, and a `SETOF <scalar>` yields a single column named
5487 /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5488 /// shows.
5489 fn exec_setof_user_function(
5490 &self,
5491 name: &str,
5492 args: &[spg_sql::ast::Expr],
5493 // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5494 // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5495 alias: Option<&str>,
5496 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5497 // The call's arguments belong to the ENCLOSING query, so they are
5498 // evaluated here and the body sees values.
5499 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5500 let arg_ctx = self.ev_ctx(&empty, None);
5501 let dummy = Row::new(alloc::vec::Vec::new());
5502 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5503 for a in args {
5504 vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5505 }
5506 self.setof_rows_of(name, &vals, alias)
5507 }
5508
5509 /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5510 /// arguments. Shared by the FROM position and the target-list expansion, so
5511 /// a function cannot behave differently depending on where it is called.
5512 pub(crate) fn setof_rows_of(
5513 &self,
5514 name: &str,
5515 arg_values: &[Value<'static>],
5516 alias: Option<&str>,
5517 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5518 let cat = self.active_catalog();
5519 let overloads = cat.functions_named(name);
5520 let def = overloads
5521 .iter()
5522 .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5523 .ok_or_else(|| {
5524 EngineError::Unsupported(alloc::format!(
5525 "function {name} does not exist with {} argument(s)",
5526 arg_values.len()
5527 ))
5528 })?;
5529 let declared = def.returns.trim().to_string();
5530 let upper = declared.to_ascii_uppercase();
5531 if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5532 return Err(EngineError::Unsupported(alloc::format!(
5533 "function {name}() does not return a set — it cannot be used in FROM"
5534 )));
5535 }
5536
5537 let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5538 // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5539 // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5540 if def.language.eq_ignore_ascii_case("plpgsql") {
5541 let out_rows = self
5542 .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5543 .map_err(EngineError::Eval)?;
5544 let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5545 let rows = out_rows.into_iter().map(Row::new).collect();
5546 return Ok((rows, cols));
5547 }
5548 let body = def.body.trim().trim_end_matches(';');
5549 let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5550 EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5551 })?;
5552 let spg_sql::ast::Statement::Select(body_select) = stmt else {
5553 return Err(EngineError::Unsupported(alloc::format!(
5554 "function {name}(): a set-returning body must be a SELECT"
5555 )));
5556 };
5557 let arg_names = spg_storage::function_arg_names(&def.args_repr);
5558 let bound = crate::eval::bind_user_fn_args(
5559 self.active_catalog(),
5560 &body_select,
5561 &arg_names,
5562 arg_values,
5563 )
5564 .map_err(EngineError::Eval)?;
5565 let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5566 let QueryResult::Rows { columns, rows } = out else {
5567 return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5568 };
5569 // Name the columns from the DECLARED shape — the same rule the plpgsql
5570 // path above uses, so a body's language cannot change the row shape.
5571 let cols = setof_column_shape_from(&declared, name, alias, &columns);
5572 Ok((rows, cols))
5573 }
5574
5575 fn exec_select_jsonb_each_text(
5576 &self,
5577 stmt: &SelectStatement,
5578 primary: &TableRef,
5579 cancel: CancelToken<'_>,
5580 ) -> Result<QueryResult, EngineError> {
5581 let (each_fn, arg_expr) = primary
5582 .jsonb_each_text_arg
5583 .as_ref()
5584 .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5585 .expect("caller guards jsonb_each_text_arg.is_some()");
5586 // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5587 // forms keep JSON rendering in the value column (JSON null
5588 // stays jsonb 'null', strings keep their quotes).
5589 let as_text = each_fn.ends_with("_text");
5590 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5591 let ctx = EvalContext::new(&empty_schema, None);
5592 let dummy_row = Row::new(alloc::vec::Vec::new());
5593 let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5594 let pairs =
5595 crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5596 let rows: alloc::vec::Vec<Row<'static>> = pairs
5597 .into_iter()
5598 .map(|(k, v)| {
5599 let key_val = Value::text(k);
5600 let value_val = match v {
5601 Some(s) if as_text => Value::text(s),
5602 Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5603 None => Value::Null,
5604 };
5605 Row::new(alloc::vec![key_val, value_val])
5606 })
5607 .collect();
5608 let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5609 let value_dtype = if as_text {
5610 spg_storage::DataType::Text
5611 } else {
5612 spg_storage::DataType::Json
5613 };
5614 let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5615 let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5616 let mut schema_cols = alloc::vec![key_col, value_col];
5617 // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5618 // LATERAL-position form of the same call already honours it.
5619 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5620 if let Some(col) = schema_cols.get_mut(i) {
5621 col.name = new_name.clone();
5622 }
5623 }
5624 // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5625 // `EvalContext::new` drops it and every catalog-dependent cast
5626 // (regclass / enum / composite / domain) silently degrades.
5627 let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5628 // WHERE.
5629 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5630 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5631 for row in rows {
5632 cancel.check()?;
5633 let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5634 if matches!(v, Value::Bool(true)) {
5635 out.push(row);
5636 }
5637 }
5638 out
5639 } else {
5640 rows
5641 };
5642 // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5643 if aggregate::uses_aggregate(stmt) {
5644 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5645 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5646 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5647 .map_err(|err| match err {
5648 EngineError::Eval(ev) => ev,
5649 other => eval::EvalError::TypeMismatch {
5650 detail: alloc::format!("{other}"),
5651 },
5652 })
5653 };
5654 // v7.39 (round 656) — hand the rows over as they are rather than
5655 // collecting a second vector of `RowRef` wrappers. Note this is
5656 // a set-returning-function path, NOT the relational scan: the
5657 // measured O(rows) cost lived in `run_single_table_aggregate`,
5658 // and converting these four first was a miss that cost a full
5659 // round — every test stayed green and the number did not move.
5660 let agg = aggregate::run(
5661 stmt,
5662 crate::join::AggRows::Owned(&filtered),
5663 &schema_cols,
5664 Some(&alias),
5665 Some(&agg_correlated),
5666 self.parallel_runner.0.as_deref(),
5667 Some(self.active_catalog()),
5668 Some(self),
5669 )?;
5670 return self.finish_agg_result(agg, stmt, cancel);
5671 }
5672 // Projection.
5673 let projection = build_projection(
5674 &stmt.items,
5675 &schema_cols,
5676 &alias,
5677 self.speaks_mysql,
5678 Some(self.active_catalog()),
5679 )?;
5680 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5681 alloc::vec::Vec::with_capacity(filtered.len());
5682 for row in &filtered {
5683 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5684 for p in &projection {
5685 let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5686 vals.push(v);
5687 }
5688 projected_rows.push(Row::new(vals));
5689 }
5690 let columns: alloc::vec::Vec<ColumnSchema> = projection
5691 .iter()
5692 // v7.39 (read01 round 54) — keep the column's enum identity through
5693 // the projection (it lives outside the DataType lattice), or a
5694 // derived table / UNION / windowed result forgets it and any outer
5695 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5696 .map(|p| p.to_column_schema())
5697 .collect();
5698 // ORDER BY.
5699 if !stmt.order_by.is_empty() {
5700 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5701 .iter()
5702 .enumerate()
5703 .map(|(i, r)| -> Result<_, EngineError> {
5704 let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5705 .order_by
5706 .iter()
5707 .map(|ob| {
5708 eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5709 })
5710 .collect();
5711 Ok((i, keys?))
5712 })
5713 .collect::<Result<_, _>>()?;
5714 indexed.sort_by(|a, b| {
5715 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5716 let o = &stmt.order_by[idx];
5717 let cmp = order_by_value_cmp_in(
5718 o.desc,
5719 o.nulls_first,
5720 ka,
5721 kb,
5722 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5723 );
5724 if cmp != core::cmp::Ordering::Equal {
5725 return cmp;
5726 }
5727 }
5728 core::cmp::Ordering::Equal
5729 });
5730 projected_rows = indexed
5731 .into_iter()
5732 .map(|(i, _)| projected_rows[i].clone())
5733 .collect();
5734 }
5735 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5736 if stmt.distinct {
5737 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
5738 // spec folds EVERY text position, so a column declared
5739 // `COLLATE utf8mb4_bin` had its values merged here exactly the
5740 // way 3b494b6e fixed on the main scan path. The projection is
5741 // already in scope at each of these sites, so the mask needs no
5742 // new plumbing -- it was simply never asked for.
5743 projected_rows = dedup_rows(
5744 projected_rows,
5745 FoldSpec::of_masks(
5746 scan_ctx.mysql_dialect,
5747 &fold_mask(&projection),
5748 &pad_mask(&projection),
5749 ),
5750 );
5751 }
5752 if let Some(offset) = stmt.offset_literal() {
5753 let off = (offset as usize).min(projected_rows.len());
5754 projected_rows.drain(..off);
5755 }
5756 if let Some(limit) = stmt.limit_literal() {
5757 projected_rows.truncate(limit as usize);
5758 }
5759 Ok(QueryResult::Rows {
5760 columns,
5761 rows: projected_rows,
5762 })
5763 }
5764
5765 /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5766 /// ( SELECT … ) alias` in primary position. The inner SELECT
5767 /// materialises once through the regular bare-select executor
5768 /// (UNION tails included), then the outer WHERE / aggregate /
5769 /// projection / ORDER BY / LIMIT pipeline runs over the
5770 /// synthetic table — the same post-materialisation shape as
5771 /// exec_select_jsonb_each_text, generalised to N columns.
5772 fn exec_select_derived(
5773 &self,
5774 stmt: &SelectStatement,
5775 primary: &TableRef,
5776 cancel: CancelToken<'_>,
5777 ) -> Result<QueryResult, EngineError> {
5778 let inner = primary
5779 .lateral_subquery
5780 .as_deref()
5781 .expect("caller guards lateral_subquery.is_some()");
5782 // exec_select_cancel is the union-aware wrapper — the inner
5783 // SELECT may carry UNION tails on stmt.unions.
5784 let QueryResult::Rows {
5785 columns: inner_cols,
5786 rows,
5787 } = self.exec_select_cancel(inner, cancel)?
5788 else {
5789 return Err(EngineError::Unsupported(
5790 "derived table subquery must return rows".into(),
5791 ));
5792 };
5793 let alias = primary
5794 .alias
5795 .clone()
5796 .unwrap_or_else(|| primary.name.clone());
5797 // `AS t(a, b)` renames the materialised columns positionally
5798 // (extra inner columns keep their own names, PG behaviour).
5799 let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5800 // v7.39 (read01 round 78) — a column-alias list longer than the item is
5801 // the error PG reports; SPG used to let the extra names through and then
5802 // fail two layers downstream with "column not found: <the extra name>".
5803 let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5804 if primary.unnest_column_aliases.len() > n_out {
5805 return Err(EngineError::Unsupported(alloc::format!(
5806 "table \"{alias}\" has {n_out} columns available but {} columns specified",
5807 primary.unnest_column_aliases.len()
5808 )));
5809 }
5810 if primary.scalar_fn_item && schema_cols.len() == 1 {
5811 schema_cols[0].scalar_row_source = true;
5812 }
5813 // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5814 // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5815 // The column-alias list, if given, names it like any other column.
5816 let mut rows = rows;
5817 if primary.with_ordinality {
5818 schema_cols.push(ColumnSchema::new(
5819 "ordinality".to_string(),
5820 DataType::BigInt,
5821 false,
5822 ));
5823 rows = rows
5824 .into_iter()
5825 .enumerate()
5826 .map(|(i, r)| {
5827 let mut v = r.values;
5828 #[allow(clippy::cast_possible_wrap)]
5829 v.push(Value::BigInt(i as i64 + 1));
5830 Row::new(v)
5831 })
5832 .collect();
5833 }
5834 for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5835 if let Some(col) = schema_cols.get_mut(i) {
5836 col.name = new_name.clone();
5837 }
5838 }
5839 self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5840 }
5841
5842 /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5843 /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5844 /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5845 /// derived-table executor and the FROM-position table functions.
5846 fn exec_select_over_rows(
5847 &self,
5848 stmt: &SelectStatement,
5849 rows: alloc::vec::Vec<Row<'static>>,
5850 schema_cols: alloc::vec::Vec<ColumnSchema>,
5851 alias: &str,
5852 cancel: CancelToken<'_>,
5853 ) -> Result<QueryResult, EngineError> {
5854 let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5855 // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5856 // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5857 // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5858 // (the same path the aggregate branch uses); the old plain eval_expr let
5859 // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5860 let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5861 // WHERE.
5862 let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5863 let mut out = alloc::vec::Vec::with_capacity(rows.len());
5864 for row in rows {
5865 cancel.check()?;
5866 let v = self.eval_expr_with_correlated(
5867 w,
5868 &row,
5869 &scan_ctx,
5870 cancel,
5871 Some(&mut corr_memo.borrow_mut()),
5872 )?;
5873 if matches!(v, Value::Bool(true)) {
5874 out.push(row);
5875 }
5876 }
5877 out
5878 } else {
5879 rows
5880 };
5881 // Aggregate dispatch.
5882 if aggregate::uses_aggregate(stmt) {
5883 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5884 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5885 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5886 .map_err(|err| match err {
5887 EngineError::Eval(ev) => ev,
5888 other => eval::EvalError::TypeMismatch {
5889 detail: alloc::format!("{other}"),
5890 },
5891 })
5892 };
5893 // v7.39 (round 656) — hand the rows over as they are rather than
5894 // collecting a second vector of `RowRef` wrappers. Note this is
5895 // a set-returning-function path, NOT the relational scan: the
5896 // measured O(rows) cost lived in `run_single_table_aggregate`,
5897 // and converting these four first was a miss that cost a full
5898 // round — every test stayed green and the number did not move.
5899 let agg = aggregate::run(
5900 stmt,
5901 crate::join::AggRows::Owned(&filtered),
5902 &schema_cols,
5903 Some(alias),
5904 Some(&agg_correlated),
5905 self.parallel_runner.0.as_deref(),
5906 Some(self.active_catalog()),
5907 Some(self),
5908 )?;
5909 return self.finish_agg_result(agg, stmt, cancel);
5910 }
5911 // Projection.
5912 let projection = build_projection(
5913 &stmt.items,
5914 &schema_cols,
5915 alias,
5916 self.speaks_mysql,
5917 Some(self.active_catalog()),
5918 )?;
5919 // v7.39 (round 621) — a target-list SRF expands here too. This tail
5920 // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5921 // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5922 // answered `function unnest(integer[]) does not exist` for a query PG
5923 // answers.
5924 let srf_idxs = self.srf_target_idxs(&projection);
5925 let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5926 let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5927 alloc::vec::Vec::with_capacity(filtered.len());
5928 if !srf_idxs.is_empty() {
5929 let (rows, src) =
5930 expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5931 projected_rows = rows;
5932 src_of_row = src;
5933 } else {
5934 for row in &filtered {
5935 let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5936 for p in &projection {
5937 let v = self.eval_expr_with_correlated(
5938 &p.expr,
5939 row,
5940 &scan_ctx,
5941 cancel,
5942 Some(&mut corr_memo.borrow_mut()),
5943 )?;
5944 vals.push(v);
5945 }
5946 projected_rows.push(Row::new(vals));
5947 }
5948 }
5949 let columns: alloc::vec::Vec<ColumnSchema> = projection
5950 .iter()
5951 // v7.39 (read01 round 54) — keep the column's enum identity through
5952 // the projection (it lives outside the DataType lattice), or a
5953 // derived table / UNION / windowed result forgets it and any outer
5954 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5955 .map(|p| p.to_column_schema())
5956 .collect();
5957 // ORDER BY over the source rows (same shape as the other
5958 // synthetic-table executors).
5959 // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
5960 // OUTPUT column. Evaluated as an expression, as it was here, the literal
5961 // `1` is just the constant 1: the same sort key for every row, so the
5962 // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
5963 // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
5964 // landing on this executor) came back in input order.
5965 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
5966 if !order_by.is_empty() {
5967 // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
5968 // SRF makes more of them than there were inputs.
5969 let out_cols = if srf_idxs.is_empty() {
5970 alloc::vec![None; order_by.len()]
5971 } else {
5972 srf_order_output_cols(&order_by, &projection)
5973 };
5974 let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
5975 .iter()
5976 .enumerate()
5977 .map(|(k, out)| -> Result<_, EngineError> {
5978 let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
5979 let keys: Result<Vec<Value<'static>>, EngineError> = order_by
5980 .iter()
5981 .zip(out_cols.iter())
5982 .map(|(ob, oc)| {
5983 // v7.39 (read01 round 54) — this path builds its
5984 // sort keys itself instead of going through
5985 // `build_order_keys`, so it skipped the enum-ordinal
5986 // substitution: an OUTER `ORDER BY <enum col>` over
5987 // a DERIVED TABLE sorted by the label TEXT, not by
5988 // member order. Silently wrong rows, not an error.
5989 let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
5990 Ok(
5991 match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
5992 Some(ord) => Value::Float(ord),
5993 None => v,
5994 },
5995 )
5996 })
5997 .collect();
5998 Ok((k, keys?))
5999 })
6000 .collect::<Result<_, _>>()?;
6001 indexed.sort_by(|a, b| {
6002 for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
6003 let o = &stmt.order_by[idx];
6004 let cmp = order_by_value_cmp_in(
6005 o.desc,
6006 o.nulls_first,
6007 ka,
6008 kb,
6009 scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
6010 );
6011 if cmp != core::cmp::Ordering::Equal {
6012 return cmp;
6013 }
6014 }
6015 core::cmp::Ordering::Equal
6016 });
6017 projected_rows = indexed
6018 .into_iter()
6019 .map(|(i, _)| projected_rows[i].clone())
6020 .collect();
6021 }
6022 // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
6023 if stmt.distinct {
6024 // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
6025 // spec folds EVERY text position, so a column declared
6026 // `COLLATE utf8mb4_bin` had its values merged here exactly the
6027 // way 3b494b6e fixed on the main scan path. The projection is
6028 // already in scope at each of these sites, so the mask needs no
6029 // new plumbing -- it was simply never asked for.
6030 projected_rows = dedup_rows(
6031 projected_rows,
6032 FoldSpec::of_masks(
6033 scan_ctx.mysql_dialect,
6034 &fold_mask(&projection),
6035 &pad_mask(&projection),
6036 ),
6037 );
6038 }
6039 if let Some(offset) = stmt.offset_literal() {
6040 let off = (offset as usize).min(projected_rows.len());
6041 projected_rows.drain(..off);
6042 }
6043 if let Some(limit) = stmt.limit_literal() {
6044 projected_rows.truncate(limit as usize);
6045 }
6046 Ok(QueryResult::Rows {
6047 columns,
6048 rows: projected_rows,
6049 })
6050 }
6051
6052 /// Constant `SELECT` with no FROM: evaluate each projection item
6053 /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
6054 fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
6055 let empty_schema: Vec<ColumnSchema> = Vec::new();
6056 let ctx = self.ev_ctx(&empty_schema, None);
6057 // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
6058 // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
6059 // `SELECT string_agg('x',',')` → x). Before this it fell through to the
6060 // scalar projection, where the aggregate name looked like an unknown
6061 // function. The WHERE filters that one row, so `… WHERE false` leaves
6062 // the aggregate zero input rows (`count(*)` → 0).
6063 if aggregate::uses_aggregate(stmt) {
6064 let dummy = Row::new(Vec::new());
6065 let passes = match &stmt.where_ {
6066 Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
6067 None => true,
6068 };
6069 let rows: Vec<RowRef<'_>> = if passes {
6070 alloc::vec![RowRef::Owned(&dummy)]
6071 } else {
6072 Vec::new()
6073 };
6074 let agg = aggregate::run(
6075 stmt,
6076 crate::join::AggRows::Refs(&rows),
6077 &empty_schema,
6078 None,
6079 None,
6080 self.parallel_runner.0.as_deref(),
6081 Some(self.active_catalog()),
6082 Some(self),
6083 )?;
6084 return self.finish_agg_result(agg, stmt, CancelToken::none());
6085 }
6086 let projection = build_projection(
6087 &stmt.items,
6088 &empty_schema,
6089 "",
6090 self.speaks_mysql,
6091 Some(self.active_catalog()),
6092 )?;
6093 // `SELECT … WHERE cond` with no FROM — the one conceptual
6094 // row survives only when the condition is true (previously
6095 // the WHERE was silently ignored: `SELECT 1 WHERE false`
6096 // returned a row).
6097 let dummy_row = Row::new(Vec::new());
6098 if let Some(w) = &stmt.where_ {
6099 let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
6100 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6101 let columns: Vec<ColumnSchema> = projection
6102 .into_iter()
6103 .map(|p| p.to_column_schema())
6104 .collect();
6105 return Ok(QueryResult::Rows {
6106 columns,
6107 rows: Vec::new(),
6108 });
6109 }
6110 }
6111 // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6112 // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6113 // desugar to unnest) expands here: one output row per SRF row, sibling
6114 // scalar columns repeated. unnest / array_elements / path_query reach a
6115 // real FROM via the parser rewrite and never land here.
6116 // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6117 let srf_idxs = self.srf_target_idxs(&projection);
6118 if !srf_idxs.is_empty() {
6119 let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6120 let columns: Vec<ColumnSchema> = projection
6121 .into_iter()
6122 .map(|p| p.to_column_schema())
6123 .collect();
6124 // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6125 // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6126 // to. This returned straight out of the expansion, so
6127 // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6128 // input order — the sort was not wrong, it never ran. (There is
6129 // exactly one conceptual input row here, which is why the ordinary
6130 // scan pipeline is not on this path at all.)
6131 if !stmt.order_by.is_empty() {
6132 let synth_ctx =
6133 EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6134 let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6135 .order_by
6136 .iter()
6137 .map(|o| {
6138 let mut o = o.clone();
6139 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6140 && *n >= 1
6141 && let Ok(idx) = usize::try_from(*n - 1)
6142 && idx < columns.len()
6143 {
6144 o.expr = Expr::Column(spg_sql::ast::ColumnName {
6145 qualifier: None,
6146 name: columns[idx].name.clone(),
6147 });
6148 }
6149 o
6150 })
6151 .collect();
6152 let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6153 let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6154 for r in rows {
6155 let keys = build_order_keys(&resolved, &r, &synth_ctx)?;
6156 tagged.push((keys, r));
6157 }
6158 sort_by_keys(&mut tagged, &descs);
6159 rows = tagged.into_iter().map(|(_, r)| r).collect();
6160 }
6161 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6162 return Ok(QueryResult::Rows { columns, rows });
6163 }
6164 let mut values = Vec::with_capacity(projection.len());
6165 for p in &projection {
6166 values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6167 }
6168 let columns: Vec<ColumnSchema> = projection
6169 .into_iter()
6170 .map(|p| p.to_column_schema())
6171 .collect();
6172 // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6173 // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6174 // returns none. (The SRF and aggregate arms above already applied
6175 // them; this tail was the one that didn't.)
6176 let mut rows = alloc::vec![Row::new(values)];
6177 apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6178 Ok(QueryResult::Rows { columns, rows })
6179 }
6180
6181 /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6182 /// circuit. Catches
6183 /// SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6184 /// BEFORE `resolve_select_subqueries` materialises the inner result
6185 /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6186 /// values into a `HashSet<i64>` directly, then probes A.pk per
6187 /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6188 /// (~150 µs / query at INSUBQ benchmark scale).
6189 pub(crate) fn try_count_star_pk_in_subquery_fast(
6190 &self,
6191 stmt: &SelectStatement,
6192 cancel: CancelToken<'_>,
6193 ) -> Result<Option<QueryResult>, EngineError> {
6194 use spg_sql::ast::SelectItem;
6195 if stmt.distinct
6196 || stmt.limit_with_ties
6197 || stmt.group_by.is_some()
6198 || stmt.having.is_some()
6199 || !stmt.unions.is_empty()
6200 || !stmt.order_by.is_empty()
6201 || stmt.limit.is_some()
6202 || stmt.offset.is_some()
6203 || stmt.items.len() != 1
6204 {
6205 return Ok(None);
6206 }
6207 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6208 return Ok(None);
6209 };
6210 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6211 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6212 if !is_count_star {
6213 return Ok(None);
6214 }
6215 let Some(from) = stmt.from.as_ref() else {
6216 return Ok(None);
6217 };
6218 if !from.joins.is_empty()
6219 || from.primary.lateral_subquery.is_some()
6220 || from.primary.unnest_expr.is_some()
6221 || from.primary.generate_series_args.is_some()
6222 || from.primary.table_fn_call.is_some()
6223 || from.primary.as_of_segment.is_some()
6224 {
6225 return Ok(None);
6226 }
6227 let Some(where_expr) = stmt.where_.as_ref() else {
6228 return Ok(None);
6229 };
6230 // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6231 // negated=false; no other predicates.
6232 let Expr::InSubquery {
6233 expr: col_expr,
6234 subquery,
6235 negated: false,
6236 } = where_expr
6237 else {
6238 return Ok(None);
6239 };
6240 let Expr::Column(c) = col_expr.as_ref() else {
6241 return Ok(None);
6242 };
6243 let outer_alias = from
6244 .primary
6245 .alias
6246 .as_deref()
6247 .unwrap_or(from.primary.name.as_str());
6248 if let Some(q) = c.qualifier.as_deref()
6249 && !q.eq_ignore_ascii_case(outer_alias)
6250 {
6251 return Ok(None);
6252 }
6253 // Outer column must be a single-column PK on integer family.
6254 let catalog = self.active_catalog();
6255 let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6256 return Ok(None);
6257 };
6258 let outer_schema = outer_table.schema();
6259 let Some(outer_pos) = outer_schema
6260 .columns
6261 .iter()
6262 .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6263 else {
6264 return Ok(None);
6265 };
6266 if !matches!(
6267 outer_schema.columns[outer_pos].ty,
6268 spg_storage::DataType::BigInt
6269 | spg_storage::DataType::Int
6270 | spg_storage::DataType::SmallInt
6271 ) {
6272 return Ok(None);
6273 }
6274 if !outer_schema
6275 .uniqueness_constraints
6276 .iter()
6277 .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6278 {
6279 return Ok(None);
6280 }
6281 let Some(idx) = outer_table.index_on(outer_pos) else {
6282 return Ok(None);
6283 };
6284 // Inner must be uncorrelated. The cheap-correlation pre-check
6285 // exists upstream; here we just attempt the bare exec.
6286 if crate::subquery::select_is_correlated(subquery) {
6287 return Ok(None);
6288 }
6289 let mut inner = (**subquery).clone();
6290 self.resolve_select_subqueries(&mut inner, cancel)?;
6291 let r = match self.exec_bare_select_cancel(&inner, cancel) {
6292 Ok(r) => r,
6293 Err(_) => return Ok(None),
6294 };
6295 let QueryResult::Rows { columns, rows, .. } = r else {
6296 return Ok(None);
6297 };
6298 if columns.len() != 1 {
6299 return Ok(None);
6300 }
6301 // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6302 // subquery projects a column known to be UNIQUE/PK on its table
6303 // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6304 // in `tbl.uniqueness_constraints`), survivor values are
6305 // guaranteed distinct and the per-survivor `HashSet::insert`
6306 // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6307 //
6308 // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6309 // projection that is a bare Column ref, table-column lookup in
6310 // catalog confirms the column appears as a unique constraint's
6311 // sole member. UNIQUE NOT NULL is required — a nullable unique
6312 // column may have multiple NULLs, but NULLs are already skipped
6313 // above (`Value::Null => continue`), so a UNIQUE-only column is
6314 // still safe to dedup-skip.
6315 let inner_unique = (|| -> bool {
6316 if inner.distinct
6317 || inner.group_by.is_some()
6318 || !inner.unions.is_empty()
6319 || inner.having.is_some()
6320 || inner.items.len() != 1
6321 {
6322 return false;
6323 }
6324 let Some(inner_from) = inner.from.as_ref() else {
6325 return false;
6326 };
6327 if !inner_from.joins.is_empty()
6328 || inner_from.primary.lateral_subquery.is_some()
6329 || inner_from.primary.unnest_expr.is_some()
6330 || inner_from.primary.generate_series_args.is_some()
6331 || inner_from.primary.table_fn_call.is_some()
6332 {
6333 return false;
6334 }
6335 let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6336 return false;
6337 };
6338 let Expr::Column(pc) = proj else {
6339 return false;
6340 };
6341 let inner_alias = inner_from
6342 .primary
6343 .alias
6344 .as_deref()
6345 .unwrap_or(inner_from.primary.name.as_str());
6346 if let Some(q) = pc.qualifier.as_deref()
6347 && !q.eq_ignore_ascii_case(inner_alias)
6348 {
6349 return false;
6350 }
6351 let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6352 return false;
6353 };
6354 let isch = inner_table.schema();
6355 let Some(ipos) = isch
6356 .columns
6357 .iter()
6358 .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6359 else {
6360 return false;
6361 };
6362 isch.uniqueness_constraints
6363 .iter()
6364 .any(|u| u.columns.as_slice() == [ipos])
6365 })();
6366 // Collect inner i64 values directly into a HashSet, then probe.
6367 let mut count: i64 = 0;
6368 let mut probed = if inner_unique {
6369 hashbrown::HashSet::<i64>::new()
6370 } else {
6371 hashbrown::HashSet::<i64>::with_capacity(rows.len())
6372 };
6373 for row in &rows {
6374 let v = row.values.first().cloned().unwrap_or(Value::Null);
6375 let n = match v {
6376 Value::BigInt(n) => n,
6377 Value::Int(n) => i64::from(n),
6378 Value::SmallInt(n) => i64::from(n),
6379 Value::Null => continue,
6380 _ => return Ok(None),
6381 };
6382 // De-duplicate inner key set so a duplicate inner value
6383 // doesn't double-count the same outer row. Skipped when
6384 // the inner projection is statically unique.
6385 if !inner_unique && !probed.insert(n) {
6386 continue;
6387 }
6388 // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6389 // the `IndexKey::from_value` enum-dispatch and the per-call
6390 // `IndexKey` wrapper construction. The outer column is
6391 // already gated to integer-family above, so an i64 key
6392 // always corresponds to a valid PK lookup.
6393 if !idx.lookup_eq_i64(n).is_empty() {
6394 count += 1;
6395 }
6396 }
6397 let columns_out = alloc::vec![ColumnSchema::new(
6398 "count".to_string(),
6399 spg_storage::DataType::BigInt,
6400 false,
6401 )];
6402 let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6403 Ok(Some(QueryResult::Rows {
6404 columns: columns_out,
6405 rows: rows_out,
6406 }))
6407 }
6408
6409 /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6410 /// SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6411 /// (the post-subquery-replacement shape of the INSUBQ probe
6412 /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6413 /// The general aggregate path materialises every seeked row into
6414 /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6415 /// For COUNT(*) we only care how many keys hit; iterate the list
6416 /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6417 /// row materialisation, the aggregate state machine, and the per-
6418 /// row WHERE re-eval (the seek already filtered by the same list).
6419 /// Returns `None` when the shape doesn't match.
6420 fn try_count_star_pk_in_list_fast(
6421 &self,
6422 stmt: &SelectStatement,
6423 table: &spg_storage::Table,
6424 schema_cols: &[ColumnSchema],
6425 alias: &str,
6426 ) -> Option<QueryResult> {
6427 use spg_sql::ast::{ColumnName, SelectItem};
6428 // Gates on the SELECT shape.
6429 if stmt.distinct
6430 || stmt.limit_with_ties
6431 || stmt.group_by.is_some()
6432 || stmt.having.is_some()
6433 || !stmt.unions.is_empty()
6434 || !stmt.order_by.is_empty()
6435 || stmt.limit.is_some()
6436 || stmt.offset.is_some()
6437 || stmt.items.len() != 1
6438 {
6439 return None;
6440 }
6441 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6442 return None;
6443 };
6444 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6445 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6446 if !is_count_star {
6447 return None;
6448 }
6449 // WHERE must be `<col> IN (literal list)` with no other
6450 // conjuncts (the seek result is a true subset of the row
6451 // population for this predicate).
6452 let where_expr = stmt.where_.as_ref()?;
6453 let Expr::InList {
6454 expr: col_expr,
6455 list,
6456 negated: false,
6457 } = where_expr
6458 else {
6459 return None;
6460 };
6461 let Expr::Column(c) = col_expr.as_ref() else {
6462 return None;
6463 };
6464 if let Some(q) = c.qualifier.as_deref()
6465 && !q.eq_ignore_ascii_case(alias)
6466 {
6467 return None;
6468 }
6469 let col_pos = schema_cols
6470 .iter()
6471 .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6472 // The column must be a single-column PK on an integer family
6473 // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6474 // so the antiset stays collision-free under `HashSet<i64>`.
6475 let schema = table.schema();
6476 if !matches!(
6477 schema.columns[col_pos].ty,
6478 spg_storage::DataType::BigInt
6479 | spg_storage::DataType::Int
6480 | spg_storage::DataType::SmallInt
6481 ) {
6482 return None;
6483 }
6484 if !schema
6485 .uniqueness_constraints
6486 .iter()
6487 .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6488 {
6489 return None;
6490 }
6491 let idx = table.index_on(col_pos)?;
6492 // Tally non-empty seek results across all literal values.
6493 let mut count: i64 = 0;
6494 for lit in list {
6495 let Expr::Literal(l) = lit else {
6496 return None;
6497 };
6498 // r1039 — through the shared resolver, so a literal spelled
6499 // in another type ('5' against an integer PK) is read as the
6500 // column's before it becomes a key. This tally answers from
6501 // the index alone, so a key in the wrong space would return a
6502 // COUNT of zero rather than fall back to a scan.
6503 let col = schema.columns.get(col_pos)?;
6504 let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6505 let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6506 if !idx.lookup_eq(&key).is_empty() {
6507 count += 1;
6508 }
6509 }
6510 let columns = alloc::vec![ColumnSchema::new(
6511 "count".to_string(),
6512 spg_storage::DataType::BigInt,
6513 false,
6514 )];
6515 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6516 let _ = ColumnName {
6517 qualifier: None,
6518 name: String::new(),
6519 };
6520 Some(QueryResult::Rows { columns, rows })
6521 }
6522
6523 /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6524 /// BETWEEN a AND b` on an indexed column. The index range walk yields
6525 /// exactly the matching (visible) rows, so we count locators directly —
6526 /// skipping the row materialisation, the aggregate state machine, and the
6527 /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6528 /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6529 /// when the shape doesn't match.
6530 fn try_count_star_indexed_range_fast(
6531 &self,
6532 stmt: &SelectStatement,
6533 table: &spg_storage::Table,
6534 schema_cols: &[ColumnSchema],
6535 alias: &str,
6536 snapshot: &spg_storage::snapshot::Snapshot,
6537 ) -> Option<QueryResult> {
6538 use spg_sql::ast::SelectItem;
6539 if stmt.distinct
6540 || stmt.limit_with_ties
6541 || stmt.group_by.is_some()
6542 || stmt.having.is_some()
6543 || !stmt.unions.is_empty()
6544 || !stmt.order_by.is_empty()
6545 || stmt.limit.is_some()
6546 || stmt.offset.is_some()
6547 || stmt.items.len() != 1
6548 {
6549 return None;
6550 }
6551 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6552 return None;
6553 };
6554 let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6555 if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6556 if !is_count_star {
6557 return None;
6558 }
6559 let where_expr = stmt.where_.as_ref()?;
6560 let count = crate::index_access::try_range_count(
6561 where_expr,
6562 schema_cols,
6563 table,
6564 alias,
6565 snapshot,
6566 self.speaks_mysql,
6567 )?;
6568 let columns = alloc::vec![ColumnSchema::new(
6569 "count".to_string(),
6570 spg_storage::DataType::BigInt,
6571 false,
6572 )];
6573 let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6574 Some(QueryResult::Rows { columns, rows })
6575 }
6576
6577 /// Single-table aggregate path: filter the (optionally index-seeked)
6578 /// rows, then hand off to the aggregate executor which does its own
6579 /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6580 fn run_single_table_aggregate<'a>(
6581 &self,
6582 stmt: &SelectStatement,
6583 table: &'a spg_storage::Table,
6584 schema_cols: &'a [ColumnSchema],
6585 alias: &str,
6586 indexed_rows: Option<crate::index_access::Seeked<'a>>,
6587 cancel: CancelToken<'_>,
6588 ) -> Result<QueryResult, EngineError> {
6589 // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6590 // REPEATABLE (see run_single_table_scan). Aggregates
6591 // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6592 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6593 let ctx = self
6594 .ev_ctx(schema_cols, Some(alias))
6595 .with_sample_rng(&sample_cell);
6596 // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6597 // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6598 // and every abandoned buffer on the way stays resident: RSS is a
6599 // high-water mark, so the intermediates are paid for even though
6600 // they are freed. Round 656 measured the scan at 17 bytes/row
6601 // where the survivor list itself only needs 8.
6602 let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6603 Vec::with_capacity(table.rows().len())
6604 } else {
6605 // With a WHERE, the row count is an UPPER bound and reserving it
6606 // is the worse trade: `… WHERE id = 5` over 50M rows would take
6607 // 400 MB of pointers to hold one survivor. Let it grow.
6608 Vec::new()
6609 };
6610 // v6.2.6 — Memoize: per-query LRU cache for correlated
6611 // scalar subqueries. Fresh per row-loop entry so each
6612 // SELECT execution gets an isolated cache.
6613 let mut memo = memoize::MemoizeCache::new();
6614 // v7.37 (perf) — single-table aggregate's WHERE filter
6615 // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6616 // correlated`) per row, even for subquery-free WHEREs that
6617 // the single-table SCAN path has compiled since v7.32
6618 // (perf knife D). The asymmetry meant a fold-to-filter
6619 // rewrite (joinfold) that swapped a JOIN for a single-table
6620 // aggregate over a compiled WHERE saw the tree-walker
6621 // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6622 // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6623 // step. Compile once if eligible; fall back to the walker
6624 // for subquery-bearing or non-compilable WHEREs.
6625 let compiled_where: Option<eval::CompiledExpr> = stmt
6626 .where_
6627 .as_ref()
6628 .filter(|w| eval::fully_compilable(w))
6629 .map(|w| {
6630 // v7.38.8 — the scan filter runs the cheap half of its
6631 // conjunction first. Called from HERE and not from
6632 // `eval::compiled`, deliberately: the row loop lives in
6633 // that file, and adding a function to it cost this
6634 // query 11 % through layout alone while doing no work
6635 // for it. See `crate::qualorder`.
6636 match crate::qualorder::reordered(w) {
6637 Some(r) => eval::compile_expr(&r, &ctx),
6638 None => eval::compile_expr(w, &ctx),
6639 }
6640 });
6641 let mut eval_stack: Vec<Value<'static>> = Vec::new();
6642 let mut row_passes_where = |row: &Row<'static>,
6643 eval_stack: &mut Vec<Value<'static>>,
6644 memo: &mut memoize::MemoizeCache|
6645 -> Result<bool, EngineError> {
6646 match (&compiled_where, &stmt.where_) {
6647 (Some(cw), _) => {
6648 // v7.39 (round 479) — the predicate wants a bool, not a
6649 // Value. The owned entry ended in `Value::into_owned`
6650 // and the caller then dropped it, once per row; round
6651 // 478's profile put that pair above the comparison
6652 // itself.
6653 Ok(eval::compiled::eval_compiled_pred(
6654 cw,
6655 row,
6656 &ctx,
6657 eval_stack,
6658 ctx.mysql_dialect,
6659 )
6660 .map_err(EngineError::Eval)?)
6661 }
6662 (None, Some(w)) => {
6663 let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6664 Ok(crate::eval::predicate_is_true(
6665 &cond,
6666 "WHERE",
6667 ctx.mysql_dialect,
6668 )?)
6669 }
6670 (None, None) => Ok(true),
6671 }
6672 };
6673 if let Some(seeked) = &indexed_rows {
6674 // v7.38.19 — an EXACT seek has already applied the whole
6675 // predicate, so asking again is asking the index's question
6676 // a second time, once per row.
6677 //
6678 // Profiled on `count(*) FROM events WHERE project_id = 3`
6679 // over 200,000 rows: `try_index_seek` 1,814 leaf samples and
6680 // `binop::compare` 1,633 — and `compare`'s first arm is
6681 // `(Int, Int) => a.cmp(b)`, so it was never that a
6682 // comparison is expensive. It was that 25,000 of them were
6683 // re-deciding what the walk had decided. The same query with
6684 // `GROUP BY project_id` bolted on ran in half the time,
6685 // doing strictly more work, because that path reached the
6686 // rows differently.
6687 //
6688 // `exact` is false for every arm that has not proven it —
6689 // the GIN, trigram and jsonb walks, an `AND` whose other
6690 // conjuncts went unapplied, a collated key, a type whose key
6691 // cannot name it. See `index_access::Seeked`.
6692 if seeked.exact {
6693 filtered.extend(seeked.rows.iter().map(Cow::as_ref));
6694 } else {
6695 for cow in &seeked.rows {
6696 let row = cow.as_ref();
6697 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6698 continue;
6699 }
6700 filtered.push(row);
6701 }
6702 }
6703 }
6704 // v7.36 (cold-tier coverage) — single-table aggregate's
6705 // non-indexed full scan was hot-only and silently lost cold
6706 // rows on COUNT/SUM/etc. Materialise cold rows once into
6707 // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6708 // shape stays unchanged; the cold rows live until the end of
6709 // the aggregate run.
6710 let cold_rows_storage = if indexed_rows.is_none() {
6711 self.iter_cold_rows_of_table(table)
6712 } else {
6713 Vec::new()
6714 };
6715 if indexed_rows.is_none() {
6716 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6717 // single-table aggregate full-scan path. Mirrors the gate on
6718 // `run_single_table_scan`: this is a user-query result path,
6719 // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6720 // reader's snapshot cannot see (e.g. tombstoned versions),
6721 // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6722 // under the default gate-off: every hot row is frozen or
6723 // committed-and-alive, so `is_row_visible` returns true.
6724 // Cold-tier rows are frozen (visible) by definition — left
6725 // ungated, matching the plain-scan path.
6726 let scan_snapshot = self.current_snapshot();
6727 // v7.39 (pg_stat knife B) — this full-scan branch walks
6728 // headers directly (serial and sharded alike); count the
6729 // sequential scan here.
6730 table.note_seq_scan();
6731 // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6732 // filter dominate the pre-aggregate wall time on big
6733 // scans (P1's ground truth: accumulation is only ~17%).
6734 // Shard THAT work when the host injected an executor and
6735 // the WHERE is compiled (the compiled evaluator is pure
6736 // over &row; the tree-walker fallback can hit correlated
6737 // subqueries and stays serial). Shards return surviving
6738 // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6739 // 'static bound — and the main thread only dereferences.
6740 let n = table.row_count();
6741 let par = self.parallel_runner.0.as_deref().filter(|_| {
6742 n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6743 });
6744 // v7.38.11 — ask the BRIN summary first. When it prunes,
6745 // the work left is a few thousand rows and sharding it
6746 // costs more than it saves, so the serial pruned loop below
6747 // takes it; the shard machinery is left exactly as it was
6748 // rather than taught about slots.
6749 let brin_slots = stmt
6750 .where_
6751 .as_ref()
6752 .and_then(|w| crate::brin::candidate_slots(w, table));
6753 let brin_prunes = brin_slots
6754 .as_ref()
6755 .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6756 if let Some(r) = par
6757 && !brin_prunes
6758 {
6759 let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6760 let chunk = n.div_ceil(n_shards);
6761 type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6762 let cw = &compiled_where;
6763 let snap_ref = &scan_snapshot;
6764 let results = r.run_shards(n_shards, &|s| {
6765 let lo = s * chunk;
6766 let hi = ((s + 1) * chunk).min(n);
6767 let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6768 // EvalContext carries Cells (sampler / row counters)
6769 // and is !Sync — each shard builds its own from the
6770 // same Sync inputs. The compiled WHERE is gated to
6771 // the pure-scalar whitelist, which reads none of the
6772 // session state the engine-built ctx would add
6773 // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6774 // sampled scans never take this branch).
6775 let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6776 let mut stack: Vec<Value<'static>> = Vec::new();
6777 let out: ShardOut = (|| {
6778 for i in lo..hi {
6779 if !table.is_row_visible(i, snap_ref) {
6780 continue;
6781 }
6782 let row = &table.rows()[i];
6783 // v7.39 (round 480) — the parallel full-scan
6784 // shard is the path the aggregate benchmark
6785 // actually takes, and it was still on the OWNED
6786 // entry: round 480's profile attributed 68.7 %
6787 // of `drop_glue<Value>` to this closure, which
6788 // is why round 479's fix to the indexed path
6789 // barely moved the total.
6790 //
6791 // The `matches!(…, Value::Bool(true))` form was
6792 // also a narrower reading than the rest of the
6793 // engine uses — `predicate_is_true` is what
6794 // handles NULL and MySQL truthiness — so the
6795 // bool entry fixes the shape as well as the cost.
6796 let pass = match cw {
6797 Some(c) => eval::compiled::eval_compiled_pred(
6798 c,
6799 row,
6800 &shard_ctx,
6801 &mut stack,
6802 shard_ctx.mysql_dialect,
6803 )
6804 .map_err(EngineError::Eval)?,
6805 None => true,
6806 };
6807 if pass {
6808 keep.push(i);
6809 }
6810 }
6811 Ok(keep)
6812 })();
6813 alloc::boxed::Box::new(out)
6814 });
6815 // v7.39 (round 567) — `rows()` is a 32-way trie, so
6816 // indexing it is four dependent loads and a scan that
6817 // reads every row paid them every row. A profile of
6818 // `SELECT sum(id)` over 500k rows put 37.8% of the
6819 // connection thread's CPU on THIS ONE LINE. The cursor
6820 // holds the leaf, making that one descent per 32.
6821 let mut rows_cur = table.rows().run_cursor();
6822 for boxed in results {
6823 let shard = boxed
6824 .downcast::<ShardOut>()
6825 .expect("runner echoes the closure's box");
6826 for i in (*shard)? {
6827 if let Some(row) = rows_cur.get(i) {
6828 filtered.push(row);
6829 }
6830 }
6831 }
6832 } else {
6833 let mut rows_cur = table.rows().run_cursor();
6834 // v7.38.11 — the slots the BRIN summary could not rule
6835 // out. The predicate still runs on every row that
6836 // survives: the summary decides what to SKIP, never
6837 // what to return.
6838 let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6839 for range in ranges {
6840 for i in range {
6841 if !table.is_row_visible(i, &scan_snapshot) {
6842 continue;
6843 }
6844 let Some(row) = rows_cur.get(i) else { continue };
6845 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6846 continue;
6847 }
6848 filtered.push(row);
6849 }
6850 }
6851 }
6852 for row in &cold_rows_storage {
6853 if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6854 continue;
6855 }
6856 filtered.push(row);
6857 }
6858 }
6859 // v7.29 — a per-query memo so correlated scalar
6860 // subqueries batch-evaluate once (group map) instead of
6861 // executing per group.
6862 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6863 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6864 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6865 .map_err(|err| match err {
6866 EngineError::Eval(ev) => ev,
6867 other => eval::EvalError::TypeMismatch {
6868 detail: alloc::format!("{other}"),
6869 },
6870 })
6871 };
6872 // v7.39 (round 656) — the plain relational scan. This collect() was
6873 // the measured defect: one 64-byte `RowRef` per surviving row to
6874 // wrap an 8-byte pointer `filtered` already holds. Scalar
6875 // aggregates measured ~81 bytes/row of working memory because of
6876 // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6877 // one number. `AggRows::Ptrs` reads the pointers directly.
6878 let agg = aggregate::run(
6879 stmt,
6880 crate::join::AggRows::Ptrs(&filtered),
6881 schema_cols,
6882 Some(alias),
6883 Some(&agg_correlated),
6884 self.parallel_runner.0.as_deref(),
6885 Some(self.active_catalog()),
6886 Some(self),
6887 )?;
6888 self.finish_agg_result(agg, stmt, cancel)
6889 }
6890
6891 /// Single-table scan + projection path: WHERE filter (compiled when
6892 /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6893 /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6894 fn run_single_table_scan<'a>(
6895 &self,
6896 stmt: &SelectStatement,
6897 table: &'a spg_storage::Table,
6898 schema_cols: &'a [ColumnSchema],
6899 alias: &str,
6900 indexed_rows: Option<crate::index_access::Seeked<'a>>,
6901 cancel: CancelToken<'_>,
6902 ) -> Result<QueryResult, EngineError> {
6903 // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6904 // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6905 // deterministic `__tsm_fract(seed)` draws share one scan-local
6906 // state (isolated from the global random() PRNG); a fresh cell per
6907 // scan makes a repeat / rescan reproduce the same sample. Unused
6908 // and cheap when the query carries no sample.
6909 let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6910 let ctx = self
6911 .ev_ctx(schema_cols, Some(alias))
6912 .with_sample_rng(&sample_cell);
6913 let projection = build_projection(
6914 &stmt.items,
6915 schema_cols,
6916 alias,
6917 self.speaks_mysql,
6918 Some(self.active_catalog()),
6919 )?;
6920 // v7.19 P5 — single-table SELECT path for SRF
6921 // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6922 // unnest in the projection list. When present, the
6923 // per-row processor emits one output row per array
6924 // element (broadcasting non-SRF projections from the
6925 // same input row). Empty / NULL arrays emit zero rows
6926 // for that input — PG semantics.
6927 // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
6928 let srf_idxs = self.srf_target_idxs(&projection);
6929 let srf_position = srf_idxs.first().copied();
6930 // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
6931 let mut srf_plan = if srf_position.is_some() {
6932 Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
6933 } else {
6934 None
6935 };
6936
6937 // Materialise the filter pass into `(order_key, projected_row)`
6938 // tuples. The order key is `None` when there's no ORDER BY clause.
6939 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
6940 // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
6941 // output row to the per-query byte budget as it is built, so a
6942 // fat single-table scan / sort REJECTS with QueryBytesExceeded
6943 // at ~the ceiling instead of materialising the whole table and
6944 // only noticing at the final enforce_row_limit check. Without
6945 // this, N concurrent fat scans peak at N×table and OOM the host.
6946 // `max_query_bytes = None` (the embedded default) = no ceiling,
6947 // so existing unbudgeted behaviour is byte-identical.
6948 let mut budget = ByteBudget::new(self.max_query_bytes);
6949 // v6.2.6 — Memoize per-row WHERE eval shares one cache.
6950 let mut memo = memoize::MemoizeCache::new();
6951 // v7.32 (perf knife D) — subquery-free WHERE compiles once;
6952 // the row loop then runs a flat step program instead of a
6953 // tree interpretation per row.
6954 let compiled_where: Option<eval::CompiledExpr> = stmt
6955 .where_
6956 .as_ref()
6957 .filter(|w| eval::fully_compilable(w))
6958 .map(|w| {
6959 // v7.38.8 — the scan filter runs the cheap half of its
6960 // conjunction first. Called from HERE and not from
6961 // `eval::compiled`, deliberately: the row loop lives in
6962 // that file, and adding a function to it cost this
6963 // query 11 % through layout alone while doing no work
6964 // for it. See `crate::qualorder`.
6965 match crate::qualorder::reordered(w) {
6966 Some(r) => eval::compile_expr(&r, &ctx),
6967 None => eval::compile_expr(w, &ctx),
6968 }
6969 });
6970 let mut eval_stack: Vec<Value<'static>> = Vec::new();
6971 // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
6972 // SELECT-item scalar subquery for the PK-probe fast path. The
6973 // analysis (gate checks + catalog lookups) takes ~500 ns; doing
6974 // it once per query instead of once per row × 100 rows saves
6975 // ~50 µs and lets the per-row evaluation reduce to a single
6976 // index probe + outer-column read.
6977 let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
6978 .iter()
6979 .map(|p| {
6980 if let Expr::ScalarSubquery(inner) = &p.expr {
6981 self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
6982 } else {
6983 None
6984 }
6985 })
6986 .collect();
6987 let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
6988 // v7.39 (round 487) — a projection item that is a bare column
6989 // reference binds its position ONCE per query.
6990 //
6991 // Per row it used to walk `eval_expr_with_correlated` (a memo
6992 // lookup for "does this have a subquery", then an un-memoised
6993 // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
6994 // then `resolve_column`, which finds the column by scanning the
6995 // schema and comparing NAMES. On `SELECT g FROM h` that chain was
6996 // 19 % of self time for what is ultimately one cell read.
6997 //
6998 // `compile_column_pos` is the Step VM's resolver, already
6999 // `pub(crate)` and already reused by the aggregate's bind-once
7000 // path: it mirrors `resolve_column`'s happy layers and returns
7001 // None for anything that would reach an error, an ambiguity, or a
7002 // miss, so those still go the interpreter's way and keep its
7003 // exact message. A composite column is excluded for the same
7004 // reason `compile_into` excludes it — it must be rehydrated from
7005 // stored JSON, which is not a cell read.
7006 let proj_direct = bind_direct_columns(&projection, &ctx);
7007 let any_proj_direct = proj_direct.iter().any(Option::is_some);
7008 // v7.39 (round 605) — a projection item that cannot depend on the row
7009 // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
7010 // allocations a row against one for a plain column, `'abc' || 'def'`
7011 // six and `upper('abc')` five, all of them producing the same value
7012 // 50,000 times. An item that fails to evaluate is left alone, so its
7013 // error still comes from the row loop in the interpreter's wording.
7014 let proj_const: Vec<Option<Value<'static>>> = projection
7015 .iter()
7016 .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
7017 .collect();
7018 let any_proj_const = proj_const.iter().any(Option::is_some);
7019 crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
7020 // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
7021 // projection. Statement prep (`resolve_order_by_position`) can only map
7022 // `ORDER BY 1` onto the first SELECT item when that item is an
7023 // expression; a `*` is not one, so the literal survived to here and was
7024 // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
7025 // at all. The parser rewrites `SELECT unnest(a) x` into
7026 // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
7027 // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
7028 // back in input order. The projection is built by now, so the Nth output
7029 // column is known — resolve against it.
7030 let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
7031 // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
7032 // EXPANDED rows, so a key naming a select-list item reads that item.
7033 let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
7034 srf_order_output_cols(&order_by, &projection)
7035 } else {
7036 Vec::new()
7037 };
7038 let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
7039 // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
7040 // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
7041 // Hoisted above the closure so the projection-eval path can
7042 // gate `memo` passing on it: the SELECT-item correlated-scalar
7043 // batch path scans the FULL inner table once (~5 ms for 12.5 k
7044 // rows) and is only a win when N outer rows is large; for small
7045 // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
7046 let early_cap: Option<usize> = if order_by.is_empty()
7047 && !stmt.distinct
7048 && !stmt.limit_with_ties
7049 && srf_position.is_none()
7050 && stmt.where_.is_none()
7051 {
7052 stmt.limit_literal()
7053 .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
7054 } else {
7055 None
7056 };
7057 // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
7058 // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
7059 // full-sort by the test gate) keep only the running top-`keep`
7060 // rows in memory instead of materialising every projected row,
7061 // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
7062 // space, not O(rows). `None` = accumulate everything (the prior
7063 // behaviour). The final `partial_sort_tagged(keep)` below still
7064 // runs and produces the identical rows.
7065 // v7.39 (round 683) — the declared collation for each ORDER BY
7066 // position, resolved once and carried beside `descs` for the same
7067 // reason `descs` is carried: it is per key position, not per row.
7068 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
7069 let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
7070 && !stmt.distinct
7071 && !stmt.limit_with_ties
7072 && srf_position.is_none()
7073 && !self.env_cfg().disable_topk
7074 {
7075 stmt.limit_literal().and_then(|l| {
7076 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
7077 (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
7078 })
7079 } else {
7080 None
7081 };
7082 // v7.38.19 — when the sort column is one the projection already
7083 // carries, build no key at all and sort by reading it.
7084 //
7085 // Restricted to the FULL sort: a top-N compares against a stored
7086 // boundary key and `WITH TIES` extends past the limit through the
7087 // keys, both of which need one to exist. DISTINCT keys on them
7088 // too, and an SRF's keys come from the EXPANDED row.
7089 // A COLLATION does not rule it out, but it has to be one that
7090 // orders these values the way bytes do -- decided on the values
7091 // themselves, further down, once they exist.
7092 let sort_by_output: Option<Vec<usize>> = if stmt.distinct
7093 || stmt.limit_with_ties
7094 || srf_position.is_some()
7095 || topk_stream.is_some()
7096 {
7097 None
7098 } else {
7099 order_by_output_cols_if_identical(&order_by, &projection, schema_cols)
7100 };
7101 // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
7102 // kept rows in `tagged`. Probing on the PROJECTED row as soon as
7103 // it is built means a duplicate costs neither a build_order_keys
7104 // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
7105 // a tagged slot, and the sort below runs over u survivors, not
7106 // n input rows — PG's hash-distinct-then-sort plan shape.
7107 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
7108 hashbrown::HashMap::new();
7109 let distinct_hb = hashbrown::DefaultHashBuilder::default();
7110 // v7.38.13 — which output positions must NOT fold. Built once per
7111 // scan from the projection, which carries the source column's
7112 // byte-wise-ness; see `FoldSpec`.
7113 let distinct_mask = fold_mask(&projection);
7114 // v7.39 (round 485) — one projection buffer for the whole scan
7115 // rather than a fresh `Vec` per input row. A row that survives
7116 // the DISTINCT probe takes the buffer with it (`mem::take`) and
7117 // the next row allocates a new one; a row that duplicates an
7118 // earlier one leaves the buffer — and its capacity — in place.
7119 // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
7120 // projected rows are duplicates, so that is 49 900 allocate /
7121 // free pairs the scan no longer performs. Shapes where every row
7122 // survives (plain projection, `DISTINCT` over a unique column)
7123 // allocate exactly as often as before.
7124 let mut proj_buf: Vec<Value<'static>> = Vec::new();
7125 // v7.39 (round 571) — buffers handed back by the top-N trim.
7126 // Round 485 made the scan share ONE projection buffer, but a
7127 // surviving row takes it (`mem::take`) and without DISTINCT
7128 // almost every row survives, so the next one starts from zero
7129 // capacity and allocates. The trim drops `keep` rows at a time
7130 // and their buffers come back here instead of being freed.
7131 let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
7132 let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
7133 // v7.39 (round 581) — the worst row the accumulator is currently
7134 // keeping. Anything that loses to it cannot reach the answer, so
7135 // it is dropped before its projection is ever built.
7136 let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
7137 // v7.38.20 — the boundary's own leading eight bytes, so a losing
7138 // row can be turned away before a key is built for it. Kept
7139 // beside the boundary and refreshed with it; `None` whenever the
7140 // boundary's first key is not one this can read, which sends
7141 // every row down the ordinary path.
7142 // v7.38.21 — and whether those bytes may be trusted under the
7143 // collation in force, which is the boundary's own text to answer.
7144 let mut topk_boundary_prefix: Option<(u64, bool)> = None;
7145 // v7.39 (round 582) — resolve each ORDER BY column once, not
7146 // once per row. See `order_by_bound_positions`.
7147 let order_bound =
7148 crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
7149 // v7.39 (round 581) — and it stops asking when the answer is
7150 // always "keep".
7151 //
7152 // The check earns its place only on rows it rejects. Over
7153 // ascending ids, `ORDER BY id DESC` never rejects one — every
7154 // row beats the current worst — so the comparison is pure
7155 // overhead there, measured at +5.5% in three batches out of
7156 // three. After a window of rows it looks at what it has
7157 // actually rejected and switches itself off if the shape is not
7158 // paying. The answers do not depend on it either way.
7159 // v7.38.21 — resolved once per query, not per row.
7160 //
7161 // No collation at all is the case v7.38.20 shipped. A DECLARED
7162 // one may still be answered by bytes, and which collations those
7163 // are is `Collated::ascii_byte_order`'s to say — the same
7164 // allowlist `byte_order_answers_the_collation` consults, so the
7165 // two cannot come to disagree about a collation. What that
7166 // allowlist requires of the TEXT is checked per row and on the
7167 // boundary, because a streaming top-N has no batch to check.
7168 let boundary_no_collation = order_colls.iter().all(Option::is_none);
7169 let boundary_collations_permit = boundary_no_collation
7170 || order_colls
7171 .iter()
7172 .flatten()
7173 .all(crate::collate::Collated::ascii_byte_order);
7174 const BOUNDARY_WINDOW: u32 = 8192;
7175 let mut boundary_checks: u32 = 0;
7176 let mut boundary_rejects: u32 = 0;
7177 let mut boundary_check_on = true;
7178 // Inline the per-row work in a closure so the indexed and full-
7179 // scan branches share the body.
7180 // v7.38.19 — `check_where` is per CALL SITE, not per closure: the
7181 // full-scan loops below must apply the predicate, and the
7182 // indexed loop must not when the seek already did. A captured
7183 // flag would have to be right for both.
7184 let mut process_row = |row: &Row<'static>,
7185 loop_idx: usize,
7186 check_where: bool|
7187 -> Result<(), EngineError> {
7188 if loop_idx.is_multiple_of(256) {
7189 cancel.check()?;
7190 }
7191 if !check_where {
7192 // The seek answered the whole predicate. See
7193 // `index_access::Seeked`.
7194 } else if let Some(cw) = &compiled_where {
7195 let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7196 .map_err(EngineError::Eval)?;
7197 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7198 return Ok(());
7199 }
7200 } else if let Some(where_expr) = &stmt.where_ {
7201 let cond =
7202 self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7203 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7204 return Ok(());
7205 }
7206 }
7207 // Under DISTINCT the keys are built AFTER the dup probe
7208 // (survivors only); the non-distinct order is unchanged.
7209 // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7210 // row further down, and building them here would evaluate the
7211 // ORDER BY against the INPUT row: a key naming the SRF's own
7212 // output became a scalar call to it, which is where
7213 // "function unnest(integer[]) does not exist" came from.
7214 let order_keys = if order_by.is_empty()
7215 || stmt.distinct
7216 || srf_position.is_some()
7217 // v7.38.19 — the branch below builds whatever key it
7218 // needs from the projected values, collation included,
7219 // so nothing has to be built here for it.
7220 //
7221 // A draft that skipped them here but still let the
7222 // COLLATED case fall through to the key-based sort put a
7223 // mixed column back in INSERT order: every key empty,
7224 // every row equal, a stable sort faithfully preserving
7225 // nothing. The rule is one decision, not two.
7226 || sort_by_output.is_some()
7227 {
7228 Vec::new()
7229 } else {
7230 // v7.38.20 — turn a decisively losing row away before
7231 // its key is built. Only the FIRST key is read, and only
7232 // its leading eight bytes; a tie there decides nothing
7233 // and falls through to the full path below.
7234 //
7235 // ASC only: under DESC the boundary is the largest kept
7236 // key and the comparison flips, which this deliberately
7237 // does not try to express — a second direction in a
7238 // fast-path predicate is how one of them ends up wrong.
7239 if boundary_check_on
7240 && let Some((_, descs)) = &topk_stream
7241 && !descs.first().copied().unwrap_or(false)
7242 && order_by.len() == 1
7243 && boundary_collations_permit
7244 && let Some((bp, boundary_is_ascii)) = topk_boundary_prefix
7245 && let Some((rp, row_is_ascii)) =
7246 crate::orderby::first_key_prefix(&order_bound, row)
7247 && (boundary_no_collation || (boundary_is_ascii && row_is_ascii))
7248 && rp > bp
7249 {
7250 boundary_checks += 1;
7251 boundary_rejects += 1;
7252 if boundary_checks == BOUNDARY_WINDOW {
7253 boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7254 }
7255 return Ok(());
7256 }
7257 let mut buf = key_pool.pop().unwrap_or_default();
7258 crate::orderby::build_order_keys_bound(
7259 &order_by,
7260 &order_bound,
7261 &order_colls,
7262 row,
7263 &ctx,
7264 &mut buf,
7265 )?;
7266 // v7.39 (round 581) — reject before projecting.
7267 //
7268 // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7269 // 50 distinct `g` decides nearly every row on the FIRST
7270 // key, and PG answers it FASTER than the single-key form
7271 // (7.4 ms against 10.4) because a rejected row costs it
7272 // one comparison. SPG built both keys AND the projected
7273 // row for all 500k before throwing them away. The keys
7274 // are needed to compare; the projection is not.
7275 if boundary_check_on
7276 && let Some((_, descs)) = &topk_stream
7277 && let Some(b) = &topk_boundary
7278 {
7279 boundary_checks += 1;
7280 let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7281 == core::cmp::Ordering::Greater;
7282 if loses {
7283 boundary_rejects += 1;
7284 }
7285 if boundary_checks == BOUNDARY_WINDOW {
7286 // Keep asking only if it has been rejecting at
7287 // least a quarter of what it saw.
7288 boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7289 }
7290 if loses {
7291 buf.clear();
7292 key_pool.push(buf);
7293 return Ok(());
7294 }
7295 }
7296 buf
7297 };
7298 if srf_position.is_some() {
7299 let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7300 for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7301 if stmt.distinct {
7302 let bucket = seen_distinct
7303 .entry(norm_hash_row(
7304 &out,
7305 &distinct_hb,
7306 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7307 ))
7308 .or_default();
7309 if bucket.iter().any(|i| {
7310 row_eq_norm(
7311 &tagged[i].1,
7312 &out,
7313 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7314 )
7315 }) {
7316 continue;
7317 }
7318 bucket.push(tagged.len());
7319 }
7320 budget.charge(approx_row_bytes(&out))?;
7321 // The keys come from THIS expanded row: a key naming a
7322 // select-list item reads its value, anything else is
7323 // still evaluated against the input row.
7324 let keys = if order_by.is_empty() {
7325 Vec::new()
7326 } else {
7327 let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7328 for (k, ob) in order_by.iter().enumerate() {
7329 kv.push(match srf_order_cols.get(k).copied().flatten() {
7330 Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7331 None => eval::eval_expr(&ob.expr, row, &ctx)
7332 .map_err(EngineError::Eval)?,
7333 });
7334 }
7335 // Packed by the same code every other ORDER BY uses,
7336 // so DESC / NULLS FIRST / the MySQL rule are not
7337 // restated here.
7338 let key_row = Row::new(kv);
7339 let mut buf = Vec::new();
7340 crate::orderby::build_order_keys_bound(
7341 &order_by,
7342 &srf_key_bound,
7343 &order_colls,
7344 &key_row,
7345 &ctx,
7346 &mut buf,
7347 )?;
7348 buf
7349 };
7350 tagged.push((keys, out));
7351 }
7352 } else {
7353 let values = &mut proj_buf;
7354 values.clear();
7355 values.reserve(projection.len());
7356 for (i, p) in projection.iter().enumerate() {
7357 // v7.37.x (docker-fair SCALARSQ attack) — pre-
7358 // analysed PK-probe fast path. The per-row work is
7359 // a read of outer.col from the row plus an index
7360 // probe — no Expr clone, no walker, no
7361 // `eval_expr_with_correlated` framework.
7362 if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7363 values.push(self.probe_with_pk_fast_path(fp, row));
7364 continue;
7365 }
7366 // v7.39 (round 605) — the same value every row.
7367 if any_proj_const && let Some(v) = &proj_const[i] {
7368 values.push(v.clone());
7369 continue;
7370 }
7371 // v7.39 (round 487) — bound column: read the cell.
7372 // This is `rehydrate_cell`'s body for a non-composite
7373 // column, which is what the whole chain below reduces
7374 // to once the name has been resolved.
7375 if any_proj_direct && let Some(pos) = proj_direct[i] {
7376 crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7377 values.push(row.values[pos].clone().into_owned());
7378 continue;
7379 }
7380 // v7.24 (round-16 B) — correlated-aware.
7381 // v7.37.x (docker-fair SCALARSQ attack) — share the
7382 // per-row memo with projection. Required for the
7383 // batch-evaluated correlated-scalar path to fire on
7384 // SELECT-item scalar subqueries; otherwise each row
7385 // re-executes the inner.
7386 //
7387 // Skip the memo when the outer row count is small
7388 // (early-limited): the batch path scans the FULL
7389 // inner table to build a GroupMap (~5 ms for a
7390 // 12.5 k-row inner), while per-row execution with a
7391 // PK index seek is ~5 µs per call — much cheaper for
7392 // N ≤ ~1000 outer rows.
7393 let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7394 let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7395 values.push(
7396 self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7397 );
7398 }
7399 crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7400 if stmt.distinct {
7401 let bucket = seen_distinct
7402 .entry(norm_hash_values(
7403 &proj_buf,
7404 &distinct_hb,
7405 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7406 ))
7407 .or_default();
7408 if bucket.iter().any(|i| {
7409 values_eq_norm(
7410 &tagged[i].1.values,
7411 &proj_buf,
7412 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7413 )
7414 }) {
7415 crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7416 return Ok(());
7417 }
7418 bucket.push(tagged.len());
7419 }
7420 let out = Row::new(core::mem::replace(
7421 &mut proj_buf,
7422 proj_pool.pop().unwrap_or_default(),
7423 ));
7424 let order_keys = if stmt.distinct && !order_by.is_empty() {
7425 // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7426 // the bound-cell path precisely so an ORDER BY key that
7427 // names a column is READ instead of evaluated, and the
7428 // non-DISTINCT branch above has passed it ever since;
7429 // this branch never did, so `SELECT DISTINCT k .. ORDER
7430 // BY k` resolved "k" by string for every surviving row.
7431 let mut buf = key_pool.pop().unwrap_or_default();
7432 crate::orderby::build_order_keys_bound(
7433 &order_by,
7434 &order_bound,
7435 &order_colls,
7436 row,
7437 &ctx,
7438 &mut buf,
7439 )?;
7440 buf
7441 } else {
7442 order_keys
7443 };
7444 budget.charge(approx_row_bytes(&out))?;
7445 tagged.push((order_keys, out));
7446 }
7447 // Streaming top-N: bound the accumulator to O(keep) rows.
7448 if let Some((k, descs)) = &topk_stream {
7449 crate::orderby::topk_trim_recycling(
7450 &mut tagged,
7451 *k,
7452 descs,
7453 &mut proj_pool,
7454 &mut key_pool,
7455 &mut topk_boundary,
7456 );
7457 // The prefix follows the boundary it summarises.
7458 topk_boundary_prefix = topk_boundary
7459 .as_ref()
7460 .and_then(|b| b.first())
7461 .and_then(crate::orderby::order_key_text_prefix);
7462 }
7463 Ok(())
7464 };
7465 // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7466 // load-bearing full-scan path. This is the primary single-table
7467 // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7468 // in-place writers retain dead/old versions, an ungated scan
7469 // here would return them, so the gate must land BEFORE the
7470 // writers flip (see the plan's activation-order rule). A no-op
7471 // today: every hot row is frozen or committed-and-alive under
7472 // the reader's snapshot, so `is_row_visible` returns true for
7473 // all of them (verified by the full e2e suite staying green).
7474 let scan_snapshot = self.current_snapshot();
7475 let mut emitted: usize = 0;
7476 if let Some(seeked) = &indexed_rows {
7477 let recheck = !seeked.exact;
7478 for (loop_idx, cow) in seeked.rows.iter().enumerate() {
7479 if let Some(cap) = early_cap
7480 && emitted >= cap
7481 {
7482 break;
7483 }
7484 process_row(cow.as_ref(), loop_idx, recheck)?;
7485 emitted = emitted.saturating_add(1);
7486 }
7487 } else {
7488 // v7.39 (round 570) — the row store is a 32-way trie, so
7489 // indexing it is four dependent loads. Round 567 measured
7490 // -18% on the aggregate scan from holding the leaf between
7491 // rows; this is the same loop for the projecting scan.
7492 let mut rows_cur = table.rows().run_cursor();
7493 // v7.38.11 — see the aggregate scan above: a BRIN index on a
7494 // column this WHERE bounds says which slots cannot match.
7495 let brin_slots = stmt
7496 .where_
7497 .as_ref()
7498 .and_then(|w| crate::brin::candidate_slots(w, table))
7499 .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7500 for i in brin_slots.into_iter().flatten() {
7501 if let Some(cap) = early_cap
7502 && emitted >= cap
7503 {
7504 break;
7505 }
7506 // Skip rows this snapshot cannot see (invisible rows do
7507 // not count toward the LIMIT).
7508 if !table.is_row_visible(i, &scan_snapshot) {
7509 continue;
7510 }
7511 let Some(row) = rows_cur.get(i) else { continue };
7512 process_row(row, i, true)?;
7513 emitted = emitted.saturating_add(1);
7514 }
7515 // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7516 // rows into the same loop. The full-scan path here is the
7517 // load-bearing single-table SELECT executor, and pre-
7518 // 7.35.1 it only walked `table.rows()` (hot), so any
7519 // `SELECT … FROM t` against a table with cold segments
7520 // silently returned a subset.
7521 let cold_rows = self.iter_cold_rows_of_table(table);
7522 for (offset, row) in cold_rows.iter().enumerate() {
7523 if let Some(cap) = early_cap
7524 && emitted >= cap
7525 {
7526 break;
7527 }
7528 process_row(row, table.row_count() + offset, true)?;
7529 emitted = emitted.saturating_add(1);
7530 }
7531 }
7532
7533 // (DISTINCT already de-duped STREAMING inside process_row, so the
7534 // sort below only sees the u survivors and the partial-sort
7535 // budget applies to DISTINCT too.)
7536 if !order_by.is_empty() {
7537 // Partial-sort fast path: when LIMIT is small relative to
7538 // the row count, select_nth_unstable + sort just the
7539 // prefix is O(n + k log k) instead of O(n log n).
7540 // WITH TIES needs the full sort so the tie extension can
7541 // scan past `limit` to find rows that share the last-kept
7542 // row's key.
7543 let keep = if stmt.limit_with_ties
7544 // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7545 // forces the full-sort fallback by suppressing the
7546 // partial-sort `keep` budget. See
7547 // `xtests/sigil/test-mode-gucs.md`.
7548 || self.env_cfg().disable_topk
7549 {
7550 None
7551 } else {
7552 stmt.limit_literal()
7553 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7554 };
7555 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7556 if let Some(cols) = &sort_by_output {
7557 // No keys were built; the sort reads the projected row.
7558 // The comparator is the value-level one the window
7559 // functions and the key path both defer to, so DESC,
7560 // NULLS placement, the MySQL fold and the collation are
7561 // not restated here.
7562 let terms: Vec<(usize, bool, Option<bool>)> = cols
7563 .iter()
7564 .zip(order_by.iter())
7565 .map(|(c, o)| (*c, o.desc, o.nulls_first))
7566 .collect();
7567 let mysql = ctx.mysql_dialect;
7568 // v7.38.19 — sort a PERMUTATION carrying the first eight
7569 // bytes, not the rows.
7570 //
7571 // The elements above are `(Vec<OrderKey>, Row)`, 48 bytes,
7572 // and driftsort moves them ~n log n times: 7.4 M moves at
7573 // 400,000 rows. Worse, every comparison chases three
7574 // dependent loads PER SIDE to reach the byte it wants --
7575 // the row's `Vec`, the `Value`, then the string's own
7576 // buffer -- and a profile of this sort put 35% of its
7577 // working samples in the sort machinery around that.
7578 //
7579 // A `(u64, u32)` is 16 bytes and the comparison reads it
7580 // straight out of the array. The u64 is the first eight
7581 // bytes big-endian, zero-padded, which ORDERS THE SAME as
7582 // the string: if two differ inside those bytes they differ
7583 // at the same index either way, and a string shorter than
7584 // eight pads with zeros exactly where `[u8]`'s own
7585 // comparison runs out. Equal prefixes fall through to the
7586 // full comparator, so nothing rests on the padding being
7587 // clever.
7588 //
7589 // The tail-break on the index is what keeps the sort
7590 // STABLE, which `sort_by` was giving for free and an
7591 // unstable sort over a permutation would not.
7592 // v7.38.19 — three ways to sort these rows, and which
7593 // one is right turns on the values, which is why it is
7594 // decided here rather than at plan time.
7595 //
7596 // * the collation orders these values the way bytes do
7597 // -- take the eight-byte key below
7598 // * it does not, but there IS a collation -- build its
7599 // sort key once per row and order the permutation on
7600 // those, which is what the key path did, done from
7601 // the projected value instead of during the scan
7602 // * no collation at all -- the eight-byte key again
7603 //
7604 // The middle case is the one a draft got wrong by
7605 // leaving the rows to a key path whose keys it had just
7606 // skipped building.
7607 let mut keep_sorted = false;
7608 let bytes_answer = byte_order_answers_the_collation(&tagged, &terms, &order_colls);
7609 if !bytes_answer && let Some(coll) = order_colls.first().and_then(Option::as_ref) {
7610 let (first_col, first_desc, _) = terms[0];
7611 let mut order: Vec<(Vec<u8>, u32)> = Vec::with_capacity(tagged.len());
7612 for (i, row) in tagged.iter().enumerate() {
7613 let k = match row.1.values.get(first_col) {
7614 Some(Value::Text(t)) => coll.sort_key_of(t).unwrap_or_else(|| {
7615 let mut v = Vec::with_capacity(t.len() + 1);
7616 v.push(0);
7617 v.extend_from_slice(t.as_bytes());
7618 v
7619 }),
7620 _ => Vec::new(),
7621 };
7622 order.push((k, u32::try_from(i).unwrap_or(u32::MAX)));
7623 }
7624 order.sort_by(|(ka, ia), (kb, ib)| {
7625 let c = ka.cmp(kb);
7626 let c = if first_desc { c.reverse() } else { c };
7627 if c != core::cmp::Ordering::Equal {
7628 return c;
7629 }
7630 row_cmp_by_index(&tagged, &terms, &order_colls, mysql, *ia, *ib)
7631 .then_with(|| ia.cmp(ib))
7632 });
7633 let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7634 core::mem::take(&mut tagged).into_iter().map(Some).collect();
7635 tagged = order
7636 .iter()
7637 .map(|&(_, i)| {
7638 slots[i as usize]
7639 .take()
7640 .expect("the permutation names each row once")
7641 })
7642 .collect();
7643 keep_sorted = true;
7644 }
7645 // v7.38.20 — a key that does NOT discriminate is still
7646 // worth sorting on, as long as the runs it leaves are
7647 // handled once instead of n log n times.
7648 //
7649 // `text (26 values)` is two hundred identical characters
7650 // drawn from twenty-six letters, so every eight-byte
7651 // prefix inside a letter is the same and 15,384 rows tie
7652 // on it. A comparison sort then asks ~7.4 M questions of
7653 // which nearly all are a two-hundred-byte `memcmp`
7654 // answering EQUAL: profiled, 30% of the working samples
7655 // sat in `memcmp` and 37% in the sort machinery.
7656 //
7657 // Sorting the integer keys is cheap. What each run needs
7658 // afterwards is ONE pass: if every value in it is equal,
7659 // input order already IS the stable answer, and proving
7660 // that costs n-1 comparisons rather than n log n. Only a
7661 // run that is not all-equal gets sorted.
7662 //
7663 // Single-term only. With a second ORDER BY column an
7664 // all-equal first term does not settle the row order --
7665 // the later terms still speak -- and the shortcut would
7666 // drop them.
7667 let all_keys = if keep_sorted {
7668 None
7669 } else {
7670 sort_keys_of(&tagged, terms[0].0)
7671 };
7672 let low_card = !keep_sorted
7673 && terms.len() == 1
7674 && all_keys
7675 .as_ref()
7676 .is_some_and(|(keys, exact)| !*exact && !key_discriminates(keys));
7677 let keyed =
7678 all_keys.filter(|(keys, exact)| *exact || key_discriminates(keys) || low_card);
7679 if keep_sorted {
7680 // The collated permutation above already placed every
7681 // row. A draft let the byte-order fallback run after
7682 // it and undo the whole thing.
7683 } else if let Some((mut order, exact)) = keyed {
7684 let (first_col, first_desc, _) = terms[0];
7685 let row_cmp = |ia: u32, ib: u32| -> core::cmp::Ordering {
7686 let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
7687 for (col, desc, nf) in &terms {
7688 let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col))
7689 else {
7690 continue;
7691 };
7692 let ord = match (va, vb) {
7693 (Value::Text(x), Value::Text(y)) if !mysql => {
7694 let c = crate::orderby::str_cmp_prefix_first(x, y);
7695 if *desc { c.reverse() } else { c }
7696 }
7697 _ => {
7698 crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7699 }
7700 };
7701 if ord != core::cmp::Ordering::Equal {
7702 return ord;
7703 }
7704 }
7705 core::cmp::Ordering::Equal
7706 };
7707 let _ = first_col;
7708 if low_card {
7709 // Integer sort first, then one pass per run.
7710 order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
7711 let c = pa.cmp(&pb);
7712 let c = if first_desc { c.reverse() } else { c };
7713 c.then_with(|| ia.cmp(&ib))
7714 });
7715 let mut lo = 0;
7716 while lo < order.len() {
7717 let mut hi = lo + 1;
7718 while hi < order.len() && order[hi].0 == order[lo].0 {
7719 hi += 1;
7720 }
7721 if hi - lo > 1 {
7722 let head = tagged[order[lo].1 as usize].1.values.get(first_col);
7723 let uniform = order[lo + 1..hi].iter().all(|&(_, i)| {
7724 tagged[i as usize].1.values.get(first_col) == head
7725 });
7726 if !uniform {
7727 order[lo..hi].sort_by(|&(_, ia), &(_, ib)| {
7728 row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
7729 });
7730 }
7731 // A uniform run is already in index
7732 // order, which IS the stable answer.
7733 }
7734 lo = hi;
7735 }
7736 } else {
7737 order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
7738 let c = pa.cmp(&pb);
7739 let c = if first_desc { c.reverse() } else { c };
7740 if c != core::cmp::Ordering::Equal {
7741 return c;
7742 }
7743 // An EXACT key that ties means the values are
7744 // equal, so only the remaining terms can speak.
7745 // A prefix that ties has decided nothing yet and
7746 // the first term must be asked again, which
7747 // `row_cmp` does by walking every term from the
7748 // start.
7749 if exact && terms.len() == 1 {
7750 return ia.cmp(&ib);
7751 }
7752 row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
7753 });
7754 }
7755 let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7756 core::mem::take(&mut tagged).into_iter().map(Some).collect();
7757 tagged = order
7758 .iter()
7759 .map(|&(_, i)| {
7760 slots[i as usize]
7761 .take()
7762 .expect("the permutation names each row once")
7763 })
7764 .collect();
7765 } else {
7766 tagged.sort_by(|a, b| {
7767 for (i, (col, desc, nf)) in terms.iter().enumerate() {
7768 let va = a.1.values.get(*col);
7769 let vb = b.1.values.get(*col);
7770 let (Some(va), Some(vb)) = (va, vb) else {
7771 continue;
7772 };
7773 let _ = i;
7774 // v7.38.19 — two non-NULL strings, no MySQL fold, is
7775 // where a text sort spends every one of its ~7 M
7776 // comparisons, and the shared comparator cannot be
7777 // inlined into this loop: it carries NULL placement,
7778 // the fold, the NUMERIC bignum gate and the float
7779 // total order. Answering that one pair here is the
7780 // same answer by the same route — `value_cmp`'s
7781 // leading same-variant arm is `x.cmp(y)`, and the
7782 // raw comparator's last act is this reverse.
7783 let ord = match (va, vb) {
7784 (Value::Text(x), Value::Text(y)) if !mysql => {
7785 let c = crate::orderby::str_cmp_prefix_first(x, y);
7786 if *desc { c.reverse() } else { c }
7787 }
7788 _ => {
7789 crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7790 }
7791 };
7792 if ord != core::cmp::Ordering::Equal {
7793 return ord;
7794 }
7795 }
7796 core::cmp::Ordering::Equal
7797 });
7798 }
7799 } else {
7800 crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7801 }
7802 }
7803
7804 // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7805 // past the truncated tail through every row that shares the
7806 // last-kept row's ORDER BY key. The tie check uses the
7807 // already-computed `(order_keys, row)` pairs so it matches
7808 // the sort comparator exactly. DISTINCT + WITH TIES falls
7809 // through to the no-ties path (PG also disallows their
7810 // combination; SPG silently drops the tie extension here so
7811 // the customer doesn't see a hard error mid-query — the
7812 // user-visible result is still correct, just narrower).
7813 let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7814 apply_offset_and_limit_tagged(
7815 &mut tagged,
7816 stmt.offset_literal(),
7817 stmt.limit_literal(),
7818 true,
7819 );
7820 tagged.into_iter().map(|(_, r)| r).collect()
7821 } else {
7822 // DISTINCT already de-duped pre-sort above.
7823 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7824 apply_offset_and_limit(
7825 &mut output_rows,
7826 stmt.offset_literal(),
7827 stmt.limit_literal(),
7828 );
7829 output_rows
7830 };
7831
7832 let columns: Vec<ColumnSchema> = projection
7833 .into_iter()
7834 .map(|p| p.to_column_schema())
7835 .collect();
7836
7837 Ok(QueryResult::Rows {
7838 columns,
7839 rows: output_rows,
7840 })
7841 }
7842
7843 /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7844 /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7845 /// select items for the surviving rows only — PG's Result-above-
7846 /// Limit shape, where SubPlan loops equal the OUTPUT row count
7847 /// (50) instead of the group count (24k).
7848 fn finish_agg_result(
7849 &self,
7850 mut agg: aggregate::AggResult,
7851 stmt: &SelectStatement,
7852 cancel: CancelToken<'_>,
7853 ) -> Result<QueryResult, EngineError> {
7854 apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7855 if !agg.deferred.is_empty() {
7856 apply_offset_and_limit(
7857 &mut agg.synth_rows,
7858 stmt.offset_literal(),
7859 stmt.limit_literal(),
7860 );
7861 let ctx = EvalContext::new(&agg.synth_schema, None);
7862 let mut memo = memoize::MemoizeCache::default();
7863 // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7864 // Deferred subqueries are referenced only by surviving
7865 // select-list rows (≤ LIMIT), so their correlation keys are
7866 // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7867 // each batchable subquery's group map over just those keys
7868 // via per-key index seek; the per-row splice loop below then
7869 // reuses the seeded map. A join-shaped or un-indexed inner
7870 // falls through to the all-keys batch inside the call (built
7871 // eagerly here instead of lazily on row 0 — same cost), so
7872 // it still pays the full scan, never the 715 ms per-row
7873 // direct eval; its index-nested-loop probe is the next
7874 // knife. Genuinely non-batchable shapes return None and are
7875 // left unseeded for the loop's per-row resolver, as before.
7876 for (_, expr) in &agg.deferred {
7877 let mut subs: Vec<&SelectStatement> = Vec::new();
7878 collect_scalar_subqueries(expr, &mut subs);
7879 for sub in subs {
7880 let repr = alloc::format!("{sub}");
7881 if memo.group_maps.contains_key(&repr) {
7882 continue;
7883 }
7884 if let Some(gm) = self.try_batch_correlated_scalar(
7885 sub,
7886 Some((&agg.synth_rows, &ctx)),
7887 cancel,
7888 )? {
7889 memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7890 }
7891 }
7892 }
7893 for (ri, srow) in agg.synth_rows.iter().enumerate() {
7894 cancel.check()?;
7895 for (col, expr) in &agg.deferred {
7896 let v =
7897 self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7898 if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7899 *cell = v;
7900 }
7901 }
7902 }
7903 }
7904 Ok(QueryResult::Rows {
7905 columns: agg.columns,
7906 rows: agg.rows,
7907 })
7908 }
7909
7910 /// v7.37 — streaming projection for the joined-non-aggregate
7911 /// shape (multi-table FROM, all projection items bound, no
7912 /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
7913 /// UNION). Walks the deferred join survivors and emits
7914 /// `&[&Value]` borrowed straight out of the source tables — no
7915 /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
7916 /// on the mailrs `PROJ` shape (about 4 ms saved).
7917 ///
7918 /// Returns `Ok(None)` when the shape doesn't qualify; the caller
7919 /// then falls back to the materialising path.
7920 /// v7.37 (round 831) — stream a joinless SELECT straight off the
7921 /// stored table, one row at a time, without ever building a row set.
7922 ///
7923 /// Returns `Ok(None)` for anything this cannot serve, and the caller
7924 /// falls through to the deferred-join path exactly as before: a
7925 /// missing table, or a cold tier whose hydration the fallback handles.
7926 /// Sort a single-table scan through the external sorter, so the
7927 /// answer's size is bounded by `work_mem` and not by the input.
7928 ///
7929 /// Sorting held every row twice — the scan's `Vec<Row>` and the
7930 /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
7931 /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
7932 /// enough ORDER BY took the server down, which is a liveness
7933 /// problem before it is a performance one.
7934 ///
7935 /// A SEPARATE walk rather than a change to `run_single_table_scan`,
7936 /// following what round 831 did for the joinless shape. That
7937 /// function is 552 lines whose projection loop is entangled with
7938 /// DISTINCT (which indexes back into the tagged vector) and with
7939 /// streaming top-N (whose boundary moves as the scan runs); both
7940 /// assume the projection has already happened when a row is
7941 /// pushed, which is exactly what spilling has to defer. Two earlier
7942 /// attempts tried to rework that loop and were reverted. Here the
7943 /// existing path is untouched and this one only claims shapes it
7944 /// can serve, so a decline costs nothing.
7945 ///
7946 /// Records are SOURCE rows, not projected ones: `finish` re-derives
7947 /// keys from what it decodes, and an ORDER BY key need not be in
7948 /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
7949 fn try_spill_sorted_scan(
7950 &self,
7951 stmt: &SelectStatement,
7952 from: &FromClause,
7953 cancel: CancelToken<'_>,
7954 ) -> Result<Option<QueryResult>, EngineError> {
7955 // Shapes this walk does not serve. Each one either needs the
7956 // whole tagged vector addressable (DISTINCT probes back into
7957 // it, WITH TIES re-reads its tail) or is already bounded
7958 // without spilling (a LIMIT makes the partial sort O(keep)).
7959 if !self.can_spill()
7960 || stmt.order_by.is_empty()
7961 || stmt.distinct
7962 || stmt.limit_with_ties
7963 || stmt.limit_literal().is_some()
7964 || !from.joins.is_empty()
7965 || from.primary.lateral_subquery.is_some()
7966 || from.primary.unnest_expr.is_some()
7967 || from.primary.generate_series_args.is_some()
7968 || select_has_window(stmt)
7969 {
7970 return Ok(None);
7971 }
7972 // A parent's rows are its children's. These walks scan the named
7973 // relation alone, so a partitioned or inherited parent comes back
7974 // short — and silently: the corpus caught `SELECT id FROM pr
7975 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
7976 // parent's own rows instead of the partitions'. `ONLY` is exactly
7977 // the case that does not fan out, so it stays, which is the test
7978 // the FROM-clause fan-out itself makes.
7979 if !from.primary.only
7980 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7981 {
7982 return Ok(None);
7983 }
7984 let Some(table) = self.active_catalog().get(&from.primary.name) else {
7985 return Ok(None);
7986 };
7987 // Cold-tier rows live outside `rows()`; this walk would drop
7988 // them silently, the same reason round 831's walk declines.
7989 if table.has_cold_rows_fast() {
7990 return Ok(None);
7991 }
7992
7993 let alias = from
7994 .primary
7995 .alias
7996 .as_deref()
7997 .unwrap_or(from.primary.name.as_str());
7998 let cols = table.schema().columns.clone();
7999 let sess = self.dml_session();
8000 let ctx = EvalContext::new(&cols, Some(alias))
8001 .with_catalog(self.active_catalog())
8002 .with_session(&sess);
8003 let projection = build_projection(
8004 &stmt.items,
8005 &cols,
8006 alias,
8007 self.speaks_mysql,
8008 Some(self.active_catalog()),
8009 )?;
8010 let order_by = stmt.order_by.clone();
8011 // The same one-shot resolution the general path does (round
8012 // 582): each ORDER BY column is bound once, not once per row.
8013 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8014 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8015 // Resolved BEFORE the scan, because it now decides what the sort
8016 // STORES and not just what it decodes (round 995).
8017 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8018
8019 // v7.38.22 — resolved HERE, because this path did not resolve
8020 // them at all.
8021 //
8022 // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8023 // "en_US.utf8"` in BYTE order on this path — and swallowed an
8024 // unknown collation name rather than raising — because the sorter
8025 // below compared with an empty collation slice. The materialising
8026 // path honoured both. Which answer a query got depended on which
8027 // path the planner took, and this is the path a plain single-table
8028 // SELECT takes.
8029 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
8030 let mut sorter = crate::extsort::ExternalSorter::new(
8031 self.temp_run_factory,
8032 self.session_work_mem_bytes(),
8033 cols.clone(),
8034 &descs,
8035 &order_colls,
8036 )
8037 .with_stats(&self.spill_stats)
8038 .with_pruned(&needed);
8039 let snapshot = self.current_snapshot();
8040 // One key buffer for the whole scan: `push` drains it and leaves
8041 // the capacity behind.
8042 let mut keys: Vec<OrderKey> = Vec::new();
8043 // r1024 — compile the predicate once for the scan.
8044 //
8045 // These two sorted-spill scans are the paths a single-table SELECT
8046 // with an ORDER BY takes, and they were the last row-returning ones
8047 // still walking the expression tree per row. r1023 did the
8048 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8049 // exactly this shape.
8050 //
8051 // Found from the profile's CALL TREE rather than its leaves. The
8052 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8053 // 261, `mod_op` 178 — and two attempts at reasoning out which
8054 // function asked for it were both wrong. The tree names the caller
8055 // chain, and it named this one.
8056 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8057 .where_
8058 .as_ref()
8059 .filter(|w| crate::eval::fully_compilable(w))
8060 .map(|w| crate::eval::compile_expr(w, &ctx));
8061 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8062 for (i, row) in table.scan_visible_from(0, &snapshot) {
8063 if i.is_multiple_of(256) {
8064 cancel.check()?;
8065 }
8066 if let Some(c) = &compiled_where {
8067 if !crate::eval::compiled::eval_compiled_pred(
8068 c,
8069 row,
8070 &ctx,
8071 &mut eval_stack,
8072 ctx.mysql_dialect,
8073 )? {
8074 continue;
8075 }
8076 } else if let Some(w) = &stmt.where_ {
8077 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8078 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8079 continue;
8080 }
8081 }
8082 keys.clear();
8083 // The same collations the sorter compares with, and the
8084 // re-derivation below is handed the same ones. `finish`'s
8085 // contract is that a key comes back the way it was pushed;
8086 // a collation is part of the way it was pushed.
8087 crate::orderby::build_order_keys_bound(
8088 &order_by,
8089 &order_bound,
8090 &order_colls,
8091 row,
8092 &ctx,
8093 &mut keys,
8094 )?;
8095 sorter.push(&mut keys, row)?;
8096 }
8097
8098 let key_ctx = &ctx;
8099 let rows = sorter.finish(
8100 |src, buf| {
8101 crate::orderby::build_order_keys_rederived(
8102 &order_by,
8103 &order_bound,
8104 &order_colls,
8105 src,
8106 key_ctx,
8107 buf,
8108 )
8109 },
8110 |src| {
8111 let mut values = Vec::with_capacity(projection.len());
8112 for p in &projection {
8113 values.push(
8114 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8115 );
8116 }
8117 Ok(Row::new(values))
8118 },
8119 )?;
8120
8121 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8122 Ok(Some(QueryResult::Rows { columns, rows }))
8123 }
8124
8125 /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
8126 /// handing each row to the consumer instead of collecting the answer.
8127 ///
8128 /// That walk bounds the SORT and then returns `QueryResult::Rows`,
8129 /// which holds every output row. Measured at `work_mem = 4 MB` over
8130 /// 200-byte rows, RSS above the server's own baseline while the
8131 /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
8132 /// at 400k — linear — while the spill underneath worked correctly
8133 /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
8134 /// removes each file, so a count taken afterwards reads 0 whatever
8135 /// happened, and an earlier reading of "no spill at all" was that
8136 /// blind witness). The growth is the collected result, not the sort.
8137 ///
8138 /// Emitting makes peak the budget, one buffer per run and a single
8139 /// row — the state a merge already holds at every step. It also
8140 /// frees each projected row as the next is built rather than
8141 /// accumulating them, which is where the time is: a profile of the
8142 /// collecting walk put the allocator at 586 samples, more than every
8143 /// sort comparison combined (420), against 19 for `push` itself.
8144 /// v7.37 (round 923) — which of a sort record's columns the output half
8145 /// reads. The record is the SOURCE row (round 836), so a narrow projection
8146 /// decoded every column: skipping one 200-byte text halves a decode
8147 /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
8148 ///
8149 /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
8150 /// column reads NULL. Answers only when every projection item is a bare
8151 /// column reference AND every ORDER BY key is a bound column; anything
8152 /// else returns empty, decoding everything as before.
8153 /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
8154 /// drops references from expression kinds it does not enumerate.
8155 ///
8156 /// ORDER BY columns are included — the merge re-derives keys from the
8157 /// decoded row on the spilled path, so pruning one would sort NULLs.
8158 pub(crate) fn sort_record_columns_needed(
8159 items: &[SelectItem],
8160 order_bound: &[Option<usize>],
8161 arity: usize,
8162 ctx: &EvalContext,
8163 ) -> Vec<bool> {
8164 let all_bare = items.iter().all(|i| {
8165 matches!(
8166 i,
8167 SelectItem::Expr {
8168 expr: Expr::Column(_),
8169 ..
8170 }
8171 )
8172 });
8173 if !all_bare || order_bound.iter().any(Option::is_none) {
8174 return Vec::new();
8175 }
8176 let mut mask = alloc::vec![false; arity];
8177 for item in items {
8178 if let SelectItem::Expr {
8179 expr: Expr::Column(c),
8180 ..
8181 } = item
8182 {
8183 match crate::eval::find_column_pos(c, ctx) {
8184 Some(p) if p < arity => mask[p] = true,
8185 _ => return Vec::new(),
8186 }
8187 }
8188 }
8189 for p in order_bound.iter().flatten() {
8190 if *p < arity {
8191 mask[*p] = true;
8192 } else {
8193 return Vec::new();
8194 }
8195 }
8196 mask
8197 }
8198
8199 /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
8200 /// of sorting.
8201 ///
8202 /// PG serves such an ordering from the index and never sorts. We sorted:
8203 /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
8204 /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
8205 /// the sorter's own round trip — `ExternalSorter::finish_each` →
8206 /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
8207 /// Every row is encoded into the sorter's arena and decoded back out,
8208 /// for an order the index already holds.
8209 ///
8210 /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
8211 /// because it was built for top-N. This is the unbounded sibling.
8212 ///
8213 /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
8214 /// from a btree, so walking one would silently drop those rows. That is
8215 /// exactly the defect r1020 fixed on the top-N path, where it had
8216 /// shipped.
8217 /// r1044 — the index this statement's ORDER BY can be WALKED on,
8218 /// instead of sorted, or `None`.
8219 ///
8220 /// Extracted so `EXPLAIN` can ask the same question the executor
8221 /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
8222 /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
8223 /// while the executor walked the primary key — 34.9 ms against
8224 /// 147.0 for the same query ordered by an unindexed column, so the
8225 /// walk was plainly running. Round 551 fixed a different case of
8226 /// this and wrote the reason down: EXPLAIN is the first thing any
8227 /// performance question opens, and an instrument that misnames the
8228 /// access path is worse than one that says nothing.
8229 ///
8230 /// The gate is here once. Two copies of it is how the plan and the
8231 /// executor come to disagree again.
8232 pub(crate) fn index_order_walk_target(
8233 &self,
8234 stmt: &SelectStatement,
8235 from: &FromClause,
8236 ) -> Option<(String, usize)> {
8237 if stmt.order_by.len() != 1
8238 || !stmt.distinct_on.is_empty()
8239 || stmt.limit_with_ties
8240 || stmt.limit.is_some()
8241 || stmt.offset.is_some()
8242 || stmt.having.is_some()
8243 || stmt.group_by.is_some()
8244 || !stmt.unions.is_empty()
8245 || !from.joins.is_empty()
8246 || from.primary.lateral_subquery.is_some()
8247 || from.primary.unnest_expr.is_some()
8248 || from.primary.as_of_segment.is_some()
8249 || from.primary.generate_series_args.is_some()
8250 || select_has_window(stmt)
8251 || aggregate::uses_aggregate(stmt)
8252 {
8253 return None;
8254 }
8255 if stmt
8256 .items
8257 .iter()
8258 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8259 {
8260 return None;
8261 }
8262 let table = self.active_catalog().get(&from.primary.name)?;
8263 if table.has_cold_rows_fast() {
8264 return None;
8265 }
8266 if !from.primary.only
8267 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8268 {
8269 return None;
8270 }
8271 let alias = from
8272 .primary
8273 .alias
8274 .as_deref()
8275 .unwrap_or(from.primary.name.as_str());
8276 let cols = &table.schema().columns;
8277 let order = &stmt.order_by[0];
8278 let Expr::Column(oc) = &order.expr else {
8279 return None;
8280 };
8281 if let Some(q) = &oc.qualifier
8282 && !q.eq_ignore_ascii_case(alias)
8283 {
8284 return None;
8285 }
8286 let order_pos = cols
8287 .iter()
8288 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8289 // r1047 — DISTINCT joins the walk when the projection IS the
8290 // order column, and only then. The index's keys are canonical
8291 // (r1039: representation equality is value equality — the
8292 // property every seek already depends on), so one key is one
8293 // distinct value and the walk can emit the first passing row of
8294 // each key group instead of hashing every row. On the release
8295 // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
8296 // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
8297 // with an ablation floor of 14.8, because the hash must
8298 // normalize and probe ALL the rows; the walk visits each key
8299 // once. A wider projection makes DISTINCT about the whole tuple,
8300 // not the key, so anything else still declines.
8301 if stmt.distinct {
8302 let only_the_order_column = stmt.items.len() == 1
8303 && match &stmt.items[0] {
8304 SelectItem::Expr {
8305 expr: Expr::Column(c),
8306 ..
8307 } => {
8308 c.name.eq_ignore_ascii_case(&oc.name)
8309 && match &c.qualifier {
8310 Some(q) => q.eq_ignore_ascii_case(alias),
8311 None => true,
8312 }
8313 }
8314 _ => false,
8315 };
8316 if !only_the_order_column {
8317 return None;
8318 }
8319 }
8320 // r1046 — a nullable key no longer refuses the walk; it changes
8321 // what the walk has to do. A NULL key is not in the btree, so
8322 // walking alone would silently drop those rows — the r1020
8323 // defect, which shipped once. The walk emits them separately, at
8324 // the end SQL puts them.
8325 //
8326 // Refusing was costing every nullable indexed column a 3.4x:
8327 // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
8328 // 72.0 ms with the column nullable and 20.2 with the same data
8329 // under NOT NULL. `NOT NULL` is not the default, so that was the
8330 // common case paying for the uncommon one.
8331 let index = table.index_on(order_pos)?;
8332 if !matches!(index.kind, spg_storage::IndexKind::BTree(_))
8333 || index.expression.is_some()
8334 || index.partial_predicate.is_some()
8335 {
8336 return None;
8337 }
8338 Some((index.name.clone(), order_pos))
8339 }
8340
8341 fn try_index_order_stream<F>(
8342 &self,
8343 stmt: &SelectStatement,
8344 from: &FromClause,
8345 cancel: CancelToken<'_>,
8346 emit: &mut F,
8347 ) -> Result<Option<usize>, EngineError>
8348 where
8349 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8350 {
8351 // r1044 — the shape gate lives in `index_order_walk_target`, so
8352 // `EXPLAIN` answers the same question. What stays here is the
8353 // part that RAISES (an illegal ORDER BY has to keep erroring
8354 // from where it did) and the bindings the walk needs.
8355 crate::orderby::check_order_by_legality(stmt)?;
8356 crate::orderby::check_order_by_positions(stmt)?;
8357 crate::window::reject_window_in_row_clauses(stmt)?;
8358 let Some((_, order_pos)) = self.index_order_walk_target(stmt, from) else {
8359 return Ok(None);
8360 };
8361 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8362 return Ok(None);
8363 };
8364 let alias = from
8365 .primary
8366 .alias
8367 .as_deref()
8368 .unwrap_or(from.primary.name.as_str());
8369 let cols = table.schema().columns.clone();
8370 let order = &stmt.order_by[0];
8371 let Some(index) = table.index_on(order_pos) else {
8372 return Ok(None);
8373 };
8374
8375 let sess = self.dml_session();
8376 let ctx = EvalContext::new(&cols, Some(alias))
8377 .with_catalog(self.active_catalog())
8378 .with_session(&sess);
8379 let projection = build_projection(
8380 &stmt.items,
8381 &cols,
8382 alias,
8383 self.speaks_mysql,
8384 Some(self.active_catalog()),
8385 )?;
8386 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8387 emit(crate::StreamItem::Header(&columns))?;
8388 let bound_pos: Vec<Option<usize>> = projection
8389 .iter()
8390 .map(|p| match &p.expr {
8391 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8392 Ok(Some(pos)) => Some(pos),
8393 _ => None,
8394 },
8395 _ => None,
8396 })
8397 .collect();
8398
8399 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8400 .where_
8401 .as_ref()
8402 .filter(|w| crate::eval::fully_compilable(w))
8403 .map(|w| crate::eval::compile_expr(w, &ctx));
8404 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8405 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8406 let snapshot = self.current_snapshot();
8407
8408 // A btree holds one locator per row VERSION, so a row whose key was
8409 // updated can sit under two keys and a dead one can sit beside its
8410 // replacement. The visibility gate drops the dead; `seen` drops a
8411 // live row that the walk reaches twice, which would otherwise be a
8412 // duplicated output row rather than a slow one.
8413 let mut emitted_rows = alloc::vec![false; table.rows().len()];
8414
8415 // r1046 — the rows the index cannot hold.
8416 //
8417 // A NULL key is not in the btree, so the walk below never reaches
8418 // those rows; they are emitted here, at the end SQL puts them.
8419 // PG's default is NULLS LAST ascending and NULLS FIRST
8420 // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
8421 // the same rule `order_by_value_cmp_raw` applies to the sort this
8422 // replaces, so the two orders agree.
8423 //
8424 // Finding them costs one pass over the column. That pass is why
8425 // this is still worth doing: the sort it replaces encodes and
8426 // decodes every row, and the walk plus the pass measured 72.0 ms
8427 // down to about 22 on 400,000 rows.
8428 let nulls_first = order.nulls_first.unwrap_or(order.desc);
8429 // r1047 — under DISTINCT the walk emits the FIRST passing row of
8430 // each key group and skips the rest; the gate admits DISTINCT
8431 // only when the projection is the order column itself, so one
8432 // canonical key is one output row. NULL is one distinct value,
8433 // so the NULL pass stops at its first emit too.
8434 let distinct = stmt.distinct;
8435 let mut count = 0usize;
8436 let mut visited = 0usize;
8437 let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
8438 eval_stack: &mut Vec<Value<'static>>,
8439 values: &mut Vec<Value<'static>>,
8440 visited: &mut usize,
8441 emit: &mut F|
8442 -> Result<usize, EngineError> {
8443 if !cols[order_pos].nullable {
8444 return Ok(0);
8445 }
8446 let mut n = 0usize;
8447 for (ri, row) in table.rows().iter().enumerate() {
8448 if !matches!(row.values.get(order_pos), Some(Value::Null)) {
8449 continue;
8450 }
8451 if emitted_rows.get(ri).copied().unwrap_or(true) {
8452 continue;
8453 }
8454 if !table.is_row_visible(ri, &snapshot) {
8455 continue;
8456 }
8457 *visited += 1;
8458 if visited.is_multiple_of(256) {
8459 cancel.check()?;
8460 }
8461 emitted_rows[ri] = true;
8462 if Self::stream_project_row(
8463 row,
8464 stmt.where_.as_ref(),
8465 compiled_where.as_ref(),
8466 eval_stack,
8467 &projection,
8468 &bound_pos,
8469 &ctx,
8470 values,
8471 emit,
8472 )? {
8473 n += 1;
8474 if distinct {
8475 break;
8476 }
8477 }
8478 }
8479 Ok(n)
8480 };
8481
8482 if nulls_first {
8483 count += emit_null_rows(
8484 &mut emitted_rows,
8485 &mut eval_stack,
8486 &mut values,
8487 &mut visited,
8488 emit,
8489 )?;
8490 }
8491
8492 let walker: alloc::boxed::Box<
8493 dyn Iterator<Item = (&spg_storage::IndexKey, &spg_storage::PostingList)>,
8494 > = if order.desc {
8495 alloc::boxed::Box::new(index.iter_desc())
8496 } else {
8497 alloc::boxed::Box::new(index.iter_asc())
8498 };
8499 for (_key, locators) in walker {
8500 for loc in locators {
8501 let spg_storage::RowLocator::Hot(ri) = *loc else {
8502 continue;
8503 };
8504 if emitted_rows.get(ri).copied().unwrap_or(true) {
8505 continue;
8506 }
8507 if !table.is_row_visible(ri, &snapshot) {
8508 continue;
8509 }
8510 let Some(row) = table.rows().get(ri) else {
8511 continue;
8512 };
8513 visited += 1;
8514 if visited.is_multiple_of(256) {
8515 cancel.check()?;
8516 }
8517 emitted_rows[ri] = true;
8518 if Self::stream_project_row(
8519 row,
8520 stmt.where_.as_ref(),
8521 compiled_where.as_ref(),
8522 &mut eval_stack,
8523 &projection,
8524 &bound_pos,
8525 &ctx,
8526 &mut values,
8527 emit,
8528 )? {
8529 count += 1;
8530 // One row per key group: the rest are the same value.
8531 if distinct {
8532 break;
8533 }
8534 }
8535 }
8536 }
8537
8538 if !nulls_first {
8539 count += emit_null_rows(
8540 &mut emitted_rows,
8541 &mut eval_stack,
8542 &mut values,
8543 &mut visited,
8544 emit,
8545 )?;
8546 }
8547 Ok(Some(count))
8548 }
8549
8550 /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
8551 /// building an `OrderKey` vector per row.
8552 ///
8553 /// The row-returning sorted scan allocates twice per row: one
8554 /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
8555 /// projection. Counted over 400 k rows (r1030,
8556 /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
8557 /// allocations and 208 MB of traffic for an answer of four hundred
8558 /// thousand integers.
8559 ///
8560 /// The key half is pure ceremony on this shape.
8561 /// `sort_tagged_by_inline_int_key` already sorts indices rather than
8562 /// rows, so the per-row vector is built, has one integer taken out of
8563 /// it, and is then dragged through the permutation — it exists to carry
8564 /// a number the row's column already held. This lane carries the number
8565 /// instead, in a fixed-size array that lives inside the buffer element
8566 /// and allocates nothing. Same idea as the predicate VM's integer lane.
8567 ///
8568 /// Declines to `None` for anything it does not cover, and every caller
8569 /// falls through to the general path, so the gate list is the
8570 /// specification.
8571 ///
8572 /// Ties: equal keys keep scan order, as the stable sort on the general
8573 /// path does. Rows that tie on every ORDER BY term are entitled to any
8574 /// order among themselves either way — see `STABILITY.md`.
8575 fn try_int_key_sorted_stream<F>(
8576 &self,
8577 stmt: &SelectStatement,
8578 from: &FromClause,
8579 cancel: CancelToken<'_>,
8580 emit: &mut F,
8581 ) -> Result<Option<usize>, EngineError>
8582 where
8583 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8584 {
8585 /// Sort terms this lane carries inline. Four covers every ORDER BY
8586 /// in the endpoint sweep and in the dogfood corpus; wider ones fall
8587 /// through rather than growing the buffer element for everybody.
8588 const MAX_KEYS: usize = 4;
8589
8590 if stmt.order_by.is_empty()
8591 || stmt.order_by.len() > MAX_KEYS
8592 // v7.38.14 — DISTINCT is admitted when the projected set is
8593 // exactly the ORDER BY set, and only then. This lane sorts, and
8594 // when the sort key determines the projected row every duplicate
8595 // lands ADJACENT to its twin -- so the de-duplication is a
8596 // comparison with the previous row rather than a hash table, and
8597 // the reason this lane declined DISTINCT disappears with it. The
8598 // seen-set it could not offer held indices into a materialised
8599 // vector; there is no seen-set now.
8600 //
8601 // The gate is as narrow as the bare-GROUP-BY rewrite's for the
8602 // same reason: `ORDER BY a` over a projection of `a, b` does NOT
8603 // place duplicates of the PAIR adjacent, so set EQUALITY, never
8604 // overlap.
8605 || (stmt.distinct && !Self::distinct_is_adjacent_after_sort(stmt))
8606 || stmt.limit_with_ties
8607 || stmt.limit.is_some()
8608 || stmt.offset.is_some()
8609 || stmt.having.is_some()
8610 || stmt.group_by.is_some()
8611 || !stmt.unions.is_empty()
8612 || !from.joins.is_empty()
8613 || from.primary.lateral_subquery.is_some()
8614 || from.primary.unnest_expr.is_some()
8615 || from.primary.as_of_segment.is_some()
8616 || from.primary.generate_series_args.is_some()
8617 || select_has_window(stmt)
8618 || aggregate::uses_aggregate(stmt)
8619 {
8620 return Ok(None);
8621 }
8622 if stmt
8623 .items
8624 .iter()
8625 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8626 {
8627 return Ok(None);
8628 }
8629 crate::orderby::check_order_by_legality(stmt)?;
8630 crate::orderby::check_order_by_positions(stmt)?;
8631 crate::window::reject_window_in_row_clauses(stmt)?;
8632 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8633 return Ok(None);
8634 };
8635 if table.has_cold_rows_fast() {
8636 return Ok(None);
8637 }
8638 if !from.primary.only
8639 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8640 {
8641 return Ok(None);
8642 }
8643 let alias = from
8644 .primary
8645 .alias
8646 .as_deref()
8647 .unwrap_or(from.primary.name.as_str());
8648 let cols = table.schema().columns.clone();
8649
8650 // Every ORDER BY term must be a NOT NULL integer column of this
8651 // table. NOT NULL is what lets the key be a bare integer: with
8652 // NULLs the lane would have to carry their ordering too, and
8653 // getting that subtly wrong is the r1020 defect.
8654 let mut key_pos = [0usize; MAX_KEYS];
8655 let mut descs = [false; MAX_KEYS];
8656 // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
8657 // which the AST records as `None`; `unwrap_or(desc)` is how the
8658 // rest of the engine resolves it.
8659 let mut nulls_first = [false; MAX_KEYS];
8660 let n_keys = stmt.order_by.len();
8661 for (slot, order) in stmt.order_by.iter().enumerate() {
8662 let Expr::Column(oc) = &order.expr else {
8663 return Ok(None);
8664 };
8665 if let Some(q) = &oc.qualifier
8666 && !q.eq_ignore_ascii_case(alias)
8667 {
8668 return Ok(None);
8669 }
8670 let Some(pos) = cols
8671 .iter()
8672 .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
8673 else {
8674 return Ok(None);
8675 };
8676 if !matches!(
8677 cols[pos].ty,
8678 spg_storage::DataType::SmallInt
8679 | spg_storage::DataType::Int
8680 | spg_storage::DataType::BigInt
8681 ) {
8682 return Ok(None);
8683 }
8684 key_pos[slot] = pos;
8685 descs[slot] = order.desc;
8686 nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
8687 }
8688
8689 let sess = self.dml_session();
8690 let ctx = EvalContext::new(&cols, Some(alias))
8691 .with_catalog(self.active_catalog())
8692 .with_session(&sess);
8693 let projection = build_projection(
8694 &stmt.items,
8695 &cols,
8696 alias,
8697 self.speaks_mysql,
8698 Some(self.active_catalog()),
8699 )?;
8700 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8701 let bound_pos: Vec<Option<usize>> = projection
8702 .iter()
8703 .map(|p| match &p.expr {
8704 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8705 Ok(Some(pos)) => Some(pos),
8706 _ => None,
8707 },
8708 _ => None,
8709 })
8710 .collect();
8711 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8712 .where_
8713 .as_ref()
8714 .filter(|w| crate::eval::fully_compilable(w))
8715 .map(|w| crate::eval::compile_expr(w, &ctx));
8716
8717 // The same first-observable point the materialising planner fires,
8718 // placed after the gates so it fires exactly once: this lane runs
8719 // BEFORE that planner and would otherwise be a hole in the
8720 // panic-isolation and cancellation-race coverage rather than a
8721 // faster path through it.
8722 crate::injection_point!("planner_first_row_fetch", &stmt.from);
8723
8724 let mut eval_stack: Vec<Value<'static>> = Vec::new();
8725 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8726 let mut budget = ByteBudget::new(self.max_query_bytes);
8727 let snapshot = self.current_snapshot();
8728 // Keys, a NULL bit per key slot, and the row. The bitmask keeps
8729 // the element small: a nullable key still costs one bit rather
8730 // than a second array.
8731 let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
8732
8733 for (ri, row) in table.rows().iter().enumerate() {
8734 if ri.is_multiple_of(256) {
8735 cancel.check()?;
8736 }
8737 if !table.is_row_visible(ri, &snapshot) {
8738 continue;
8739 }
8740 // The key comes from the STORED row, before projection: an
8741 // ORDER BY column need not appear in the select list.
8742 let mut keys = [0i64; MAX_KEYS];
8743 let mut nulls = 0u8;
8744 let mut keyed = true;
8745 for slot in 0..n_keys {
8746 match row.values.get(key_pos[slot]) {
8747 Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
8748 Some(Value::Int(v)) => keys[slot] = i64::from(*v),
8749 Some(Value::BigInt(v)) => keys[slot] = *v,
8750 Some(Value::Null) | None => nulls |= 1 << slot,
8751 // An integer column holding something else is a row
8752 // this lane cannot order; hand the whole query back
8753 // rather than guess at it.
8754 _ => {
8755 keyed = false;
8756 break;
8757 }
8758 }
8759 }
8760 if !keyed {
8761 return Ok(None);
8762 }
8763 if !Self::stream_filter_project(
8764 row,
8765 stmt.where_.as_ref(),
8766 compiled_where.as_ref(),
8767 &mut eval_stack,
8768 &projection,
8769 &bound_pos,
8770 &ctx,
8771 &mut values,
8772 )? {
8773 continue;
8774 }
8775 budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
8776 sorted.push((keys, nulls, core::mem::take(&mut values)));
8777 values.reserve(projection.len());
8778 }
8779
8780 sorted.sort_by(|a, b| {
8781 use core::cmp::Ordering;
8782 for slot in 0..n_keys {
8783 let bit = 1u8 << slot;
8784 let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
8785 (true, true) => Ordering::Equal,
8786 // Where the NULLs go is already decided — `nulls_first`
8787 // resolved DESC's default when it was read. Reversing
8788 // this for DESC as well would apply the direction
8789 // twice and put them at the wrong end.
8790 (true, false) => {
8791 if nulls_first[slot] {
8792 Ordering::Less
8793 } else {
8794 Ordering::Greater
8795 }
8796 }
8797 (false, true) => {
8798 if nulls_first[slot] {
8799 Ordering::Greater
8800 } else {
8801 Ordering::Less
8802 }
8803 }
8804 (false, false) => {
8805 let o = a.0[slot].cmp(&b.0[slot]);
8806 if descs[slot] { o.reverse() } else { o }
8807 }
8808 };
8809 if ord != Ordering::Equal {
8810 return ord;
8811 }
8812 }
8813 Ordering::Equal
8814 });
8815
8816 emit(crate::StreamItem::Header(&columns))?;
8817 // v7.38.14 — DISTINCT, de-duplicated against the PREVIOUS row.
8818 //
8819 // The gate above only admits DISTINCT when the sort key determines
8820 // the projected row, so every duplicate is adjacent to its twin by
8821 // the time this loop runs and one comparison replaces a hash table
8822 // of every row seen. Equality is `values_eq_norm` with the same mask
8823 // the materialising path builds -- deliberately the same function,
8824 // because a de-duplication that disagreed with the one on the other
8825 // path would make the answer depend on which lane a query took.
8826 //
8827 // A query that did not ask for DISTINCT pays one already-false bool
8828 // test per row: the short-circuit means the comparison never runs
8829 // and `prev` is never written.
8830 let dedup_mask = fold_mask(&projection);
8831 let fold = FoldSpec::of(self.speaks_mysql, &dedup_mask);
8832 let mut count = 0usize;
8833 let mut prev: Option<&[Value<'static>]> = None;
8834 for (_, _, vals) in &sorted {
8835 if stmt.distinct
8836 && let Some(p) = prev
8837 && values_eq_norm(p, vals, fold)
8838 {
8839 continue;
8840 }
8841 emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
8842 count += 1;
8843 if stmt.distinct {
8844 prev = Some(vals);
8845 }
8846 }
8847 Ok(Some(count))
8848 }
8849
8850 /// v7.38.14 — would sorting place every duplicate next to its twin?
8851 ///
8852 /// True when the projected expressions and the ORDER BY expressions are the
8853 /// same SET. Then the sort key determines the projected row, so equal rows
8854 /// are adjacent afterwards and an adjacent comparison de-duplicates exactly
8855 /// as a hash would -- and, because both sort paths are stable, the survivor
8856 /// is the first-seen row, which is the one the hash keeps too.
8857 ///
8858 /// A wildcard's expansion is not known here, so it is not a set this can
8859 /// compare; an ordinal ORDER BY names a select-list position rather than a
8860 /// value and is left alone.
8861 fn distinct_is_adjacent_after_sort(stmt: &SelectStatement) -> bool {
8862 if stmt.order_by.is_empty() || !stmt.distinct_on.is_empty() {
8863 return false;
8864 }
8865 let mut projected: alloc::vec::Vec<&Expr> =
8866 alloc::vec::Vec::with_capacity(stmt.items.len());
8867 for item in &stmt.items {
8868 match item {
8869 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return false,
8870 SelectItem::Expr { expr, .. } => projected.push(expr),
8871 }
8872 }
8873 if projected.is_empty() {
8874 return false;
8875 }
8876 let keys: alloc::vec::Vec<&Expr> = stmt.order_by.iter().map(|o| &o.expr).collect();
8877 if keys
8878 .iter()
8879 .any(|k| matches!(k, Expr::Literal(spg_sql::ast::Literal::Integer(_))))
8880 {
8881 return false;
8882 }
8883 projected.iter().all(|p| keys.contains(p)) && keys.iter().all(|k| projected.contains(k))
8884 }
8885
8886 fn try_spill_sorted_stream<F>(
8887 &self,
8888 stmt: &SelectStatement,
8889 from: &FromClause,
8890 cancel: CancelToken<'_>,
8891 emit: &mut F,
8892 ) -> Result<Option<usize>, EngineError>
8893 where
8894 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8895 {
8896 // The shapes `try_spill_sorted_scan` declines, plus the ones the
8897 // streaming executor does not carry (a LIMIT is already bounded
8898 // by a partial sort; the rest need the answer addressable).
8899 if !self.can_spill()
8900 || stmt.order_by.is_empty()
8901 || stmt.distinct
8902 || stmt.limit_with_ties
8903 || stmt.limit.is_some()
8904 || stmt.offset.is_some()
8905 || stmt.having.is_some()
8906 || stmt.group_by.is_some()
8907 || !stmt.unions.is_empty()
8908 || !from.joins.is_empty()
8909 || from.primary.lateral_subquery.is_some()
8910 || from.primary.unnest_expr.is_some()
8911 || from.primary.as_of_segment.is_some()
8912 || from.primary.generate_series_args.is_some()
8913 || select_has_window(stmt)
8914 || aggregate::uses_aggregate(stmt)
8915 {
8916 return Ok(None);
8917 }
8918 if stmt
8919 .items
8920 .iter()
8921 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8922 {
8923 return Ok(None);
8924 }
8925 // Everything `exec_bare_select_cancel` does before it scans runs
8926 // BELOW this path, so a statement claimed here skips it. Three of
8927 // those were missed on the way in and each was caught by a
8928 // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
8929 // ORDER BY 2` sorted happily instead of raising 42P10), the
8930 // cancellation check by another, the partition fan-out by the
8931 // differential corpus. What is reconciled, item by item: with-ties
8932 // needs ORDER BY (gated above), USING/NATURAL and RLS join
8933 // rewrites (joins gated above), the single-table RLS predicate
8934 // (the dispatcher declines a policy-subject table before this is
8935 // reached), the meta-view dispatch (those names are not in the
8936 // catalog, so the lookup below declines). These three are calls,
8937 // so the message and SQLSTATE are the ones the fall-back gives —
8938 // `select_has_window` above reads the select list and ORDER BY but
8939 // not WHERE, which is the case the third one covers.
8940 crate::orderby::check_order_by_legality(stmt)?;
8941 crate::orderby::check_order_by_positions(stmt)?;
8942 crate::window::reject_window_in_row_clauses(stmt)?;
8943 // A parent's rows are its children's. These walks scan the named
8944 // relation alone, so a partitioned or inherited parent comes back
8945 // short — and silently: the corpus caught `SELECT id FROM pr
8946 // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8947 // parent's own rows instead of the partitions'. `ONLY` is exactly
8948 // the case that does not fan out, so it stays, which is the test
8949 // the FROM-clause fan-out itself makes.
8950 if !from.primary.only
8951 && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8952 {
8953 return Ok(None);
8954 }
8955 let Some(table) = self.active_catalog().get(&from.primary.name) else {
8956 return Ok(None);
8957 };
8958 // Cold-tier rows live outside `rows()`; this walk would drop
8959 // them silently, the same reason round 831's walk declines.
8960 if table.has_cold_rows_fast() {
8961 return Ok(None);
8962 }
8963
8964 let alias = from
8965 .primary
8966 .alias
8967 .as_deref()
8968 .unwrap_or(from.primary.name.as_str());
8969 let cols = table.schema().columns.clone();
8970 let sess = self.dml_session();
8971 let ctx = EvalContext::new(&cols, Some(alias))
8972 .with_catalog(self.active_catalog())
8973 .with_session(&sess);
8974 let projection = build_projection(
8975 &stmt.items,
8976 &cols,
8977 alias,
8978 self.speaks_mysql,
8979 Some(self.active_catalog()),
8980 )?;
8981 let order_by = stmt.order_by.clone();
8982 // The same one-shot resolution the general path does (round
8983 // 582): each ORDER BY column is bound once, not once per row.
8984 let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8985 let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8986 // Resolved BEFORE the scan, because it now decides what the sort
8987 // STORES and not just what it decodes (round 995).
8988 let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8989
8990 // v7.38.22 — resolved HERE, because this path did not resolve
8991 // them at all.
8992 //
8993 // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8994 // "en_US.utf8"` in BYTE order on this path — and swallowed an
8995 // unknown collation name rather than raising — because the sorter
8996 // below compared with an empty collation slice. The materialising
8997 // path honoured both. Which answer a query got depended on which
8998 // path the planner took, and this is the path a plain single-table
8999 // SELECT takes.
9000 let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
9001 let mut sorter = crate::extsort::ExternalSorter::new(
9002 self.temp_run_factory,
9003 self.session_work_mem_bytes(),
9004 cols.clone(),
9005 &descs,
9006 &order_colls,
9007 )
9008 .with_stats(&self.spill_stats)
9009 .with_pruned(&needed);
9010 let snapshot = self.current_snapshot();
9011 // One key buffer for the whole scan: `push` drains it and leaves
9012 // the capacity behind.
9013 let mut keys: Vec<OrderKey> = Vec::new();
9014 // r1024 — compile the predicate once for the scan.
9015 //
9016 // These two sorted-spill scans are the paths a single-table SELECT
9017 // with an ORDER BY takes, and they were the last row-returning ones
9018 // still walking the expression tree per row. r1023 did the
9019 // no-ORDER-BY sibling; the sweep's two remaining losing cells are
9020 // exactly this shape.
9021 //
9022 // Found from the profile's CALL TREE rather than its leaves. The
9023 // leaves say what is expensive — `eval_expr` 320, `apply_binary`
9024 // 261, `mod_op` 178 — and two attempts at reasoning out which
9025 // function asked for it were both wrong. The tree names the caller
9026 // chain, and it named this one.
9027 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9028 .where_
9029 .as_ref()
9030 .filter(|w| crate::eval::fully_compilable(w))
9031 .map(|w| crate::eval::compile_expr(w, &ctx));
9032 let mut eval_stack: Vec<Value<'static>> = Vec::new();
9033 for (i, row) in table.scan_visible_from(0, &snapshot) {
9034 if i.is_multiple_of(256) {
9035 cancel.check()?;
9036 }
9037 if let Some(c) = &compiled_where {
9038 if !crate::eval::compiled::eval_compiled_pred(
9039 c,
9040 row,
9041 &ctx,
9042 &mut eval_stack,
9043 ctx.mysql_dialect,
9044 )? {
9045 continue;
9046 }
9047 } else if let Some(w) = &stmt.where_ {
9048 let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
9049 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9050 continue;
9051 }
9052 }
9053 keys.clear();
9054 // The same collations the sorter compares with, and the
9055 // re-derivation below is handed the same ones. `finish`'s
9056 // contract is that a key comes back the way it was pushed;
9057 // a collation is part of the way it was pushed.
9058 crate::orderby::build_order_keys_bound(
9059 &order_by,
9060 &order_bound,
9061 &order_colls,
9062 row,
9063 &ctx,
9064 &mut keys,
9065 )?;
9066 sorter.push(&mut keys, row)?;
9067 }
9068
9069 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9070 emit(crate::StreamItem::Header(&columns))?;
9071
9072 let key_ctx = &ctx;
9073 let mut emitted_since_check = 0usize;
9074 let n = sorter.finish_each(
9075 |src, buf| {
9076 crate::orderby::build_order_keys_rederived(
9077 &order_by,
9078 &order_bound,
9079 &order_colls,
9080 src,
9081 key_ctx,
9082 buf,
9083 )
9084 },
9085 |src, values| {
9086 for p in &projection {
9087 values.push(
9088 crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
9089 );
9090 }
9091 Ok(())
9092 },
9093 |cells| {
9094 // The merge is the long half of a big sort, and the scan's
9095 // check above stops running once it ends: a cancelled
9096 // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
9097 // anyway. Same stride as the scan.
9098 emitted_since_check += 1;
9099 if emitted_since_check >= 256 {
9100 emitted_since_check = 0;
9101 cancel.check()?;
9102 }
9103 emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
9104 },
9105 )?;
9106 Ok(Some(n))
9107 }
9108
9109 /// One row of the single-table streaming walk: the WHERE test, the
9110 /// projection, the emit. Returns whether a row was emitted.
9111 ///
9112 /// v7.39 (round 970) — factored out because the walk now has two ways
9113 /// to reach a row, the sequential scan and an index seek's candidate
9114 /// positions, and both must do IDENTICALLY this. A copy in each is how
9115 /// two paths for one job drift; this file already carries the cost of
9116 /// that lesson twice (rounds 823 and 961, both resolvers).
9117 ///
9118 /// `#[inline]` so the scan loop keeps the shape round 957 measured it
9119 /// in — a shared hot path pays for a new abstraction whether or not it
9120 /// uses it, and this one is on the scan.
9121 #[inline]
9122 #[allow(clippy::too_many_arguments)]
9123 fn stream_filter_project(
9124 row: &spg_storage::Row<'static>,
9125 where_: Option<&Expr>,
9126 // r1023 — the same WHERE, compiled once by the caller. `None` means
9127 // the expression did not qualify and `where_` is evaluated as before.
9128 compiled_where: Option<&crate::eval::CompiledExpr>,
9129 eval_stack: &mut Vec<Value<'static>>,
9130 projection: &[ProjectedItem],
9131 bound_pos: &[Option<usize>],
9132 ctx: &crate::eval::EvalContext<'_>,
9133 values: &mut Vec<Value<'static>>,
9134 ) -> Result<bool, EngineError> {
9135 // r1023 — this scan ran its predicate through the TREE INTERPRETER,
9136 // once per row, and it was the only row-returning path that did.
9137 // The aggregate path, `table_access`, and the PK walker all compile
9138 // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
9139 // server's live samples were `eval_expr` 99, `apply_binary` 81,
9140 // `mod_op` 29 — the interpreter, not delivery.
9141 //
9142 // The arithmetic accounted for it exactly. Over the wire, the same
9143 // filter costs 6.375 ms returning rows and 0.679 ms counting them;
9144 // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
9145 // which is what an interpreted predicate costs against the compiled
9146 // lane's 11.7. It was named "delivery after a filter" before this
9147 // profile, and it was never delivery.
9148 if let Some(c) = compiled_where {
9149 if !crate::eval::compiled::eval_compiled_pred(
9150 c,
9151 row,
9152 ctx,
9153 eval_stack,
9154 ctx.mysql_dialect,
9155 )? {
9156 return Ok(false);
9157 }
9158 } else if let Some(w) = where_ {
9159 let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
9160 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9161 return Ok(false);
9162 }
9163 }
9164 values.clear();
9165 for (p, bound) in projection.iter().zip(bound_pos) {
9166 values.push(match bound {
9167 Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
9168 None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
9169 });
9170 }
9171 Ok(true)
9172 }
9173
9174 /// The same filter and projection, then emit. Split from
9175 /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
9176 /// before it can emit them — a sort — runs the identical predicate and
9177 /// projection rather than a second copy of them.
9178 #[allow(clippy::too_many_arguments)]
9179 fn stream_project_row<F>(
9180 row: &spg_storage::Row<'static>,
9181 where_: Option<&Expr>,
9182 compiled_where: Option<&crate::eval::CompiledExpr>,
9183 eval_stack: &mut Vec<Value<'static>>,
9184 projection: &[ProjectedItem],
9185 bound_pos: &[Option<usize>],
9186 ctx: &crate::eval::EvalContext<'_>,
9187 values: &mut Vec<Value<'static>>,
9188 emit: &mut F,
9189 ) -> Result<bool, EngineError>
9190 where
9191 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9192 {
9193 if !Self::stream_filter_project(
9194 row,
9195 where_,
9196 compiled_where,
9197 eval_stack,
9198 projection,
9199 bound_pos,
9200 ctx,
9201 values,
9202 )? {
9203 return Ok(false);
9204 }
9205 emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
9206 Ok(true)
9207 }
9208
9209 fn try_stream_single_table<F>(
9210 &self,
9211 stmt: &SelectStatement,
9212 from: &FromClause,
9213 cancel: CancelToken<'_>,
9214 emit: &mut F,
9215 ) -> Result<Option<usize>, EngineError>
9216 where
9217 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9218 {
9219 let Some(table) = self.active_catalog().get(&from.primary.name) else {
9220 return Ok(None);
9221 };
9222 // Cold-tier rows live outside `rows()`; the materialising fallback
9223 // covers both tiers and this walk would silently drop them.
9224 if table.has_cold_rows_fast() {
9225 return Ok(None);
9226 }
9227 let alias = from
9228 .primary
9229 .alias
9230 .as_deref()
9231 .unwrap_or(from.primary.name.as_str());
9232 let cols = table.schema().columns.clone();
9233 let sess = self.dml_session();
9234 let ctx = EvalContext::new(&cols, Some(alias))
9235 .with_catalog(self.active_catalog())
9236 .with_session(&sess);
9237 let projection = build_projection(
9238 &stmt.items,
9239 &cols,
9240 alias,
9241 self.speaks_mysql,
9242 Some(self.active_catalog()),
9243 )?;
9244
9245 let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9246 emit(crate::StreamItem::Header(&columns))?;
9247
9248 // v7.37 (round 957) — resolve each bare-column projection ONCE
9249 // instead of once per row. `find_column_pos`-style resolution is a
9250 // linear walk of the schema comparing column-name strings, and the
9251 // row loop below ran it for every cell of every row: measured at
9252 // 400k rows, binding it out of the loop took `SELECT pad` from
9253 // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
9254 //
9255 // ORDER BY has bound its keys this way since round 582
9256 // (`order_by_bound_positions`); the projection never did.
9257 //
9258 // `locate_column` is the same resolution `resolve_column` performs,
9259 // returning the site instead of the value, so the two cannot drift
9260 // apart the way a second hand-written resolver would. Anything it
9261 // declines — an expression, a whole-row reference, a name that does
9262 // not resolve — binds to `None` and takes the general path below,
9263 // errors included, so an empty table still reports nothing rather
9264 // than raising at bind time.
9265 let bound_pos: Vec<Option<usize>> = projection
9266 .iter()
9267 .map(|p| match &p.expr {
9268 Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9269 Ok(Some(pos)) => Some(pos),
9270 _ => None,
9271 },
9272 _ => None,
9273 })
9274 .collect();
9275
9276 // One snapshot for the whole scan, as the materialising path takes.
9277 let snapshot = self.current_snapshot();
9278
9279 // v7.39 (round 970) — ask the indices BEFORE walking the table.
9280 //
9281 // This walk had no index step at all, and it is preferred over the
9282 // materialising path, which does have one (`pick_indexed_rows` ->
9283 // `try_index_seek`). So a primary-key point lookup — the commonest
9284 // statement there is — read every row: measured on 500k rows,
9285 // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
9286 // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
9287 // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
9288 //
9289 // The control that named it: `... OFFSET 0` — semantically the same
9290 // query — answered in 0.159 ms, because OFFSET is one of the shape
9291 // gates that declines this walk and sends the statement to the path
9292 // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
9293 // no semantics in common; what they share is making this function
9294 // stand down.
9295 //
9296 // The seek only NARROWS: every candidate still goes through the
9297 // full WHERE below, exactly as the mutation paths use it, so a
9298 // partial index match cannot change an answer. Positions come back
9299 // already visibility-filtered and already capped at a quarter of the
9300 // table (round 490), so a seek can never cost more than the scan it
9301 // replaces, and `None` means "walk the table" as before.
9302 //
9303 // Sorted because the scan would have produced table order and the
9304 // index produces key order. Without an ORDER BY neither is promised,
9305 // but a walk that silently reorders its answer when an index happens
9306 // to exist is a difference nobody asked for.
9307 let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
9308 crate::index_access::try_index_seek_positions(
9309 w,
9310 &cols,
9311 table,
9312 alias,
9313 &snapshot,
9314 self.speaks_mysql,
9315 )
9316 });
9317
9318 let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9319 // r1023 — compile the predicate once for the whole scan. Same gate
9320 // every other path uses: `fully_compilable` or keep the interpreter,
9321 // so a shape the VM cannot take answers exactly as it did before.
9322 let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9323 .where_
9324 .as_ref()
9325 .filter(|w| crate::eval::fully_compilable(w))
9326 .map(|w| crate::eval::compile_expr(w, &ctx));
9327 let mut eval_stack: Vec<Value<'static>> = Vec::new();
9328 let mut count: usize = 0;
9329 match seek_positions {
9330 Some(mut positions) => {
9331 positions.sort_unstable();
9332 for (n, pos) in positions.into_iter().enumerate() {
9333 if n.is_multiple_of(256) {
9334 cancel.check()?;
9335 }
9336 let Some(row) = table.rows().get(pos) else {
9337 continue;
9338 };
9339 if Self::stream_project_row(
9340 row,
9341 stmt.where_.as_ref(),
9342 compiled_where.as_ref(),
9343 &mut eval_stack,
9344 &projection,
9345 &bound_pos,
9346 &ctx,
9347 &mut values,
9348 emit,
9349 )? {
9350 count += 1;
9351 }
9352 }
9353 }
9354 None => {
9355 // v7.38.11 — the streaming scan is the path a client
9356 // reaches over the wire, so it is the one that has to
9357 // ask the BRIN summary which slots can be skipped. The
9358 // predicate still runs on every row that survives.
9359 let slots = stmt
9360 .where_
9361 .as_ref()
9362 .and_then(|w| crate::brin::candidate_slots(w, table))
9363 .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
9364 for (i, row) in table.scan_visible_slots(slots, &snapshot) {
9365 if i.is_multiple_of(256) {
9366 cancel.check()?;
9367 }
9368 if Self::stream_project_row(
9369 row,
9370 stmt.where_.as_ref(),
9371 compiled_where.as_ref(),
9372 &mut eval_stack,
9373 &projection,
9374 &bound_pos,
9375 &ctx,
9376 &mut values,
9377 emit,
9378 )? {
9379 count += 1;
9380 }
9381 }
9382 }
9383 }
9384 Ok(Some(count))
9385 }
9386
9387 pub(crate) fn try_exec_joined_streaming<F>(
9388 &self,
9389 stmt: &SelectStatement,
9390 cancel: CancelToken<'_>,
9391 emit: &mut F,
9392 ) -> Result<Option<usize>, EngineError>
9393 where
9394 F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9395 {
9396 // Shape gates — keep the streamable surface narrow on
9397 // purpose. The fall-back path still handles everything else.
9398 let Some(from) = &stmt.from else {
9399 return Ok(None);
9400 };
9401 // v7.37 (round 830) — decline anything a row-security policy binds
9402 // for this session. Policies are injected in
9403 // `exec_bare_select_cancel`, below this path, so a statement claimed
9404 // here would read the table unfiltered: measured, `SELECT val FROM
9405 // sec` returned all three rows to a session whose policy allows two,
9406 // while `SELECT upper(val) FROM sec` — declined by the shape gates
9407 // and so materialised — returned the correct two.
9408 //
9409 // Declining sends it to the path that enforces. Teaching this one to
9410 // inject the predicate itself would keep the streaming benefit for
9411 // RLS tables and is the better end state; it is not what a
9412 // correctness fix should carry, and the fall-back is exactly as
9413 // correct, only slower.
9414 if self.select_reads_policy_subject_table(stmt) {
9415 return Ok(None);
9416 }
9417 // r1058 — a WITH list this path never materialises: the CTE
9418 // name would be resolved as a physical relation and error
9419 // ("relation \"big\" does not exist" over the extended
9420 // protocol, caught by the perm-runner's wire legs). The
9421 // materialising fallback owns CTE execution.
9422 if !stmt.ctes.is_empty() {
9423 return Ok(None);
9424 }
9425 // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
9426 // tables` and kin) exist only as synth arms on the
9427 // materialising path; claiming one here errored "relation
9428 // does not exist" over the extended protocol for a query the
9429 // simple protocol answered. Prefix test only — a genuinely
9430 // missing relation must keep erroring in-path.
9431 if from.primary.name.starts_with("__spg_")
9432 || from
9433 .joins
9434 .iter()
9435 .any(|j| j.table.name.starts_with("__spg_"))
9436 {
9437 return Ok(None);
9438 }
9439 // r1058 — decline partitioned / inheritance parents, same
9440 // shape of bug as the RLS decline above: this path scans the
9441 // named table's own (empty) heap, so `SELECT id, region FROM
9442 // cust` on a partition parent streamed ZERO rows over the wire
9443 // while COUNT(*) — an aggregate, materialised below — said 3.
9444 // Caught by the perm-runner's server permutations; the
9445 // materialising fallback expands children correctly.
9446 if crate::partition::has_children(self.active_catalog(), &from.primary.name)
9447 || from
9448 .joins
9449 .iter()
9450 .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
9451 {
9452 return Ok(None);
9453 }
9454 // v7.39 (round 790) — single-table SELECTs stream too. This
9455 // gate said "joins only" because the path was written for
9456 // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
9457 // fell to the materialising fallback, which builds the whole
9458 // `Vec<Row<'static>>` and only then iterates it. Measured on
9459 // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
9460 // reached through a one-row JOIN — 2.6x, purely for lacking a
9461 // join. The deferred-join structure handles one source as the
9462 // degenerate stride-1 case, so the walk below is unchanged.
9463 let _single_table = from.joins.is_empty();
9464 // An ORDER BY that the bounded sort can serve streams; everything
9465 // else still falls to the materialising fallback below.
9466 // r1025 — an ordering the index already holds needs no sort at all.
9467 // Tried before the spill sort, which is the path it replaces.
9468 if !stmt.order_by.is_empty()
9469 && from.joins.is_empty()
9470 && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
9471 {
9472 return Ok(Some(n));
9473 }
9474 if !stmt.order_by.is_empty()
9475 && from.joins.is_empty()
9476 && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
9477 {
9478 return Ok(Some(n));
9479 }
9480 // r1031 — integer keys carried inline instead of an `OrderKey`
9481 // vector per row. Tried AFTER the spill sort on purpose: this lane
9482 // buffers the whole answer, so anything the spill path would take
9483 // must keep taking it rather than be turned back into an in-memory
9484 // sort that answers with a budget error.
9485 if !stmt.order_by.is_empty()
9486 && from.joins.is_empty()
9487 && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
9488 {
9489 return Ok(Some(n));
9490 }
9491 if !stmt.order_by.is_empty()
9492 || stmt.limit.is_some()
9493 || stmt.offset.is_some()
9494 || stmt.having.is_some()
9495 || stmt.group_by.is_some()
9496 || stmt.distinct
9497 || !stmt.unions.is_empty()
9498 || stmt.limit_with_ties
9499 {
9500 return Ok(None);
9501 }
9502 if aggregate::uses_aggregate(stmt) {
9503 return Ok(None);
9504 }
9505 // No window / SRF on the streaming path.
9506 if select_has_window(stmt) {
9507 return Ok(None);
9508 }
9509 if stmt
9510 .items
9511 .iter()
9512 .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9513 {
9514 return Ok(None);
9515 }
9516 // v7.37 (round 831) — a joinless FROM over a plain stored table
9517 // never needs the deferred structure, and building one costs the
9518 // whole table. `materialise_table_ref_filtered` clones every row
9519 // into a `Vec<Row<'static>>` before anything is filtered or
9520 // projected, so peak cost tracks the TABLE, not the result:
9521 // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
9522 // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
9523 // projection saving nothing, while an arithmetic projection — which
9524 // the shape gates decline, so it materialises through the ordinary
9525 // executor — cost +21 MB.
9526 //
9527 // Scanning in batches and releasing each one is what `cursor_fill`
9528 // already does for a lazy cursor, and it is the same walk: resume
9529 // from a slot, take visible rows, evaluate, hand them over, drop
9530 // them. Round 800's finding stands and is why this reads rows OUT
9531 // rather than seeding the join by index — touching the stored
9532 // `PersistentVec` in place makes the whole table resident, which is
9533 // worse than the copy. Each batch is copied, then freed.
9534 if from.joins.is_empty()
9535 && from.primary.unnest_expr.is_none()
9536 && from.primary.lateral_subquery.is_none()
9537 && from.primary.as_of_segment.is_none()
9538 && from.primary.generate_series_args.is_none()
9539 && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
9540 {
9541 return Ok(Some(n));
9542 }
9543 // Build the deferred join under the regular byte budget.
9544 let mut budget = ByteBudget::new(self.max_query_bytes);
9545 let deferred = {
9546 let mut needed = alloc::collections::BTreeSet::new();
9547 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
9548 self.build_joined_filtered_rows(
9549 from,
9550 stmt.where_.as_ref(),
9551 cancel,
9552 if prunable { Some(&needed) } else { None },
9553 &mut budget,
9554 )?
9555 };
9556 let combined_schema = &deferred.combined_schema;
9557 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
9558 // `::regclass` / enum cast in a joined projection or HAVING needs it.
9559 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
9560 // the same predicate the unjoined shape carries.
9561 let joined_sess = self.dml_session();
9562 // v7.38.18 — and the DIALECT. This context carried the catalog and
9563 // the session and not the one field that decides how text
9564 // compares, so a joined row was evaluated in PostgreSQL
9565 // semantics inside a MySQL session.
9566 //
9567 // It showed up only where the two sides had DIFFERENT text types:
9568 // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
9569 // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
9570 // were fine and the same comparison inside one table was fine.
9571 // Same-type pairs agree byte-for-byte after an ASCII lowercase,
9572 // so the wrong semantics were invisible until a CHAR's padding
9573 // had to be stripped and PostgreSQL's arm does not strip it.
9574 //
9575 // `with_engine` is what sets it; the next line already reaches
9576 // for `self.backslash_escapes`, so the dialect was in hand.
9577 let ctx = EvalContext::new(combined_schema, None)
9578 .with_catalog(self.active_catalog())
9579 .with_engine(self)
9580 .with_session(&joined_sess);
9581 let projection = build_projection(
9582 &stmt.items,
9583 combined_schema,
9584 "",
9585 self.speaks_mysql,
9586 Some(self.active_catalog()),
9587 )?;
9588 // Every projection item must be a bound qualified column —
9589 // anything that needs `eval_expr_with_correlated` keeps the
9590 // materialising path.
9591 let bound_pos = |e: &Expr| -> Option<usize> {
9592 match e {
9593 // v7.39 (round 822) — an UNQUALIFIED column resolves here
9594 // too. The `qualifier.is_some()` guard this replaces meant
9595 // `SELECT pad FROM big` — the commonest projection there is
9596 // — never reached the streaming walk: it fell out at this
9597 // gate and re-ran on the materialising path, after the
9598 // deferred join structure had already been built and paid
9599 // for. Measured (round 821, statement_timeout=120 over 400k
9600 // rows): `big.pad` and `b.pad` streamed and cancelled at
9601 // ~65k rows in 0.14 s, while bare `pad` ran to completion in
9602 // 0.80 s with the timeout never consulted. `find_column_pos`
9603 // has always handled the unqualified case (it falls through
9604 // to a by-name match), so the guard narrowed the gate for no
9605 // reason it recorded.
9606 Expr::Column(c) => eval::find_column_pos(c, &ctx),
9607 _ => None,
9608 }
9609 };
9610 let proj_decomposed: Vec<(usize, usize)> = {
9611 let mut out = Vec::with_capacity(projection.len());
9612 for p in &projection {
9613 let Some(abs) = bound_pos(&p.expr) else {
9614 return Ok(None);
9615 };
9616 let Some(k) = deferred
9617 .offsets
9618 .partition_point(|&o| o <= abs)
9619 .checked_sub(1)
9620 else {
9621 return Ok(None);
9622 };
9623 out.push((k, abs - deferred.offsets[k]));
9624 }
9625 out
9626 };
9627 // Emit columns once.
9628 let columns: Vec<ColumnSchema> = projection
9629 .iter()
9630 // v7.39 (read01 round 54) — keep the column's enum identity through
9631 // the projection (it lives outside the DataType lattice), or a
9632 // derived table / UNION / windowed result forgets it and any outer
9633 // `ORDER BY <enum col>` silently sorts by the label's TEXT.
9634 .map(|p| p.to_column_schema())
9635 .collect();
9636 emit(crate::StreamItem::Header(&columns))?;
9637 let sources_ref = &deferred.sources;
9638 let stride = deferred.stride;
9639 let survivors_ref = &deferred.survivors;
9640 let n_surv = if stride == 0 {
9641 0
9642 } else {
9643 survivors_ref.len() / stride
9644 };
9645 // Reused per-row cell-ref scratch — pushes are zero-alloc
9646 // after the first row.
9647 let null_value = Value::Null;
9648 let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
9649 let mut count: usize = 0;
9650 for surv_i in 0..n_surv {
9651 if surv_i.is_multiple_of(256) {
9652 cancel.check()?;
9653 }
9654 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9655 cell_refs.clear();
9656 for &(k, col_in_src) in &proj_decomposed {
9657 let ri = tuple[k];
9658 let v: &Value = if ri == usize::MAX {
9659 &null_value
9660 } else {
9661 sources_ref[k]
9662 .get(ri)
9663 .and_then(|r| r.values.get(col_in_src))
9664 .unwrap_or(&null_value)
9665 };
9666 cell_refs.push(v);
9667 }
9668 emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
9669 count += 1;
9670 }
9671 Ok(Some(count))
9672 }
9673
9674 fn exec_joined_select(
9675 &self,
9676 stmt: &SelectStatement,
9677 from: &FromClause,
9678 cancel: CancelToken<'_>,
9679 ) -> Result<QueryResult, EngineError> {
9680 // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
9681 // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
9682 // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
9683 // FROM B WHERE B.k = A.k)` into
9684 // SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
9685 // WHERE B.k IS NULL
9686 // The general join executor builds a hash, probes every outer
9687 // tuple, materialises (left_padded_with_null) for every miss,
9688 // then runs the aggregate over the result set. For COUNT(*) we
9689 // only need the count — skip the tuple materialisation. Build
9690 // a HashSet of B's unique join values, scan A's PK index, and
9691 // increment the counter on each miss. PG's Merge Anti-Join
9692 // does roughly this; ours becomes a simple HashSet probe.
9693 if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
9694 return Ok(out);
9695 }
9696 // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
9697 // When ORDER BY is on an indexed primary column, walking the
9698 // btree in the requested direction lets the streamer break
9699 // after `LIMIT + OFFSET` survivors without ever materialising
9700 // the rest of the join — the 80 ms `mailrs_prod_not_exists`
9701 // plateau is exactly this shape.
9702 if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
9703 return Ok(out);
9704 }
9705 // v7.30.3 (mailrs round-26) — the bounded single-join path
9706 // first; peak memory scales with LIMIT instead of the table.
9707 if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
9708 return Ok(out);
9709 }
9710 // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
9711 // WHERE materialisation to the shared helper so the LATERAL
9712 // / UNNEST / regular-catalog paths route through one place.
9713 // (`build_joined_filtered_rows` carries LATERAL support as
9714 // of Phase 3.P0-41.) Downstream we still handle aggregate /
9715 // projection / ORDER BY / DISTINCT / LIMIT inline because
9716 // those depend on the SelectStatement's items list.
9717 let mut budget = ByteBudget::new(self.max_query_bytes);
9718 let deferred = {
9719 let mut needed = alloc::collections::BTreeSet::new();
9720 let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
9721 self.build_joined_filtered_rows(
9722 from,
9723 stmt.where_.as_ref(),
9724 cancel,
9725 if prunable { Some(&needed) } else { None },
9726 &mut budget,
9727 )?
9728 };
9729 let combined_schema = &deferred.combined_schema;
9730 // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
9731 // `::regclass` / enum cast in a joined projection or HAVING needs it.
9732 // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
9733 // the same predicate the unjoined shape carries.
9734 let joined_sess = self.dml_session();
9735 // v7.38.18 — and the DIALECT. This context carried the catalog and
9736 // the session and not the one field that decides how text
9737 // compares, so a joined row was evaluated in PostgreSQL
9738 // semantics inside a MySQL session.
9739 //
9740 // It showed up only where the two sides had DIFFERENT text types:
9741 // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
9742 // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
9743 // were fine and the same comparison inside one table was fine.
9744 // Same-type pairs agree byte-for-byte after an ASCII lowercase,
9745 // so the wrong semantics were invisible until a CHAR's padding
9746 // had to be stripped and PostgreSQL's arm does not strip it.
9747 //
9748 // `with_engine` is what sets it; the next line already reaches
9749 // for `self.backslash_escapes`, so the dialect was in hand.
9750 let ctx = EvalContext::new(combined_schema, None)
9751 .with_catalog(self.active_catalog())
9752 .with_engine(self)
9753 .with_session(&joined_sess);
9754 // Aggregate path: handle GROUP BY / aggregate calls over the
9755 // joined+filtered rows.
9756 if aggregate::uses_aggregate(stmt) {
9757 // v7.32 (P4 borrow channel, increment 2) — borrow each
9758 // surviving join tuple as a RowRef::Tuple; the aggregate
9759 // engine reads source cells by reference (bound fast path =
9760 // zero clone) instead of consuming materialised combined
9761 // Rows. This is where the +211k materialise_tuple_vals
9762 // clones disappear for the join+aggregate shape.
9763 let refs = deferred.row_refs();
9764 // v7.29 — a per-query memo so correlated scalar
9765 // subqueries batch-evaluate once (group map) instead of
9766 // executing per group.
9767 let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
9768 let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
9769 self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
9770 .map_err(|err| match err {
9771 EngineError::Eval(ev) => ev,
9772 other => eval::EvalError::TypeMismatch {
9773 detail: alloc::format!("{other}"),
9774 },
9775 })
9776 };
9777 let agg = aggregate::run(
9778 stmt,
9779 crate::join::AggRows::Refs(&refs),
9780 combined_schema,
9781 None,
9782 Some(&agg_correlated),
9783 self.parallel_runner.0.as_deref(),
9784 Some(self.active_catalog()),
9785 Some(self),
9786 )?;
9787 return self.finish_agg_result(agg, stmt, cancel);
9788 }
9789
9790 let projection = build_projection(
9791 &stmt.items,
9792 combined_schema,
9793 "",
9794 self.speaks_mysql,
9795 Some(self.active_catalog()),
9796 )?;
9797 // v7.39 (round 734) — a set-returning projection over a JOIN.
9798 // This executor's projection loop treats every item as a scalar,
9799 // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
9800 // "function unnest(integer[]) does not exist" where PG expands
9801 // it. The row-set executor already carries the full SRF pipeline
9802 // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
9803 // sharding): materialise the joined survivors and hand over. The
9804 // WHERE is cleared — the join already applied it, and combined
9805 // columns resolve identically in both executors.
9806 if !self.srf_target_idxs(&projection).is_empty() {
9807 let refs = deferred.row_refs();
9808 let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
9809 let mut s2 = stmt.clone();
9810 s2.where_ = None;
9811 let schema = combined_schema.clone();
9812 return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
9813 }
9814 // v7.33 (P4 borrow channel, increment 3) — project directly off
9815 // the deferred row-index tuples instead of materialising an
9816 // intermediate combined Row per survivor. A bound qualified
9817 // column is read by reference (`RowRef::get` → `tuple_value`) and
9818 // cloned ONCE into the output row; the old `materialise()` (a full
9819 // combined Row plus a source→intermediate clone per referenced
9820 // cell, for every survivor) is gone. A row materialises on demand
9821 // only when a projection or ORDER BY expression needs the eval
9822 // path (subquery / function / arithmetic / unqualified column).
9823 // Same bind-once classification the aggregate input fast path uses
9824 // (`accumulate_groups`), reading the same `tuple_value` mapping the
9825 // differential gate already covers.
9826 let refs = deferred.row_refs();
9827 let bound_pos = |e: &Expr| -> Option<usize> {
9828 match e {
9829 Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
9830 _ => None,
9831 }
9832 };
9833 let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
9834 let all_proj_bound = proj_pos.iter().all(Option::is_some);
9835 // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
9836 // pre-decompose each bound projection position into
9837 // `(source_k, col_in_source)` so the per-row column read
9838 // skips the per-cell `tuple_value` partition_point + slice
9839 // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
9840 // calls) that walk dominated; this version reaches into
9841 // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
9842 let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
9843 .iter()
9844 .map(|p| {
9845 p.and_then(|abs| {
9846 let k = deferred
9847 .offsets
9848 .partition_point(|&o| o <= abs)
9849 .checked_sub(1)?;
9850 Some((k, abs - deferred.offsets[k]))
9851 })
9852 })
9853 .collect();
9854 // v7.39 (round 962) — which projection items are whole-row
9855 // references, and to which join source. The test is
9856 // `locate_column` declining the name, which is the SAME resolver
9857 // the evaluation path uses, so this cannot drift from it: a real
9858 // column carrying an alias's name resolves to a position and is
9859 // not reported here. The source index comes from the alias
9860 // prefix, the way the combined schema names its columns.
9861 let whole_row_src: Vec<Option<usize>> = projection
9862 .iter()
9863 .map(|p| {
9864 let Expr::Column(c) = &p.expr else {
9865 return None;
9866 };
9867 if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
9868 return None;
9869 }
9870 let prefix = alloc::format!("{name}.", name = c.name);
9871 let abs = deferred
9872 .combined_schema
9873 .iter()
9874 .position(|s| s.name.starts_with(&prefix))?;
9875 deferred
9876 .offsets
9877 .partition_point(|&o| o <= abs)
9878 .checked_sub(1)
9879 })
9880 .collect();
9881 // ORDER BY (when present) still evaluates against a materialised
9882 // Row — keep the order-key encoder correct rather than fork it.
9883 let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
9884 let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
9885 let mut proj_memo = memoize::MemoizeCache::default();
9886 let sources_ref = &deferred.sources;
9887 let stride = deferred.stride;
9888 let survivors_ref = &deferred.survivors;
9889 let n_surv = survivors_ref.len() / stride.max(1);
9890 // v7.38 (read01 B8) — streaming top-N budget (see the sibling
9891 // single-table path). Bounds this JOIN projection's accumulator
9892 // to O(keep) for `ORDER BY … LIMIT k`.
9893 let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
9894 && !stmt.distinct
9895 && !stmt.limit_with_ties
9896 && !self.env_cfg().disable_topk
9897 {
9898 stmt.limit_literal().and_then(|l| {
9899 let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
9900 (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
9901 })
9902 } else {
9903 None
9904 };
9905 // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
9906 let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9907 hashbrown::HashMap::new();
9908 let distinct_hb = hashbrown::DefaultHashBuilder::default();
9909 // v7.38.13 — which output positions must NOT fold. Built once per
9910 // scan from the projection, which carries the source column's
9911 // byte-wise-ness; see `FoldSpec`.
9912 let distinct_mask = fold_mask(&projection);
9913 for surv_i in 0..n_surv {
9914 let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9915 let row = &refs[surv_i];
9916 let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
9917 Some(row.as_row())
9918 } else {
9919 None
9920 };
9921 let mut values = Vec::with_capacity(projection.len());
9922 for (i, p) in projection.iter().enumerate() {
9923 if let Some((k, col_in_src)) = proj_decomposed[i] {
9924 // v7.36 — direct (source_k, col) lookup, no
9925 // partition_point. tuple[k] is the row index in
9926 // sources[k]; LEFT-NULL slots are `usize::MAX`.
9927 let ri = tuple[k];
9928 let v: Value<'static> = if ri == usize::MAX {
9929 Value::Null
9930 } else {
9931 sources_ref[k]
9932 .get(ri)
9933 .and_then(|r| r.values.get(col_in_src))
9934 .cloned()
9935 .map(Value::into_owned)
9936 .unwrap_or(Value::Null)
9937 };
9938 values.push(v);
9939 } else if let Some(pos) = proj_pos[i] {
9940 // Bound but couldn't decompose (shouldn't normally
9941 // happen — keep as a safe path).
9942 values.push(
9943 row.get(pos)
9944 .cloned()
9945 .map(Value::into_owned)
9946 .unwrap_or(Value::Null),
9947 );
9948 } else if let Some(k) = whole_row_src[i]
9949 && tuple[k] == usize::MAX
9950 {
9951 // v7.39 (round 962) — a whole-row reference to a side
9952 // an OUTER join null-extended is NULL, not a
9953 // composite whose fields are all NULL. PG18.4 answers
9954 // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
9955 // an empty cell; round 961 answered `(,)`.
9956 //
9957 // The evaluator below cannot tell the two apart: it
9958 // reads the MATERIALISED combined row, where a
9959 // null-extended side is indistinguishable from a real
9960 // row whose every column is NULL — and that row is
9961 // `(,)` in PG too, so guessing by "all fields NULL"
9962 // would trade one wrong answer for another. The
9963 // tuple, which is still in hand here, does know:
9964 // `usize::MAX` is the sentinel the join writes for
9965 // exactly this.
9966 values.push(Value::Null);
9967 } else {
9968 // Eval path — `materialised` is Some whenever any
9969 // projection item is non-bound (need_eval_row true).
9970 // v7.24 (round-16 B) — select-list subqueries under a
9971 // JOIN go through the correlated-aware evaluator too.
9972 let mrow = materialised.as_deref().expect("materialised for eval");
9973 values.push(self.eval_expr_with_correlated(
9974 &p.expr,
9975 mrow,
9976 &ctx,
9977 cancel,
9978 Some(&mut proj_memo),
9979 )?);
9980 }
9981 }
9982 let out_row = Row::new(values);
9983 // v7.37.16 — streaming DISTINCT (see the scan-path twin):
9984 // probe on the projected row; duplicates skip the
9985 // build_order_keys eval and never enter `tagged`.
9986 if stmt.distinct {
9987 let bucket = seen_distinct
9988 .entry(norm_hash_row(
9989 &out_row,
9990 &distinct_hb,
9991 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9992 ))
9993 .or_default();
9994 if bucket.iter().any(|i| {
9995 row_eq_norm(
9996 &tagged[i].1,
9997 &out_row,
9998 FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9999 )
10000 }) {
10001 continue;
10002 }
10003 bucket.push(tagged.len());
10004 }
10005 let order_keys = if stmt.order_by.is_empty() {
10006 Vec::new()
10007 } else {
10008 let mrow = materialised.as_deref().expect("materialised for order by");
10009 build_order_keys(&stmt.order_by, mrow, &ctx)?
10010 };
10011 budget.charge(approx_row_bytes(&out_row))?;
10012 tagged.push((order_keys, out_row));
10013 if let Some((k, descs)) = &topk_stream {
10014 topk_trim(&mut tagged, *k, descs);
10015 }
10016 }
10017 if !stmt.order_by.is_empty() {
10018 // v7.38 元机制 D acceptor — see other call site above.
10019 let keep = if self.env_cfg().disable_topk {
10020 None
10021 } else {
10022 stmt.limit_literal()
10023 .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
10024 };
10025 let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
10026 // v7.39 (round 688) — the join's ORDER BY resolves its keys
10027 // against `ctx`, which is built from `build_combined_schema`, so
10028 // this is where a declared collation reaches the sort. There was
10029 // exactly ONE resolver call in the engine before this — the
10030 // single-table scan's — which is why every other shape sorted by
10031 // bytes no matter what the schemas carried.
10032 let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
10033 crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
10034 }
10035 let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
10036 apply_offset_and_limit(
10037 &mut output_rows,
10038 stmt.offset_literal(),
10039 stmt.limit_literal(),
10040 );
10041 let columns: Vec<ColumnSchema> = projection
10042 .into_iter()
10043 .map(|p| p.to_column_schema())
10044 .collect();
10045 Ok(QueryResult::Rows {
10046 columns,
10047 rows: output_rows,
10048 })
10049 }
10050}
10051
10052impl Engine {
10053 /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
10054 /// by id, decodes each row body against the table's current
10055 /// schema, applies the SELECT's projection + optional WHERE +
10056 /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
10057 /// / ORDER BY are unsupported on this path (STABILITY carve-
10058 /// out); operators wanting them should restore the segment
10059 /// into a regular table first.
10060 fn exec_select_as_of_segment(
10061 &self,
10062 stmt: &SelectStatement,
10063 from: &spg_sql::ast::FromClause,
10064 segment_id: u32,
10065 ) -> Result<QueryResult, EngineError> {
10066 // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
10067 // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
10068 if !from.joins.is_empty()
10069 || stmt.group_by.is_some()
10070 || stmt.having.is_some()
10071 || !stmt.unions.is_empty()
10072 || !stmt.order_by.is_empty()
10073 || stmt.offset.is_some()
10074 || stmt.distinct
10075 || aggregate::uses_aggregate(stmt)
10076 {
10077 return Err(EngineError::Unsupported(
10078 "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
10079 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
10080 .into(),
10081 ));
10082 }
10083 let table = self
10084 .active_catalog()
10085 .get(&from.primary.name)
10086 .ok_or_else(|| StorageError::TableNotFound {
10087 name: from.primary.name.clone(),
10088 })?;
10089 let schema = table.schema().clone();
10090 let schema_cols = &schema.columns;
10091 let alias = from
10092 .primary
10093 .alias
10094 .as_deref()
10095 .unwrap_or(from.primary.name.as_str());
10096 let ctx = self.ev_ctx(schema_cols, Some(alias));
10097 let seg = self
10098 .active_catalog()
10099 .cold_segment(segment_id)
10100 .ok_or_else(|| {
10101 EngineError::Unsupported(alloc::format!(
10102 "AS OF SEGMENT: cold segment {segment_id} not registered"
10103 ))
10104 })?;
10105 let mut out_rows: Vec<Row<'static>> = Vec::new();
10106 let mut limit_remaining: Option<usize> =
10107 stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
10108 for (_key, body) in seg.scan() {
10109 let (row, _consumed) =
10110 spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
10111 .map_err(EngineError::Storage)?;
10112 if let Some(where_expr) = &stmt.where_ {
10113 let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
10114 if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
10115 continue;
10116 }
10117 }
10118 // Projection.
10119 let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
10120 out_rows.push(projected);
10121 if let Some(rem) = limit_remaining.as_mut() {
10122 if *rem == 0 {
10123 out_rows.pop();
10124 break;
10125 }
10126 *rem -= 1;
10127 }
10128 }
10129 // Output column schema: derive from SELECT items.
10130 let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
10131 Ok(QueryResult::Rows {
10132 columns,
10133 rows: out_rows,
10134 })
10135 }
10136
10137 /// v6.10.2 — simple-path WHERE eval that doesn't go through
10138 /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
10139 /// scan paths predicate against a snapshot frozen segment, no
10140 /// cross-row state.
10141 fn eval_expr_simple(
10142 &self,
10143 expr: &Expr,
10144 row: &Row<'static>,
10145 ctx: &EvalContext,
10146 ) -> Result<Value<'static>, EngineError> {
10147 let cancel = CancelToken::none();
10148 self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
10149 }
10150}
10151
10152// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
10153
10154/// One row-producing projection: an expression to evaluate, the resulting
10155/// column's user-visible name, its inferred type, and nullability.
10156#[derive(Debug, Clone)]
10157pub(crate) struct ProjectedItem {
10158 pub(crate) expr: Expr,
10159 pub(crate) output_name: String,
10160 pub(crate) ty: DataType,
10161 pub(crate) nullable: bool,
10162 /// v7.39 (read01 round 54) — a projected enum column keeps its enum
10163 /// identity. Enum-ness lives outside the DataType lattice (the value is a
10164 /// Text), so a projection that dropped this made the RESULT schema forget
10165 /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
10166 /// that schema, silently fell back to TEXT order instead of member order.
10167 pub(crate) user_enum_type: Option<String>,
10168 /// v7.39 (round 425) — a projected MySQL temporal column keeps its
10169 /// declared fractional-seconds precision, so the renderer can pad to
10170 /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
10171 /// a whole second). Like `user_enum_type` this lives outside the
10172 /// DataType lattice, so a projection that dropped it made the RESULT
10173 /// schema forget how wide the fraction should print.
10174 pub(crate) mysql_fsp: Option<u8>,
10175 /// v7.39 (round 688) — and its declared collation, the third thing to
10176 /// live outside the DataType lattice and the third to be lost the same
10177 /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
10178 /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
10179 /// projection rebuilt the output column and the ORDER BY resolves
10180 /// against THAT schema.
10181 pub(crate) collation_name: Option<String>,
10182 /// v7.38.13 — and whether this position must NOT fold when DISTINCT
10183 /// de-dups it. The fourth thing to live outside the DataType lattice
10184 /// and the fourth to be lost the same way: a column declared
10185 /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
10186 /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
10187 /// returns two.
10188 ///
10189 /// A BOOL rather than the `Collation` enum on purpose. The enum's
10190 /// storage default is `Binary`, but the FOLD default under MySQL is
10191 /// case-insensitive — carrying the enum would silently mean
10192 /// "exempt" for every projected expression that is not a column.
10193 /// This field states the question it answers.
10194 pub(crate) fold_exempt: bool,
10195 /// v7.38.18 — does this column's collation make trailing spaces
10196 /// insignificant? A separate question from `fold_exempt`:
10197 /// `utf8mb4_bin` is fold-exempt AND pads, `utf8mb4_0900_ai_ci`
10198 /// folds and does not. Read off the same column, at the same
10199 /// place, so the two masks cannot drift apart.
10200 pub(crate) pads: bool,
10201}
10202
10203impl ProjectedItem {
10204 /// v7.38.14 — the output column this projected item describes.
10205 ///
10206 /// There were TWENTY-ONE places converting a `ProjectedItem` into a
10207 /// `ColumnSchema`, each written as `ColumnSchema::new(..)` followed by a
10208 /// hand-picked list of attributes to copy after it, and the lists did not
10209 /// agree: six carried enum identity, the collation NAME and MySQL fsp; ten
10210 /// carried the first and last but not the name; five carried nothing at
10211 /// all. Not one carried `collation`, the enum every MySQL text comparison
10212 /// actually reads.
10213 ///
10214 /// That is how a declared collation vanished between a subquery and the
10215 /// query that selects from it: the inner SELECT's output schema claimed
10216 /// `ColumnSchema::new`'s default, which is `Binary` — a value downstream
10217 /// reads as "byte-wise ON PURPOSE" rather than as "unknown", so the loss
10218 /// presents as a deliberate declaration.
10219 ///
10220 /// One conversion, so a field added to either type has one place to be
10221 /// remembered instead of twenty-one.
10222 pub(crate) fn to_column_schema(&self) -> ColumnSchema {
10223 let mut c = ColumnSchema::new(self.output_name.clone(), self.ty, self.nullable);
10224 c.user_enum_type.clone_from(&self.user_enum_type);
10225 c.collation_name.clone_from(&self.collation_name);
10226 c.mysql_fsp = self.mysql_fsp;
10227 // `fold_exempt` is the projection's answer to the same question
10228 // `ColumnSchema::collation` answers downstream, and it was computed
10229 // from the source column. Keeping the two in step here is what stops
10230 // a de-duplication site further on from asking the schema and being
10231 // told the opposite of what the projection knew.
10232 c.collation = if self.fold_exempt {
10233 spg_storage::Collation::Binary
10234 } else {
10235 spg_storage::Collation::CaseInsensitive
10236 };
10237 c
10238 }
10239}
10240
10241/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
10242/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
10243/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
10244/// the spec's "two NULLs are not distinct"; the second is a tolerated
10245/// quirk for v1 (no NaN literals are reachable from the SQL surface).
10246/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
10247fn expr_is_aggregate_call(e: &Expr) -> bool {
10248 match e {
10249 Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
10250 Expr::AggregateOrdered { .. } => true,
10251 _ => false,
10252 }
10253}
10254
10255/// Collect distinct top-level aggregate call expressions (dedup by value). Does
10256/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
10257/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
10258/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
10259/// than today — never a regression on a working query).
10260fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
10261 if expr_is_aggregate_call(e) {
10262 if !out.iter().any(|x| x == e) {
10263 out.push(e.clone());
10264 }
10265 return;
10266 }
10267 match e {
10268 Expr::Binary { lhs, rhs, .. } => {
10269 collect_agg_exprs(lhs, out);
10270 collect_agg_exprs(rhs, out);
10271 }
10272 Expr::Unary { expr, .. }
10273 | Expr::Cast { expr, .. }
10274 | Expr::IsNull { expr, .. }
10275 | Expr::BoolTest { expr, .. }
10276 | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
10277 Expr::FunctionCall { args, .. } => {
10278 for a in args {
10279 collect_agg_exprs(a, out);
10280 }
10281 }
10282 Expr::Like { expr, pattern, .. } => {
10283 collect_agg_exprs(expr, out);
10284 collect_agg_exprs(pattern, out);
10285 }
10286 Expr::Extract { source, .. } => collect_agg_exprs(source, out),
10287 Expr::WindowFunction {
10288 args,
10289 partition_by,
10290 order_by,
10291 ..
10292 } => {
10293 for a in args {
10294 collect_agg_exprs(a, out);
10295 }
10296 for p in partition_by {
10297 collect_agg_exprs(p, out);
10298 }
10299 for (o, _, _) in order_by {
10300 collect_agg_exprs(o, out);
10301 }
10302 }
10303 _ => {}
10304 }
10305}
10306
10307/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
10308fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
10309 if expr_is_aggregate_call(e) {
10310 if let Some(idx) = aggs.iter().position(|x| x == e) {
10311 *e = Expr::Column(ColumnName {
10312 qualifier: None,
10313 name: alloc::format!("__agg{idx}"),
10314 });
10315 }
10316 return;
10317 }
10318 match e {
10319 Expr::Binary { lhs, rhs, .. } => {
10320 replace_agg_exprs(lhs, aggs);
10321 replace_agg_exprs(rhs, aggs);
10322 }
10323 Expr::Unary { expr, .. }
10324 | Expr::Cast { expr, .. }
10325 | Expr::IsNull { expr, .. }
10326 | Expr::BoolTest { expr, .. }
10327 | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
10328 Expr::FunctionCall { args, .. } => {
10329 for a in args {
10330 replace_agg_exprs(a, aggs);
10331 }
10332 }
10333 Expr::Like { expr, pattern, .. } => {
10334 replace_agg_exprs(expr, aggs);
10335 replace_agg_exprs(pattern, aggs);
10336 }
10337 Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
10338 Expr::WindowFunction {
10339 args,
10340 partition_by,
10341 order_by,
10342 ..
10343 } => {
10344 for a in args {
10345 replace_agg_exprs(a, aggs);
10346 }
10347 for p in partition_by {
10348 replace_agg_exprs(p, aggs);
10349 }
10350 for (o, _, _) in order_by {
10351 replace_agg_exprs(o, aggs);
10352 }
10353 }
10354 _ => {}
10355 }
10356}
10357
10358/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
10359/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
10360/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
10361/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
10362/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
10363/// Returns None outside the bounded subset (leaves current behaviour). Only fires
10364/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
10365/// window-only / aggregate-only queries.
10366fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
10367 if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
10368 return None;
10369 }
10370 // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
10371 if !stmt.unions.is_empty() {
10372 return None;
10373 }
10374 let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
10375 if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
10376 return None;
10377 }
10378 stmt.from.as_ref()?;
10379 // Collect the aggregate calls to hoist from projection + outer ORDER BY.
10380 let mut aggs: Vec<Expr> = Vec::new();
10381 for item in &stmt.items {
10382 if let SelectItem::Expr { expr, .. } = item {
10383 collect_agg_exprs(expr, &mut aggs);
10384 }
10385 }
10386 for ob in &stmt.order_by {
10387 collect_agg_exprs(&ob.expr, &mut aggs);
10388 }
10389 // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
10390 let mut inner_items: Vec<SelectItem> = Vec::new();
10391 for g in &group_cols {
10392 inner_items.push(SelectItem::Expr {
10393 expr: g.clone(),
10394 alias: None,
10395 });
10396 }
10397 for (i, a) in aggs.iter().enumerate() {
10398 inner_items.push(SelectItem::Expr {
10399 expr: a.clone(),
10400 alias: Some(alloc::format!("__agg{i}")),
10401 });
10402 }
10403 let inner = SelectStatement {
10404 items: inner_items,
10405 distinct: false,
10406 distinct_on: Vec::new(),
10407 unions: Vec::new(),
10408 order_by: Vec::new(),
10409 limit: None,
10410 offset: None,
10411 limit_with_ties: false,
10412 window_check_exprs: Vec::new(),
10413 ..stmt.clone()
10414 };
10415 let derived = TableRef {
10416 name: "__aggwin".into(),
10417 alias: Some("__aggwin".into()),
10418 only: false,
10419 as_of_segment: None,
10420 unnest_expr: None,
10421 unnest_column_aliases: Vec::new(),
10422 with_ordinality: false,
10423 generate_series_args: None,
10424 lateral_subquery: Some(alloc::boxed::Box::new(inner)),
10425 jsonb_each_text_arg: None,
10426 table_fn_call: None,
10427 rows_from: None,
10428 json_table: None,
10429 scalar_fn_item: false,
10430 };
10431 // Outer window query over the derived rows: aggregates → __aggN column refs.
10432 let mut outer_items = stmt.items.clone();
10433 for item in &mut outer_items {
10434 if let SelectItem::Expr { expr, alias } = item {
10435 // Preserve PG's column label for a bare aggregate projection.
10436 if alias.is_none()
10437 && let Expr::FunctionCall { name, .. } = expr
10438 && crate::aggregate::is_aggregate_name(name)
10439 {
10440 *alias = Some(name.to_ascii_lowercase());
10441 }
10442 replace_agg_exprs(expr, &aggs);
10443 }
10444 }
10445 let mut outer_order = stmt.order_by.clone();
10446 for ob in &mut outer_order {
10447 replace_agg_exprs(&mut ob.expr, &aggs);
10448 }
10449 let mut outer_distinct_on = stmt.distinct_on.clone();
10450 for e in &mut outer_distinct_on {
10451 replace_agg_exprs(e, &aggs);
10452 }
10453 Some(SelectStatement {
10454 locking: None,
10455 ctes: Vec::new(),
10456 distinct: stmt.distinct,
10457 distinct_on: outer_distinct_on,
10458 items: outer_items,
10459 from: Some(FromClause {
10460 primary: derived,
10461 joins: Vec::new(),
10462 }),
10463 where_: None,
10464 group_by: None,
10465 group_by_all: false,
10466 having: None,
10467 unions: Vec::new(),
10468 order_by: outer_order,
10469 limit: stmt.limit.clone(),
10470 offset: stmt.offset.clone(),
10471 limit_with_ties: stmt.limit_with_ties,
10472 window_check_exprs: Vec::new(),
10473 })
10474}
10475
10476/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
10477/// membership.
10478///
10479/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
10480/// there?", and all four answered by scanning the whole right side once per
10481/// left row. The cost was (left rows x right rows), which is why
10482/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
10483/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
10484/// row that does not pays for all of it. Over 100k left rows, raising the
10485/// right side from 100 to 10,000 took 35 ms to 2848.
10486///
10487/// This is the shape round 485 already solved for DISTINCT, and it reuses
10488/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
10489/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
10490/// every bucket with the exact comparator, so a collision costs time and
10491/// never an answer.
10492struct PeerIndex<'r> {
10493 bh: hashbrown::DefaultHashBuilder,
10494 buckets: hashbrown::HashMap<u64, Vec<usize>>,
10495 rows: &'r [Row<'static>],
10496 fold: FoldSpec<'r>,
10497}
10498
10499impl<'r> PeerIndex<'r> {
10500 fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
10501 // ONE hasher for the whole pass: the default builder is seeded per
10502 // instance, so a fresh one per row would put equal rows in different
10503 // buckets.
10504 let bh = hashbrown::DefaultHashBuilder::default();
10505 let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
10506 hashbrown::HashMap::with_capacity(rows.len());
10507 for (i, r) in rows.iter().enumerate() {
10508 buckets
10509 .entry(norm_hash_row(r, &bh, fold))
10510 .or_default()
10511 .push(i);
10512 }
10513 Self {
10514 bh,
10515 buckets,
10516 rows,
10517 fold,
10518 }
10519 }
10520
10521 fn contains(&self, r: &Row<'static>) -> bool {
10522 let h = norm_hash_row(r, &self.bh, self.fold);
10523 self.buckets
10524 .get(&h)
10525 .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
10526 }
10527
10528 /// Remove ONE occurrence, so the multiset forms cancel row for row the
10529 /// way the pool they replaced did.
10530 fn take_one(&mut self, r: &Row<'static>) -> bool {
10531 let h = norm_hash_row(r, &self.bh, self.fold);
10532 let Some(b) = self.buckets.get_mut(&h) else {
10533 return false;
10534 };
10535 let Some(pos) = b
10536 .iter()
10537 .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
10538 else {
10539 return false;
10540 };
10541 b.swap_remove(pos);
10542 true
10543 }
10544}
10545
10546pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
10547 dedup_by_row(rows, |r| r, fold)
10548}
10549
10550/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
10551/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
10552/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
10553/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
10554/// order is preserved, and correctness needs only the one-way guarantee
10555/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
10556/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
10557fn dedup_by_row<T>(
10558 items: Vec<T>,
10559 row_of: impl Fn(&T) -> &Row<'static>,
10560 fold: FoldSpec<'_>,
10561) -> Vec<T> {
10562 if items.len() <= 32 {
10563 let mut out: Vec<T> = Vec::with_capacity(items.len());
10564 for it in items {
10565 if !out
10566 .iter()
10567 .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
10568 {
10569 out.push(it);
10570 }
10571 }
10572 return out;
10573 }
10574 // ONE BuildHasher instance for the whole pass — the default builder
10575 // is randomly seeded PER INSTANCE, so a fresh one per row would give
10576 // equal rows different hashes and never dedup.
10577 let bh = hashbrown::DefaultHashBuilder::default();
10578 let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
10579 let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
10580 hashbrown::HashMap::with_capacity(items.len());
10581 for it in items {
10582 let h = norm_hash_row(row_of(&it), &bh, fold);
10583 let bucket = buckets.entry(h).or_default();
10584 if !bucket
10585 .iter()
10586 .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
10587 {
10588 bucket.push(out.len());
10589 out.push(it);
10590 }
10591 }
10592 out
10593}
10594
10595/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
10596/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
10597/// rows may collide (buckets are re-checked with the exact comparator).
10598///
10599/// Domain design mirrors `value_cmp`'s equivalence classes:
10600/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
10601/// shares one domain: a value that is an integer fitting i64 hashes the
10602/// i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
10603/// anything else hashes the f64 approximation computed by THE SAME
10604/// formula the value_cmp float arms use (`numeric_to_f64`), so
10605/// `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
10606/// hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
10607/// Known un-closable corner: an integer in [2^53, 2^63) can compare
10608/// Equal to a float via value_cmp's lossy f64 arm while hashing in the
10609/// exact-i64 domain — mixed int/float rows at that magnitude may miss a
10610/// dedup (PG itself compares int8↔float8 in the lossy float8 domain).
10611/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
10612/// compares them blank-insensitively; plain Text pairs that differ only
10613/// in trailing blanks merely collide and are separated exactly).
10614/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
10615/// hash their fields under a distinct tag.
10616/// - Everything value_cmp falls back to debug-format ordering for
10617/// (Json, arrays, vectors, geometry, ranges, …) shares one constant
10618/// bucket — degrades to the exact linear scan, never wrong.
10619fn norm_hash_row(
10620 row: &Row<'static>,
10621 bh: &hashbrown::DefaultHashBuilder,
10622 fold: FoldSpec<'_>,
10623) -> u64 {
10624 norm_hash_values(&row.values, bh, fold)
10625}
10626
10627/// v7.39 (round 485) — the same hash over a bare value slice, so the
10628/// DISTINCT probe can run against a reused buffer instead of demanding a
10629/// `Row` that has to be allocated first (see `values_eq_norm`).
10630fn norm_hash_values(
10631 values: &[Value<'static>],
10632 bh: &hashbrown::DefaultHashBuilder,
10633 fold: FoldSpec<'_>,
10634) -> u64 {
10635 use core::hash::{BuildHasher, Hash, Hasher};
10636 let mut h = bh.build_hasher();
10637 for (i, v) in values.iter().enumerate() {
10638 // v7.39 (round 410) — hash the folded key when the MySQL collation
10639 // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
10640 // `'A'` vs `'a '`) share a hash bucket.
10641 //
10642 // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
10643 // byte-wise column that folded here while the comparator did not
10644 // would scatter equal rows across buckets and stop de-duplicating
10645 // at all; the hash and the comparator have to read the same mask.
10646 if fold.folds(i)
10647 && let Some(folded) = mysql_dedup_fold(v, fold.pads_at(i))
10648 {
10649 folded.hash(&mut h);
10650 continue;
10651 }
10652 norm_hash_value(v, &mut h);
10653 }
10654 h.finish()
10655}
10656
10657/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
10658///
10659/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
10660const fn pow10_i128(p: u16) -> Option<i128> {
10661 const P: [i128; 39] = {
10662 let mut t = [1i128; 39];
10663 let mut i = 1;
10664 while i < 39 {
10665 t[i] = t[i - 1] * 10;
10666 i += 1;
10667 }
10668 t
10669 };
10670 if (p as usize) < P.len() {
10671 Some(P[p as usize])
10672 } else {
10673 None
10674 }
10675}
10676
10677fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
10678 const TAG_NULL: u8 = 0;
10679 const TAG_BOOL: u8 = 1;
10680 const TAG_NUM_I64: u8 = 2;
10681 const TAG_NUM_F64: u8 = 3;
10682 const TAG_TEXT: u8 = 4;
10683 const TAG_DATE: u8 = 6;
10684 const TAG_TIME: u8 = 7;
10685 const TAG_TIMESTAMP: u8 = 8;
10686 const TAG_TIMETZ: u8 = 10;
10687 const TAG_UUID: u8 = 11;
10688 const TAG_MONEY: u8 = 12;
10689 const TAG_BYTES: u8 = 13;
10690 const TAG_INTERVAL: u8 = 14;
10691 const TAG_CHAR1: u8 = 15;
10692 const TAG_OPAQUE: u8 = 255;
10693 // One shared writer for the numeric family: an integer value
10694 // representable as i64 goes exact (round-trip probe — no_std, so no
10695 // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
10696 // through 0i64, folding it into 0.0 as value_cmp requires.
10697 let num_f64 = |h: &mut H, x: f64| {
10698 if x.is_nan() {
10699 h.write_u8(TAG_NUM_F64);
10700 h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
10701 return;
10702 }
10703 const TWO63: f64 = 9_223_372_036_854_775_808.0;
10704 if (-TWO63..TWO63).contains(&x) {
10705 #[allow(clippy::cast_possible_truncation)]
10706 let n = x as i64;
10707 #[allow(clippy::cast_precision_loss)]
10708 if (n as f64) == x {
10709 h.write_u8(TAG_NUM_I64);
10710 h.write_i64(n);
10711 return;
10712 }
10713 }
10714 h.write_u8(TAG_NUM_F64);
10715 h.write_u64(x.to_bits());
10716 };
10717 match v {
10718 Value::Null => h.write_u8(TAG_NULL),
10719 Value::Bool(b) => {
10720 h.write_u8(TAG_BOOL);
10721 h.write_u8(u8::from(*b));
10722 }
10723 Value::SmallInt(n) => {
10724 h.write_u8(TAG_NUM_I64);
10725 h.write_i64(i64::from(*n));
10726 }
10727 Value::Int(n) => {
10728 h.write_u8(TAG_NUM_I64);
10729 h.write_i64(i64::from(*n));
10730 }
10731 Value::BigInt(n) => {
10732 h.write_u8(TAG_NUM_I64);
10733 h.write_i64(*n);
10734 }
10735 Value::Float(x) => num_f64(h, *x),
10736 Value::Numeric {
10737 scaled,
10738 scale,
10739 kind,
10740 } => match kind {
10741 spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
10742 spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
10743 spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
10744 spg_storage::NumericKind::Finite => {
10745 // Reduce trailing fractional zeros so 1.50 and 1.5 share a
10746 // representation, then: exact integers fitting i64 go to the
10747 // i64 domain; everything else uses numeric_to_f64 — the SAME
10748 // formula value_cmp's Numeric↔Float arm compares with.
10749 // r1044 — the reduction is required (`1.5` and `1.50` are
10750 // one value and must land in one bucket) and it used to
10751 // walk one digit at a time. That is O(scale), and scale
10752 // is not small in practice: `n / 100` on a NUMERIC
10753 // column stores `9.1900000000000000`, scale 16, so the
10754 // loop ran fourteen times PER ROW.
10755 //
10756 // Priced by ablation rather than guessed at — removing
10757 // the loop entirely took `SELECT DISTINCT n FROM t ORDER
10758 // BY n` over 400,000 rows from 52 ms to 14.8, against
10759 // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
10760 // tried first moved it not at all, which is why this one
10761 // was measured before it was written.
10762 //
10763 // Binary search over the same powers finds the whole
10764 // run of trailing zeros in at most six tests and one
10765 // division, instead of one test and one division per
10766 // digit.
10767 let (mut s, mut sc) = (*scaled, *scale);
10768 if sc > 0 && s != 0 {
10769 let mut lo: u16 = 0;
10770 let mut hi: u16 = sc;
10771 while lo < hi {
10772 let mid = (lo + hi).div_ceil(2);
10773 match pow10_i128(mid) {
10774 Some(p) if s % p == 0 => lo = mid,
10775 _ => hi = mid - 1,
10776 }
10777 }
10778 if lo > 0 {
10779 if let Some(p) = pow10_i128(lo) {
10780 s /= p;
10781 sc -= lo;
10782 }
10783 }
10784 }
10785 if sc == 0 {
10786 if let Ok(n) = i64::try_from(s) {
10787 h.write_u8(TAG_NUM_I64);
10788 h.write_i64(n);
10789 } else {
10790 num_f64(h, crate::orderby::numeric_to_f64(s, 0));
10791 }
10792 } else {
10793 num_f64(h, crate::orderby::numeric_to_f64(s, sc));
10794 }
10795 }
10796 },
10797 // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
10798 // value that also fits i128 reuses the Numeric path above so
10799 // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
10800 // any i128-representable value — constant bucket is safe.
10801 Value::NumericBig(b) => match b.to_i128() {
10802 Some(s) => norm_hash_value(
10803 &Value::Numeric {
10804 scaled: s,
10805 scale: b.scale(),
10806 kind: spg_storage::NumericKind::Finite,
10807 },
10808 h,
10809 ),
10810 None => h.write_u8(TAG_OPAQUE),
10811 },
10812 // value_cmp compares Text↔BpChar blank-insensitively (both sides
10813 // trimmed), so both hash the trimmed bytes. Text pairs differing
10814 // only in trailing blanks collide and are split exactly in-bucket.
10815 Value::Text(s) | Value::BpChar(s) => {
10816 h.write_u8(TAG_TEXT);
10817 h.write(s.trim_end_matches(' ').as_bytes());
10818 }
10819 Value::Char1(c) => {
10820 h.write_u8(TAG_CHAR1);
10821 h.write_u8(*c);
10822 }
10823 Value::Date(d) => {
10824 h.write_u8(TAG_DATE);
10825 h.write_i32(*d);
10826 }
10827 Value::Time(t) => {
10828 h.write_u8(TAG_TIME);
10829 h.write_i64(*t);
10830 }
10831 Value::Timestamp(t) => {
10832 h.write_u8(TAG_TIMESTAMP);
10833 h.write_i64(*t);
10834 }
10835 Value::TimeTz { us, offset_secs } => {
10836 h.write_u8(TAG_TIMETZ);
10837 h.write_i64(*us);
10838 h.write_i32(*offset_secs);
10839 }
10840 Value::Uuid(u) => {
10841 h.write_u8(TAG_UUID);
10842 h.write(u);
10843 }
10844 Value::Money(c) => {
10845 h.write_u8(TAG_MONEY);
10846 h.write_i64(*c);
10847 }
10848 Value::Bytes(b) => {
10849 h.write_u8(TAG_BYTES);
10850 h.write(b.as_ref());
10851 }
10852 Value::Interval {
10853 months,
10854 days,
10855 micros,
10856 kind,
10857 } => {
10858 h.write_u8(TAG_INTERVAL);
10859 h.write_i32(*months);
10860 h.write_i32(*days);
10861 h.write_i64(*micros);
10862 }
10863 // v7.37.16 — REAL joined the numeric value_cmp family (widened
10864 // to f64, same formulas as the arms), so it hashes in the shared
10865 // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
10866 // f32→f64 is exact, so equal-under-cmp implies equal bits here.
10867 Value::Real(x) => num_f64(h, f64::from(*x)),
10868 // Json (structural equality), vector families (float rendering),
10869 // arrays / geometry / net / ranges / composites (debug-format
10870 // fallback): one constant bucket — exact linear within.
10871 _ => h.write_u8(TAG_OPAQUE),
10872 }
10873}
10874
10875/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
10876/// treats numerically-equal exact values as one regardless of type or scale
10877/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
10878/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
10879/// `Row` `==` would keep them distinct.
10880/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
10881/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
10882/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
10883/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
10884/// the folded comparison key for a text value, None for anything else (which
10885/// keeps the byte-exact `value_cmp` path).
10886fn mysql_dedup_fold(v: &Value, pads: bool) -> Option<String> {
10887 match v {
10888 // v7.38.17 — CHAR's trailing spaces are padding; TEXT's are
10889 // data. The comment above named `utf8mb4_uca1400_ai_ci`, which
10890 // is MariaDB's default and PAD SPACE. SPG advertises MySQL 8.0,
10891 // whose default is NO PAD, so `'alpha'` and `'alpha '` are two
10892 // rows to a `SELECT DISTINCT` and one to `count(DISTINCT)` was
10893 // the same question answered twice.
10894 // v7.38.18 — a CHAR's padding is the TYPE's and never counts; a
10895 // TEXT's is the collation's, which `pads` carries per position.
10896 Value::BpChar(s) => Some(spg_storage::mysql_compare_fold_char(s)),
10897 Value::Text(s) if pads => Some(spg_storage::mysql_compare_fold_char(s)),
10898 Value::Text(s) => Some(spg_storage::mysql_compare_fold(s)),
10899 _ => None,
10900 }
10901}
10902
10903/// v7.39 (round 485) — how many projected rows the single-table scan
10904/// builds, and how many of those the DISTINCT probe throws away again.
10905///
10906/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
10907/// 21 % of all samples in malloc/free called straight from the scan
10908/// closure. The closure's one per-row allocation is the projected
10909/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
10910/// instructions later — but "most" is a guess until it is a number, so
10911/// these count it. (Round 480 was spent acting on an inference about a
10912/// branch that turned out never to run.)
10913/// v7.39 (round 488) — reachability counters for round 487's projection
10914/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
10915/// and a never-called-function probe rules out code layout — so the
10916/// question is whether that shape reaches this code at all, which is a
10917/// number, not an inference.
10918pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10919pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10920
10921pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10922pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
10923 core::sync::atomic::AtomicU64::new(0);
10924
10925/// v7.38.13 — how DISTINCT must compare one row of output.
10926///
10927/// The MySQL default collation folds case and trailing spaces when it
10928/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
10929/// must not fold — `e2e_mysql_collate_binary_round370` calls the
10930/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
10931/// one when the schema asked to keep them apart", and names DISTINCT as
10932/// one of the sites that has to honour it.
10933///
10934/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
10935/// value in a MySQL session, because a bool cannot see a column. The
10936/// GROUP BY path consults the schema and was right all along; the test
10937/// only ever exercised that spelling, so the DISTINCT hole was never
10938/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
10939///
10940/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
10941/// which is what a caller with no schema to offer gets.
10942#[derive(Clone, Copy)]
10943pub(crate) struct FoldSpec<'c> {
10944 mysql: bool,
10945 binary: &'c [bool],
10946 /// v7.38.18 — the padding mask, in lockstep with `binary`. Read the
10947 /// note on `folds`: a hash and its comparator must consult the same
10948 /// masks or equal rows scatter across buckets.
10949 pads: &'c [bool],
10950}
10951
10952impl<'c> FoldSpec<'c> {
10953 /// No column information — every Text position folds under MySQL.
10954 pub(crate) const fn dialect(mysql: bool) -> Self {
10955 Self {
10956 mysql,
10957 binary: &[],
10958 pads: &[],
10959 }
10960 }
10961
10962 /// The mask read off the output columns.
10963 pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
10964 Self {
10965 mysql,
10966 binary,
10967 pads: &[],
10968 }
10969 }
10970
10971 /// The masks read off the output columns — fold-exemption AND
10972 /// padding, which are different questions about the same collation.
10973 pub(crate) fn of_masks(mysql: bool, binary: &'c [bool], pads: &'c [bool]) -> Self {
10974 Self {
10975 mysql,
10976 binary,
10977 pads,
10978 }
10979 }
10980
10981 /// Does position `i` treat trailing spaces as insignificant?
10982 #[inline]
10983 fn pads_at(&self, i: usize) -> bool {
10984 self.pads.get(i).copied().unwrap_or(false)
10985 }
10986
10987 /// Does position `i` fold?
10988 #[inline]
10989 fn folds(&self, i: usize) -> bool {
10990 self.mysql && !self.binary.get(i).copied().unwrap_or(false)
10991 }
10992}
10993
10994/// The fold-exempt mask for a projection.
10995///
10996/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
10997/// projection rebuilds that schema through `ColumnSchema::new`, whose
10998/// collation default is `Binary` — a mask built from it would mark
10999/// EVERY column byte-wise and stop DISTINCT folding at all.
11000/// The padding mask for a projection, read off the same items as
11001/// [`fold_mask`] so the two cannot come from different places.
11002pub(crate) fn pad_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11003 projection.iter().map(|p| p.pads).collect()
11004}
11005
11006pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11007 projection.iter().map(|p| p.fold_exempt).collect()
11008}
11009
11010/// v7.38.14 — the same mask, from an OUTPUT SCHEMA instead of a
11011/// projection.
11012///
11013/// Some de-duplication sites hold `Vec<ColumnSchema>` and never see the
11014/// `ProjectedItem`s it came from. `ProjectedItem::fold_exempt` is built
11015/// from exactly this test (`select.rs`, `build_projection`), so the two
11016/// must keep answering identically -- a site that decided "byte-wise" one
11017/// way while its neighbour decided the other is how the answer came to
11018/// depend on which executor ran the query.
11019///
11020/// The direction matters: `Collation::Binary` is `ColumnSchema::new`'s
11021/// DEFAULT, so a schema rebuilt without carrying the field reads as
11022/// "byte-wise on purpose" here. That is a real trap and it has caught
11023/// five fields so far; it is why S4 of this release exists.
11024/// v7.38.18 — the padding mask from output columns, the sibling of
11025/// [`fold_mask_of_columns`]. Whether a column folds and whether it
11026/// pads are different questions about the same collation.
11027pub(crate) fn pad_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11028 columns
11029 .iter()
11030 .map(|c| crate::collate::pads_space(c.collation_name.as_deref()))
11031 .collect()
11032}
11033
11034pub(crate) fn fold_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11035 columns
11036 .iter()
11037 .map(|c| matches!(c.collation, spg_storage::Collation::Binary))
11038 .collect()
11039}
11040
11041pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
11042 values_eq_norm(&a.values, &b.values, fold)
11043}
11044
11045/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
11046/// DISTINCT probe can compare a reused projection buffer against a kept
11047/// row without building a `Row` for it.
11048pub(crate) fn values_eq_norm(
11049 a: &[Value<'static>],
11050 b: &[Value<'static>],
11051 fold: FoldSpec<'_>,
11052) -> bool {
11053 a.len() == b.len()
11054 && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
11055 if fold.folds(i)
11056 && let (Some(fx), Some(fy)) = (
11057 mysql_dedup_fold(x, fold.pads_at(i)),
11058 mysql_dedup_fold(y, fold.pads_at(i)),
11059 )
11060 {
11061 return fx == fy;
11062 }
11063 crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
11064 })
11065}
11066
11067/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
11068/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
11069/// order via the byte values; vectors are not sortable.
11070pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
11071 // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
11072 // so values sharing a ≥6-byte common prefix (`product_001` vs
11073 // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
11074 // order by their exact bytes instead of the old lossy f64 coarse key.
11075 // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
11076 // matches PG's default C / binary text collation. Every other type
11077 // keeps the lossless-enough `f64` fast path below.
11078 if let Value::Text(s) = v {
11079 return Ok(OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())));
11080 }
11081 // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
11082 // byte order (PG bpcharcmp under C collation), so mixed-pad values of
11083 // the same logical string order equal.
11084 if let Value::BpChar(s) = v {
11085 return Ok(OrderKey::Text(crate::orderby::CompactText::new(
11086 s.trim_end_matches(' '),
11087 )));
11088 }
11089 // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
11090 // carry the parsed value and compare it structurally (see
11091 // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
11092 if let Value::Json(s) = v {
11093 return Ok(match crate::json::parse(s) {
11094 Ok(jv) => OrderKey::Json(jv),
11095 Err(_) => OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())),
11096 });
11097 }
11098 // v7.37 — byte-orderable types PG sorts byte-wise but that have no
11099 // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
11100 // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
11101 // matching PG's network ordering.
11102 match v {
11103 Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
11104 // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
11105 Value::NumericBig(b) => {
11106 return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11107 spg_storage::NumericKey::from_big(b),
11108 )));
11109 }
11110 Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
11111 Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11112 Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11113 Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
11114 Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
11115 let mut key = alloc::vec::Vec::with_capacity(18);
11116 key.push(*family);
11117 key.extend_from_slice(addr);
11118 key.push(*bits);
11119 return Ok(OrderKey::Bytes(key));
11120 }
11121 _ => {}
11122 }
11123 // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
11124 // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
11125 // own OrderKey so integer arrays sort numerically; a NULL element rides to
11126 // the end via the +INF sentinel.
11127 let inf = || OrderKey::NullBig;
11128 let arr = match v {
11129 Value::IntArray(a) => Some(
11130 a.iter()
11131 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11132 .collect(),
11133 ),
11134 Value::SmallIntArray(a) => Some(
11135 a.iter()
11136 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11137 .collect(),
11138 ),
11139 Value::BigIntArray(a) => Some(
11140 a.iter()
11141 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11142 .collect(),
11143 ),
11144 Value::BoolArray(a) => Some(
11145 a.iter()
11146 .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
11147 .collect(),
11148 ),
11149 Value::TextArray(a) => Some(
11150 a.iter()
11151 .map(|o| {
11152 o.as_ref()
11153 .map_or_else(inf, |s| OrderKey::Text(crate::orderby::CompactText::new(s)))
11154 })
11155 .collect(),
11156 ),
11157 #[allow(clippy::cast_precision_loss)]
11158 Value::FloatArray(a) => Some(
11159 a.iter()
11160 .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
11161 .collect(),
11162 ),
11163 // r1040 — array elements take the same exact key their scalar
11164 // form does; an f64 projection here would order `{0.1}` against
11165 // `{0.1000000000000000001}` by luck.
11166 Value::NumericArray(a) => Some(
11167 a.iter()
11168 .map(|o| {
11169 o.map_or_else(inf, |(m, s)| {
11170 OrderKey::Numeric(alloc::boxed::Box::new(
11171 spg_storage::NumericKey::from_numeric(
11172 m,
11173 s,
11174 spg_storage::NumericKind::Finite,
11175 ),
11176 ))
11177 })
11178 })
11179 .collect(),
11180 ),
11181 Value::DateArray(a) => Some(
11182 a.iter()
11183 .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11184 .collect(),
11185 ),
11186 _ => None,
11187 };
11188 if let Some(elements) = arr {
11189 return Ok(OrderKey::Array(elements));
11190 }
11191 // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
11192 // right, which is exactly the lexicographic element order an Array key
11193 // already gives: `(2,'b') < (9,'a')` because the leading field decides.
11194 if let Value::Composite(fields) = v {
11195 let elements = fields
11196 .iter()
11197 .map(|(_, fv)| value_to_order_key(fv))
11198 .collect::<Result<alloc::vec::Vec<_>, _>>()?;
11199 return Ok(OrderKey::Array(elements));
11200 }
11201 // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
11202 // Projecting these to f64 (the historic path) silently collapses BigInt /
11203 // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
11204 // the wrong order for large ids and microsecond timestamps.
11205 match v {
11206 Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11207 Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
11208 Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11209 // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
11210 // integer (days / micros / cents / calendar year); TIMETZ by the
11211 // UTC-equivalent micros (local wall - offset) so the same physical
11212 // instant in different zones sorts equal.
11213 Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
11214 Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
11215 Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
11216 Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
11217 Value::TimeTz { us, offset_secs } => {
11218 return Ok(OrderKey::Int(
11219 i128::from(*us) - i128::from(*offset_secs) * 1_000_000,
11220 ));
11221 }
11222 Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
11223 _ => {}
11224 }
11225 let num = match v {
11226 // Callers without NULLS FIRST/LAST context (array elements,
11227 // histogram sampling) put NULL last, as before.
11228 Value::Null => return Ok(OrderKey::NullBig),
11229 // v7.17.0 Phase 3.P0-38 — range ordering is not supported
11230 // in v7.17.0 (needs lex-then-inclusivity tiebreak).
11231 Value::Range { .. } => {
11232 return Err(EngineError::Unsupported(
11233 "ORDER BY of a range value is not supported in v7.17.0".into(),
11234 ));
11235 }
11236 // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
11237 Value::Hstore(_) => {
11238 return Err(EngineError::Unsupported(
11239 "ORDER BY of a hstore value is not supported".into(),
11240 ));
11241 }
11242 // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
11243 Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
11244 return Err(EngineError::Unsupported(
11245 "ORDER BY of a 2D array is not supported in v7.17.0".into(),
11246 ));
11247 }
11248 // r1039/r1040 — the exact canonical key, not an f64 projection.
11249 //
11250 // r1039 fixed the three specials, which carry a canonical zero in
11251 // `scaled` and so all sorted as the number 0. The projection
11252 // itself was the rest of the defect: "precision losses here only
11253 // matter for tie-breaks well past 15 significant digits" was the
11254 // comment, and the measurement disagreed — f64 called
11255 // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
11256 // returned them in insertion order. Three of ten values came back
11257 // in the wrong place against PG18.4.
11258 Value::Numeric {
11259 scaled,
11260 scale,
11261 kind,
11262 } => {
11263 return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11264 spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
11265 )));
11266 }
11267 Value::Float(x) => *x,
11268 // v7.37.16 — REAL sorts by its exact f64 widening (it had no
11269 // arm and fell through to the unsupported error).
11270 Value::Real(x) => f64::from(*x),
11271 Value::Bool(b) => {
11272 if *b {
11273 1.0
11274 } else {
11275 0.0
11276 }
11277 }
11278 Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
11279 return Err(EngineError::Unsupported(
11280 "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
11281 ));
11282 }
11283 // v7.37 — PG orders INTERVAL by its total time, treating a month as
11284 // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
11285 // f64 is exact for any interval under ~285 years, and only ORDER BY
11286 // tie-breaks past that magnitude lose precision. Matches the
11287 // min/max(interval) comparator in aggregate.rs.
11288 #[allow(clippy::cast_precision_loss)]
11289 Value::Interval {
11290 months,
11291 days,
11292 micros,
11293 kind,
11294 } => {
11295 let total = i128::from(*months) * 30 * 86_400_000_000
11296 + i128::from(*days) * 86_400_000_000
11297 + i128::from(*micros);
11298 total as f64
11299 }
11300 Value::Json(_) => {
11301 return Err(EngineError::Unsupported(
11302 "ORDER BY of a JSON value is not supported — cast the document to text first"
11303 .into(),
11304 ));
11305 }
11306 // v7.5.0 — Value is #[non_exhaustive]; future variants need
11307 // an explicit ORDER BY mapping. Surface as Unsupported until
11308 // engine support is added.
11309 _ => {
11310 return Err(EngineError::Unsupported(
11311 "ORDER BY of this value type is not supported".into(),
11312 ));
11313 }
11314 };
11315 Ok(OrderKey::Num(num))
11316}
11317
11318/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
11319/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
11320/// `EngineError` so the projection-build path keeps `UnknownQualifier`
11321/// vs `ColumnNotFound` distinct.
11322/// PG's name for the physical row identity. It is reserved there — no table
11323/// can have a column called this — which is what lets `*` skip it by name.
11324pub(crate) const CTID_COLUMN: &str = "ctid";
11325
11326/// v7.39 (round 512) — PG's system columns, in the order they are appended.
11327/// All six are reserved names there, which is what lets `*` skip them and
11328/// lets a scan tell them from a user column without a flag.
11329pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
11330
11331/// Is this name one of them?
11332pub(crate) fn is_system_column(name: &str) -> bool {
11333 SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
11334}
11335
11336/// Where the scan's appended system columns begin, if this schema carries
11337/// them: the trailing six, named in order. A catalog view with a column of
11338/// its own called `xmin` does not match, which is the point.
11339fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
11340 let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
11341 cols[start..]
11342 .iter()
11343 .zip(SYSTEM_COLUMNS)
11344 .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
11345 .then_some(start)
11346}
11347
11348/// v7.39 (round 540) — which positions `*` must skip.
11349///
11350/// The rule stays round 512's — the synthetic columns are the trailing
11351/// six of a relation's block, matched by POSITION so a genuine `xmin`
11352/// column is not lost — but a JOINED schema names its columns
11353/// `alias.column` and lays the peers out end to end, so a peer's six sit
11354/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
11355/// "trailing six" test back on the block it was written for.
11356fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11357 let mut skip = alloc::vec![false; cols.len()];
11358 fn qualifier(n: &str) -> Option<&str> {
11359 n.rsplit_once('.').map(|(q, _)| q)
11360 }
11361 fn bare(n: &str) -> &str {
11362 n.rsplit('.').next().unwrap_or(n)
11363 }
11364 let mut i = 0;
11365 while i < cols.len() {
11366 let q = qualifier(&cols[i].name);
11367 let mut end = i;
11368 while end < cols.len() && qualifier(&cols[end].name) == q {
11369 end += 1;
11370 }
11371 if let Some(start) = (end - i)
11372 .checked_sub(SYSTEM_COLUMNS.len())
11373 .map(|off| i + off)
11374 && cols[start..end]
11375 .iter()
11376 .zip(SYSTEM_COLUMNS)
11377 .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
11378 {
11379 for s in skip.iter_mut().take(end).skip(start) {
11380 *s = true;
11381 }
11382 }
11383 i = end;
11384 }
11385 skip
11386}
11387
11388/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
11389/// read? Only then is the column materialised.
11390pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
11391 let mut found = false;
11392 crate::expr_analysis::visit_expr_columns_and_subqueries(
11393 e,
11394 &mut |c| {
11395 if is_system_column(&c.name) {
11396 found = true;
11397 }
11398 },
11399 &mut |_| {},
11400 );
11401 found
11402}
11403
11404fn references_ctid(stmt: &SelectStatement) -> bool {
11405 let in_expr = expr_references_ctid;
11406 stmt.items.iter().any(|i| match i {
11407 SelectItem::Expr { expr, .. } => in_expr(expr),
11408 _ => false,
11409 }) || stmt.where_.as_ref().is_some_and(in_expr)
11410 || stmt.order_by.iter().any(|o| in_expr(&o.expr))
11411 || stmt
11412 .group_by
11413 .as_ref()
11414 .is_some_and(|g| g.iter().any(in_expr))
11415 || stmt.having.as_ref().is_some_and(in_expr)
11416}
11417
11418/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
11419/// is a name the projection has to TYPE before any row exists.
11420///
11421/// Evaluation has answered this since round T9 (`resolve_column` builds a
11422/// `Value::Composite` of every column), but the typing side below had no
11423/// such branch and raised `column "t" does not exist` first — so the
11424/// feature was unreachable through a projection. Measured against PG18.4:
11425/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
11426///
11427/// The type is `Jsonb` + a composite marker, which is exactly how a
11428/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
11429/// the value travels as a `Value::Composite` and renders in the canonical
11430/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
11431/// so the marker names the alias and no rehydration keys off it — the
11432/// value arrives already built.
11433fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
11434 let mut s = ColumnSchema::new(
11435 alloc::string::String::from(alias),
11436 spg_storage::DataType::Jsonb,
11437 true,
11438 );
11439 s.user_composite_type = Some(alloc::string::String::from(alias));
11440 s
11441}
11442
11443/// v7.39.3 — `mysql` makes the name comparison case-INSENSITIVE, which
11444/// is MySQL's rule for column names (measured on 9.7.2: `mycol`,
11445/// `MYCOL` and a backquoted `MyCol` all resolve the same column).
11446///
11447/// SPG compared byte for byte and its lexer folds an UNQUOTED
11448/// identifier, so a table restored from a `mysqldump` — where every
11449/// identifier is backquoted and keeps its case — had every mixed-case
11450/// column unreachable from ordinary unquoted SQL. Same "two spellings,
11451/// two things" defect v7.39.1 closed for relation names.
11452pub(crate) fn resolve_projection_column<'a>(
11453 c: &ColumnName,
11454 schema_cols: &'a [ColumnSchema],
11455 table_alias: &str,
11456 mysql: bool,
11457) -> Result<Cow<'a, ColumnSchema>, EngineError> {
11458 let same = |a: &str, b: &str| {
11459 if mysql {
11460 a.eq_ignore_ascii_case(b)
11461 } else {
11462 a == b
11463 }
11464 };
11465 if let Some(q) = &c.qualifier {
11466 let composite = alloc::format!("{q}.{name}", name = c.name);
11467 if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &composite)) {
11468 return Ok(Cow::Borrowed(s));
11469 }
11470 // Single-table case: the qualifier may equal the active alias —
11471 // then look for the bare column name.
11472 if same(q, table_alias)
11473 && let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name))
11474 {
11475 return Ok(Cow::Borrowed(s));
11476 }
11477 // For multi-table schemas the qualifier is unknown only if no
11478 // column bears the "<q>." prefix. For single-table, the alias
11479 // mismatch alone is enough.
11480 let prefix = alloc::format!("{q}.");
11481 let qualifier_known =
11482 same(q, table_alias) || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
11483 if !qualifier_known {
11484 return Err(EngineError::Eval(EvalError::UnknownQualifier {
11485 qualifier: q.clone(),
11486 column: c.name.clone(),
11487 }));
11488 }
11489 return Err(EngineError::Eval(EvalError::ColumnNotFound {
11490 name: c.name.clone(),
11491 }));
11492 }
11493 if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name)) {
11494 return Ok(Cow::Borrowed(s));
11495 }
11496 let suffix = alloc::format!(".{name}", name = c.name);
11497 let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
11498 let first = matches.next();
11499 let extra = matches.next();
11500 match (first, extra) {
11501 (Some(s), None) => Ok(Cow::Borrowed(s)),
11502 (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
11503 detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
11504 })),
11505 // The whole-row reference, checked LAST so a real column carrying
11506 // the alias's name still wins — the same precedence
11507 // `resolve_column` applies on the evaluation side.
11508 //
11509 // Two schema shapes reach here. A single-table (or subquery, or
11510 // CTE) scan carries its alias and bare column names, so the name
11511 // has to equal the alias. A JOIN's combined schema carries no
11512 // alias at all and qualifies every column `alias.col`, so the
11513 // alias is identified by the prefix instead — which is exactly
11514 // how `whole_row_composite` picks the fields out on the
11515 // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
11516 // answers `(7,z)` on PG18.4 and errored here until this arm
11517 // covered the joined shape too.
11518 _ if !table_alias.is_empty() && c.name == table_alias => {
11519 Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
11520 }
11521 _ if table_alias.is_empty() && {
11522 let prefix = alloc::format!("{name}.", name = c.name);
11523 schema_cols.iter().any(|s| s.name.starts_with(&prefix))
11524 } =>
11525 {
11526 Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
11527 }
11528 _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
11529 name: c.name.clone(),
11530 })),
11531 }
11532}
11533
11534/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
11535/// parser to carry per-branch GROUPING() masks into a grouping-set query's
11536/// ORDER BY. They must never reach the output. No-op unless such a column is
11537/// present, so the common path is untouched.
11538/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
11539///
11540/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
11541/// a `LIMIT 2` that should have answered two groups answered one.
11542fn apply_deferred_limit(
11543 rows: alloc::vec::Vec<Row<'static>>,
11544 deferred: &(
11545 Option<spg_sql::ast::LimitExpr>,
11546 Option<spg_sql::ast::LimitExpr>,
11547 ),
11548) -> alloc::vec::Vec<Row<'static>> {
11549 let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
11550 Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
11551 _ => None,
11552 };
11553 let mut rows = rows;
11554 if let Some(off) = count(&deferred.1) {
11555 rows = rows.split_off(off.min(rows.len()));
11556 }
11557 if let Some(lim) = count(&deferred.0) {
11558 rows.truncate(lim);
11559 }
11560 rows
11561}
11562
11563fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
11564 let QueryResult::Rows { columns, rows } = result else {
11565 return result;
11566 };
11567 if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
11568 return QueryResult::Rows { columns, rows };
11569 }
11570 let keep: Vec<usize> = columns
11571 .iter()
11572 .enumerate()
11573 .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
11574 .map(|(i, _)| i)
11575 .collect();
11576 let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
11577 let new_rows: Vec<Row<'static>> = rows
11578 .into_iter()
11579 .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
11580 .collect();
11581 QueryResult::Rows {
11582 columns: new_cols,
11583 rows: new_rows,
11584 }
11585}
11586
11587/// v7.39 (round 487) — bind every projection item that is a bare column
11588/// reference to its position, once per query.
11589///
11590/// `#[inline(never)]` and out of line on purpose. Round 486 established
11591/// that adding code inside these scan bodies moves neighbouring hot
11592/// functions around under fat LTO: the first version of this had the loop
11593/// inline in `run_single_table_scan` and four aggregate shapes that never
11594/// touch that function — `full_agg`, `join_agg`, `group_500k`,
11595/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
11596/// the same machine. Keeping it out of line kept them still.
11597#[inline(never)]
11598fn bind_direct_columns(
11599 projection: &[ProjectedItem],
11600 ctx: &eval::EvalContext<'_>,
11601) -> Vec<Option<usize>> {
11602 projection
11603 .iter()
11604 .map(|p| match &p.expr {
11605 Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
11606 // Same exclusion `compile_into` makes: a composite column
11607 // has to be rehydrated from stored JSON, which is not a
11608 // cell read.
11609 ctx.columns
11610 .get(*pos)
11611 .is_none_or(|sc| sc.user_composite_type.is_none())
11612 }),
11613 _ => None,
11614 })
11615 .collect()
11616}
11617
11618/// v7.39 (round 505) — the name an un-aliased projected expression reports.
11619///
11620/// PG18 names a call for its function and everything else `?column?`;
11621/// measured with `\gdesc`. SPG used to print the parsed expression back
11622/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
11623/// name-keyed row access found nothing under `upper`.
11624///
11625/// The MySQL half is NOT this rule and is deliberately left alone here:
11626/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
11627/// which needs the parser to hand over spans the AST does not carry yet.
11628/// Until it does, a MySQL session keeps the printed form — closer to what
11629/// MariaDB answers than `?column?` would be.
11630pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
11631 if mysql {
11632 return expr.to_string();
11633 }
11634 spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
11635}
11636
11637pub(crate) fn build_projection(
11638 items: &[SelectItem],
11639 schema_cols: &[ColumnSchema],
11640 table_alias: &str,
11641 mysql: bool,
11642 cat: Option<&Catalog>,
11643) -> Result<Vec<ProjectedItem>, EngineError> {
11644 build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0, cat)
11645}
11646
11647/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
11648/// invisible to `*`.
11649///
11650/// The windowed-SELECT path appends a synthetic `__win_N` column per window
11651/// function so the rewritten projection can reference the computed values as
11652/// ordinary columns. `*` then expanded them too, and
11653/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
11654/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
11655/// silent one: the row simply had one more field than the client asked for.
11656///
11657/// Hidden by POSITION rather than by name, for the reason round 512 recorded
11658/// about the system columns: a name test looks safe until a real column
11659/// happens to carry the name. These are appended last, so the count is what
11660/// identifies them.
11661pub(crate) fn build_projection_hiding_tail(
11662 items: &[SelectItem],
11663 schema_cols: &[ColumnSchema],
11664 table_alias: &str,
11665 mysql: bool,
11666 hidden_tail: usize,
11667 // v7.38.19 — the catalog, so a user-defined function's DECLARED
11668 // return type reaches the projection. Without it `describe_expr`
11669 // cannot type `f_sql()` and the column falls back to text, which is
11670 // what psql reads to decide alignment: `SELECT 7::bigint, f_sql()`
11671 // right-aligned one cell and left-aligned the other while both held
11672 // a bigint. Reported by sentori against 7.38.18 (their §2.2), who
11673 // also established that the EXECUTOR was never confused -- CTAS off
11674 // the same expression gives a bigint column, and arithmetic on it
11675 // works. Only the type travelling in the RowDescription was wrong.
11676 cat: Option<&Catalog>,
11677) -> Result<Vec<ProjectedItem>, EngineError> {
11678 let visible = schema_cols.len().saturating_sub(hidden_tail);
11679 // v7.39 (round 462) — a join's combined schema qualifies every column
11680 // `alias.col` so the deferred-join cell lookups resolve by composite
11681 // name. That is an internal convention, and `*` was handing it to the
11682 // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
11683 // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
11684 // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
11685 // already learned this for `q.*`; plain `*` never got the same rule.
11686 //
11687 // The signal is the schema itself, not the call site: only a combined
11688 // join schema arrives with no table alias AND every column qualified.
11689 // A single-table schema carries its alias, an empty schema has nothing
11690 // to strip, and a synthetic schema's names carry no dot.
11691 let joined_schema = table_alias.is_empty()
11692 && !schema_cols.is_empty()
11693 && schema_cols.iter().all(|c| c.name.contains('.'));
11694 let bare_name = |name: &str| -> String {
11695 if !joined_schema {
11696 return name.to_string();
11697 }
11698 match name.split_once('.') {
11699 Some((_, rest)) if !rest.is_empty() => rest.to_string(),
11700 _ => name.to_string(),
11701 }
11702 };
11703 let mut out = Vec::new();
11704 for item in items {
11705 match item {
11706 SelectItem::Wildcard => {
11707 // v7.39 (round 511) — `*` never expands a system column, as
11708 // PG's does not. They join the schema only when the statement
11709 // asked for them, so this matters for the mixed shape
11710 // `SELECT *, ctid FROM t`.
11711 //
11712 // v7.39 (round 512) — by POSITION, not by name. Matching on
11713 // the name alone looked safe because PG reserves them, and it
11714 // is not: `pg_replication_slots` genuinely has a column called
11715 // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
11716 // Only the trailing six, in the order the scan appends them,
11717 // are the synthetic ones.
11718 let sys_skip = synthetic_system_positions(schema_cols);
11719 for (idx, col) in schema_cols.iter().enumerate() {
11720 if sys_skip[idx] || idx >= visible {
11721 continue;
11722 }
11723 out.push(ProjectedItem {
11724 expr: Expr::Column(ColumnName {
11725 qualifier: None,
11726 name: col.name.clone(),
11727 }),
11728 output_name: bare_name(&col.name),
11729 ty: col.ty,
11730 nullable: col.nullable,
11731 user_enum_type: col.user_enum_type.clone(),
11732 mysql_fsp: col.mysql_fsp,
11733 collation_name: col.collation_name.clone(),
11734 fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
11735 pads: crate::collate::pads_space(col.collation_name.as_deref()),
11736 });
11737 }
11738 }
11739 // v7.39 (round 128) — `q.*` expands to every column belonging to
11740 // the qualifier `q`. Single-table schemas carry bare column names
11741 // reachable via `table_alias`; a join's combined schema carries
11742 // `alias.col` names, so a column belongs to `q` when its name has
11743 // the `q.` prefix. PG labels the expanded columns by their bare
11744 // name, so the `alias.` prefix is stripped from the output name.
11745 SelectItem::QualifiedWildcard(q) => {
11746 let prefix = alloc::format!("{q}.");
11747 let single_table = !table_alias.is_empty() && q == table_alias;
11748 let mut matched = 0usize;
11749 for col in &schema_cols[..visible] {
11750 let belongs =
11751 col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
11752 if !belongs {
11753 continue;
11754 }
11755 matched += 1;
11756 let output_name = col
11757 .name
11758 .strip_prefix(&prefix)
11759 .unwrap_or(&col.name)
11760 .to_string();
11761 out.push(ProjectedItem {
11762 expr: Expr::Column(ColumnName {
11763 qualifier: None,
11764 name: col.name.clone(),
11765 }),
11766 output_name,
11767 ty: col.ty,
11768 nullable: col.nullable,
11769 user_enum_type: col.user_enum_type.clone(),
11770 mysql_fsp: col.mysql_fsp,
11771 collation_name: col.collation_name.clone(),
11772 fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
11773 pads: crate::collate::pads_space(col.collation_name.as_deref()),
11774 });
11775 }
11776 if matched == 0 {
11777 // `q.*` names no column, so the reference IS the star.
11778 return Err(EngineError::Eval(EvalError::UnknownQualifier {
11779 qualifier: q.clone(),
11780 column: alloc::string::String::from("*"),
11781 }));
11782 }
11783 }
11784 SelectItem::Expr { expr, alias } => {
11785 // Plain column ref keeps full schema info (real type +
11786 // nullability). For compound expressions try the
11787 // describe-side function-return-type table first
11788 // (e.g. `SELECT now()` → Timestamptz, `SELECT
11789 // concat(…)` → Text). Falls back to nullable Text
11790 // for shapes the describe path can't resolve.
11791 if let Expr::Column(c) = expr {
11792 let sch = resolve_projection_column(c, schema_cols, table_alias, mysql)?;
11793 let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
11794 out.push(ProjectedItem {
11795 expr: expr.clone(),
11796 output_name,
11797 ty: sch.ty,
11798 nullable: sch.nullable,
11799 // v7.39 (read01 round 54) — a bare enum column keeps
11800 // its enum identity through the projection.
11801 user_enum_type: sch.user_enum_type.clone(),
11802 mysql_fsp: sch.mysql_fsp,
11803 collation_name: sch.collation_name.clone(),
11804 // v7.38.13 — and its byte-wise-ness. This is the
11805 // site `SELECT DISTINCT t FROM t` arrives at.
11806 fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
11807 pads: crate::collate::pads_space(sch.collation_name.as_deref()),
11808 });
11809 } else if let Some(shape) = describe::describe_expr_in(expr, schema_cols, cat) {
11810 let output_name = alias
11811 .clone()
11812 .unwrap_or_else(|| default_output_name(expr, mysql));
11813 out.push(ProjectedItem {
11814 expr: expr.clone(),
11815 // v7.38.18 — a projected EXPRESSION has no column collation
11816 // to read, so it takes the session default, which is MySQL
11817 // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
11818 pads: false,
11819 output_name,
11820 ty: shape.ty,
11821 // v7.39 (round 258) — a projected EXPRESSION keeps its
11822 // enum identity too, not just a bare column. `FROM
11823 // (VALUES ('happy'::mood), …) t(m)` lowers to constant
11824 // SELECTs, so the derived column arrived here as a cast
11825 // and lost the enum — making the outer ORDER BY / min /
11826 // max / array_agg sort by the label's TEXT.
11827 nullable: shape.nullable,
11828 user_enum_type: None,
11829 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11830 // A bare column reference keeps its collation; any
11831 // other expression produces a new value and has none.
11832 collation_name: match expr {
11833 Expr::Column(c) => schema_cols
11834 .iter()
11835 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11836 .and_then(|sc| sc.collation_name.clone()),
11837 _ => None,
11838 },
11839 fold_exempt: match expr {
11840 Expr::Column(c) => schema_cols
11841 .iter()
11842 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11843 .is_some_and(|sc| {
11844 matches!(sc.collation, spg_storage::Collation::Binary)
11845 }),
11846 // Not a column: no declared collation to honour,
11847 // so the session default applies and it folds.
11848 _ => false,
11849 },
11850 });
11851 } else {
11852 let output_name = alias
11853 .clone()
11854 .unwrap_or_else(|| default_output_name(expr, mysql));
11855 out.push(ProjectedItem {
11856 expr: expr.clone(),
11857 // v7.38.18 — a projected EXPRESSION has no column collation
11858 // to read, so it takes the session default, which is MySQL
11859 // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
11860 pads: false,
11861 output_name,
11862 // A user ENUM has no DataType of its own, so
11863 // `describe_expr` cannot type `'ok'::mood` and the
11864 // item lands HERE, defaulting to text — which is why
11865 // pg_typeof answered `text` and a derived table sorted
11866 // enum values by their label.
11867 ty: DataType::Text,
11868 nullable: true,
11869 user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
11870 .map(alloc::string::String::from),
11871 mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11872 collation_name: match expr {
11873 Expr::Column(c) => schema_cols
11874 .iter()
11875 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11876 .and_then(|sc| sc.collation_name.clone()),
11877 _ => None,
11878 },
11879 fold_exempt: match expr {
11880 Expr::Column(c) => schema_cols
11881 .iter()
11882 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11883 .is_some_and(|sc| {
11884 matches!(sc.collation, spg_storage::Collation::Binary)
11885 }),
11886 // Not a column: no declared collation to honour,
11887 // so the session default applies and it folds.
11888 _ => false,
11889 },
11890 });
11891 }
11892 }
11893 }
11894 }
11895 Ok(out)
11896}
11897
11898// ---- v4.12 window-function helpers ----
11899// The (partition-key, order-key, original-index) tuple shape used
11900// across these helpers is intrinsic to the planner. Factoring it
11901// into a typedef adds indirection without making the code clearer,
11902// so several lints are allowed inline on the affected functions
11903// rather than module-wide.
11904
11905/// v4.22: pick more specific column types from observed rows when
11906/// the projection builder defaulted to Text (the v1.x behavior for
11907/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
11908/// land an Int column in the CTE storage table rather than failing
11909/// the insert with "expected TEXT, got INT".
11910pub(crate) fn infer_column_types(
11911 columns: &[ColumnSchema],
11912 rows: &[Row<'static>],
11913) -> Vec<ColumnSchema> {
11914 let mut out = columns.to_vec();
11915 for (col_idx, col) in out.iter_mut().enumerate() {
11916 if col.ty != DataType::Text {
11917 continue;
11918 }
11919 let mut inferred: Option<DataType> = None;
11920 let mut all_null = true;
11921 for row in rows {
11922 let Some(v) = row.values.get(col_idx) else {
11923 continue;
11924 };
11925 let ty = match v {
11926 Value::Null => continue,
11927 Value::SmallInt(_) => DataType::SmallInt,
11928 Value::Int(_) => DataType::Int,
11929 Value::BigInt(_) => DataType::BigInt,
11930 Value::Float(_) => DataType::Float,
11931 Value::Bool(_) => DataType::Bool,
11932 Value::Vector(_) => DataType::Vector {
11933 dim: 0,
11934 encoding: VecEncoding::F32,
11935 },
11936 // v7.38 (read01 U16) — carry array values through with an
11937 // array type so a recursive CTE that projects an array
11938 // (e.g. a SEARCH/CYCLE ord / path column) types the working
11939 // column as an array, not Text.
11940 Value::TextArray(_) => DataType::TextArray,
11941 Value::IntArray(_) => DataType::IntArray,
11942 Value::BigIntArray(_) => DataType::BigIntArray,
11943 Value::SmallIntArray(_) => DataType::SmallIntArray,
11944 Value::FloatArray(_) => DataType::FloatArray,
11945 Value::BoolArray(_) => DataType::BoolArray,
11946 // v7.39 (GUC knife 2) — an interval projection describes
11947 // as INTERVAL (typed drivers read the RowDescription OID).
11948 Value::Interval { .. } => DataType::Interval,
11949 _ => DataType::Text,
11950 };
11951 all_null = false;
11952 inferred = Some(match inferred {
11953 None => ty,
11954 Some(prev) if prev == ty => prev,
11955 Some(_) => DataType::Text,
11956 });
11957 }
11958 if let Some(t) = inferred {
11959 col.ty = t;
11960 col.nullable = true;
11961 } else if all_null {
11962 col.nullable = true;
11963 }
11964 }
11965 out
11966}
11967
11968/// Numeric widening rank for UNION type resolution (higher = wider).
11969fn numeric_rank(t: DataType) -> Option<u8> {
11970 match t {
11971 DataType::SmallInt => Some(1),
11972 DataType::Int => Some(2),
11973 DataType::BigInt => Some(3),
11974 DataType::Numeric { .. } => Some(4),
11975 DataType::Float => Some(5),
11976 _ => None,
11977 }
11978}
11979
11980/// Resolve the common result type for a UNION / VALUES column from the
11981/// set of concrete (non-NULL) branch types, following the safe subset
11982/// of PG's type resolution:
11983/// * all-numeric → the widest numeric (int ∪ bigint → bigint, … ∪
11984/// numeric → numeric, … ∪ float → float);
11985/// * DATE ∪ TIMESTAMP → TIMESTAMP;
11986/// * exactly one concrete non-TEXT type mixed with TEXT literals →
11987/// that concrete type (the TEXT cells get parsed into it).
11988/// Returns `None` for anything ambiguous, so the caller leaves the
11989/// column untouched rather than risk a wrong or failing coercion.
11990fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
11991 // NB: types are collected from RUNTIME values, which are coarser
11992 // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
11993 // a single-concrete-type fast path must NOT overwrite the column
11994 // type — it would downgrade tstz to ts. NULL-only unification (PG:
11995 // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
11996 // row's pg_typeof) needs schema-level resolution — recorded, not
11997 // attempted here.
11998 if types.len() < 2 {
11999 return None;
12000 }
12001 if types.iter().all(|t| numeric_rank(*t).is_some()) {
12002 return types
12003 .iter()
12004 .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
12005 .copied();
12006 }
12007 let non_text: Vec<&DataType> = types
12008 .iter()
12009 .filter(|t| !matches!(t, DataType::Text))
12010 .collect();
12011 // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
12012 // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
12013 // if any is timestamp the result is timestamp (ts ∪ date). All values are
12014 // the same UTC-micros instant, so widening date/ts to tstz is lossless.
12015 if non_text.iter().all(|t| {
12016 matches!(
12017 t,
12018 DataType::Date | DataType::Timestamp | DataType::Timestamptz
12019 )
12020 }) && non_text
12021 .iter()
12022 .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
12023 {
12024 if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
12025 return Some(DataType::Timestamptz);
12026 }
12027 return Some(DataType::Timestamp);
12028 }
12029 // A single concrete non-TEXT type mixed with TEXT literals.
12030 if non_text.len() == 1 {
12031 return Some(*non_text[0]);
12032 }
12033 // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
12034 // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
12035 // text): resolve the concrete set first (PG treats the unknown-
12036 // typed string literals as castable to whatever the knowns
12037 // resolve to), then the TEXT cells parse into that target — the
12038 // caller's coercion dry-run still abandons the column if any
12039 // literal doesn't parse.
12040 if !non_text.is_empty() && non_text.len() < types.len() {
12041 let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
12042 return resolve_union_common_type(&concrete);
12043 }
12044 None
12045}
12046
12047/// Coerce every cell of a UNION / VALUES result column to one common
12048/// type (see [`resolve_union_common_type`]). Conservative: a column
12049/// whose branches already agree, or whose types don't resolve, or where
12050/// any cell fails to coerce, is left exactly as it was — this never
12051/// turns a previously-working query into an error.
12052fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
12053 for col_idx in 0..columns.len() {
12054 let mut seen: Vec<DataType> = Vec::new();
12055 for row in rows.iter() {
12056 if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
12057 if !seen.contains(&dt) {
12058 seen.push(dt);
12059 }
12060 }
12061 }
12062 // v7.37.16 — a single concrete runtime type under a TEXT-typed
12063 // column means the column type came off a NULL (or unknown-text)
12064 // branch: NULL literals describe as TEXT (`L::Null → Text`), so
12065 // `VALUES (NULL),(1.5)` left the column "text" while every
12066 // non-NULL cell is numeric. Adopt the concrete type — schema
12067 // only, no cell changes. tstz-safe by construction: a real
12068 // timestamptz column's schema type is Timestamptz, not Text, so
12069 // the coarser runtime type (Value::Timestamp) can't downgrade it
12070 // through this arm; and a real text column's non-NULL cells are
12071 // Text, which keeps seen == [Text] and skips it.
12072 if seen.len() == 1
12073 && matches!(columns[col_idx].ty, DataType::Text)
12074 && !matches!(seen[0], DataType::Text)
12075 {
12076 columns[col_idx].ty = seen[0];
12077 continue;
12078 }
12079 let Some(target) = resolve_union_common_type(&seen) else {
12080 continue;
12081 };
12082 // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
12083 // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
12084 // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
12085 // existing numeric cell untouched and only promote integers (to scale 0)
12086 // rather than rescaling everything to the widest scale.
12087 let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
12088 // Dry-run the coercion; abandon the whole column if any fails.
12089 let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
12090 let mut ok = true;
12091 for row in rows.iter() {
12092 match row.values.get(col_idx) {
12093 Some(Value::Numeric { .. }) if scale_preserving_numeric => {
12094 coerced.push(Some(row.values[col_idx].clone()));
12095 }
12096 Some(v) => {
12097 let cell_target = if scale_preserving_numeric {
12098 DataType::Numeric {
12099 precision: 0,
12100 scale: 0,
12101 }
12102 } else {
12103 target
12104 };
12105 match crate::conversions::coerce_value(
12106 v.clone(),
12107 cell_target,
12108 &columns[col_idx].name,
12109 col_idx,
12110 ) {
12111 Ok(cv) => coerced.push(Some(cv)),
12112 Err(_) => {
12113 ok = false;
12114 break;
12115 }
12116 }
12117 }
12118 None => coerced.push(None),
12119 }
12120 }
12121 if !ok {
12122 continue;
12123 }
12124 for (row, cv) in rows.iter_mut().zip(coerced) {
12125 if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
12126 *slot = nv;
12127 }
12128 }
12129 columns[col_idx].ty = target;
12130 }
12131}
12132
12133/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
12134/// dedup inside the recursive iteration. Crude but deterministic
12135/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
12136fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
12137 let mut out = Vec::new();
12138 for v in &row.values {
12139 // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
12140 // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
12141 // like PG (and like GROUP BY, which already normalizes). The old
12142 // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
12143 // the exact-decimal family through one scale-stripped canonical form.
12144 match v {
12145 Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12146 Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12147 Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12148 Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
12149 other => {
12150 let s = alloc::format!("{other:?}|");
12151 out.extend_from_slice(s.as_bytes());
12152 }
12153 }
12154 }
12155 out
12156}
12157
12158/// Append a scale-independent canonical key for an exact-decimal value: strip
12159/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
12160/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
12161fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
12162 while scale > 0 && scaled % 10 == 0 {
12163 scaled /= 10;
12164 scale -= 1;
12165 }
12166 let s = alloc::format!("\u{1}{scaled}e-{scale}|");
12167 out.extend_from_slice(s.as_bytes());
12168}
12169
12170/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
12171/// (uncorrelated; outer refs were substituted upstream), then zip
12172/// them in parallel, NULL-padding shorter arrays to the longest
12173/// (PG's ROWS FROM shorthand). Shared by the primary-position
12174/// executor and the join-position materialiser, which both detect
12175/// the parser's `__unnest_zip` marker call.
12176pub(crate) fn unnest_zip_rows(
12177 args: &[Expr],
12178) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
12179 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12180 let ctx = EvalContext::new(&empty_schema, None);
12181 let dummy_row = Row::new(alloc::vec::Vec::new());
12182 let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
12183 let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
12184 alloc::vec::Vec::with_capacity(args.len());
12185 for a in args {
12186 let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
12187 let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = match v {
12188 Value::Null => (DataType::Text, alloc::vec::Vec::new()),
12189 Value::TextArray(xs) => (
12190 DataType::Text,
12191 xs.into_iter()
12192 .map(|x| x.map(Value::text).unwrap_or(Value::Null))
12193 .collect(),
12194 ),
12195 Value::IntArray(xs) => (
12196 DataType::Int,
12197 xs.into_iter()
12198 .map(|x| x.map(Value::Int).unwrap_or(Value::Null))
12199 .collect(),
12200 ),
12201 Value::BigIntArray(xs) => (
12202 DataType::BigInt,
12203 xs.into_iter()
12204 .map(|x| x.map(Value::BigInt).unwrap_or(Value::Null))
12205 .collect(),
12206 ),
12207 other => {
12208 return Err(EngineError::Unsupported(alloc::format!(
12209 "unnest() expects array arguments, got {}",
12210 crate::conversions::pg_type_name_for_error_opt(other.data_type())
12211 )));
12212 }
12213 };
12214 dtypes.push(dt);
12215 columns.push(items);
12216 }
12217 let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
12218 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
12219 for i in 0..max_len {
12220 let vals: alloc::vec::Vec<Value<'static>> = columns
12221 .iter()
12222 .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
12223 .collect();
12224 rows.push(Row::new(vals));
12225 }
12226 Ok((dtypes, rows))
12227}
12228
12229/// Detect the parser's multi-arg unnest marker on an unnest_expr.
12230pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
12231 match expr {
12232 Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
12233 _ => None,
12234 }
12235}
12236
12237/// Evaluate generate_series arguments (uncorrelated — outer refs
12238/// were substituted upstream where applicable) and build the row
12239/// stream. Dispatches on the start value's shape and rejects
12240/// mixed-shape calls early (e.g. start = timestamp, stop =
12241/// integer) so the caller gets a clean error rather than a panic.
12242/// Shared by the primary-position executor and the join-position
12243/// materialiser.
12244pub(crate) fn generate_series_rows(
12245 args: &[Expr],
12246 cancel: &CancelToken<'_>,
12247) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12248 let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12249 let ctx = EvalContext::new(&empty_schema, None);
12250 let dummy_row = Row::new(alloc::vec::Vec::new());
12251 let mut arg_values: alloc::vec::Vec<Value<'static>> =
12252 alloc::vec::Vec::with_capacity(args.len());
12253 for a in args {
12254 arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
12255 }
12256 generate_series_from_values(arg_values, args, cancel)
12257}
12258
12259/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
12260/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
12261/// full integer / numeric / timestamp overload set with the FROM-clause path.
12262/// Before this split the target-list arm reimplemented only the integer case,
12263/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
12264/// NULL for the timestamp column instead of the series. `arg_values` are the
12265/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
12266/// timestamp type resolution (it inspects the argument expressions' types).
12267pub(crate) fn generate_series_from_values(
12268 mut arg_values: alloc::vec::Vec<Value<'static>>,
12269 args: &[Expr],
12270 cancel: &CancelToken<'_>,
12271) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12272 // PG: a NULL bound or step yields zero rows (also keeps the
12273 // NULL-padded lateral probe alive — schema without data).
12274 if arg_values.iter().any(|v| matches!(v, Value::Null)) {
12275 return Ok((DataType::BigInt, alloc::vec::Vec::new()));
12276 }
12277 // PG resolves `generate_series(date, date, interval)` to the
12278 // timestamp/timestamptz overload by implicitly casting each date
12279 // bound up to a timestamp at midnight (verified vs live PG18.4:
12280 // date args yield rows anchored at 00:00:00). SPG's TZ-naive
12281 // timestamp model renders the same instants, so fold any Date
12282 // bound to its midnight Timestamp (canonical `days *
12283 // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
12284 // the shape match so the existing timestamp arm drives the walk.
12285 // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
12286 // `generate_series(date, date, interval)` has no date overload, and among
12287 // the two candidates PG prefers the timestamptz one (timestamptz is the
12288 // preferred type of the datetime category), so the column comes back
12289 // `timestamp with time zone` — the rows render with a `+00` offset. A
12290 // timestamptz bound obviously lands there too. Only genuinely
12291 // timestamp-typed bounds keep the TZ-naive result type.
12292 let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12293 let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
12294 || args.iter().any(|a| {
12295 crate::describe::describe_expr(a, &empty_cols)
12296 .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
12297 });
12298 for v in &mut arg_values {
12299 if let Value::Date(d) = *v {
12300 *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
12301 }
12302 }
12303 match arg_values.as_slice() {
12304 [Value::Timestamp(start), Value::Timestamp(stop), step] => {
12305 let interval_step = match step {
12306 Value::Interval { .. } => step.clone(),
12307 // v7.38 (read01) — PG resolves an unknown-type string step
12308 // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
12309 // a bare text step by parsing it the same way `::interval` does.
12310 Value::Text(s) => crate::conversions::coerce_value(
12311 Value::text(s.as_ref()),
12312 DataType::Interval,
12313 "",
12314 0,
12315 )
12316 .map_err(|_| {
12317 EngineError::Unsupported(alloc::format!(
12318 "generate_series(timestamp, timestamp, …): \
12319 could not parse step {s:?} as INTERVAL"
12320 ))
12321 })?,
12322 other => {
12323 return Err(EngineError::Unsupported(alloc::format!(
12324 "generate_series(timestamp, timestamp, …): \
12325 step must be INTERVAL, got {}",
12326 crate::conversions::pg_type_name_for_error_opt(other.data_type())
12327 )));
12328 }
12329 };
12330 let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
12331 Ok((
12332 if tz {
12333 DataType::Timestamptz
12334 } else {
12335 DataType::Timestamp
12336 },
12337 rows,
12338 ))
12339 }
12340 [start, stop, step]
12341 if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
12342 {
12343 let s = value_to_i64(start);
12344 let e = value_to_i64(stop);
12345 let st = value_to_i64(step);
12346 // PG types the series by the argument type: int4 args → int4
12347 // elements, int8 (bigint) args → int8. Any BigInt operand widens.
12348 let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
12349 let rows = generate_series_integers(s, e, st, wide, cancel)?;
12350 Ok((
12351 if wide {
12352 DataType::BigInt
12353 } else {
12354 DataType::Int
12355 },
12356 rows,
12357 ))
12358 }
12359 [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
12360 let s = value_to_i64(start);
12361 let e = value_to_i64(stop);
12362 let wide = value_is_bigint(start) || value_is_bigint(stop);
12363 let rows = generate_series_integers(s, e, 1, wide, cancel)?;
12364 Ok((
12365 if wide {
12366 DataType::BigInt
12367 } else {
12368 DataType::Int
12369 },
12370 rows,
12371 ))
12372 }
12373 // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
12374 // series in exact numeric arithmetic; NaN / infinity bounds and a
12375 // zero step get dedicated wordings, and a mixed int/numeric call
12376 // resolves here via the implicit int→numeric cast.
12377 [_, _] | [_, _, _]
12378 if arg_values
12379 .iter()
12380 .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
12381 && arg_values.iter().all(|v| {
12382 matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
12383 }) =>
12384 {
12385 use spg_storage::NumericKind as K;
12386 let words: [(&str, &str); 3] = [
12387 (
12388 "start value cannot be NaN",
12389 "start value cannot be infinity",
12390 ),
12391 ("stop value cannot be NaN", "stop value cannot be infinity"),
12392 ("step size cannot be NaN", "step size cannot be infinity"),
12393 ];
12394 for (i, v) in arg_values.iter().enumerate() {
12395 if let Value::Numeric { kind, .. } = v {
12396 if *kind != K::Finite {
12397 let (nan_w, inf_w) = words[i];
12398 return Err(EngineError::Unsupported(
12399 if *kind == K::NaN { nan_w } else { inf_w }.into(),
12400 ));
12401 }
12402 }
12403 }
12404 let big =
12405 |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
12406 let start = big(&arg_values[0]);
12407 let stop = big(&arg_values[1]);
12408 let step = if arg_values.len() == 3 {
12409 big(&arg_values[2])
12410 } else {
12411 spg_storage::bignum::BigNumeric::from_i128(1, 0)
12412 };
12413 if step.is_zero() {
12414 return Err(EngineError::Unsupported(
12415 "step size cannot equal zero".into(),
12416 ));
12417 }
12418 let descending = step.parts().0;
12419 let mut rows = alloc::vec::Vec::new();
12420 let mut cur = start;
12421 const MAX_ROWS: usize = 10_000_000;
12422 loop {
12423 cancel.check()?;
12424 let c = cur.cmp(&stop);
12425 if descending {
12426 if c == core::cmp::Ordering::Less {
12427 break;
12428 }
12429 } else if c == core::cmp::Ordering::Greater {
12430 break;
12431 }
12432 if rows.len() >= MAX_ROWS {
12433 return Err(EngineError::Unsupported(alloc::format!(
12434 "generate_series() result exceeds {MAX_ROWS} rows"
12435 )));
12436 }
12437 rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
12438 cur.clone()
12439 )]));
12440 cur = cur.add(&step);
12441 }
12442 Ok((
12443 DataType::Numeric {
12444 precision: 0,
12445 scale: 0,
12446 },
12447 rows,
12448 ))
12449 }
12450 _ => Err(EngineError::Unsupported(alloc::format!(
12451 "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
12452 argument shapes; got {}",
12453 arg_values
12454 .iter()
12455 .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
12456 .collect::<alloc::vec::Vec<_>>()
12457 .join(", ")
12458 ))),
12459 }
12460}
12461
12462/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
12463/// Step direction follows the sign: positive step iterates upward
12464/// (stops when current > stop); negative iterates downward; zero
12465/// errors. Caller-facing row stream is `BigInt`-typed so a single
12466/// projection schema covers SmallInt / Int / BigInt callers.
12467fn generate_series_integers(
12468 start: i64,
12469 stop: i64,
12470 step: i64,
12471 wide: bool,
12472 cancel: &CancelToken<'_>,
12473) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
12474 if step == 0 {
12475 return Err(EngineError::Unsupported(
12476 "step size cannot equal zero".into(),
12477 ));
12478 }
12479 let mut out = alloc::vec::Vec::new();
12480 let mut cur = start;
12481 // Hard cap to keep a runaway call from eating all memory. PG
12482 // has no such cap but does honour query timeout; SPG's cancel
12483 // token will fire too — this is a defense-in-depth backstop.
12484 const MAX_ROWS: usize = 10_000_000;
12485 loop {
12486 cancel.check()?;
12487 if step > 0 && cur > stop {
12488 break;
12489 }
12490 if step < 0 && cur < stop {
12491 break;
12492 }
12493 out.push(Row::new(alloc::vec![if wide {
12494 Value::BigInt(cur)
12495 } else {
12496 Value::Int(cur as i32)
12497 }]));
12498 if out.len() > MAX_ROWS {
12499 return Err(EngineError::Unsupported(alloc::format!(
12500 "generate_series(): exceeded {MAX_ROWS} rows; \
12501 narrow start/stop or use a larger step"
12502 )));
12503 }
12504 cur = match cur.checked_add(step) {
12505 Some(n) => n,
12506 None => break,
12507 };
12508 }
12509 Ok(out)
12510}
12511
12512/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
12513/// `Value::Interval { months, micros }` per the caller's guard;
12514/// each iteration adds the interval via `apply_binary_interval`
12515/// so month-shifting handles short-month rollover (PG semantics).
12516fn generate_series_timestamps(
12517 start: i64,
12518 stop: i64,
12519 step: Value,
12520 cancel: &CancelToken<'_>,
12521) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
12522 let (months, days, micros) = match &step {
12523 Value::Interval {
12524 months,
12525 days,
12526 micros,
12527 kind,
12528 } => (*months, *days, *micros),
12529 _ => unreachable!("caller guards step.is_interval"),
12530 };
12531 if months == 0 && days == 0 && micros == 0 {
12532 return Err(EngineError::Unsupported(
12533 "generate_series(): INTERVAL step cannot be zero".into(),
12534 ));
12535 }
12536 let ascending = months > 0 || days > 0 || micros > 0;
12537 let mut out = alloc::vec::Vec::new();
12538 let mut cur = Value::Timestamp(start);
12539 const MAX_ROWS: usize = 10_000_000;
12540 loop {
12541 cancel.check()?;
12542 let cur_t = match cur {
12543 Value::Timestamp(t) => t,
12544 _ => unreachable!("loop invariant: cur is Timestamp"),
12545 };
12546 if ascending && cur_t > stop {
12547 break;
12548 }
12549 if !ascending && cur_t < stop {
12550 break;
12551 }
12552 out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
12553 if out.len() > MAX_ROWS {
12554 return Err(EngineError::Unsupported(alloc::format!(
12555 "generate_series(): exceeded {MAX_ROWS} rows; \
12556 narrow start/stop or use a larger step"
12557 )));
12558 }
12559 let next = eval::apply_binary_interval(
12560 spg_sql::ast::BinOp::Add,
12561 &cur,
12562 &Value::Interval {
12563 months,
12564 days,
12565 micros,
12566 kind: spg_storage::IntervalKind::Finite,
12567 },
12568 )
12569 .map_err(EngineError::Eval)?;
12570 cur = match next {
12571 Some(v) => v,
12572 None => break,
12573 };
12574 }
12575 Ok(out)
12576}
12577
12578/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
12579/// WITH TIES` requires an `ORDER BY`. Without one, there's no
12580/// way to identify "ties" deterministically, so PG errors at
12581/// plan time. SPG mirrors that surface so the same DDL / app
12582/// behaviour holds on cutover.
12583fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
12584 if stmt.limit_with_ties && stmt.order_by.is_empty() {
12585 return Err(EngineError::Unsupported(alloc::string::String::from(
12586 "WITH TIES cannot be specified without ORDER BY clause",
12587 )));
12588 }
12589 Ok(())
12590}
12591
12592/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
12593/// (case-insensitive). Used by `exec_select_cancel`'s
12594/// projection loop to detect Set-Returning-Function rows that
12595/// need per-row expansion. Only the top-level call counts —
12596/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
12597/// projection's perspective; it would surface as an "unknown
12598/// function" mismatch downstream, which is what we want
12599/// (multi-SRF / nested SRF is documented carve-out for v7.19).
12600fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
12601 top_level_srf_kind(expr).is_some()
12602}
12603
12604/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
12605/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
12606/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
12607/// source row.
12608#[derive(Clone, Copy, PartialEq, Eq)]
12609pub(crate) enum SrfKind {
12610 Unnest,
12611 /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
12612 /// list. It used to be handled ONLY by the parser's lift into FROM, so a
12613 /// second one in the same list came back as "unknown function".
12614 GenerateSeries,
12615 GenerateSubscripts,
12616 /// `_text` variants unwrap scalars to their lexeme; the plain forms render
12617 /// every value as compact JSON text.
12618 ArrayElements {
12619 as_text: bool,
12620 },
12621 PathQuery,
12622 RegexpMatches,
12623 Each {
12624 as_text: bool,
12625 },
12626 ObjectKeys,
12627}
12628
12629/// Case-insensitive match against any of `names`.
12630fn name_is(name: &str, names: &[&str]) -> bool {
12631 names.iter().any(|n| name.eq_ignore_ascii_case(n))
12632}
12633
12634pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
12635 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
12636 return None;
12637 };
12638 let n = args.len();
12639 // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
12640 // SELECT list (it returned an array there before) and shares the unnest
12641 // expansion machinery.
12642 if n == 1 && name.eq_ignore_ascii_case("unnest") {
12643 return Some(SrfKind::Unnest);
12644 }
12645 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
12646 return Some(SrfKind::GenerateSeries);
12647 }
12648 if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
12649 return Some(SrfKind::GenerateSubscripts);
12650 }
12651 // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
12652 // per element / match in the SELECT list; they collapsed to a single row
12653 // (a TextArray, or an "unknown function" error for `each`) before.
12654 if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
12655 return Some(SrfKind::ArrayElements { as_text: false });
12656 }
12657 if n == 1
12658 && name_is(
12659 name,
12660 &["jsonb_array_elements_text", "json_array_elements_text"],
12661 )
12662 {
12663 return Some(SrfKind::ArrayElements { as_text: true });
12664 }
12665 // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
12666 if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
12667 return Some(SrfKind::PathQuery);
12668 }
12669 if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
12670 return Some(SrfKind::RegexpMatches);
12671 }
12672 if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
12673 return Some(SrfKind::Each { as_text: false });
12674 }
12675 if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
12676 return Some(SrfKind::Each { as_text: true });
12677 }
12678 if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
12679 return Some(SrfKind::ObjectKeys);
12680 }
12681 None
12682}
12683
12684/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
12685/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
12686/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
12687/// rows, as in PG).
12688pub(crate) fn top_level_srf_output(
12689 expr: &spg_sql::ast::Expr,
12690 row: &Row<'static>,
12691 ctx: &EvalContext<'_>,
12692) -> Result<Vec<Value<'static>>, EngineError> {
12693 let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
12694 (top_level_srf_kind(expr), expr)
12695 else {
12696 return Err(EngineError::Unsupported(
12697 "expected a SELECT-list SRF call".into(),
12698 ));
12699 };
12700 match kind {
12701 SrfKind::Unnest => {
12702 // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
12703 // the elements DIRECTLY: the old path built the whole
12704 // Value::Array (one eval + a clone per element) only for
12705 // array_value_to_elements to clone every element back out.
12706 // Any other argument shape (a column, a function result)
12707 // keeps the build-then-split path.
12708 if let spg_sql::ast::Expr::Array(items) = &args[0] {
12709 return items
12710 .iter()
12711 .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
12712 .collect();
12713 }
12714 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12715 array_value_to_elements(&arr)
12716 }
12717 SrfKind::GenerateSeries => {
12718 // v7.39 (read01 round 96) — evaluate the args against the actual
12719 // row, then hand off to the shared core so the numeric and
12720 // timestamp/timestamptz overloads work here too (this arm used to
12721 // handle only integers, silently NULLing a temporal/numeric series
12722 // when it shared a target list with another SRF).
12723 let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
12724 for a in args {
12725 arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
12726 }
12727 let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
12728 Ok(rows
12729 .into_iter()
12730 .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
12731 .collect())
12732 }
12733 SrfKind::GenerateSubscripts => {
12734 let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12735 let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
12736 if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
12737 return Ok(Vec::new());
12738 }
12739 let len = array_value_to_elements(&arr)?.len();
12740 Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
12741 }
12742 // One Value per array element (`_text` → text / SQL NULL, plain → the
12743 // element's compact JSON text) — the element list the FROM-clause form
12744 // materialises.
12745 SrfKind::ArrayElements { as_text } => {
12746 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12747 if matches!(arg, Value::Null) {
12748 return Ok(Vec::new());
12749 }
12750 let items =
12751 crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
12752 Ok(items
12753 .into_iter()
12754 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
12755 .collect())
12756 }
12757 // The scalar form already yields a TextArray of the keys (or errors on
12758 // a non-object, like PG); expand it into rows.
12759 SrfKind::ObjectKeys => {
12760 let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
12761 array_value_to_elements(&v)
12762 }
12763 // One row per match, each a text[] of the pattern's capture groups.
12764 SrfKind::RegexpMatches => {
12765 let vals: Vec<Value<'static>> = args
12766 .iter()
12767 .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
12768 .collect::<Result<_, _>>()?;
12769 crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
12770 }
12771 // One composite `(key, value)` row per object member (plain → jsonb
12772 // value, `_text` → text / SQL NULL).
12773 SrfKind::Each { as_text } => {
12774 let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12775 if matches!(arg, Value::Null) {
12776 return Ok(Vec::new());
12777 }
12778 let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
12779 Ok(pairs
12780 .into_iter()
12781 .map(|(k, v)| {
12782 let val = if as_text {
12783 v.map(Value::text).unwrap_or(Value::Null)
12784 } else {
12785 v.map(Value::json).unwrap_or(Value::Null)
12786 };
12787 Value::Composite(alloc::vec![
12788 ("key".to_string(), Value::text(k)),
12789 ("value".to_string(), val),
12790 ])
12791 })
12792 .collect())
12793 }
12794 // One Value per matched JSON value.
12795 SrfKind::PathQuery => {
12796 let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12797 let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
12798 // v7.39 — optional vars document (3rd arg).
12799 let vars = match args.get(2) {
12800 Some(a) => {
12801 let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
12802 crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
12803 }
12804 None => None,
12805 };
12806 match crate::json::path_query_vars(&doc, &path, vars.as_ref())
12807 .map_err(EngineError::Eval)?
12808 {
12809 Value::Null => Ok(Vec::new()),
12810 Value::TextArray(items) => Ok(items
12811 .into_iter()
12812 .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
12813 .collect()),
12814 other => Ok(alloc::vec![other]),
12815 }
12816 }
12817 }
12818}
12819
12820/// v7.19 P5 — turn an array-typed `Value` into the element list
12821/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
12822/// = (no rows)`). Non-array values fall through to a type-mismatch
12823/// error.
12824pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
12825 // v7.39 (round 236) — PG unnests a multidimensional array into its
12826 // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
12827 // rows). SPG stores 2-D arrays as their own variants, which fell
12828 // through to the type-mismatch arm below.
12829 if let Some(flat) = crate::eval::values::flatten_2d(v) {
12830 return array_value_to_elements(&flat);
12831 }
12832 match v {
12833 Value::Null => Ok(Vec::new()),
12834 Value::TextArray(items) => Ok(items
12835 .iter()
12836 .map(|opt| {
12837 opt.as_ref()
12838 .map(|s| Value::text(s.clone()))
12839 .unwrap_or(Value::Null)
12840 })
12841 .collect()),
12842 Value::IntArray(items) => Ok(items
12843 .iter()
12844 .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
12845 .collect()),
12846 Value::BigIntArray(items) => Ok(items
12847 .iter()
12848 .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
12849 .collect()),
12850 // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
12851 // range per canonical span.
12852 Value::Multirange { kind, ranges } => Ok(ranges
12853 .iter()
12854 .map(|s| Value::Range {
12855 kind: *kind,
12856 lower: s.lower.clone(),
12857 upper: s.upper.clone(),
12858 lower_inc: s.lower_inc,
12859 upper_inc: s.upper_inc,
12860 empty: false,
12861 })
12862 .collect()),
12863 other => Err(EngineError::Eval(EvalError::TypeMismatch {
12864 detail: alloc::format!(
12865 "unnest() expects an array argument, got {}",
12866 crate::conversions::pg_type_name_for_error_opt(other.data_type())
12867 ),
12868 })),
12869 }
12870}
12871
12872impl Engine {
12873 /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
12874 /// the SELECT's FROM / JOIN graph, re-parse each view's body
12875 /// source, and prepend it as a synthetic CTE on the
12876 /// returned SelectStatement. Returns `None` when no view
12877 /// references are found (caller proceeds with the original
12878 /// statement); returns `Some(rewritten)` otherwise (caller
12879 /// re-runs exec_select_cancel on the rewritten form so the
12880 /// regular CTE materialiser handles it).
12881 fn expand_views_in_select(
12882 &self,
12883 stmt: &SelectStatement,
12884 ) -> Result<Option<SelectStatement>, EngineError> {
12885 let cat = self.active_catalog();
12886 let mut referenced: Vec<String> = Vec::new();
12887 if let Some(from) = &stmt.from {
12888 collect_view_refs(&from.primary, cat, &mut referenced);
12889 for j in &from.joins {
12890 collect_view_refs(&j.table, cat, &mut referenced);
12891 }
12892 }
12893 // Don't expand a view name that's already shadowed by a
12894 // CTE on the same SELECT — the CTE wins per PG.
12895 referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
12896 if referenced.is_empty() {
12897 return Ok(None);
12898 }
12899 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
12900 for name in &referenced {
12901 let view = cat.view(name).ok_or_else(|| {
12902 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
12903 "view {name:?} disappeared mid-expansion"
12904 )))
12905 })?;
12906 let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
12907 EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
12908 })?;
12909 let Statement::Select(body) = parsed else {
12910 return Err(EngineError::Unsupported(alloc::format!(
12911 "view {name:?} body is not a SELECT (catalog corruption)"
12912 )));
12913 };
12914 new_ctes.push(spg_sql::ast::Cte {
12915 name: name.clone(),
12916 body: spg_sql::ast::CteBody::Select(body),
12917 recursive: false,
12918 column_overrides: view.columns.clone(),
12919 search: None,
12920 cycle: None,
12921 });
12922 }
12923 let mut out = stmt.clone();
12924 // Prepend so view CTEs are visible to caller-supplied CTEs.
12925 new_ctes.extend(out.ctes);
12926 out.ctes = new_ctes;
12927 Ok(Some(out))
12928 }
12929
12930 /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
12931 /// any partition-parent table, rewrite the SELECT so each parent
12932 /// reference resolves to a CTE whose body is a `UNION ALL` over the
12933 /// children that pass the WHERE-derived partition-key range. Returns
12934 /// `None`(no rewrite needed)when no parent is referenced or all
12935 /// references are shadowed by a same-name CTE.
12936 ///
12937 /// Pruning vocabulary at v7.37.6-B:
12938 /// * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
12939 /// and `<key> BETWEEN literal AND literal`.
12940 /// * Anything outside that(OR / nested IN / function call on the
12941 /// key)defaults to "no pruning" — every child + DEFAULT lands
12942 /// in the UNION. Correctness is preserved; only the plan size
12943 /// widens.
12944 fn expand_partition_parents_in_select(
12945 &self,
12946 stmt: &SelectStatement,
12947 ) -> Result<Option<SelectStatement>, EngineError> {
12948 let cat = self.active_catalog();
12949 let Some(from) = &stmt.from else {
12950 return Ok(None);
12951 };
12952 let mut parent_refs: Vec<String> = Vec::new();
12953 collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
12954 for j in &from.joins {
12955 collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
12956 }
12957 // Drop names shadowed by a CTE on the same SELECT(PG semantics
12958 // — same as view expansion above).
12959 parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
12960 if parent_refs.is_empty() {
12961 return Ok(None);
12962 }
12963 // Synthesise a CTE name per parent so the existing
12964 // "CTE shadows a real table" guard doesn't fire (the parent
12965 // IS a real table in the catalog, unlike VIEW expansion's
12966 // case). The FROM-clause TableRef walker below rewrites
12967 // every parent reference to point at the synthetic CTE.
12968 let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
12969 let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
12970 let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
12971 for parent_name in &parent_refs {
12972 // No children = no rewrite. The parent itself is a real
12973 // (empty-rows) table — the regular FROM-resolution path
12974 // will scan it and return 0 rows, matching the
12975 // "partition parent with no children" plan. Skipping the
12976 // CTE here also avoids `SELECT * FROM parent` re-entering
12977 // this rewrite on the synthetic body (infinite recursion).
12978 let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
12979 continue;
12980 };
12981 new_ctes.push(spg_sql::ast::Cte {
12982 name: synth_name(parent_name),
12983 body: spg_sql::ast::CteBody::Select(body),
12984 recursive: false,
12985 column_overrides: Vec::new(),
12986 search: None,
12987 cycle: None,
12988 });
12989 expanded_parents.push(parent_name.clone());
12990 }
12991 if expanded_parents.is_empty() {
12992 return Ok(None);
12993 }
12994 let mut out = stmt.clone();
12995 if let Some(from) = out.from.as_mut() {
12996 rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
12997 for j in &mut from.joins {
12998 rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
12999 }
13000 }
13001 new_ctes.extend(out.ctes);
13002 out.ctes = new_ctes;
13003 Ok(Some(out))
13004 }
13005
13006 /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
13007 /// Children include every overlap-hit `Range` plus(always)the
13008 /// `Default` child(if any). Returns `Ok(None)` when no children
13009 /// would survive — caller skips the CTE injection and lets the
13010 /// parent fall through to the regular(empty-rows)scan path,
13011 /// avoiding the infinite recursion that an empty-body CTE
13012 /// referencing the parent name would trigger.
13013 /// v7.37.16 (16.10) — public helper invoked from explain.rs to
13014 /// surface "which children survive the WHERE-clause prune" in
13015 /// EXPLAIN output. Returns `None` when `parent_name` isn't
13016 /// actually a partition parent; otherwise returns the list of
13017 /// children the planner would scan (same algorithm as
13018 /// [`Self::build_partition_parent_union_body`] but without the
13019 /// SQL re-parse).
13020 /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
13021 /// expression (the PG-shaped EXPLAIN's scan builder has no full
13022 /// SelectStatement in hand). Wraps the original by synthesising a
13023 /// minimal statement carrying just the predicate.
13024 pub(crate) fn explain_partition_kept_children_by_where(
13025 &self,
13026 parent_name: &str,
13027 where_: Option<&spg_sql::ast::Expr>,
13028 ) -> Option<Vec<alloc::string::String>> {
13029 let mut synth = SelectStatement::default();
13030 synth.where_ = where_.cloned();
13031 self.explain_partition_kept_children(parent_name, &synth)
13032 }
13033
13034 pub(crate) fn explain_partition_kept_children(
13035 &self,
13036 parent_name: &str,
13037 outer: &SelectStatement,
13038 ) -> Option<Vec<alloc::string::String>> {
13039 use spg_storage::PartitionRole;
13040 let cat = self.active_catalog();
13041 let parent = cat.get(parent_name)?;
13042 let (key_position, parent_kind) = match &parent.schema().partition_role {
13043 Some(PartitionRole::Parent {
13044 key_column_positions,
13045 kind,
13046 ..
13047 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13048 _ => return None,
13049 };
13050 let key_col_name = parent.schema().columns[key_position].name.clone();
13051 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13052 Some(expr) => extract_key_range(expr, &key_col_name),
13053 None => (None, None),
13054 };
13055 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13056 Some(expr) => extract_key_eq_value(expr, &key_col_name),
13057 None => None,
13058 };
13059 let children = crate::partition::children_of_parent(cat, parent_name);
13060 let mut kept: Vec<alloc::string::String> = Vec::new();
13061 let mut default_child: Option<alloc::string::String> = None;
13062 for child_name in &children {
13063 let Some(child) = cat.get(child_name) else {
13064 continue;
13065 };
13066 match &child.schema().partition_role {
13067 Some(PartitionRole::Range { lower, upper, .. }) => {
13068 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13069 kept.push(child_name.clone());
13070 }
13071 }
13072 Some(PartitionRole::List { values, .. }) => match &eq_value {
13073 Some(v) => {
13074 if values.iter().any(|b| b.equals_value(v)) {
13075 kept.push(child_name.clone());
13076 }
13077 }
13078 None => kept.push(child_name.clone()),
13079 },
13080 Some(PartitionRole::Hash {
13081 modulus, remainder, ..
13082 }) => match &eq_value {
13083 Some(v) => {
13084 let h = crate::partition::pg_compatible_hash(v);
13085 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13086 kept.push(child_name.clone());
13087 }
13088 }
13089 None => kept.push(child_name.clone()),
13090 },
13091 Some(PartitionRole::Default { .. }) => {
13092 default_child = Some(child_name.clone());
13093 }
13094 _ => {}
13095 }
13096 }
13097 let _ = parent_kind;
13098 if let Some(d) = default_child {
13099 if kept.is_empty() || eq_value.is_none() {
13100 kept.push(d);
13101 }
13102 }
13103 Some(kept)
13104 }
13105
13106 fn build_partition_parent_union_body(
13107 &self,
13108 parent_name: &str,
13109 outer: &SelectStatement,
13110 ) -> Result<Option<SelectStatement>, EngineError> {
13111 use spg_storage::PartitionRole;
13112 let cat = self.active_catalog();
13113 let parent = cat.get(parent_name).ok_or_else(|| {
13114 EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13115 "partition parent {parent_name:?} disappeared mid-expansion"
13116 )))
13117 })?;
13118 let (key_position, parent_kind) = match &parent.schema().partition_role {
13119 Some(PartitionRole::Parent {
13120 key_column_positions,
13121 kind,
13122 ..
13123 }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13124 // v7.39 (round 645) — an INHERITANCE parent, which has no
13125 // role of its own: the relationship is recorded only in the
13126 // children. Three things differ from a partition parent and
13127 // all three are in this body.
13128 //
13129 // * The parent HOLDS ROWS, so it is a term of the union —
13130 // `FROM ONLY`, or expanding it would recurse.
13131 // * There is no partition key, so there is nothing to
13132 // prune: every child is a term.
13133 // * A child may declare columns of its own, so the terms
13134 // name the PARENT's columns rather than `*`. PG's
13135 // `SELECT * FROM parent` returns the parent's shape.
13136 //
13137 // Answered from this match rather than a branch before it —
13138 // round 644 measured what an extra early return beside an
13139 // existing test costs in this file.
13140 _ if crate::partition::has_inheritance_children(cat, parent_name) => {
13141 let cols = parent
13142 .schema()
13143 .columns
13144 .iter()
13145 .map(|c| quote_ident_for_sql(&c.name))
13146 .collect::<Vec<_>>()
13147 .join(", ");
13148 let carry_sys = references_ctid(outer);
13149 let sys = if carry_sys {
13150 let mut t = alloc::string::String::new();
13151 for s in SYSTEM_COLUMNS {
13152 t.push_str(", ");
13153 t.push_str(s);
13154 }
13155 t
13156 } else {
13157 alloc::string::String::new()
13158 };
13159 let mut body = alloc::format!(
13160 "SELECT {cols}{sys} FROM ONLY {}",
13161 quote_ident_for_sql(parent_name)
13162 );
13163 for child in crate::partition::children_of_parent(cat, parent_name) {
13164 body.push_str(&alloc::format!(
13165 " UNION ALL SELECT {cols}{sys} FROM {}",
13166 quote_ident_for_sql(&child)
13167 ));
13168 }
13169 return parse_select_or_corrupt(&body).map(Some);
13170 }
13171 _ => {
13172 return Err(EngineError::Unsupported(alloc::format!(
13173 "partition expansion: {parent_name:?} is not a parent"
13174 )));
13175 }
13176 };
13177 let key_col_name = parent.schema().columns[key_position].name.clone();
13178 // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
13179 // off the WHERE; for LIST / HASH we extract a single `=`
13180 // literal (and the rest of the planner falls back to "keep
13181 // every child" — same conservative path as 16.1/16.2).
13182 let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13183 Some(expr) => extract_key_range(expr, &key_col_name),
13184 None => (None, None),
13185 };
13186 let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13187 Some(expr) => extract_key_eq_value(expr, &key_col_name),
13188 None => None,
13189 };
13190 let children = crate::partition::children_of_parent(cat, parent_name);
13191 let mut kept: Vec<String> = Vec::new();
13192 let mut default_child: Option<String> = None;
13193 // First pass — apply per-strategy gates, defer DEFAULT until
13194 // we know whether some non-DEFAULT child matched.
13195 for child_name in &children {
13196 let Some(child) = cat.get(child_name) else {
13197 continue;
13198 };
13199 match &child.schema().partition_role {
13200 Some(PartitionRole::Range { lower, upper, .. }) => {
13201 if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13202 kept.push(child_name.clone());
13203 }
13204 }
13205 // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
13206 // = <lit>`, only the child whose values contain that
13207 // literal survives. Otherwise (no equality predicate
13208 // or planner couldn't extract one) keep the child
13209 // conservatively.
13210 Some(PartitionRole::List { values, .. }) => match &eq_value {
13211 Some(v) => {
13212 if values.iter().any(|b| b.equals_value(v)) {
13213 kept.push(child_name.clone());
13214 }
13215 }
13216 None => kept.push(child_name.clone()),
13217 },
13218 // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
13219 // we know the residue class deterministically, so
13220 // only the matching REMAINDER child survives.
13221 Some(PartitionRole::Hash {
13222 modulus, remainder, ..
13223 }) => match &eq_value {
13224 Some(v) => {
13225 let h = crate::partition::pg_compatible_hash(v);
13226 if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13227 kept.push(child_name.clone());
13228 }
13229 }
13230 None => kept.push(child_name.clone()),
13231 },
13232 Some(PartitionRole::Default { .. }) => {
13233 default_child = Some(child_name.clone());
13234 }
13235 _ => {}
13236 }
13237 }
13238 // PG-style DEFAULT semantics: the DEFAULT child must be
13239 // scanned iff some row could fall outside every concrete
13240 // child's bound predicate. We approximate that as "no
13241 // concrete child matched" (== full prune) — strictly
13242 // conservative for LIST / HASH (DEFAULT also catches rows
13243 // outside the union of value-sets / residues), and matches
13244 // PG for the equality case where we *do* know the routing
13245 // outcome.
13246 let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
13247 if let Some(d) = default_child {
13248 if kept.is_empty() {
13249 kept.push(d);
13250 } else if eq_value.is_none() {
13251 // Without an equality literal, the DEFAULT child may
13252 // still hold matching rows (e.g. LIKE on TEXT keys
13253 // for which a LIST partition exists). Keep it.
13254 kept.push(d);
13255 }
13256 }
13257 // Build the UNION ALL body text and re-parse — keeps the
13258 // rewrite expressible in surface SQL so the engine's existing
13259 // parser path handles the AST shape uniformly.
13260 if kept.is_empty() {
13261 // No children survive — caller falls back to scanning the
13262 // (empty) parent table. Returning None here is what
13263 // prevents the synthetic CTE from referring back to the
13264 // parent name and re-entering this rewrite pass.
13265 let _ = parent_name;
13266 return Ok(None);
13267 }
13268 // v7.39 (round 622, S05a) — the system columns of the CHILD the row
13269 // actually lives in.
13270 //
13271 // The parent is read through a synthetic CTE, so a `tableoid` on it
13272 // resolved against that CTE: every row of every child reported
13273 // `__spg_partition_pm`, an internal name no user ever typed, where
13274 // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
13275 // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
13276 // one asks "which partition is this row in", answering 0 rows where
13277 // PG answers 1. `ctid` had the same shape: it numbered the CTE's
13278 // output, so rows in different children got distinct ctids instead
13279 // of each child's own physical position.
13280 //
13281 // Naming them in the term is what carries them: the child scan
13282 // materialises its own six because the statement now references
13283 // them, and they land in SYSTEM_COLUMNS order right after the user
13284 // columns — the exact layout the positional `*` skip already
13285 // expects. Only done when the outer statement asks for one, so a
13286 // plain `SELECT * FROM parent` scans exactly what it scanned.
13287 let carry_sys = references_ctid(outer);
13288 let mut body = alloc::string::String::new();
13289 for (i, child_name) in kept.iter().enumerate() {
13290 if i > 0 {
13291 body.push_str(" UNION ALL ");
13292 }
13293 body.push_str("SELECT *");
13294 if carry_sys {
13295 for sys in SYSTEM_COLUMNS {
13296 body.push_str(", ");
13297 body.push_str(sys);
13298 }
13299 }
13300 body.push_str(" FROM ");
13301 body.push_str("e_ident_for_sql(child_name));
13302 }
13303 parse_select_or_corrupt(&body).map(Some)
13304 }
13305}
13306
13307/// Rewrite a `TableRef` pointing at a partition parent so it
13308/// references the synthetic CTE created by the expansion. If the
13309/// original ref had no alias, preserve the parent name as an alias
13310/// so column references like `events_partitioned.received_at`
13311/// keep resolving.
13312fn rewrite_partition_parent_table_ref(
13313 t: &mut spg_sql::ast::TableRef,
13314 parents: &[alloc::string::String],
13315 synth_name: &impl Fn(&str) -> alloc::string::String,
13316) {
13317 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13318 return;
13319 }
13320 // v7.39 (round 644) — an ONLY reference stays pointed at the parent
13321 // itself. The rewrite is keyed on the NAME, so in
13322 // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
13323 // parent list and this then rewrote BOTH — including the one that
13324 // asked not to descend. PG answers 0 for that join; SPG answered 2.
13325 // Folded into the existing test — see the note in
13326 // `collect_partition_parent_refs` for what a separate one cost.
13327 if t.only || !parents.iter().any(|p| p == &t.name) {
13328 return;
13329 }
13330 if t.alias.is_none() {
13331 t.alias = Some(t.name.clone());
13332 }
13333 t.name = synth_name(&t.name);
13334}
13335
13336/// Walk a `TableRef` and push its `name` if it resolves to a partition
13337/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
13338/// `generate_series_args` references — those aren't catalog tables.
13339fn collect_partition_parent_refs(
13340 t: &spg_sql::ast::TableRef,
13341 cat: &spg_storage::Catalog,
13342 out: &mut Vec<alloc::string::String>,
13343) {
13344 if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13345 return;
13346 }
13347 // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
13348 // The keyword used to be absorbed at parse time, so this fanned out
13349 // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
13350 // answered 2 where PG answers 0.
13351 //
13352 // Folded into the existing test rather than given an early return of
13353 // its own: as two extra lines in this function's body it cost
13354 // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
13355 // outside the panel. Rounds 641 and 643 met the same wall from the
13356 // other two directions — adding to a hot function and taking away
13357 // from a cold one. What goes in a body near the row loop is a
13358 // codegen decision whatever its shape.
13359 if !t.only && crate::partition::has_children(cat, &t.name) {
13360 out.push(t.name.clone());
13361 }
13362}
13363
13364/// v7.37.6-B partition-key range derived from a WHERE expression.
13365/// `i64` microseconds since epoch with the same sign convention as
13366/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
13367/// / `=`),`false` ⇒ exclusive(`>` / `<`).
13368#[derive(Debug, Clone, Copy)]
13369pub(crate) struct PartitionFilterBound {
13370 pub micros: i64,
13371 pub inclusive: bool,
13372}
13373
13374/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
13375/// shapes; tighten the running lo / hi as we go. Anything outside that
13376/// (OR / nested calls / non-key columns)is ignored — caller treats
13377/// `None` as "no constraint on that side."
13378fn extract_key_range(
13379 expr: &spg_sql::ast::Expr,
13380 key_col: &str,
13381) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
13382 let mut lo: Option<PartitionFilterBound> = None;
13383 let mut hi: Option<PartitionFilterBound> = None;
13384 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
13385 while let Some(e) = stack.pop() {
13386 match e {
13387 spg_sql::ast::Expr::Binary {
13388 lhs,
13389 op: spg_sql::ast::BinOp::And,
13390 rhs,
13391 } => {
13392 stack.push(lhs);
13393 stack.push(rhs);
13394 }
13395 // BETWEEN is desugared at parse time into `lhs >= low AND
13396 // lhs <= high`, so it lands here as two regular Binary
13397 // arms via the AND walker above.
13398 spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
13399 let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
13400 (Some(lhs.as_ref()), rhs.as_ref(), false)
13401 } else if is_column_ref(rhs, key_col) {
13402 (Some(rhs.as_ref()), lhs.as_ref(), true)
13403 } else {
13404 (None, lhs.as_ref(), false)
13405 };
13406 if col_ref.is_none() {
13407 continue;
13408 }
13409 let Some(lit) = literal_to_micros(lit_side) else {
13410 continue;
13411 };
13412 use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
13413 let effective_op = if swapped {
13414 match op {
13415 Lt => Gt,
13416 LtEq => GtEq,
13417 Gt => Lt,
13418 GtEq => LtEq,
13419 other => *other,
13420 }
13421 } else {
13422 *op
13423 };
13424 match effective_op {
13425 Eq => {
13426 tighten_lo(
13427 &mut lo,
13428 PartitionFilterBound {
13429 micros: lit,
13430 inclusive: true,
13431 },
13432 );
13433 tighten_hi(
13434 &mut hi,
13435 PartitionFilterBound {
13436 micros: lit,
13437 inclusive: true,
13438 },
13439 );
13440 }
13441 GtEq => {
13442 tighten_lo(
13443 &mut lo,
13444 PartitionFilterBound {
13445 micros: lit,
13446 inclusive: true,
13447 },
13448 );
13449 }
13450 Gt => {
13451 tighten_lo(
13452 &mut lo,
13453 PartitionFilterBound {
13454 micros: lit,
13455 inclusive: false,
13456 },
13457 );
13458 }
13459 LtEq => {
13460 tighten_hi(
13461 &mut hi,
13462 PartitionFilterBound {
13463 micros: lit,
13464 inclusive: true,
13465 },
13466 );
13467 }
13468 Lt => {
13469 tighten_hi(
13470 &mut hi,
13471 PartitionFilterBound {
13472 micros: lit,
13473 inclusive: false,
13474 },
13475 );
13476 }
13477 _ => {}
13478 }
13479 }
13480 _ => {}
13481 }
13482 }
13483 (lo, hi)
13484}
13485
13486fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
13487 match slot {
13488 None => *slot = Some(new),
13489 Some(cur) => {
13490 if new.micros > cur.micros
13491 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
13492 {
13493 *slot = Some(new);
13494 }
13495 }
13496 }
13497}
13498
13499fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
13500 match slot {
13501 None => *slot = Some(new),
13502 Some(cur) => {
13503 if new.micros < cur.micros
13504 || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
13505 {
13506 *slot = Some(new);
13507 }
13508 }
13509 }
13510}
13511
13512fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
13513 if let spg_sql::ast::Expr::Column(c) = e {
13514 c.name.eq_ignore_ascii_case(key_col)
13515 } else {
13516 false
13517 }
13518}
13519
13520/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
13521/// `key_col = <literal>` predicate out for LIST/HASH partition
13522/// pruning. Returns `None` when no equality literal can be lifted
13523/// (planner then keeps every child — correctness preserved). The
13524/// returned `Value<'static>` is an owned coercion so the caller can
13525/// outlive any AST node it was extracted from.
13526pub(crate) fn extract_key_eq_value(
13527 expr: &spg_sql::ast::Expr,
13528 key_col: &str,
13529) -> Option<spg_storage::Value<'static>> {
13530 let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
13531 while let Some(e) = stack.pop() {
13532 match e {
13533 spg_sql::ast::Expr::Binary {
13534 lhs,
13535 op: spg_sql::ast::BinOp::And,
13536 rhs,
13537 } => {
13538 stack.push(lhs);
13539 stack.push(rhs);
13540 }
13541 spg_sql::ast::Expr::Binary {
13542 lhs,
13543 op: spg_sql::ast::BinOp::Eq,
13544 rhs,
13545 } => {
13546 let lit_side = if is_column_ref(lhs, key_col) {
13547 rhs.as_ref()
13548 } else if is_column_ref(rhs, key_col) {
13549 lhs.as_ref()
13550 } else {
13551 continue;
13552 };
13553 let cloned = lit_side.clone();
13554 let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
13555 continue;
13556 };
13557 // Coerce to an owned Value<'static> so the caller
13558 // can hold it past the WHERE expression's lifetime.
13559 let owned: spg_storage::Value<'static> = match v {
13560 spg_storage::Value::Text(s) => {
13561 spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
13562 }
13563 spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
13564 spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
13565 spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
13566 spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
13567 spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
13568 spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
13569 spg_storage::Value::Null => spg_storage::Value::Null,
13570 // Anything else (Vector / Json / Bytes / Numeric /
13571 // arrays / interval / …) isn't a current partition
13572 // key type; skip without pruning.
13573 _ => continue,
13574 };
13575 return Some(owned);
13576 }
13577 _ => {}
13578 }
13579 }
13580 None
13581}
13582
13583/// Coerce a literal Expr(after the parser folded sequence calls etc.)
13584/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
13585/// pruning and routing agree on the literal vocabulary. Returns
13586/// `None` when the literal isn't recognised(planner then skips
13587/// pruning on that branch — correctness preserved).
13588fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
13589 let cloned = e.clone();
13590 let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
13591 match value {
13592 spg_storage::Value::Timestamp(m) => Some(m),
13593 spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
13594 spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
13595 _ => None,
13596 }
13597}
13598
13599/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
13600/// satisfying the WHERE-derived filter range. PG-style half-open:
13601/// child upper exclusive. Filter inclusivity is honoured per-bound.
13602fn range_satisfies_filter(
13603 range_lo: &spg_storage::PartitionBound,
13604 range_hi: &spg_storage::PartitionBound,
13605 filter_lo: Option<&PartitionFilterBound>,
13606 filter_hi: Option<&PartitionFilterBound>,
13607) -> bool {
13608 use spg_storage::PartitionBound;
13609 // For each filter side, reject children that can't host any row
13610 // matching the predicate.
13611 if let Some(lo) = filter_lo {
13612 // child upper bound vs filter lower:
13613 // if filter is x >= L, child rejects iff child.hi <= L
13614 // if filter is x > L, child rejects iff child.hi <= L
13615 // (child.hi exclusive, so equality with L still rejects)
13616 match range_hi {
13617 PartitionBound::MinValue => return false,
13618 PartitionBound::MaxValue => {}
13619 PartitionBound::TimestampTz(hi) => {
13620 if *hi <= lo.micros {
13621 return false;
13622 }
13623 }
13624 // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
13625 // matched against TIMESTAMPTZ filters here; keep child
13626 // (conservative: don't prune).
13627 PartitionBound::BigInt(_)
13628 | PartitionBound::Int(_)
13629 | PartitionBound::SmallInt(_)
13630 | PartitionBound::Date(_)
13631 | PartitionBound::Text(_) => {}
13632 }
13633 }
13634 if let Some(hi) = filter_hi {
13635 // child lower bound vs filter upper:
13636 // if filter is x <= U, child rejects iff child.lo > U
13637 // if filter is x < U, child rejects iff child.lo >= U
13638 match range_lo {
13639 PartitionBound::MaxValue => return false,
13640 PartitionBound::MinValue => {}
13641 PartitionBound::TimestampTz(lo) => {
13642 let rejects = if hi.inclusive {
13643 *lo > hi.micros
13644 } else {
13645 *lo >= hi.micros
13646 };
13647 if rejects {
13648 return false;
13649 }
13650 }
13651 PartitionBound::BigInt(_)
13652 | PartitionBound::Int(_)
13653 | PartitionBound::SmallInt(_)
13654 | PartitionBound::Date(_)
13655 | PartitionBound::Text(_) => {}
13656 }
13657 }
13658 true
13659}
13660
13661fn quote_ident_for_sql(name: &str) -> alloc::string::String {
13662 // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
13663 // identifier, otherwise quoted). Conservative: always quote so
13664 // children with reserved names round-trip safely through the
13665 // CTE-body parse.
13666 let mut out = alloc::string::String::with_capacity(name.len() + 2);
13667 out.push('"');
13668 for c in name.chars() {
13669 if c == '"' {
13670 out.push('"');
13671 }
13672 out.push(c);
13673 }
13674 out.push('"');
13675 out
13676}
13677
13678fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
13679 let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
13680 EngineError::Unsupported(alloc::format!(
13681 "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
13682 ))
13683 })?;
13684 let Statement::Select(body) = parsed else {
13685 return Err(EngineError::Unsupported(alloc::format!(
13686 "partition expansion: generated SQL {sql:?} is not a SELECT"
13687 )));
13688 };
13689 Ok(body)
13690}
13691
13692/// v7.39 (read01 round 65/66) — the column shape a set-returning function
13693/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
13694/// yields ONE column named after the call's alias when there is one (`FROM
13695/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
13696/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
13697fn setof_column_shape_from(
13698 declared: &str,
13699 name: &str,
13700 alias: Option<&str>,
13701 got: &[ColumnSchema],
13702) -> alloc::vec::Vec<ColumnSchema> {
13703 let upper = declared.to_ascii_uppercase();
13704 if upper.starts_with("TABLE(") {
13705 let raw = &declared["TABLE(".len()..declared.len() - 1];
13706 return raw
13707 .split(',')
13708 .zip(got.iter())
13709 .map(|(decl, g)| {
13710 let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
13711 ColumnSchema::new(cname.to_string(), g.ty, true)
13712 })
13713 .collect();
13714 }
13715 let cname = alias.unwrap_or(name);
13716 got.first()
13717 .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
13718 .unwrap_or_default()
13719}
13720
13721/// The plpgsql twin: the interpreter hands back raw value rows, so the types
13722/// come off the first row.
13723fn setof_column_shape(
13724 declared: &str,
13725 name: &str,
13726 alias: Option<&str>,
13727 first_row: Option<&alloc::vec::Vec<Value<'static>>>,
13728) -> alloc::vec::Vec<ColumnSchema> {
13729 let got: alloc::vec::Vec<ColumnSchema> = first_row
13730 .map(|r| {
13731 r.iter()
13732 .enumerate()
13733 .map(|(i, v)| {
13734 ColumnSchema::new(
13735 alloc::format!("col{i}"),
13736 v.data_type().unwrap_or(DataType::Text),
13737 true,
13738 )
13739 })
13740 .collect()
13741 })
13742 .unwrap_or_default();
13743 setof_column_shape_from(declared, name, alias, &got)
13744}
13745
13746/// v7.39 (read01 round 67) — expand every set-returning call in a target list
13747/// for ONE input row, PG's ProjectSet semantics.
13748///
13749/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
13750/// output has as many rows as the LONGEST of them, and a shorter one is padded
13751/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
13752/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
13753/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
13754/// is zero rows, not one NULL row.
13755///
13756/// Non-SRF items repeat, evaluated once per output row from the same input row.
13757/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
13758/// used to reach the scalar function dispatcher, which reported the aggregate as
13759/// an *unknown function* — the same "symptom two layers above the cause" shape
13760/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
13761/// sees a call, not the clause it came from. The statement knows.
13762/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
13763/// clause may appear.
13764///
13765/// PG rejects `FOR UPDATE` on exactly the shapes that have no
13766/// identifiable base row to lock, each with its own wording. SPG
13767/// accepted all of them and locked nothing, so a query that PG refuses
13768/// outright came back looking like it had taken locks.
13769///
13770/// Every wording read off live PG 18.4.
13771impl crate::Engine {
13772 /// v7.39.2 — a column name in WHERE / ORDER BY / GROUP BY / HAVING
13773 /// that names nothing is refused before the scan, not when a row
13774 /// reaches it.
13775 ///
13776 /// The projection resolves its names eagerly; a predicate only meets
13777 /// them per row. So on an EMPTY table `SELECT a FROM t WHERE nosuch
13778 /// = 1` answered zero rows and no error, and the same statement over
13779 /// a table with one row raised. Measured on PostgreSQL 18.6 and
13780 /// MySQL 9.7.2: both refuse it whatever the row count. A typo in a
13781 /// predicate therefore passed a test written against an empty
13782 /// fixture and failed in production — or, worse, ran nightly over an
13783 /// empty window and reported nothing.
13784 ///
13785 /// Deliberately narrow: ONE plain base table, nothing else. A join,
13786 /// a CTE, a set operation, a lateral or function source, or a
13787 /// subquery in the clause all bring a second scope into which a name
13788 /// may legitimately resolve, and refusing one of those would be a
13789 /// worse defect than the one this closes. Those shapes keep the
13790 /// old behaviour; the walk below does not descend into a subquery
13791 /// for the same reason.
13792 /// v7.39.2 — refuse a call whose argument count no overload accepts,
13793 /// BEFORE the scan rather than per row.
13794 ///
13795 /// `SELECT lower(t, n) FROM t` answered zero rows and no error over
13796 /// an EMPTY table and raised the moment the table had one row in it,
13797 /// because the arity check lives inside the row-time dispatch. It is
13798 /// the same shape as the unknown-column-in-a-predicate defect closed
13799 /// earlier in this release, and it hides in the same place: a query
13800 /// written against an empty fixture passes its test.
13801 ///
13802 /// The accepted counts come from `eval::arity::REFUSED_ARITIES`,
13803 /// which is derived by asking the dispatch itself offline and can
13804 /// only ever UNDER-refuse — see that file for why the two other
13805 /// candidate oracles were refuted.
13806 /// v7.39.3 — MySQL's column names are case-insensitive; PostgreSQL's
13807 /// quoted ones are not. See `EvalContext::col_eq`.
13808 fn col_name_eq(&self, a: &str, b: &str) -> bool {
13809 if self.speaks_mysql {
13810 a.eq_ignore_ascii_case(b)
13811 } else {
13812 a == b
13813 }
13814 }
13815
13816 pub(crate) fn validate_function_arity(
13817 &self,
13818 stmt: &SelectStatement,
13819 ) -> Result<(), EngineError> {
13820 let mut calls: Vec<(alloc::string::String, Vec<Expr>)> = Vec::new();
13821 for it in &stmt.items {
13822 if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
13823 collect_function_calls(expr, &mut calls);
13824 }
13825 }
13826 if let Some(w) = &stmt.where_ {
13827 collect_function_calls(w, &mut calls);
13828 }
13829 for o in &stmt.order_by {
13830 collect_function_calls(&o.expr, &mut calls);
13831 }
13832 // The columns a name in this statement could resolve to. Only
13833 // plain base tables; anything else and the types are not
13834 // statically knowable, so nothing is refused early.
13835 let cat = self.active_catalog();
13836 let mut cols: Vec<ColumnSchema> = Vec::new();
13837 if let Some(from) = &stmt.from {
13838 for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
13839 if let Some(table) = cat.get(&t.name) {
13840 cols.extend(table.schema().columns.iter().cloned());
13841 }
13842 }
13843 }
13844 for (name, args) in calls {
13845 let Ok(i) = crate::eval::arity::REFUSED_ARITIES
13846 .binary_search_by(|(n, _)| (*n).cmp(name.as_str()))
13847 else {
13848 continue;
13849 };
13850 if !crate::eval::arity::REFUSED_ARITIES[i]
13851 .1
13852 .contains(&args.len())
13853 {
13854 continue;
13855 }
13856 // v7.39.2 — PostgreSQL names the SIGNATURE it could not
13857 // match, and before the scan there are no values to read a
13858 // type from. Where every argument's type is knowable
13859 // statically — a column of a source table, or a literal —
13860 // the sentence is PostgreSQL's exactly; where one is not,
13861 // this leaves the call to the row-time raise, which has the
13862 // values. Refusing early with a WORSE message would trade
13863 // one defect for another.
13864 let mut types: Vec<alloc::string::String> = Vec::new();
13865 for a in &args {
13866 let Some(t) = static_arg_type(a, &cols) else {
13867 types.clear();
13868 break;
13869 };
13870 types.push(t);
13871 }
13872 if types.len() != args.len() {
13873 continue;
13874 }
13875 return Err(EngineError::Eval(EvalError::WrongArity {
13876 name,
13877 types: types.join(", "),
13878 }));
13879 }
13880 Ok(())
13881 }
13882
13883 pub(crate) fn validate_clause_columns(
13884 &self,
13885 stmt: &SelectStatement,
13886 ) -> Result<(), EngineError> {
13887 let Some(from) = &stmt.from else {
13888 return Ok(());
13889 };
13890 if !stmt.ctes.is_empty() {
13891 return Ok(());
13892 }
13893 // v7.39.2 — every source, not just the first. A join is checkable
13894 // for the same reason one table is: with no CTE and no
13895 // subquery-shaped source, a bare name has to come from one of
13896 // them. Refusing the check for joins left `SELECT … FROM a JOIN b
13897 // … WHERE nosuch = 1` labelled `'field list'` where MySQL 9.7.2
13898 // says `'where clause'`.
13899 let plain = |t: &spg_sql::ast::TableRef| -> bool {
13900 t.unnest_expr.is_none()
13901 && t.generate_series_args.is_none()
13902 && t.lateral_subquery.is_none()
13903 && t.jsonb_each_text_arg.is_none()
13904 && t.table_fn_call.is_none()
13905 && t.rows_from.is_none()
13906 && t.json_table.is_none()
13907 && !t.scalar_fn_item
13908 };
13909 let cat = self.active_catalog();
13910 let mut sources: Vec<(String, &spg_storage::Table)> = Vec::new();
13911 for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
13912 if !plain(t) {
13913 return Ok(());
13914 }
13915 let Some(table) = cat.get(&t.name) else {
13916 return Ok(());
13917 };
13918 sources.push((t.alias.clone().unwrap_or_else(|| t.name.clone()), table));
13919 }
13920 let known = |c: &spg_sql::ast::ColumnName| -> bool {
13921 // A system column is not in a table's list and is a perfectly
13922 // good predicate: `WHERE ctid = '(0,4)'::tid` and `WHERE
13923 // tableoid::regclass::text = 'pm_a'` are both real, and the
13924 // first draft of this check refused them. The e2e suite said
13925 // so immediately, which is what it is for.
13926 if is_system_column(&c.name) {
13927 return true;
13928 }
13929 if let Some(q) = &c.qualifier {
13930 // A qualifier must name one of this statement's sources,
13931 // and that source must carry the column. An alias
13932 // REPLACES the written name, which is PostgreSQL's rule
13933 // and MySQL's: `FROM pg_cast c WHERE pg_cast.oid <> 0`
13934 // is an error on both.
13935 return match sources.iter().find(|(a, _)| a == q) {
13936 Some((_, t)) => t
13937 .schema()
13938 .columns
13939 .iter()
13940 .any(|sc| self.col_name_eq(&sc.name, &c.name)),
13941 None => false,
13942 };
13943 }
13944 sources
13945 .iter()
13946 .any(|(_, t)| {
13947 t.schema()
13948 .columns
13949 .iter()
13950 .any(|sc| self.col_name_eq(&sc.name, &c.name))
13951 })
13952 // An output name the statement itself defines: ORDER BY,
13953 // GROUP BY and HAVING may all name one.
13954 || stmt.items.iter().any(|it| match it {
13955 SelectItem::Expr { expr, alias } => {
13956 alias.as_deref() == Some(c.name.as_str())
13957 || matches!(expr, Expr::Column(pc) if pc.name == c.name)
13958 }
13959 _ => false,
13960 })
13961 };
13962 // v7.39.2 — the CLAUSE travels with the reference, because MySQL
13963 // names it: `Unknown column 'x' in 'where clause'`, `'order
13964 // clause'`, `'group statement'`, `'having clause'`. Measured on
13965 // 9.7.2, and a driver's error handling reads the sentence as well
13966 // as the number. PostgreSQL says only `column "x" does not
13967 // exist`, with no clause, so its wording is unchanged.
13968 //
13969 // This walk is the only place the clause is still known: by the
13970 // time a row-time resolver meets the name, the expression has
13971 // been detached from the statement that held it.
13972 let mut refs: Vec<(spg_sql::ast::ColumnName, &'static str)> = Vec::new();
13973 let mut push = |e: &Expr, ctx: &'static str, out: &mut Vec<_>| {
13974 let mut here: Vec<spg_sql::ast::ColumnName> = Vec::new();
13975 collect_plain_column_refs(e, &mut here);
13976 out.extend(here.into_iter().map(|c| (c, ctx)));
13977 };
13978 if let Some(w) = &stmt.where_ {
13979 push(w, "where clause", &mut refs);
13980 }
13981 if let Some(g) = &stmt.group_by {
13982 for e in g {
13983 push(e, "group statement", &mut refs);
13984 }
13985 }
13986 if let Some(h) = &stmt.having {
13987 push(h, "having clause", &mut refs);
13988 }
13989 for o in &stmt.order_by {
13990 push(&o.expr, "order clause", &mut refs);
13991 }
13992 // v7.39.2 — and the join predicates, which MySQL calls the `on
13993 // clause`. Measured on 9.7.2: `Unknown column 'j1.nosuch' in 'on
13994 // clause'`, qualifier and all.
13995 for j in &from.joins {
13996 if let Some(on) = &j.on {
13997 push(on, "on clause", &mut refs);
13998 }
13999 }
14000 for (c, ctx) in &refs {
14001 if !known(c) {
14002 if self.speaks_mysql {
14003 // The QUALIFIER travels with it: MySQL 9.7.2 answers
14004 // `Unknown column 'j1.nosuch' in 'on clause'`, not the
14005 // bare name. Measured.
14006 let shown = match &c.qualifier {
14007 Some(q) => alloc::format!("{q}.{}", c.name),
14008 None => c.name.clone(),
14009 };
14010 return Err(EngineError::Eval(EvalError::TypeMismatch {
14011 detail: alloc::format!("Unknown column '{shown}' in '{ctx}'"),
14012 }));
14013 }
14014 // PostgreSQL 18.6 names the missing TABLE when the
14015 // qualifier is the part that resolves to nothing
14016 // (`missing FROM-clause entry for table "pg_cast"`) and
14017 // the COLUMN otherwise. Raising the column error for both
14018 // dropped the table name a caller matches on.
14019 if let Some(q) = &c.qualifier
14020 && !sources.iter().any(|(a, _)| a == q)
14021 {
14022 return Err(EngineError::Eval(EvalError::UnknownQualifier {
14023 qualifier: q.clone(),
14024 column: c.name.clone(),
14025 }));
14026 }
14027 // v7.39.2 — and a qualified reference whose qualifier
14028 // DOES resolve prints the whole thing, unquoted:
14029 // `column ea.no_such does not exist` (measured on PG
14030 // 18.6). The bare `column "no_such" does not exist` drops
14031 // the alias a caller matches on, which is what the
14032 // sqlx round-20 pin says.
14033 if let Some(q) = &c.qualifier {
14034 return Err(EngineError::Eval(EvalError::QualifiedColumnNotFound {
14035 qualifier: q.clone(),
14036 column: c.name.clone(),
14037 }));
14038 }
14039 return Err(EngineError::Eval(EvalError::ColumnNotFound {
14040 name: c.name.clone(),
14041 }));
14042 }
14043 }
14044 Ok(())
14045 }
14046}
14047
14048/// v7.39.2 — the column references of an expression, NOT descending into
14049/// a subquery.
14050///
14051/// A correlated subquery resolves its names against an outer scope this
14052/// walk cannot see, so descending would refuse valid queries. Missing a
14053/// typo inside one is the safe direction; refusing a good query is not.
14054/// v7.39.2 — the type PostgreSQL would name for an argument, when it
14055/// can be known without a row: a column of a source table, or a
14056/// literal. `None` for anything else, which is what keeps the pre-scan
14057/// refusal from printing a worse sentence than the row-time one.
14058pub(crate) fn static_arg_type(e: &Expr, cols: &[ColumnSchema]) -> Option<alloc::string::String> {
14059 use spg_sql::ast::Literal as L;
14060 match e {
14061 Expr::Column(c) => cols
14062 .iter()
14063 .find(|s| s.name.eq_ignore_ascii_case(&c.name))
14064 .map(|s| crate::conversions::pg_type_name_for_error(s.ty)),
14065 // A bare literal has no type yet on PostgreSQL — it names it
14066 // `unknown` in this very sentence — except where the lexeme
14067 // fixes one.
14068 Expr::Literal(L::String(_)) | Expr::Literal(L::Null) => {
14069 Some(alloc::string::String::from("unknown"))
14070 }
14071 Expr::Literal(L::Integer(_)) => Some(alloc::string::String::from("integer")),
14072 Expr::Literal(L::Bool(_)) => Some(alloc::string::String::from("boolean")),
14073 _ => None,
14074 }
14075}
14076
14077/// v7.39.2 — the function calls of an expression, name and argument
14078/// count, NOT descending into a subquery (its scope is its own).
14079fn collect_function_calls(e: &Expr, out: &mut Vec<(alloc::string::String, Vec<Expr>)>) {
14080 match e {
14081 Expr::FunctionCall { name, args } => {
14082 out.push((name.to_ascii_lowercase(), args.clone()));
14083 for a in args {
14084 collect_function_calls(a, out);
14085 }
14086 }
14087 Expr::Binary { lhs, rhs, .. } => {
14088 collect_function_calls(lhs, out);
14089 collect_function_calls(rhs, out);
14090 }
14091 Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14092 collect_function_calls(expr, out);
14093 }
14094 _ => {}
14095 }
14096}
14097
14098fn collect_plain_column_refs(e: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
14099 match e {
14100 Expr::Column(c) => out.push(c.clone()),
14101 Expr::Binary { lhs, rhs, .. } => {
14102 collect_plain_column_refs(lhs, out);
14103 collect_plain_column_refs(rhs, out);
14104 }
14105 Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14106 collect_plain_column_refs(expr, out);
14107 }
14108 Expr::FunctionCall { args, .. } => {
14109 for a in args {
14110 collect_plain_column_refs(a, out);
14111 }
14112 }
14113 _ => {}
14114 }
14115}
14116
14117fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
14118 let Some(lock) = &stmt.locking else {
14119 return Ok(());
14120 };
14121 let verb = lock_clause_verb(lock.strength);
14122 let refuse = |what: &str| {
14123 Err(EngineError::Unsupported(alloc::format!(
14124 "{verb} is not allowed with {what}"
14125 )))
14126 };
14127 if !stmt.unions.is_empty() {
14128 return refuse("UNION/INTERSECT/EXCEPT");
14129 }
14130 if stmt.distinct || !stmt.distinct_on.is_empty() {
14131 return refuse("DISTINCT clause");
14132 }
14133 if stmt.group_by.is_some() || stmt.group_by_all {
14134 return refuse("GROUP BY clause");
14135 }
14136 let has_agg = stmt.items.iter().any(|it| match it {
14137 spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
14138 _ => false,
14139 });
14140 if has_agg {
14141 return refuse("aggregate functions");
14142 }
14143 // `FOR UPDATE OF t` must name a relation that is actually in FROM.
14144 for want in &lock.of_tables {
14145 if !locking_from_names(stmt)
14146 .iter()
14147 .any(|n| n.eq_ignore_ascii_case(want))
14148 {
14149 return Err(EngineError::Unsupported(alloc::format!(
14150 "relation \"{want}\" in {verb} clause not found in FROM clause"
14151 )));
14152 }
14153 }
14154 Ok(())
14155}
14156
14157/// How PG names the clause in its diagnostics.
14158const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
14159 use spg_sql::ast::LockStrength as LS;
14160 match s {
14161 LS::Update => "FOR UPDATE",
14162 LS::NoKeyUpdate => "FOR NO KEY UPDATE",
14163 LS::Share => "FOR SHARE",
14164 LS::KeyShare => "FOR KEY SHARE",
14165 }
14166}
14167
14168/// Every relation name (or alias) the FROM clause exposes.
14169fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
14170 let mut out = alloc::vec::Vec::new();
14171 if let Some(f) = &stmt.from {
14172 let mut push = |t: &spg_sql::ast::TableRef| {
14173 if let Some(a) = &t.alias {
14174 out.push(a.clone());
14175 }
14176 out.push(t.name.clone());
14177 };
14178 push(&f.primary);
14179 for j in &f.joins {
14180 push(&j.table);
14181 }
14182 }
14183 out
14184}
14185
14186fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
14187 use spg_sql::ast::Expr;
14188 if let Some(w) = &stmt.where_
14189 && aggregate::contains_aggregate(w)
14190 {
14191 return Err(EngineError::Unsupported(
14192 "aggregate functions are not allowed in WHERE".into(),
14193 ));
14194 }
14195 let mut nested = false;
14196 let mut check = |e: &Expr| {
14197 let mut probe = e.clone();
14198 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14199 let args = match n {
14200 Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
14201 _ => return false,
14202 };
14203 if args.iter().any(aggregate::contains_aggregate) {
14204 nested = true;
14205 }
14206 false
14207 });
14208 };
14209 for it in &stmt.items {
14210 if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14211 check(expr);
14212 }
14213 }
14214 if let Some(h) = &stmt.having {
14215 check(h);
14216 }
14217 for o in &stmt.order_by {
14218 check(&o.expr);
14219 }
14220 if nested {
14221 return Err(EngineError::Unsupported(
14222 "aggregate function calls cannot be nested".into(),
14223 ));
14224 }
14225 Ok(())
14226}
14227
14228/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
14229/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
14230/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
14231/// to a set and then applies the enclosing expression once per element. SPG only
14232/// ever recognised an SRF that WAS the item, so everything above died on
14233/// "unknown function unnest" — the set-returning call, wrapped in anything at
14234/// all, fell through to the scalar function dispatcher which has no such name.
14235///
14236/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
14237/// rewritten to read that column, and the rewritten expression is evaluated once
14238/// per output row against the input row extended with the lifted values. The
14239/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
14240/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
14241/// executors (the single-table scan, the synthetic-table pipeline, and the
14242/// unnest FROM path) each evaluated the key as an ordinary expression, where the
14243/// literal `n` is just the constant n — the same sort key for every row. The
14244/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
14245/// back in input order, not in a wrong order. Statement prep resolves the common
14246/// case, but only when the SELECT item is an expression — a `*` is not one, and
14247/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
14248/// spelling landed on exactly the shape prep could not resolve.
14249///
14250/// A set-returning item is left alone: copying it into ORDER BY would make the
14251/// key "the whole set", evaluated once per INPUT row.
14252fn resolve_positional_order_by(
14253 order_by: &[spg_sql::ast::OrderBy],
14254 projection: &[ProjectedItem],
14255) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
14256 order_by
14257 .iter()
14258 .filter_map(|o| {
14259 let mut o = o.clone();
14260 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
14261 && *n >= 1
14262 && let Ok(idx) = usize::try_from(*n - 1)
14263 && let Some(item) = projection.get(idx)
14264 && !expr_contains_builtin_srf(&item.expr)
14265 {
14266 // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
14267 // item is itself an integer LITERAL must not be
14268 // substituted textually: the literal would read as an
14269 // ordinal again downstream, and `SELECT 10 … ORDER BY
14270 // 1` died with "position 10 is not in select list"
14271 // where PG happily returns the rows. Ordering by a
14272 // constant orders nothing, so the key drops.
14273 if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
14274 return None;
14275 }
14276 o.expr = item.expr.clone();
14277 }
14278 Some(o)
14279 })
14280 .collect()
14281}
14282
14283/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
14284/// this expression? Statement preparation (`resolve_order_by_position`) runs
14285/// before any catalog is in hand, and it only needs to know "is this item's value
14286/// a set", which the builtin SRFs answer syntactically.
14287pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
14288 let mut found = false;
14289 let mut probe = e.clone();
14290 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14291 if is_top_level_unnest(n) {
14292 found = true;
14293 return true;
14294 }
14295 false
14296 });
14297 found
14298}
14299
14300/// v7.39 (round 599) — everything about a target-list SRF that does not
14301/// depend on the row.
14302///
14303/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
14304/// each SRF-bearing projection expression, walked and rewrote the tree,
14305/// formatted a `__srf_N` name per node, and copied the whole column schema.
14306/// A counting allocator put the path at 24 allocations per input row for a
14307/// single-element `unnest`, against 0 for the same scan without one — 211 MB
14308/// where the plain scan took 4.3 — and the shape held whatever the array
14309/// contained, which is what invariant work looks like.
14310struct SrfPlan {
14311 /// The lifted SRF calls, in slot order.
14312 nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
14313 /// Per projection position, the expression with its SRF calls replaced
14314 /// by `__srf_N` column references. `None` means the item has none.
14315 rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
14316 /// The input schema followed by one column per slot. Only the slots'
14317 /// TYPES vary per row, and they are patched in place.
14318 ext_cols: alloc::vec::Vec<ColumnSchema>,
14319 /// v7.39 (round 743) — the rewritten projection COMPILED against the
14320 /// extended schema, once per plan. The per-output-row evaluation ran
14321 /// the interpreter (~560 ns/row on the unnest panel cell); the Step
14322 /// VM reads the `__srf_N` slots as plain columns. `None` = that item
14323 /// is not fully compilable and keeps the interpreter.
14324 compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
14325 base_cols: usize,
14326}
14327
14328fn build_srf_plan(
14329 engine: &Engine,
14330 projection: &[ProjectedItem],
14331 srf_idxs: &[usize],
14332 ctx: &EvalContext<'_>,
14333) -> Result<SrfPlan, EngineError> {
14334 // Lift every SRF node out of every item that contains one.
14335 let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
14336 let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
14337 let mut reject: Option<EngineError> = None;
14338 for &i in srf_idxs {
14339 let mut e = projection[i].expr.clone();
14340 crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
14341 if reject.is_some() {
14342 return true;
14343 }
14344 // PG refuses a set-returning function inside a conditional: the set
14345 // would have to be produced before anyone knows whether the branch
14346 // is even taken.
14347 let conditional = match n {
14348 spg_sql::ast::Expr::Case { .. } => Some("CASE"),
14349 spg_sql::ast::Expr::FunctionCall { name, .. }
14350 if name.eq_ignore_ascii_case("coalesce") =>
14351 {
14352 Some("COALESCE")
14353 }
14354 _ => None,
14355 };
14356 if let Some(kind) = conditional
14357 && engine.expr_contains_srf(n)
14358 {
14359 reject = Some(EngineError::Unsupported(alloc::format!(
14360 "set-returning functions are not allowed in {kind}"
14361 )));
14362 return true;
14363 }
14364 if !engine.is_srf_node(n) {
14365 return false;
14366 }
14367 let slot = nodes.len();
14368 nodes.push(n.clone());
14369 *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
14370 qualifier: None,
14371 name: alloc::format!("__srf_{slot}"),
14372 });
14373 true
14374 });
14375 rewritten[i] = Some(e);
14376 }
14377 if let Some(err) = reject {
14378 return Err(err);
14379 }
14380 let base_cols = ctx.columns.len();
14381 let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
14382 for slot in 0..nodes.len() {
14383 ext_cols.push(ColumnSchema::new(
14384 alloc::format!("__srf_{slot}"),
14385 DataType::Text,
14386 true,
14387 ));
14388 }
14389 // v7.39 (round 743) — compile the rewritten items against the
14390 // EXTENDED schema. The slot columns' declared type is a per-row
14391 // patched detail the compiled column read does not consult.
14392 let compiled: Vec<Option<eval::CompiledExpr>> = {
14393 let mut ext_ctx = ctx.clone();
14394 ext_ctx.columns = &ext_cols;
14395 projection
14396 .iter()
14397 .enumerate()
14398 .map(|(i, p)| {
14399 let e = rewritten[i].as_ref().unwrap_or(&p.expr);
14400 if eval::fully_compilable(e) {
14401 Some(eval::compile_expr(e, &ext_ctx))
14402 } else {
14403 None
14404 }
14405 })
14406 .collect()
14407 };
14408 Ok(SrfPlan {
14409 nodes,
14410 rewritten,
14411 ext_cols,
14412 compiled,
14413 base_cols,
14414 })
14415}
14416
14417/// One input row expanded through a plan built once for the whole scan.
14418/// v7.39 (round 621) — expand a projection whose target list contains
14419/// set-returning items, remembering which INPUT row each output row came from.
14420///
14421/// The three materialised-source tails — `FROM unnest(…)`, `FROM
14422/// generate_series(…)`, and the one that serves VALUES / a derived table /
14423/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
14424/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
14425/// v(x)` answered `function unnest(integer[]) does not exist` on all the
14426/// others, for a query PG answers. Sharing the expansion is the point: a
14427/// fourth copy would have been the fourth place to forget.
14428fn expand_projection_srfs(
14429 engine: &Engine,
14430 projection: &[ProjectedItem],
14431 srf_idxs: &[usize],
14432 filtered: &[Row<'static>],
14433 ctx: &EvalContext<'_>,
14434) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
14435 let mut out = alloc::vec::Vec::with_capacity(filtered.len());
14436 let mut src = alloc::vec::Vec::with_capacity(filtered.len());
14437 // v7.39 (round 726) — ONE plan for the whole scan. The per-row
14438 // spelling rebuilt it for every input row: a full clone of the
14439 // rewritten projection trees and the extended schema, 50k times on
14440 // the panel's unnest cell.
14441 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
14442 // v7.39 (round 733) — shard the expansion. Each shard clones the
14443 // plan (its ext_cols slot types are per-row mutable) and builds a
14444 // MINIMAL context — EvalContext is not Sync — which is sound only
14445 // when every expression involved is pure: the whole projection and
14446 // every SRF argument must be fully_compilable, or the row loop
14447 // stays serial with the full session context.
14448 // The projection is judged in its REWRITTEN form — the SRF call
14449 // itself is never compilable, but after the lift it is a plain
14450 // `__srf_N` column reference.
14451 let all_pure = projection
14452 .iter()
14453 .enumerate()
14454 .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
14455 && plan.nodes.iter().all(|n| match n {
14456 Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
14457 other => eval::fully_compilable(other),
14458 });
14459 if all_pure
14460 && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
14461 && let Some(r) = engine.parallel_runner.0.as_deref()
14462 {
14463 let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
14464 let chunk = filtered.len().div_ceil(n_shards);
14465 type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
14466 let schema_cols = ctx.columns;
14467 let alias = ctx.table_alias;
14468 let mysql = ctx.mysql_dialect;
14469 let style = ctx.render_style;
14470 let plan_ref = &plan;
14471 let results = r.run_shards(n_shards, &|si| {
14472 let lo = si * chunk;
14473 let hi = ((si + 1) * chunk).min(filtered.len());
14474 let mut sctx = eval::EvalContext::new(schema_cols, alias);
14475 sctx.mysql_dialect = mysql;
14476 sctx.render_style = style;
14477 // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
14478 // compiled programs); each shard rebuilds it, which also
14479 // recompiles against the shard's own context. Build errors
14480 // were already surfaced by the outer build above.
14481 let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
14482 Ok(p) => p,
14483 Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
14484 };
14485 let mut run = || -> ShardOut {
14486 let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
14487 let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
14488 for (i, row) in filtered[lo..hi].iter().enumerate() {
14489 let expanded =
14490 expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
14491 sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
14492 o.extend(expanded);
14493 }
14494 Ok((o, sidx))
14495 };
14496 alloc::boxed::Box::new(run())
14497 });
14498 for boxed in results {
14499 let shard = boxed
14500 .downcast::<ShardOut>()
14501 .expect("runner echoes the closure's box");
14502 let (o, sidx) = (*shard)?;
14503 out.extend(o);
14504 src.extend(sidx);
14505 }
14506 return Ok((out, src));
14507 }
14508 for (i, row) in filtered.iter().enumerate() {
14509 let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
14510 src.extend(core::iter::repeat_n(i, expanded.len()));
14511 out.extend(expanded);
14512 }
14513 Ok((out, src))
14514}
14515
14516/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
14517///
14518/// A key that names a select-list item reads it out of the EXPANDED row,
14519/// because PG sorts after the expansion. A key that names a source column the
14520/// query does not project is evaluated against the input row that output row
14521/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
14522fn srf_order_key(
14523 ob: &spg_sql::ast::OrderBy,
14524 out_col: Option<usize>,
14525 out: &Row<'static>,
14526 src: &Row<'static>,
14527 ctx: &EvalContext<'_>,
14528) -> Result<Value<'static>, EngineError> {
14529 match out_col {
14530 Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
14531 None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
14532 }
14533}
14534
14535fn expand_srf_row_with(
14536 engine: &Engine,
14537 plan: &mut SrfPlan,
14538 projection: &[ProjectedItem],
14539 row: &Row<'static>,
14540 ctx: &EvalContext<'_>,
14541) -> Result<Vec<Row<'static>>, EngineError> {
14542 let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
14543 for n in &plan.nodes {
14544 lists.push(engine.srf_values(n, row, ctx)?);
14545 }
14546 let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
14547 // Only the slots' element types depend on the row; the names and the
14548 // input schema around them do not.
14549 for (slot, list) in lists.iter().enumerate() {
14550 plan.ext_cols[plan.base_cols + slot].ty = list
14551 .iter()
14552 .find_map(|v| v.data_type())
14553 .unwrap_or(DataType::Text);
14554 }
14555 let mut ext_ctx = ctx.clone();
14556 ext_ctx.columns = &plan.ext_cols;
14557 let mut out = Vec::with_capacity(n_rows);
14558 // v7.39 (round 726) — the base columns are the SAME for every
14559 // expanded row; clone them once and rewrite only the SRF slots per
14560 // k. The old form cloned the whole input row per OUTPUT row — for
14561 // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
14562 // TEXT column the projection never reads.
14563 let base_len = row.values.len();
14564 let mut ext_vals = row.values.clone();
14565 ext_vals.resize(base_len + lists.len(), Value::Null);
14566 let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
14567 for k in 0..n_rows {
14568 for (slot, list) in lists.iter().enumerate() {
14569 // Past the end of THIS srf's rows → NULL (PG pads).
14570 ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
14571 }
14572 let ext_row = Row::new(core::mem::take(&mut ext_vals));
14573 let mut vals = Vec::with_capacity(projection.len());
14574 for (i, p) in projection.iter().enumerate() {
14575 // v7.39 (round 743) — compiled when possible; the
14576 // interpreter for the rest, with its exact wording.
14577 vals.push(match &plan.compiled[i] {
14578 Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
14579 .map_err(EngineError::Eval)?,
14580 None => {
14581 let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
14582 eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
14583 }
14584 });
14585 }
14586 ext_vals = ext_row.values;
14587 out.push(Row::new(vals));
14588 }
14589 Ok(out)
14590}
14591
14592/// The one-shot spelling, for the callers that expand a single row.
14593/// v7.39 (round 600) — which output column each ORDER BY key names, for a
14594/// query whose target list contains a set-returning function.
14595///
14596/// The keys used to be built from the INPUT row, before the SRF expanded, so
14597/// anything that named the SRF's own output was evaluated as a scalar call:
14598/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
14599/// "function unnest(integer[]) does not exist", and so did the spellings that
14600/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
14601/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
14602/// back in input order. PG sorts AFTER the expansion, so a key that names a
14603/// select-list item reads that item's value out of the expanded row.
14604///
14605/// `None` keeps the key on the input row, which is where an ORDER BY naming
14606/// a column the query does not project has to be evaluated.
14607/// v7.38.19 — the output column an ORDER BY term reads, when reading it
14608/// is provably the same as building a key from the input row.
14609///
14610/// A sort key is a COPY of the sort column, made because the source row
14611/// is gone by the time the sort runs — only the projection survives. On
14612/// `SELECT s_long FROM t ORDER BY s_long` that copy is of data the
14613/// projected row already holds, and on 400,000 rows of 192-character
14614/// text it is 400,000 allocations, 400,000 frees and 77 MB of copying.
14615/// A profile of that cell put the allocator at 2,025 leaf samples of the
14616/// working set, second only to the comparison chain.
14617///
14618/// The condition is narrow on purpose. `srf_order_output_cols` resolves
14619/// an ORDER BY term the way SQL does — a positional ordinal, or a name
14620/// matching the select list — and SQL resolves against the select list
14621/// BEFORE the input columns. The key path resolves against the INPUT
14622/// columns. For `SELECT g AS id … ORDER BY id` on a table that also has
14623/// an `id`, those are different columns, and swapping one for the other
14624/// would change answers rather than timings.
14625///
14626/// So this takes only the case where the two cannot disagree: a bare
14627/// unqualified column name, matching exactly one output item, whose own
14628/// expression is that same column. The projected cell then IS the input
14629/// cell, and the key would have been its copy.
14630/// True when comparing two of this column's VALUES gives the same order
14631/// as comparing the sort KEYS built from them.
14632///
14633/// It does not hold widely. A user ENUM stores its label as text but
14634/// orders by DECLARATION position; an array orders element-wise; a
14635/// domain or composite carries its own rules. For those the two paths
14636/// answer differently, and a sort that skipped the key would silently
14637/// reorder the result. This is the short list where they agree.
14638fn value_order_is_key_order(col: &ColumnSchema) -> bool {
14639 use spg_storage::DataType as T;
14640 col.user_enum_type.is_none()
14641 && col.user_domain_type.is_none()
14642 && col.user_composite_type.is_none()
14643 && col.collation_name.is_none()
14644 && col.collation == spg_storage::Collation::Binary
14645 && matches!(
14646 col.ty,
14647 T::SmallInt | T::Int | T::BigInt | T::Text | T::Varchar(_) | T::Bool | T::Uuid
14648 )
14649}
14650
14651/// The full ORDER BY comparison between two rows, named by index.
14652///
14653/// v7.38.19 — what a permutation sort falls back to when its key ties.
14654fn row_cmp_by_index(
14655 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14656 terms: &[(usize, bool, Option<bool>)],
14657 colls: &[Option<crate::collate::Collated>],
14658 mysql: bool,
14659 ia: u32,
14660 ib: u32,
14661) -> core::cmp::Ordering {
14662 let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
14663 for (i, (col, desc, nf)) in terms.iter().enumerate() {
14664 let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col)) else {
14665 continue;
14666 };
14667 let ord = match (va, vb) {
14668 (Value::Text(x), Value::Text(y)) => match colls.get(i).and_then(Option::as_ref) {
14669 Some(c) => {
14670 let o = c.compare(x, y);
14671 if *desc { o.reverse() } else { o }
14672 }
14673 None if !mysql => {
14674 let o = crate::orderby::str_cmp_prefix_first(x, y);
14675 if *desc { o.reverse() } else { o }
14676 }
14677 None => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
14678 },
14679 _ => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
14680 };
14681 if ord != core::cmp::Ordering::Equal {
14682 return ord;
14683 }
14684 }
14685 core::cmp::Ordering::Equal
14686}
14687
14688/// Whether ordering these rows by BYTES is what the collation in force
14689/// would have answered anyway.
14690///
14691/// v7.38.19 — a collated sort used to be shut out of the keyed path
14692/// entirely, and the cost of that showed up the moment the byte path
14693/// got fast: on the same fixture, the same binary took 92 ms under `C`
14694/// and 371 ms under `en_US`, so declaring a collation had become a
14695/// four-fold tax on a query that sorts md5 hex.
14696///
14697/// It need not be. For several locales `[0-9a-z]` orders exactly as
14698/// bytes do -- `collate::ascii_byte_order` carries that fact, and the
14699/// test beside it re-derives the whole allowlist by sorting a corpus
14700/// twice rather than asserting it. So when the collation is one of
14701/// those AND every value in every sort column is drawn from that
14702/// alphabet, the byte answer IS the collated answer.
14703///
14704/// Both halves are required. A collation outside the list can put `z`
14705/// between `s` and `t`; a value outside the alphabet can be `Ápple`,
14706/// which no locale in the list orders by its bytes. Either one and this
14707/// returns false, and the sort takes the collator's own path.
14708fn byte_order_answers_the_collation(
14709 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14710 terms: &[(usize, bool, Option<bool>)],
14711 colls: &[Option<crate::collate::Collated>],
14712) -> bool {
14713 if colls.iter().all(Option::is_none) {
14714 return true;
14715 }
14716 if !colls
14717 .iter()
14718 .flatten()
14719 .all(crate::collate::Collated::ascii_byte_order)
14720 {
14721 return false;
14722 }
14723 tagged.iter().all(|(_, row)| {
14724 terms.iter().all(|(col, _, _)| match row.values.get(*col) {
14725 // Only TEXT is collation-sensitive; a number or a NULL
14726 // orders the same under every collation there is.
14727 Some(Value::Text(t)) => crate::collate::is_ascii_alnum_lower(t),
14728 _ => true,
14729 })
14730 })
14731}
14732
14733/// An eight-byte key for each row's sort column, paired with the row's
14734/// index — or `None` when the column cannot give one on every row.
14735///
14736/// v7.38.19 — the pair is what the sort array holds instead of the row.
14737/// Two kinds of column can supply it:
14738///
14739/// * an INTEGER, whose whole value fits. Flipping the sign bit maps
14740/// the signed order onto the unsigned one, so the key is EXACT and
14741/// a comparison never has to look at the row at all.
14742/// * TEXT, as the first eight bytes big-endian, zero-padded. That
14743/// orders the same as the string — two that differ inside those
14744/// bytes differ at the same index either way, and one shorter than
14745/// eight pads with zeros exactly where `[u8]`'s own comparison runs
14746/// out — but it is a PREFIX, so equal keys must still ask the full
14747/// comparator.
14748///
14749/// The `None` is the safety of it: a NULL or any other type has no
14750/// faithful eight-byte key, so such a column takes the ordinary path
14751/// rather than being given a made-up one.
14752fn sort_keys_of(
14753 tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14754 col: usize,
14755) -> Option<(Vec<(u64, u32)>, bool)> {
14756 let n = u32::try_from(tagged.len()).ok()?;
14757 let mut out: Vec<(u64, u32)> = Vec::with_capacity(tagged.len());
14758 let exact = match tagged.first()?.1.values.get(col)? {
14759 Value::Text(_) => false,
14760 Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => true,
14761 _ => return None,
14762 };
14763 for (i, row) in (0..n).zip(tagged.iter()) {
14764 let key = match row.1.values.get(col) {
14765 Some(Value::Text(t)) if !exact => {
14766 let mut k = [0u8; 8];
14767 let bytes = t.as_bytes();
14768 let take = bytes.len().min(8);
14769 k[..take].copy_from_slice(&bytes[..take]);
14770 u64::from_be_bytes(k)
14771 }
14772 Some(Value::SmallInt(v)) if exact => (i64::from(*v) as u64) ^ (1 << 63),
14773 Some(Value::Int(v)) if exact => (i64::from(*v) as u64) ^ (1 << 63),
14774 Some(Value::BigInt(v)) if exact => (*v as u64) ^ (1 << 63),
14775 _ => return None,
14776 };
14777 out.push((key, i));
14778 }
14779 Some((out, exact))
14780}
14781
14782/// Whether a PREFIX key is worth sorting a permutation on.
14783///
14784/// v7.38.19 — it is not always, and the panel says so in one cell. The
14785/// `text (26 values)` fixture is two hundred identical characters drawn
14786/// from twenty-six letters, so every eight-byte prefix inside a letter
14787/// is the same and 15,000 rows tie on it. Each tie then pays the prefix
14788/// compare, a two-hundred-byte comparison, AND a random read into a
14789/// 400,000-element array — while sorting the rows in place keeps the
14790/// partition contiguous. Measured: 160 ms sorting rows, 247 ms sorting
14791/// the permutation, on the very fixture built to be degenerate.
14792///
14793/// So the permutation is taken when the key DECIDES, and a sample says
14794/// whether it does. An exact key always decides; a prefix has to earn
14795/// it.
14796fn key_discriminates(keys: &[(u64, u32)]) -> bool {
14797 const SAMPLE: usize = 1024;
14798 let step = (keys.len() / SAMPLE).max(1);
14799 let mut seen: Vec<u64> = keys
14800 .iter()
14801 .step_by(step)
14802 .take(SAMPLE)
14803 .map(|&(k, _)| k)
14804 .collect();
14805 let taken = seen.len();
14806 if taken < 8 {
14807 return true;
14808 }
14809 seen.sort_unstable();
14810 seen.dedup();
14811 seen.len() * 2 >= taken
14812}
14813
14814fn order_by_output_cols_if_identical(
14815 order_by: &[spg_sql::ast::OrderBy],
14816 projection: &[ProjectedItem],
14817 schema_cols: &[ColumnSchema],
14818) -> Option<Vec<usize>> {
14819 if order_by.is_empty() {
14820 return None;
14821 }
14822 let mut out = Vec::with_capacity(order_by.len());
14823 for ob in order_by {
14824 let Expr::Column(c) = &ob.expr else {
14825 return None;
14826 };
14827 if c.qualifier.is_some() {
14828 return None;
14829 }
14830 let mut hit = None;
14831 for (i, p) in projection.iter().enumerate() {
14832 if !p.output_name.eq_ignore_ascii_case(&c.name) {
14833 continue;
14834 }
14835 if hit.is_some() {
14836 return None; // ambiguous — SQL would reject it too
14837 }
14838 // The item must BE that column, not merely be named for it.
14839 let Expr::Column(pc) = &p.expr else {
14840 return None;
14841 };
14842 if !pc.name.eq_ignore_ascii_case(&c.name) {
14843 return None;
14844 }
14845 let sc = schema_cols
14846 .iter()
14847 .find(|s| s.name.eq_ignore_ascii_case(&c.name))?;
14848 if !value_order_is_key_order(sc) {
14849 return None;
14850 }
14851 hit = Some(i);
14852 }
14853 out.push(hit?);
14854 }
14855 Some(out)
14856}
14857
14858fn srf_order_output_cols(
14859 order_by: &[spg_sql::ast::OrderBy],
14860 projection: &[ProjectedItem],
14861) -> Vec<Option<usize>> {
14862 order_by
14863 .iter()
14864 .map(|ob| {
14865 // A positive ordinal is the Nth output column, directly.
14866 // `resolve_positional_order_by` deliberately leaves an ordinal
14867 // pointing at a set-returning item alone — copying the call into
14868 // ORDER BY would have made the key "the whole set" back when keys
14869 // came from the input row. Reading the expanded row's column is
14870 // what it should have meant, and is what this does.
14871 if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
14872 && *n >= 1
14873 && let Ok(idx) = usize::try_from(*n - 1)
14874 && idx < projection.len()
14875 {
14876 return Some(idx);
14877 }
14878 // An unqualified name matching exactly one output name. SQL
14879 // resolves ORDER BY against the select list first, so this wins
14880 // over an input column of the same name — which is the whole
14881 // point of `SELECT g AS id … ORDER BY id`.
14882 if let Expr::Column(c) = &ob.expr
14883 && c.qualifier.is_none()
14884 {
14885 let mut hit = None;
14886 for (i, p) in projection.iter().enumerate() {
14887 if p.output_name.eq_ignore_ascii_case(&c.name) {
14888 if hit.is_some() {
14889 hit = None;
14890 break;
14891 }
14892 hit = Some(i);
14893 }
14894 }
14895 if hit.is_some() {
14896 return hit;
14897 }
14898 }
14899 // Or the same expression as a select-list item — which is what
14900 // `ORDER BY 1` becomes once `resolve_positional_order_by` has
14901 // run, and what a repeated `ORDER BY unnest(…)` is.
14902 projection.iter().position(|p| p.expr == ob.expr)
14903 })
14904 .collect()
14905}
14906
14907fn expand_srf_row(
14908 engine: &Engine,
14909 projection: &[ProjectedItem],
14910 srf_idxs: &[usize],
14911 row: &Row<'static>,
14912 ctx: &EvalContext<'_>,
14913) -> Result<Vec<Row<'static>>, EngineError> {
14914 let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
14915 expand_srf_row_with(engine, &mut plan, projection, row, ctx)
14916}
14917
14918impl Engine {
14919 /// The rows one target-list SRF yields for an input row. `None` from
14920 /// `srf_target_idxs` means the expression is not set-returning at all.
14921 fn srf_values(
14922 &self,
14923 expr: &spg_sql::ast::Expr,
14924 row: &Row<'static>,
14925 ctx: &EvalContext<'_>,
14926 ) -> Result<Vec<Value<'static>>, EngineError> {
14927 if top_level_srf_kind(expr).is_some() {
14928 return top_level_srf_output(expr, row, ctx);
14929 }
14930 // A user set-returning function. Its body runs through the real
14931 // executor, like every function body since round 63.
14932 let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
14933 return Err(EngineError::Unsupported(
14934 "expected a SELECT-list SRF call".into(),
14935 ));
14936 };
14937 let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
14938 for a in args {
14939 vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
14940 }
14941 let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
14942 // v7.39 (read01 round 68) — in a target list a multi-column function is
14943 // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
14944 // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
14945 // what it is for. A single-column function contributes its bare value.
14946 Ok(rows
14947 .into_iter()
14948 .map(|r| {
14949 if r.values.len() == 1 {
14950 r.values.into_iter().next().unwrap_or(Value::Null)
14951 } else {
14952 Value::Composite(
14953 cols.iter()
14954 .map(|c| c.name.clone())
14955 .zip(r.values)
14956 .collect::<alloc::vec::Vec<_>>(),
14957 )
14958 }
14959 })
14960 .collect())
14961 }
14962
14963 /// Is THIS node a set-returning call: one of the builtin kinds, or a user
14964 /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
14965 fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
14966 if is_top_level_unnest(e) {
14967 return true;
14968 }
14969 let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
14970 return false;
14971 };
14972 self.active_catalog().functions_named(name).iter().any(|f| {
14973 let r = f.returns.trim().to_ascii_uppercase();
14974 r.starts_with("SETOF") || r.starts_with("TABLE(")
14975 })
14976 }
14977
14978 /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
14979 fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
14980 let mut found = false;
14981 let mut probe = e.clone();
14982 crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14983 if self.is_srf_node(n) {
14984 found = true;
14985 return true;
14986 }
14987 false
14988 });
14989 found
14990 }
14991
14992 /// Which projection items CONTAIN a set-returning call. Before round 78 this
14993 /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
14994 /// ordinary scalar call all the way down to the function dispatcher, which
14995 /// then reported `unnest` as an unknown function.
14996 fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
14997 projection
14998 .iter()
14999 .enumerate()
15000 .filter(|(_, p)| self.expr_contains_srf(&p.expr))
15001 .map(|(i, _)| i)
15002 .collect()
15003 }
15004}
15005
15006impl Engine {
15007 /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
15008 /// no `(f(args)).*` item.
15009 fn lower_record_expansion(
15010 &self,
15011 stmt: &SelectStatement,
15012 ) -> Result<Option<SelectStatement>, EngineError> {
15013 use spg_sql::ast::{Expr, SelectItem};
15014 let is_marker = |it: &SelectItem| {
15015 matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
15016 if name == "__record_expand")
15017 };
15018 if !stmt.items.iter().any(is_marker) {
15019 return Ok(None);
15020 }
15021 let mut out = stmt.clone();
15022 let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
15023 let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
15024 for (n, item) in stmt.items.iter().enumerate() {
15025 if !is_marker(item) {
15026 items.push(item.clone());
15027 continue;
15028 }
15029 let SelectItem::Expr {
15030 expr: Expr::FunctionCall { args, .. },
15031 ..
15032 } = item
15033 else {
15034 unreachable!("checked by is_marker");
15035 };
15036 let Some(Expr::FunctionCall {
15037 name: fname,
15038 args: fargs,
15039 }) = args.first()
15040 else {
15041 return Err(EngineError::Unsupported(
15042 "(<expr>).* expands a function's record — it needs a function call".into(),
15043 ));
15044 };
15045 let cols = self.setof_declared_columns(fname)?;
15046 let alias = alloc::format!("__rec{n}");
15047 let mut tref = bare_table_ref_named(&alias);
15048 tref.table_fn_call = Some(alloc::boxed::Box::new((
15049 fname.to_ascii_lowercase(),
15050 fargs.clone(),
15051 )));
15052 tref.alias = Some(alias.clone());
15053 lateral_refs.push(tref);
15054 for c in cols {
15055 items.push(SelectItem::Expr {
15056 expr: Expr::Column(spg_sql::ast::ColumnName {
15057 qualifier: Some(alias.clone()),
15058 name: c,
15059 }),
15060 alias: None,
15061 });
15062 }
15063 }
15064 out.items = items;
15065 // The function joins the FROM. With no FROM it BECOMES the FROM; with one
15066 // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
15067 // (the arguments may reference the outer row — the round-69 correlation).
15068 for tref in lateral_refs {
15069 match &mut out.from {
15070 None => {
15071 out.from = Some(spg_sql::ast::FromClause {
15072 primary: tref,
15073 joins: alloc::vec::Vec::new(),
15074 });
15075 }
15076 Some(from) => from.joins.push(spg_sql::ast::FromJoin {
15077 kind: spg_sql::ast::JoinKind::Cross,
15078 table: tref,
15079 on: None,
15080 using_cols: None,
15081 natural: false,
15082 }),
15083 }
15084 }
15085 Ok(Some(out))
15086 }
15087
15088 /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
15089 /// v text)` names them; a `SETOF <scalar>` is one column named after the
15090 /// function.
15091 fn setof_declared_columns(
15092 &self,
15093 name: &str,
15094 ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
15095 let cat = self.active_catalog();
15096 let overloads = cat.functions_named(name);
15097 let def = overloads.first().ok_or_else(|| {
15098 EngineError::Unsupported(alloc::format!("function {name} does not exist"))
15099 })?;
15100 let declared = def.returns.trim();
15101 let upper = declared.to_ascii_uppercase();
15102 if upper.starts_with("TABLE(") {
15103 let raw = &declared["TABLE(".len()..declared.len() - 1];
15104 return Ok(raw
15105 .split(',')
15106 .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
15107 .collect());
15108 }
15109 Ok(alloc::vec![name.to_string()])
15110 }
15111}
15112
15113/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
15114/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
15115/// COLUMNS list (data-independent), NESTED children inlined in
15116/// declaration order (PG's flattened output shape).
15117/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
15118/// correlated JSON_TABLE's static schema without evaluating its doc.
15119pub(crate) fn json_table_schema_pub(
15120 cols: &[spg_sql::ast::JsonTableColumn],
15121) -> alloc::vec::Vec<ColumnSchema> {
15122 json_table_schema(cols)
15123}
15124
15125fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
15126 use spg_sql::ast::JsonTableColumn as C;
15127 let mut out = alloc::vec::Vec::new();
15128 for c in cols {
15129 match c {
15130 C::Ordinality { name } => {
15131 out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
15132 }
15133 C::Regular {
15134 name, ty, exists, ..
15135 } => {
15136 let dt = if *exists {
15137 DataType::Bool
15138 } else {
15139 crate::conversions::column_type_to_data_type(*ty)
15140 };
15141 out.push(ColumnSchema::new(name.clone(), dt, true));
15142 }
15143 C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
15144 }
15145 }
15146 out
15147}
15148
15149/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
15150/// JSON_TABLE column's declared type (the DEFAULT expr may be a
15151/// string literal like `'none'` that must land as the column type).
15152fn coerce_json_table_default(
15153 v: Value<'static>,
15154 ty: spg_sql::ast::ColumnTypeName,
15155 name: &str,
15156) -> Result<Value<'static>, EngineError> {
15157 if v.is_null() {
15158 return Ok(Value::Null);
15159 }
15160 let dt = crate::conversions::column_type_to_data_type(ty);
15161 crate::conversions::coerce_value(v, dt, name, 0)
15162}
15163
15164/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
15165fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
15166 use crate::json::JsonValue as J;
15167 match v {
15168 Value::Null => J::Null,
15169 Value::Bool(b) => J::Bool(*b),
15170 Value::SmallInt(n) => J::Number(f64::from(*n)),
15171 Value::Int(n) => J::Number(f64::from(*n)),
15172 Value::BigInt(n) => J::Number(*n as f64),
15173 Value::Float(x) => J::Number(*x),
15174 Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
15175 other => J::String(crate::eval::value_to_text(other)),
15176 }
15177}
15178
15179fn bare_table_ref_named(name: &str) -> TableRef {
15180 TableRef {
15181 name: name.to_string(),
15182 alias: None,
15183 only: false,
15184 as_of_segment: None,
15185 unnest_expr: None,
15186 unnest_column_aliases: alloc::vec::Vec::new(),
15187 with_ordinality: false,
15188 generate_series_args: None,
15189 lateral_subquery: None,
15190 jsonb_each_text_arg: None,
15191 table_fn_call: None,
15192 rows_from: None,
15193 json_table: None,
15194 scalar_fn_item: false,
15195 }
15196}
15197
15198impl Engine {
15199 /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
15200 /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
15201 /// entries are the array-able SRFs, already lowered by the parser into their
15202 /// scalar array form.
15203 fn rows_from_rows(
15204 &self,
15205 primary: &TableRef,
15206 ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
15207 let entries = primary
15208 .rows_from
15209 .as_ref()
15210 .expect("caller guards rows_from.is_some()");
15211 let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
15212 let ctx = self.ev_ctx(&empty, None);
15213 let dummy = Row::new(alloc::vec::Vec::new());
15214 let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
15215 let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
15216 for (name, args) in entries {
15217 let (vals, colname) = if name == "__array" {
15218 // The parser lowered this one to `<array expr>`; its rows are the
15219 // array's elements.
15220 let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
15221 (
15222 array_value_to_elements(&arr)?,
15223 alloc::string::String::from("unnest"),
15224 )
15225 } else {
15226 let call = spg_sql::ast::Expr::FunctionCall {
15227 name: name.clone(),
15228 args: args.clone(),
15229 };
15230 (self.srf_values(&call, &dummy, &ctx)?, name.clone())
15231 };
15232 let ty = vals
15233 .first()
15234 .and_then(spg_storage::Value::data_type)
15235 .unwrap_or(DataType::Text);
15236 cols.push(ColumnSchema::new(colname, ty, true));
15237 lists.push(vals);
15238 }
15239 let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
15240 let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
15241 for k in 0..n {
15242 let mut vals: alloc::vec::Vec<Value<'static>> =
15243 alloc::vec::Vec::with_capacity(lists.len() + 1);
15244 for l in &lists {
15245 vals.push(l.get(k).cloned().unwrap_or(Value::Null));
15246 }
15247 rows.push(Row::new(vals));
15248 }
15249 if primary.with_ordinality {
15250 cols.push(ColumnSchema::new(
15251 "ordinality".to_string(),
15252 DataType::BigInt,
15253 false,
15254 ));
15255 rows = rows
15256 .into_iter()
15257 .enumerate()
15258 .map(|(i, r)| {
15259 let mut v = r.values;
15260 v.push(Value::BigInt(i as i64 + 1));
15261 Row::new(v)
15262 })
15263 .collect();
15264 }
15265 Ok((rows, cols))
15266 }
15267}
15268
15269/// v7.39 (round 232) — PG names the offending set operation in its
15270/// arity / type-mismatch messages ("each UNION query must have the same
15271/// number of columns"). `UNION ALL` is still spelled UNION there.
15272fn set_op_name(kind: UnionKind) -> &'static str {
15273 match kind {
15274 UnionKind::All | UnionKind::Distinct => "UNION",
15275 UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
15276 UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
15277 }
15278}
15279
15280/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
15281/// type: a bare string or NULL literal that no context has typed yet. SPG
15282/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
15283/// be the syntax. A wildcard or a non-literal expression is never unknown.
15284/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
15285/// LABELS as text (the wire render) but the value is an oid-carrying
15286/// dual, so a UNION with a numeric column must not be refused on the
15287/// label (pg_dump: `SELECT classid … UNION ALL SELECT
15288/// 'pg_opfamily'::regclass …`).
15289fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
15290 fn is_regcast(e: &Expr) -> bool {
15291 matches!(
15292 e,
15293 Expr::Cast {
15294 target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
15295 ..
15296 }
15297 )
15298 }
15299 stmt.items
15300 .iter()
15301 .map(|item| match item {
15302 SelectItem::Expr { expr, .. } => is_regcast(expr),
15303 _ => false,
15304 })
15305 .collect()
15306}
15307
15308fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
15309 stmt.items
15310 .iter()
15311 .map(|item| match item {
15312 SelectItem::Expr { expr, .. } => matches!(
15313 expr,
15314 Expr::Literal(spg_sql::ast::Literal::String(_))
15315 | Expr::Literal(spg_sql::ast::Literal::Null)
15316 ),
15317 _ => false,
15318 })
15319 .collect()
15320}
15321
15322/// v7.39 (round 233) — retype one branch column's cells, reporting the
15323/// conversion failure the way PG does rather than leaving the column
15324/// half-converted. Used when the other branch typed an untyped literal.
15325fn coerce_branch_column(
15326 rows: &mut [Row<'static>],
15327 col_idx: usize,
15328 target: DataType,
15329 col_name: &str,
15330) -> Result<(), EngineError> {
15331 for row in rows.iter_mut() {
15332 let Some(slot) = row.values.get_mut(col_idx) else {
15333 continue;
15334 };
15335 if matches!(slot, Value::Null) {
15336 continue;
15337 }
15338 *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
15339 }
15340 Ok(())
15341}
15342
15343/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
15344/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
15345/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
15346/// reference to q's output columns substituted by the underlying column.
15347///
15348/// Admission is deliberately narrow — anything that changes cardinality,
15349/// order, or scope stays on the materialising path:
15350/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
15351/// FROM with no ordinality or positional column aliases, and no
15352/// subquery anywhere its expressions (an inner scope could reference
15353/// q too — descending is a later knife);
15354/// * inner: one stored table, bare-column projection only, no
15355/// CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
15356/// * every outer column reference must resolve inside q's output list —
15357/// a name that does not is an ERROR today, and flattening would
15358/// silently legalise it against the base table.
15359fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
15360 use spg_sql::ast::SelectItem;
15361 let inner = primary.lateral_subquery.as_deref()?;
15362 // Outer shape.
15363 if !stmt.ctes.is_empty()
15364 || !stmt.unions.is_empty()
15365 || stmt.distinct
15366 || !stmt.distinct_on.is_empty()
15367 || !stmt.window_check_exprs.is_empty()
15368 || stmt.locking.is_some()
15369 || primary.with_ordinality
15370 || !primary.unnest_column_aliases.is_empty()
15371 {
15372 return None;
15373 }
15374 // Inner shape.
15375 if !inner.ctes.is_empty()
15376 || !inner.unions.is_empty()
15377 || inner.distinct
15378 || !inner.distinct_on.is_empty()
15379 || inner.group_by.is_some()
15380 || inner.group_by_all
15381 || inner.having.is_some()
15382 || !inner.order_by.is_empty()
15383 || inner.limit.is_some()
15384 || inner.offset.is_some()
15385 || !inner.window_check_exprs.is_empty()
15386 || inner.locking.is_some()
15387 {
15388 return None;
15389 }
15390 let ifrom = inner.from.as_ref()?;
15391 let it = &ifrom.primary;
15392 if !ifrom.joins.is_empty()
15393 || it.name.is_empty()
15394 || it.lateral_subquery.is_some()
15395 || it.unnest_expr.is_some()
15396 || it.generate_series_args.is_some()
15397 || it.as_of_segment.is_some()
15398 || it.jsonb_each_text_arg.is_some()
15399 || it.table_fn_call.is_some()
15400 || it.rows_from.is_some()
15401 || it.json_table.is_some()
15402 || it.with_ordinality
15403 || !it.unnest_column_aliases.is_empty()
15404 {
15405 return None;
15406 }
15407 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
15408 return None;
15409 }
15410 // The output map: q's visible name -> the underlying column.
15411 let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
15412 let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
15413 alloc::collections::BTreeMap::new();
15414 for item in &inner.items {
15415 let SelectItem::Expr { expr, alias } = item else {
15416 return None;
15417 };
15418 let Expr::Column(c) = expr else {
15419 return None;
15420 };
15421 if let Some(q) = c.qualifier.as_deref()
15422 && !q.eq_ignore_ascii_case(&inner_alias)
15423 {
15424 return None;
15425 }
15426 let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
15427 // A duplicated output name would make substitution ambiguous.
15428 if map
15429 .insert(out_name.to_ascii_lowercase(), c.clone())
15430 .is_some()
15431 {
15432 return None;
15433 }
15434 }
15435 if map.is_empty() {
15436 return None;
15437 }
15438 let derived_alias = primary
15439 .alias
15440 .clone()
15441 .unwrap_or_else(|| primary.name.clone())
15442 .to_ascii_lowercase();
15443 // Substitute in a clone; bail (None) on the first reference the map
15444 // cannot answer.
15445 let mut out = stmt.clone();
15446 let ok = core::cell::Cell::new(true);
15447 let mut subst = |e: &mut Expr| -> bool {
15448 match e {
15449 Expr::Column(c) => {
15450 match c.qualifier.as_deref() {
15451 Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
15452 None => {}
15453 Some(_) => {
15454 ok.set(false);
15455 return true;
15456 }
15457 }
15458 match map.get(&c.name.to_ascii_lowercase()) {
15459 Some(target) => *c = target.clone(),
15460 None => ok.set(false),
15461 }
15462 true
15463 }
15464 // Any subquery could reference q from its own scope;
15465 // descending is a later knife — bail for now.
15466 Expr::ScalarSubquery(_)
15467 | Expr::Exists { .. }
15468 | Expr::InSubquery { .. }
15469 | Expr::RowInSubquery { .. }
15470 | Expr::RowCmpSubquery { .. } => {
15471 ok.set(false);
15472 true
15473 }
15474 _ => false,
15475 }
15476 };
15477 for item in &mut out.items {
15478 match item {
15479 SelectItem::Expr { expr, .. } => {
15480 crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
15481 }
15482 // `SELECT * FROM (…) q` means q's columns, in q's order.
15483 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
15484 }
15485 }
15486 if let Some(w) = &mut out.where_ {
15487 crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
15488 }
15489 if let Some(gs) = &mut out.group_by {
15490 for g in gs {
15491 crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
15492 }
15493 }
15494 if let Some(h) = &mut out.having {
15495 crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
15496 }
15497 for o in &mut out.order_by {
15498 crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
15499 }
15500 for d in &mut out.distinct_on {
15501 crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
15502 }
15503 if !ok.get() {
15504 return None;
15505 }
15506 // FROM becomes the stored table; the filters conjoin.
15507 out.from = Some(spg_sql::ast::FromClause {
15508 primary: it.clone(),
15509 joins: Vec::new(),
15510 });
15511 out.where_ = match (inner.where_.clone(), out.where_.take()) {
15512 (Some(a), Some(b)) => Some(Expr::Binary {
15513 lhs: alloc::boxed::Box::new(a),
15514 op: spg_sql::ast::BinOp::And,
15515 rhs: alloc::boxed::Box::new(b),
15516 }),
15517 (Some(a), None) => Some(a),
15518 (None, b) => b,
15519 };
15520 Some(out)
15521}
15522
15523/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
15524/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
15525/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
15526/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
15527/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
15528/// DISTINCT, an SRF, or an unprovable inner shape stays put.
15529fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
15530 use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
15531 let inner = primary.lateral_subquery.as_deref()?;
15532 // Outer: exactly `SELECT count(*)`, nothing else.
15533 if !stmt.ctes.is_empty()
15534 || !stmt.unions.is_empty()
15535 || stmt.distinct
15536 || !stmt.distinct_on.is_empty()
15537 || stmt.where_.is_some()
15538 || stmt.group_by.is_some()
15539 || stmt.having.is_some()
15540 || !stmt.order_by.is_empty()
15541 || stmt.limit.is_some()
15542 || stmt.offset.is_some()
15543 || stmt.items.len() != 1
15544 {
15545 return None;
15546 }
15547 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
15548 return None;
15549 };
15550 let E::FunctionCall { name, args } = expr else {
15551 return None;
15552 };
15553 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
15554 return None;
15555 }
15556 // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
15557 let Some(LimitExpr::Literal(k)) = &inner.offset else {
15558 return None;
15559 };
15560 let k = i64::from(*k);
15561 if inner.limit.is_some() || inner.order_by.is_empty() {
15562 return None;
15563 }
15564 let mut counted = inner.clone();
15565 counted.order_by = Vec::new();
15566 counted.offset = None;
15567 // The stripped inner must now be a provable simple shape (its
15568 // items become irrelevant — count(*) reads none of them — but an
15569 // SRF item would change the row count, so the flatten predicate's
15570 // scrutiny still applies).
15571 let base = matview_flatten_probe(&counted)?;
15572 let mut out = stmt.clone();
15573 out.items = alloc::vec![SelectItem::Expr {
15574 expr: E::FunctionCall {
15575 name: String::from("greatest"),
15576 args: alloc::vec![
15577 E::Binary {
15578 lhs: alloc::boxed::Box::new(E::FunctionCall {
15579 name: String::from("count_star"),
15580 args: alloc::vec![],
15581 }),
15582 op: spg_sql::ast::BinOp::Sub,
15583 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
15584 },
15585 E::Literal(spg_sql::ast::Literal::Integer(0)),
15586 ],
15587 },
15588 alias: Some(String::from("count")),
15589 }];
15590 out.from = Some(spg_sql::ast::FromClause {
15591 primary: base,
15592 joins: Vec::new(),
15593 });
15594 out.where_ = counted.where_.clone();
15595 Some(out)
15596}
15597
15598/// The inner-shape probe `try_count_over_offset` shares with the
15599/// flatten: single stored table, no modifiers, no subqueries, no SRF
15600/// items. Returns the base TableRef.
15601fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
15602 use spg_sql::ast::SelectItem;
15603 if !inner.ctes.is_empty()
15604 || !inner.unions.is_empty()
15605 || inner.distinct
15606 || !inner.distinct_on.is_empty()
15607 || inner.group_by.is_some()
15608 || inner.group_by_all
15609 || inner.having.is_some()
15610 || !inner.order_by.is_empty()
15611 || inner.limit.is_some()
15612 || inner.offset.is_some()
15613 || !inner.window_check_exprs.is_empty()
15614 || inner.locking.is_some()
15615 {
15616 return None;
15617 }
15618 let ifrom = inner.from.as_ref()?;
15619 let it = &ifrom.primary;
15620 if !ifrom.joins.is_empty()
15621 || it.name.is_empty()
15622 || it.lateral_subquery.is_some()
15623 || it.unnest_expr.is_some()
15624 || it.generate_series_args.is_some()
15625 || it.as_of_segment.is_some()
15626 || it.jsonb_each_text_arg.is_some()
15627 || it.table_fn_call.is_some()
15628 || it.rows_from.is_some()
15629 || it.json_table.is_some()
15630 || it.with_ordinality
15631 {
15632 return None;
15633 }
15634 for item in &inner.items {
15635 match item {
15636 SelectItem::Expr { expr, .. } => {
15637 if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
15638 return None;
15639 }
15640 }
15641 SelectItem::Wildcard => {}
15642 SelectItem::QualifiedWildcard(_) => return None,
15643 }
15644 }
15645 if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
15646 return None;
15647 }
15648 Some(it.clone())
15649}
15650
15651/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
15652/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
15653/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
15654/// constant-LENGTH array literal unnests to exactly k rows per input
15655/// row (NULL elements are rows too). One SRF item only, elements
15656/// subquery-free, and the stripped inner must pass the same probe the
15657/// count-over-offset rewrite uses.
15658fn try_count_over_const_unnest(
15659 stmt: &SelectStatement,
15660 primary: &TableRef,
15661) -> Option<SelectStatement> {
15662 use spg_sql::ast::{Expr as E, SelectItem};
15663 let inner = primary.lateral_subquery.as_deref()?;
15664 if !stmt.ctes.is_empty()
15665 || !stmt.unions.is_empty()
15666 || stmt.distinct
15667 || !stmt.distinct_on.is_empty()
15668 || stmt.where_.is_some()
15669 || stmt.group_by.is_some()
15670 || stmt.having.is_some()
15671 || !stmt.order_by.is_empty()
15672 || stmt.limit.is_some()
15673 || stmt.offset.is_some()
15674 || stmt.items.len() != 1
15675 {
15676 return None;
15677 }
15678 let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
15679 return None;
15680 };
15681 let E::FunctionCall { name, args } = expr else {
15682 return None;
15683 };
15684 if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
15685 return None;
15686 }
15687 // Inner: exactly one item, and it is unnest(ARRAY[...]).
15688 if inner.items.len() != 1
15689 || !inner.order_by.is_empty()
15690 || inner.limit.is_some()
15691 || inner.offset.is_some()
15692 {
15693 return None;
15694 }
15695 let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
15696 return None;
15697 };
15698 let E::FunctionCall {
15699 name: fname,
15700 args: fargs,
15701 } = item
15702 else {
15703 return None;
15704 };
15705 if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
15706 return None;
15707 }
15708 let E::Array(elems) = &fargs[0] else {
15709 return None;
15710 };
15711 if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
15712 return None;
15713 }
15714 let k = elems.len() as i64;
15715 // The stripped inner (the SRF item replaced by a plain constant)
15716 // must be the provable simple shape.
15717 let mut counted = inner.clone();
15718 counted.items = alloc::vec![SelectItem::Expr {
15719 expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
15720 alias: None,
15721 }];
15722 let base = matview_flatten_probe(&counted)?;
15723 let mut out = stmt.clone();
15724 out.items = alloc::vec![SelectItem::Expr {
15725 expr: E::Binary {
15726 lhs: alloc::boxed::Box::new(E::FunctionCall {
15727 name: String::from("count_star"),
15728 args: alloc::vec![],
15729 }),
15730 op: spg_sql::ast::BinOp::Mul,
15731 rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
15732 },
15733 alias: Some(String::from("count")),
15734 }];
15735 out.from = Some(spg_sql::ast::FromClause {
15736 primary: base,
15737 joins: Vec::new(),
15738 });
15739 out.where_ = counted.where_.clone();
15740 Some(out)
15741}