Skip to main content

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<(crate::orderby::PrefixKind, 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((bkind, bp, boundary_is_ascii)) = topk_boundary_prefix
7245                    && let Some((rkind, rp, row_is_ascii)) =
7246                        crate::orderby::first_key_prefix(&order_bound, row)
7247                    && bkind == rkind
7248                    && (boundary_no_collation || (boundary_is_ascii && row_is_ascii))
7249                    && rp > bp
7250                {
7251                    boundary_checks += 1;
7252                    boundary_rejects += 1;
7253                    if boundary_checks == BOUNDARY_WINDOW {
7254                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7255                    }
7256                    return Ok(());
7257                }
7258                let mut buf = key_pool.pop().unwrap_or_default();
7259                crate::orderby::build_order_keys_bound(
7260                    &order_by,
7261                    &order_bound,
7262                    &order_colls,
7263                    row,
7264                    &ctx,
7265                    &mut buf,
7266                )?;
7267                // v7.39 (round 581) — reject before projecting.
7268                //
7269                // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7270                // 50 distinct `g` decides nearly every row on the FIRST
7271                // key, and PG answers it FASTER than the single-key form
7272                // (7.4 ms against 10.4) because a rejected row costs it
7273                // one comparison. SPG built both keys AND the projected
7274                // row for all 500k before throwing them away. The keys
7275                // are needed to compare; the projection is not.
7276                if boundary_check_on
7277                    && let Some((_, descs)) = &topk_stream
7278                    && let Some(b) = &topk_boundary
7279                {
7280                    boundary_checks += 1;
7281                    let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7282                        == core::cmp::Ordering::Greater;
7283                    if loses {
7284                        boundary_rejects += 1;
7285                    }
7286                    if boundary_checks == BOUNDARY_WINDOW {
7287                        // Keep asking only if it has been rejecting at
7288                        // least a quarter of what it saw.
7289                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7290                    }
7291                    if loses {
7292                        buf.clear();
7293                        key_pool.push(buf);
7294                        return Ok(());
7295                    }
7296                }
7297                buf
7298            };
7299            if srf_position.is_some() {
7300                let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7301                for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7302                    if stmt.distinct {
7303                        let bucket = seen_distinct
7304                            .entry(norm_hash_row(
7305                                &out,
7306                                &distinct_hb,
7307                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7308                            ))
7309                            .or_default();
7310                        if bucket.iter().any(|i| {
7311                            row_eq_norm(
7312                                &tagged[i].1,
7313                                &out,
7314                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7315                            )
7316                        }) {
7317                            continue;
7318                        }
7319                        bucket.push(tagged.len());
7320                    }
7321                    budget.charge(approx_row_bytes(&out))?;
7322                    // The keys come from THIS expanded row: a key naming a
7323                    // select-list item reads its value, anything else is
7324                    // still evaluated against the input row.
7325                    let keys = if order_by.is_empty() {
7326                        Vec::new()
7327                    } else {
7328                        let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7329                        for (k, ob) in order_by.iter().enumerate() {
7330                            kv.push(match srf_order_cols.get(k).copied().flatten() {
7331                                Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7332                                None => eval::eval_expr(&ob.expr, row, &ctx)
7333                                    .map_err(EngineError::Eval)?,
7334                            });
7335                        }
7336                        // Packed by the same code every other ORDER BY uses,
7337                        // so DESC / NULLS FIRST / the MySQL rule are not
7338                        // restated here.
7339                        let key_row = Row::new(kv);
7340                        let mut buf = Vec::new();
7341                        crate::orderby::build_order_keys_bound(
7342                            &order_by,
7343                            &srf_key_bound,
7344                            &order_colls,
7345                            &key_row,
7346                            &ctx,
7347                            &mut buf,
7348                        )?;
7349                        buf
7350                    };
7351                    tagged.push((keys, out));
7352                }
7353            } else {
7354                let values = &mut proj_buf;
7355                values.clear();
7356                values.reserve(projection.len());
7357                for (i, p) in projection.iter().enumerate() {
7358                    // v7.37.x (docker-fair SCALARSQ attack) — pre-
7359                    // analysed PK-probe fast path. The per-row work is
7360                    // a read of outer.col from the row plus an index
7361                    // probe — no Expr clone, no walker, no
7362                    // `eval_expr_with_correlated` framework.
7363                    if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7364                        values.push(self.probe_with_pk_fast_path(fp, row));
7365                        continue;
7366                    }
7367                    // v7.39 (round 605) — the same value every row.
7368                    if any_proj_const && let Some(v) = &proj_const[i] {
7369                        values.push(v.clone());
7370                        continue;
7371                    }
7372                    // v7.39 (round 487) — bound column: read the cell.
7373                    // This is `rehydrate_cell`'s body for a non-composite
7374                    // column, which is what the whole chain below reduces
7375                    // to once the name has been resolved.
7376                    if any_proj_direct && let Some(pos) = proj_direct[i] {
7377                        crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7378                        values.push(row.values[pos].clone().into_owned());
7379                        continue;
7380                    }
7381                    // v7.24 (round-16 B) — correlated-aware.
7382                    // v7.37.x (docker-fair SCALARSQ attack) — share the
7383                    // per-row memo with projection. Required for the
7384                    // batch-evaluated correlated-scalar path to fire on
7385                    // SELECT-item scalar subqueries; otherwise each row
7386                    // re-executes the inner.
7387                    //
7388                    // Skip the memo when the outer row count is small
7389                    // (early-limited): the batch path scans the FULL
7390                    // inner table to build a GroupMap (~5 ms for a
7391                    // 12.5 k-row inner), while per-row execution with a
7392                    // PK index seek is ~5 µs per call — much cheaper for
7393                    // N ≤ ~1000 outer rows.
7394                    let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7395                    let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7396                    values.push(
7397                        self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7398                    );
7399                }
7400                crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7401                if stmt.distinct {
7402                    let bucket = seen_distinct
7403                        .entry(norm_hash_values(
7404                            &proj_buf,
7405                            &distinct_hb,
7406                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7407                        ))
7408                        .or_default();
7409                    if bucket.iter().any(|i| {
7410                        values_eq_norm(
7411                            &tagged[i].1.values,
7412                            &proj_buf,
7413                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7414                        )
7415                    }) {
7416                        crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7417                        return Ok(());
7418                    }
7419                    bucket.push(tagged.len());
7420                }
7421                let out = Row::new(core::mem::replace(
7422                    &mut proj_buf,
7423                    proj_pool.pop().unwrap_or_default(),
7424                ));
7425                let order_keys = if stmt.distinct && !order_by.is_empty() {
7426                    // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7427                    // the bound-cell path precisely so an ORDER BY key that
7428                    // names a column is READ instead of evaluated, and the
7429                    // non-DISTINCT branch above has passed it ever since;
7430                    // this branch never did, so `SELECT DISTINCT k .. ORDER
7431                    // BY k` resolved "k" by string for every surviving row.
7432                    let mut buf = key_pool.pop().unwrap_or_default();
7433                    crate::orderby::build_order_keys_bound(
7434                        &order_by,
7435                        &order_bound,
7436                        &order_colls,
7437                        row,
7438                        &ctx,
7439                        &mut buf,
7440                    )?;
7441                    buf
7442                } else {
7443                    order_keys
7444                };
7445                budget.charge(approx_row_bytes(&out))?;
7446                tagged.push((order_keys, out));
7447            }
7448            // Streaming top-N: bound the accumulator to O(keep) rows.
7449            if let Some((k, descs)) = &topk_stream {
7450                crate::orderby::topk_trim_recycling(
7451                    &mut tagged,
7452                    *k,
7453                    descs,
7454                    &mut proj_pool,
7455                    &mut key_pool,
7456                    &mut topk_boundary,
7457                );
7458                // The prefix follows the boundary it summarises.
7459                topk_boundary_prefix = topk_boundary
7460                    .as_ref()
7461                    .and_then(|b| b.first())
7462                    .and_then(crate::orderby::order_key_prefix);
7463            }
7464            Ok(())
7465        };
7466        // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7467        // load-bearing full-scan path. This is the primary single-table
7468        // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7469        // in-place writers retain dead/old versions, an ungated scan
7470        // here would return them, so the gate must land BEFORE the
7471        // writers flip (see the plan's activation-order rule). A no-op
7472        // today: every hot row is frozen or committed-and-alive under
7473        // the reader's snapshot, so `is_row_visible` returns true for
7474        // all of them (verified by the full e2e suite staying green).
7475        let scan_snapshot = self.current_snapshot();
7476        let mut emitted: usize = 0;
7477        if let Some(seeked) = &indexed_rows {
7478            let recheck = !seeked.exact;
7479            for (loop_idx, cow) in seeked.rows.iter().enumerate() {
7480                if let Some(cap) = early_cap
7481                    && emitted >= cap
7482                {
7483                    break;
7484                }
7485                process_row(cow.as_ref(), loop_idx, recheck)?;
7486                emitted = emitted.saturating_add(1);
7487            }
7488        } else {
7489            // v7.39 (round 570) — the row store is a 32-way trie, so
7490            // indexing it is four dependent loads. Round 567 measured
7491            // -18% on the aggregate scan from holding the leaf between
7492            // rows; this is the same loop for the projecting scan.
7493            let mut rows_cur = table.rows().run_cursor();
7494            // v7.38.11 — see the aggregate scan above: a BRIN index on a
7495            // column this WHERE bounds says which slots cannot match.
7496            let brin_slots = stmt
7497                .where_
7498                .as_ref()
7499                .and_then(|w| crate::brin::candidate_slots(w, table))
7500                .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7501            for i in brin_slots.into_iter().flatten() {
7502                if let Some(cap) = early_cap
7503                    && emitted >= cap
7504                {
7505                    break;
7506                }
7507                // Skip rows this snapshot cannot see (invisible rows do
7508                // not count toward the LIMIT).
7509                if !table.is_row_visible(i, &scan_snapshot) {
7510                    continue;
7511                }
7512                let Some(row) = rows_cur.get(i) else { continue };
7513                process_row(row, i, true)?;
7514                emitted = emitted.saturating_add(1);
7515            }
7516            // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7517            // rows into the same loop. The full-scan path here is the
7518            // load-bearing single-table SELECT executor, and pre-
7519            // 7.35.1 it only walked `table.rows()` (hot), so any
7520            // `SELECT … FROM t` against a table with cold segments
7521            // silently returned a subset.
7522            let cold_rows = self.iter_cold_rows_of_table(table);
7523            for (offset, row) in cold_rows.iter().enumerate() {
7524                if let Some(cap) = early_cap
7525                    && emitted >= cap
7526                {
7527                    break;
7528                }
7529                process_row(row, table.row_count() + offset, true)?;
7530                emitted = emitted.saturating_add(1);
7531            }
7532        }
7533
7534        // (DISTINCT already de-duped STREAMING inside process_row, so the
7535        // sort below only sees the u survivors and the partial-sort
7536        // budget applies to DISTINCT too.)
7537        if !order_by.is_empty() {
7538            // Partial-sort fast path: when LIMIT is small relative to
7539            // the row count, select_nth_unstable + sort just the
7540            // prefix is O(n + k log k) instead of O(n log n).
7541            // WITH TIES needs the full sort so the tie extension can
7542            // scan past `limit` to find rows that share the last-kept
7543            // row's key.
7544            let keep = if stmt.limit_with_ties
7545                // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7546                // forces the full-sort fallback by suppressing the
7547                // partial-sort `keep` budget. See
7548                // `xtests/sigil/test-mode-gucs.md`.
7549                || self.env_cfg().disable_topk
7550            {
7551                None
7552            } else {
7553                stmt.limit_literal()
7554                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7555            };
7556            let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7557            if let Some(cols) = &sort_by_output {
7558                // No keys were built; the sort reads the projected row.
7559                // The comparator is the value-level one the window
7560                // functions and the key path both defer to, so DESC,
7561                // NULLS placement, the MySQL fold and the collation are
7562                // not restated here.
7563                let terms: Vec<(usize, bool, Option<bool>)> = cols
7564                    .iter()
7565                    .zip(order_by.iter())
7566                    .map(|(c, o)| (*c, o.desc, o.nulls_first))
7567                    .collect();
7568                let mysql = ctx.mysql_dialect;
7569                // v7.38.19 — sort a PERMUTATION carrying the first eight
7570                // bytes, not the rows.
7571                //
7572                // The elements above are `(Vec<OrderKey>, Row)`, 48 bytes,
7573                // and driftsort moves them ~n log n times: 7.4 M moves at
7574                // 400,000 rows. Worse, every comparison chases three
7575                // dependent loads PER SIDE to reach the byte it wants --
7576                // the row's `Vec`, the `Value`, then the string's own
7577                // buffer -- and a profile of this sort put 35% of its
7578                // working samples in the sort machinery around that.
7579                //
7580                // A `(u64, u32)` is 16 bytes and the comparison reads it
7581                // straight out of the array. The u64 is the first eight
7582                // bytes big-endian, zero-padded, which ORDERS THE SAME as
7583                // the string: if two differ inside those bytes they differ
7584                // at the same index either way, and a string shorter than
7585                // eight pads with zeros exactly where `[u8]`'s own
7586                // comparison runs out. Equal prefixes fall through to the
7587                // full comparator, so nothing rests on the padding being
7588                // clever.
7589                //
7590                // The tail-break on the index is what keeps the sort
7591                // STABLE, which `sort_by` was giving for free and an
7592                // unstable sort over a permutation would not.
7593                // v7.38.19 — three ways to sort these rows, and which
7594                // one is right turns on the values, which is why it is
7595                // decided here rather than at plan time.
7596                //
7597                //   * the collation orders these values the way bytes do
7598                //     -- take the eight-byte key below
7599                //   * it does not, but there IS a collation -- build its
7600                //     sort key once per row and order the permutation on
7601                //     those, which is what the key path did, done from
7602                //     the projected value instead of during the scan
7603                //   * no collation at all -- the eight-byte key again
7604                //
7605                // The middle case is the one a draft got wrong by
7606                // leaving the rows to a key path whose keys it had just
7607                // skipped building.
7608                let mut keep_sorted = false;
7609                let bytes_answer = byte_order_answers_the_collation(&tagged, &terms, &order_colls);
7610                if !bytes_answer && let Some(coll) = order_colls.first().and_then(Option::as_ref) {
7611                    let (first_col, first_desc, _) = terms[0];
7612                    let mut order: Vec<(Vec<u8>, u32)> = Vec::with_capacity(tagged.len());
7613                    for (i, row) in tagged.iter().enumerate() {
7614                        let k = match row.1.values.get(first_col) {
7615                            Some(Value::Text(t)) => coll.sort_key_of(t).unwrap_or_else(|| {
7616                                let mut v = Vec::with_capacity(t.len() + 1);
7617                                v.push(0);
7618                                v.extend_from_slice(t.as_bytes());
7619                                v
7620                            }),
7621                            _ => Vec::new(),
7622                        };
7623                        order.push((k, u32::try_from(i).unwrap_or(u32::MAX)));
7624                    }
7625                    order.sort_by(|(ka, ia), (kb, ib)| {
7626                        let c = ka.cmp(kb);
7627                        let c = if first_desc { c.reverse() } else { c };
7628                        if c != core::cmp::Ordering::Equal {
7629                            return c;
7630                        }
7631                        row_cmp_by_index(&tagged, &terms, &order_colls, mysql, *ia, *ib)
7632                            .then_with(|| ia.cmp(ib))
7633                    });
7634                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7635                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7636                    tagged = order
7637                        .iter()
7638                        .map(|&(_, i)| {
7639                            slots[i as usize]
7640                                .take()
7641                                .expect("the permutation names each row once")
7642                        })
7643                        .collect();
7644                    keep_sorted = true;
7645                }
7646                // v7.38.20 — a key that does NOT discriminate is still
7647                // worth sorting on, as long as the runs it leaves are
7648                // handled once instead of n log n times.
7649                //
7650                // `text (26 values)` is two hundred identical characters
7651                // drawn from twenty-six letters, so every eight-byte
7652                // prefix inside a letter is the same and 15,384 rows tie
7653                // on it. A comparison sort then asks ~7.4 M questions of
7654                // which nearly all are a two-hundred-byte `memcmp`
7655                // answering EQUAL: profiled, 30% of the working samples
7656                // sat in `memcmp` and 37% in the sort machinery.
7657                //
7658                // Sorting the integer keys is cheap. What each run needs
7659                // afterwards is ONE pass: if every value in it is equal,
7660                // input order already IS the stable answer, and proving
7661                // that costs n-1 comparisons rather than n log n. Only a
7662                // run that is not all-equal gets sorted.
7663                //
7664                // Single-term only. With a second ORDER BY column an
7665                // all-equal first term does not settle the row order --
7666                // the later terms still speak -- and the shortcut would
7667                // drop them.
7668                let all_keys = if keep_sorted {
7669                    None
7670                } else {
7671                    sort_keys_of(&tagged, terms[0].0)
7672                };
7673                let low_card = !keep_sorted
7674                    && terms.len() == 1
7675                    && all_keys
7676                        .as_ref()
7677                        .is_some_and(|(keys, exact)| !*exact && !key_discriminates(keys));
7678                let keyed =
7679                    all_keys.filter(|(keys, exact)| *exact || key_discriminates(keys) || low_card);
7680                if keep_sorted {
7681                    // The collated permutation above already placed every
7682                    // row. A draft let the byte-order fallback run after
7683                    // it and undo the whole thing.
7684                } else if let Some((mut order, exact)) = keyed {
7685                    let (first_col, first_desc, _) = terms[0];
7686                    let row_cmp = |ia: u32, ib: u32| -> core::cmp::Ordering {
7687                        let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
7688                        for (col, desc, nf) in &terms {
7689                            let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col))
7690                            else {
7691                                continue;
7692                            };
7693                            let ord = match (va, vb) {
7694                                (Value::Text(x), Value::Text(y)) if !mysql => {
7695                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7696                                    if *desc { c.reverse() } else { c }
7697                                }
7698                                _ => {
7699                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7700                                }
7701                            };
7702                            if ord != core::cmp::Ordering::Equal {
7703                                return ord;
7704                            }
7705                        }
7706                        core::cmp::Ordering::Equal
7707                    };
7708                    let _ = first_col;
7709                    if low_card {
7710                        // Integer sort first, then one pass per run.
7711                        order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
7712                            let c = pa.cmp(&pb);
7713                            let c = if first_desc { c.reverse() } else { c };
7714                            c.then_with(|| ia.cmp(&ib))
7715                        });
7716                        let mut lo = 0;
7717                        while lo < order.len() {
7718                            let mut hi = lo + 1;
7719                            while hi < order.len() && order[hi].0 == order[lo].0 {
7720                                hi += 1;
7721                            }
7722                            if hi - lo > 1 {
7723                                let head = tagged[order[lo].1 as usize].1.values.get(first_col);
7724                                let uniform = order[lo + 1..hi].iter().all(|&(_, i)| {
7725                                    tagged[i as usize].1.values.get(first_col) == head
7726                                });
7727                                if !uniform {
7728                                    order[lo..hi].sort_by(|&(_, ia), &(_, ib)| {
7729                                        row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
7730                                    });
7731                                }
7732                                // A uniform run is already in index
7733                                // order, which IS the stable answer.
7734                            }
7735                            lo = hi;
7736                        }
7737                    } else {
7738                        order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
7739                            let c = pa.cmp(&pb);
7740                            let c = if first_desc { c.reverse() } else { c };
7741                            if c != core::cmp::Ordering::Equal {
7742                                return c;
7743                            }
7744                            // An EXACT key that ties means the values are
7745                            // equal, so only the remaining terms can speak.
7746                            // A prefix that ties has decided nothing yet and
7747                            // the first term must be asked again, which
7748                            // `row_cmp` does by walking every term from the
7749                            // start.
7750                            if exact && terms.len() == 1 {
7751                                return ia.cmp(&ib);
7752                            }
7753                            row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
7754                        });
7755                    }
7756                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7757                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7758                    tagged = order
7759                        .iter()
7760                        .map(|&(_, i)| {
7761                            slots[i as usize]
7762                                .take()
7763                                .expect("the permutation names each row once")
7764                        })
7765                        .collect();
7766                } else {
7767                    tagged.sort_by(|a, b| {
7768                        for (i, (col, desc, nf)) in terms.iter().enumerate() {
7769                            let va = a.1.values.get(*col);
7770                            let vb = b.1.values.get(*col);
7771                            let (Some(va), Some(vb)) = (va, vb) else {
7772                                continue;
7773                            };
7774                            let _ = i;
7775                            // v7.38.19 — two non-NULL strings, no MySQL fold, is
7776                            // where a text sort spends every one of its ~7 M
7777                            // comparisons, and the shared comparator cannot be
7778                            // inlined into this loop: it carries NULL placement,
7779                            // the fold, the NUMERIC bignum gate and the float
7780                            // total order. Answering that one pair here is the
7781                            // same answer by the same route — `value_cmp`'s
7782                            // leading same-variant arm is `x.cmp(y)`, and the
7783                            // raw comparator's last act is this reverse.
7784                            let ord = match (va, vb) {
7785                                (Value::Text(x), Value::Text(y)) if !mysql => {
7786                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7787                                    if *desc { c.reverse() } else { c }
7788                                }
7789                                _ => {
7790                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7791                                }
7792                            };
7793                            if ord != core::cmp::Ordering::Equal {
7794                                return ord;
7795                            }
7796                        }
7797                        core::cmp::Ordering::Equal
7798                    });
7799                }
7800            } else {
7801                crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7802            }
7803        }
7804
7805        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7806        // past the truncated tail through every row that shares the
7807        // last-kept row's ORDER BY key. The tie check uses the
7808        // already-computed `(order_keys, row)` pairs so it matches
7809        // the sort comparator exactly. DISTINCT + WITH TIES falls
7810        // through to the no-ties path (PG also disallows their
7811        // combination; SPG silently drops the tie extension here so
7812        // the customer doesn't see a hard error mid-query — the
7813        // user-visible result is still correct, just narrower).
7814        let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7815            apply_offset_and_limit_tagged(
7816                &mut tagged,
7817                stmt.offset_literal(),
7818                stmt.limit_literal(),
7819                true,
7820            );
7821            tagged.into_iter().map(|(_, r)| r).collect()
7822        } else {
7823            // DISTINCT already de-duped pre-sort above.
7824            let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7825            apply_offset_and_limit(
7826                &mut output_rows,
7827                stmt.offset_literal(),
7828                stmt.limit_literal(),
7829            );
7830            output_rows
7831        };
7832
7833        let columns: Vec<ColumnSchema> = projection
7834            .into_iter()
7835            .map(|p| p.to_column_schema())
7836            .collect();
7837
7838        Ok(QueryResult::Rows {
7839            columns,
7840            rows: output_rows,
7841        })
7842    }
7843
7844    /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7845    /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7846    /// select items for the surviving rows only — PG's Result-above-
7847    /// Limit shape, where SubPlan loops equal the OUTPUT row count
7848    /// (50) instead of the group count (24k).
7849    fn finish_agg_result(
7850        &self,
7851        mut agg: aggregate::AggResult,
7852        stmt: &SelectStatement,
7853        cancel: CancelToken<'_>,
7854    ) -> Result<QueryResult, EngineError> {
7855        apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7856        if !agg.deferred.is_empty() {
7857            apply_offset_and_limit(
7858                &mut agg.synth_rows,
7859                stmt.offset_literal(),
7860                stmt.limit_literal(),
7861            );
7862            let ctx = EvalContext::new(&agg.synth_schema, None);
7863            let mut memo = memoize::MemoizeCache::default();
7864            // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7865            // Deferred subqueries are referenced only by surviving
7866            // select-list rows (≤ LIMIT), so their correlation keys are
7867            // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7868            // each batchable subquery's group map over just those keys
7869            // via per-key index seek; the per-row splice loop below then
7870            // reuses the seeded map. A join-shaped or un-indexed inner
7871            // falls through to the all-keys batch inside the call (built
7872            // eagerly here instead of lazily on row 0 — same cost), so
7873            // it still pays the full scan, never the 715 ms per-row
7874            // direct eval; its index-nested-loop probe is the next
7875            // knife. Genuinely non-batchable shapes return None and are
7876            // left unseeded for the loop's per-row resolver, as before.
7877            for (_, expr) in &agg.deferred {
7878                let mut subs: Vec<&SelectStatement> = Vec::new();
7879                collect_scalar_subqueries(expr, &mut subs);
7880                for sub in subs {
7881                    let repr = alloc::format!("{sub}");
7882                    if memo.group_maps.contains_key(&repr) {
7883                        continue;
7884                    }
7885                    if let Some(gm) = self.try_batch_correlated_scalar(
7886                        sub,
7887                        Some((&agg.synth_rows, &ctx)),
7888                        cancel,
7889                    )? {
7890                        memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7891                    }
7892                }
7893            }
7894            for (ri, srow) in agg.synth_rows.iter().enumerate() {
7895                cancel.check()?;
7896                for (col, expr) in &agg.deferred {
7897                    let v =
7898                        self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7899                    if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7900                        *cell = v;
7901                    }
7902                }
7903            }
7904        }
7905        Ok(QueryResult::Rows {
7906            columns: agg.columns,
7907            rows: agg.rows,
7908        })
7909    }
7910
7911    /// v7.37 — streaming projection for the joined-non-aggregate
7912    /// shape (multi-table FROM, all projection items bound, no
7913    /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
7914    /// UNION). Walks the deferred join survivors and emits
7915    /// `&[&Value]` borrowed straight out of the source tables — no
7916    /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
7917    /// on the mailrs `PROJ` shape (about 4 ms saved).
7918    ///
7919    /// Returns `Ok(None)` when the shape doesn't qualify; the caller
7920    /// then falls back to the materialising path.
7921    /// v7.37 (round 831) — stream a joinless SELECT straight off the
7922    /// stored table, one row at a time, without ever building a row set.
7923    ///
7924    /// Returns `Ok(None)` for anything this cannot serve, and the caller
7925    /// falls through to the deferred-join path exactly as before: a
7926    /// missing table, or a cold tier whose hydration the fallback handles.
7927    /// Sort a single-table scan through the external sorter, so the
7928    /// answer's size is bounded by `work_mem` and not by the input.
7929    ///
7930    /// Sorting held every row twice — the scan's `Vec<Row>` and the
7931    /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
7932    /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
7933    /// enough ORDER BY took the server down, which is a liveness
7934    /// problem before it is a performance one.
7935    ///
7936    /// A SEPARATE walk rather than a change to `run_single_table_scan`,
7937    /// following what round 831 did for the joinless shape. That
7938    /// function is 552 lines whose projection loop is entangled with
7939    /// DISTINCT (which indexes back into the tagged vector) and with
7940    /// streaming top-N (whose boundary moves as the scan runs); both
7941    /// assume the projection has already happened when a row is
7942    /// pushed, which is exactly what spilling has to defer. Two earlier
7943    /// attempts tried to rework that loop and were reverted. Here the
7944    /// existing path is untouched and this one only claims shapes it
7945    /// can serve, so a decline costs nothing.
7946    ///
7947    /// Records are SOURCE rows, not projected ones: `finish` re-derives
7948    /// keys from what it decodes, and an ORDER BY key need not be in
7949    /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
7950    fn try_spill_sorted_scan(
7951        &self,
7952        stmt: &SelectStatement,
7953        from: &FromClause,
7954        cancel: CancelToken<'_>,
7955    ) -> Result<Option<QueryResult>, EngineError> {
7956        // Shapes this walk does not serve. Each one either needs the
7957        // whole tagged vector addressable (DISTINCT probes back into
7958        // it, WITH TIES re-reads its tail) or is already bounded
7959        // without spilling (a LIMIT makes the partial sort O(keep)).
7960        if !self.can_spill()
7961            || stmt.order_by.is_empty()
7962            || stmt.distinct
7963            || stmt.limit_with_ties
7964            || stmt.limit_literal().is_some()
7965            || !from.joins.is_empty()
7966            || from.primary.lateral_subquery.is_some()
7967            || from.primary.unnest_expr.is_some()
7968            || from.primary.generate_series_args.is_some()
7969            || select_has_window(stmt)
7970        {
7971            return Ok(None);
7972        }
7973        // A parent's rows are its children's. These walks scan the named
7974        // relation alone, so a partitioned or inherited parent comes back
7975        // short — and silently: the corpus caught `SELECT id FROM pr
7976        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
7977        // parent's own rows instead of the partitions'. `ONLY` is exactly
7978        // the case that does not fan out, so it stays, which is the test
7979        // the FROM-clause fan-out itself makes.
7980        if !from.primary.only
7981            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7982        {
7983            return Ok(None);
7984        }
7985        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7986            return Ok(None);
7987        };
7988        // Cold-tier rows live outside `rows()`; this walk would drop
7989        // them silently, the same reason round 831's walk declines.
7990        if table.has_cold_rows_fast() {
7991            return Ok(None);
7992        }
7993
7994        let alias = from
7995            .primary
7996            .alias
7997            .as_deref()
7998            .unwrap_or(from.primary.name.as_str());
7999        let cols = table.schema().columns.clone();
8000        let sess = self.dml_session();
8001        let ctx = EvalContext::new(&cols, Some(alias))
8002            .with_catalog(self.active_catalog())
8003            .with_session(&sess);
8004        let projection = build_projection(
8005            &stmt.items,
8006            &cols,
8007            alias,
8008            self.speaks_mysql,
8009            Some(self.active_catalog()),
8010        )?;
8011        let order_by = stmt.order_by.clone();
8012        // The same one-shot resolution the general path does (round
8013        // 582): each ORDER BY column is bound once, not once per row.
8014        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8015        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8016        // Resolved BEFORE the scan, because it now decides what the sort
8017        // STORES and not just what it decodes (round 995).
8018        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8019
8020        // v7.38.22 — resolved HERE, because this path did not resolve
8021        // them at all.
8022        //
8023        // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8024        // "en_US.utf8"` in BYTE order on this path — and swallowed an
8025        // unknown collation name rather than raising — because the sorter
8026        // below compared with an empty collation slice. The materialising
8027        // path honoured both. Which answer a query got depended on which
8028        // path the planner took, and this is the path a plain single-table
8029        // SELECT takes.
8030        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
8031        let mut sorter = crate::extsort::ExternalSorter::new(
8032            self.temp_run_factory,
8033            self.session_work_mem_bytes(),
8034            cols.clone(),
8035            &descs,
8036            &order_colls,
8037        )
8038        .with_stats(&self.spill_stats)
8039        .with_pruned(&needed);
8040        let snapshot = self.current_snapshot();
8041        // One key buffer for the whole scan: `push` drains it and leaves
8042        // the capacity behind.
8043        let mut keys: Vec<OrderKey> = Vec::new();
8044        // r1024 — compile the predicate once for the scan.
8045        //
8046        // These two sorted-spill scans are the paths a single-table SELECT
8047        // with an ORDER BY takes, and they were the last row-returning ones
8048        // still walking the expression tree per row. r1023 did the
8049        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8050        // exactly this shape.
8051        //
8052        // Found from the profile's CALL TREE rather than its leaves. The
8053        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8054        // 261, `mod_op` 178 — and two attempts at reasoning out which
8055        // function asked for it were both wrong. The tree names the caller
8056        // chain, and it named this one.
8057        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8058            .where_
8059            .as_ref()
8060            .filter(|w| crate::eval::fully_compilable(w))
8061            .map(|w| crate::eval::compile_expr(w, &ctx));
8062        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8063        for (i, row) in table.scan_visible_from(0, &snapshot) {
8064            if i.is_multiple_of(256) {
8065                cancel.check()?;
8066            }
8067            if let Some(c) = &compiled_where {
8068                if !crate::eval::compiled::eval_compiled_pred(
8069                    c,
8070                    row,
8071                    &ctx,
8072                    &mut eval_stack,
8073                    ctx.mysql_dialect,
8074                )? {
8075                    continue;
8076                }
8077            } else if let Some(w) = &stmt.where_ {
8078                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8079                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8080                    continue;
8081                }
8082            }
8083            keys.clear();
8084            // The same collations the sorter compares with, and the
8085            // re-derivation below is handed the same ones. `finish`'s
8086            // contract is that a key comes back the way it was pushed;
8087            // a collation is part of the way it was pushed.
8088            crate::orderby::build_order_keys_bound(
8089                &order_by,
8090                &order_bound,
8091                &order_colls,
8092                row,
8093                &ctx,
8094                &mut keys,
8095            )?;
8096            sorter.push(&mut keys, row)?;
8097        }
8098
8099        let key_ctx = &ctx;
8100        let rows = sorter.finish(
8101            |src, buf| {
8102                crate::orderby::build_order_keys_rederived(
8103                    &order_by,
8104                    &order_bound,
8105                    &order_colls,
8106                    src,
8107                    key_ctx,
8108                    buf,
8109                )
8110            },
8111            |src| {
8112                let mut values = Vec::with_capacity(projection.len());
8113                for p in &projection {
8114                    values.push(
8115                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8116                    );
8117                }
8118                Ok(Row::new(values))
8119            },
8120        )?;
8121
8122        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8123        Ok(Some(QueryResult::Rows { columns, rows }))
8124    }
8125
8126    /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
8127    /// handing each row to the consumer instead of collecting the answer.
8128    ///
8129    /// That walk bounds the SORT and then returns `QueryResult::Rows`,
8130    /// which holds every output row. Measured at `work_mem = 4 MB` over
8131    /// 200-byte rows, RSS above the server's own baseline while the
8132    /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
8133    /// at 400k — linear — while the spill underneath worked correctly
8134    /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
8135    /// removes each file, so a count taken afterwards reads 0 whatever
8136    /// happened, and an earlier reading of "no spill at all" was that
8137    /// blind witness). The growth is the collected result, not the sort.
8138    ///
8139    /// Emitting makes peak the budget, one buffer per run and a single
8140    /// row — the state a merge already holds at every step. It also
8141    /// frees each projected row as the next is built rather than
8142    /// accumulating them, which is where the time is: a profile of the
8143    /// collecting walk put the allocator at 586 samples, more than every
8144    /// sort comparison combined (420), against 19 for `push` itself.
8145    /// v7.37 (round 923) — which of a sort record's columns the output half
8146    /// reads. The record is the SOURCE row (round 836), so a narrow projection
8147    /// decoded every column: skipping one 200-byte text halves a decode
8148    /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
8149    ///
8150    /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
8151    /// column reads NULL. Answers only when every projection item is a bare
8152    /// column reference AND every ORDER BY key is a bound column; anything
8153    /// else returns empty, decoding everything as before.
8154    /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
8155    /// drops references from expression kinds it does not enumerate.
8156    ///
8157    /// ORDER BY columns are included — the merge re-derives keys from the
8158    /// decoded row on the spilled path, so pruning one would sort NULLs.
8159    pub(crate) fn sort_record_columns_needed(
8160        items: &[SelectItem],
8161        order_bound: &[Option<usize>],
8162        arity: usize,
8163        ctx: &EvalContext,
8164    ) -> Vec<bool> {
8165        let all_bare = items.iter().all(|i| {
8166            matches!(
8167                i,
8168                SelectItem::Expr {
8169                    expr: Expr::Column(_),
8170                    ..
8171                }
8172            )
8173        });
8174        if !all_bare || order_bound.iter().any(Option::is_none) {
8175            return Vec::new();
8176        }
8177        let mut mask = alloc::vec![false; arity];
8178        for item in items {
8179            if let SelectItem::Expr {
8180                expr: Expr::Column(c),
8181                ..
8182            } = item
8183            {
8184                match crate::eval::find_column_pos(c, ctx) {
8185                    Some(p) if p < arity => mask[p] = true,
8186                    _ => return Vec::new(),
8187                }
8188            }
8189        }
8190        for p in order_bound.iter().flatten() {
8191            if *p < arity {
8192                mask[*p] = true;
8193            } else {
8194                return Vec::new();
8195            }
8196        }
8197        mask
8198    }
8199
8200    /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
8201    /// of sorting.
8202    ///
8203    /// PG serves such an ordering from the index and never sorts. We sorted:
8204    /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
8205    /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
8206    /// the sorter's own round trip — `ExternalSorter::finish_each` →
8207    /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
8208    /// Every row is encoded into the sorter's arena and decoded back out,
8209    /// for an order the index already holds.
8210    ///
8211    /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
8212    /// because it was built for top-N. This is the unbounded sibling.
8213    ///
8214    /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
8215    /// from a btree, so walking one would silently drop those rows. That is
8216    /// exactly the defect r1020 fixed on the top-N path, where it had
8217    /// shipped.
8218    /// r1044 — the index this statement's ORDER BY can be WALKED on,
8219    /// instead of sorted, or `None`.
8220    ///
8221    /// Extracted so `EXPLAIN` can ask the same question the executor
8222    /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
8223    /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
8224    /// while the executor walked the primary key — 34.9 ms against
8225    /// 147.0 for the same query ordered by an unindexed column, so the
8226    /// walk was plainly running. Round 551 fixed a different case of
8227    /// this and wrote the reason down: EXPLAIN is the first thing any
8228    /// performance question opens, and an instrument that misnames the
8229    /// access path is worse than one that says nothing.
8230    ///
8231    /// The gate is here once. Two copies of it is how the plan and the
8232    /// executor come to disagree again.
8233    pub(crate) fn index_order_walk_target(
8234        &self,
8235        stmt: &SelectStatement,
8236        from: &FromClause,
8237    ) -> Option<(String, usize)> {
8238        if stmt.order_by.len() != 1
8239            || !stmt.distinct_on.is_empty()
8240            || stmt.limit_with_ties
8241            || stmt.limit.is_some()
8242            || stmt.offset.is_some()
8243            || stmt.having.is_some()
8244            || stmt.group_by.is_some()
8245            || !stmt.unions.is_empty()
8246            || !from.joins.is_empty()
8247            || from.primary.lateral_subquery.is_some()
8248            || from.primary.unnest_expr.is_some()
8249            || from.primary.as_of_segment.is_some()
8250            || from.primary.generate_series_args.is_some()
8251            || select_has_window(stmt)
8252            || aggregate::uses_aggregate(stmt)
8253        {
8254            return None;
8255        }
8256        if stmt
8257            .items
8258            .iter()
8259            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8260        {
8261            return None;
8262        }
8263        let table = self.active_catalog().get(&from.primary.name)?;
8264        if table.has_cold_rows_fast() {
8265            return None;
8266        }
8267        if !from.primary.only
8268            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8269        {
8270            return None;
8271        }
8272        let alias = from
8273            .primary
8274            .alias
8275            .as_deref()
8276            .unwrap_or(from.primary.name.as_str());
8277        let cols = &table.schema().columns;
8278        let order = &stmt.order_by[0];
8279        let Expr::Column(oc) = &order.expr else {
8280            return None;
8281        };
8282        if let Some(q) = &oc.qualifier
8283            && !q.eq_ignore_ascii_case(alias)
8284        {
8285            return None;
8286        }
8287        let order_pos = cols
8288            .iter()
8289            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8290        // r1047 — DISTINCT joins the walk when the projection IS the
8291        // order column, and only then. The index's keys are canonical
8292        // (r1039: representation equality is value equality — the
8293        // property every seek already depends on), so one key is one
8294        // distinct value and the walk can emit the first passing row of
8295        // each key group instead of hashing every row. On the release
8296        // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
8297        // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
8298        // with an ablation floor of 14.8, because the hash must
8299        // normalize and probe ALL the rows; the walk visits each key
8300        // once. A wider projection makes DISTINCT about the whole tuple,
8301        // not the key, so anything else still declines.
8302        if stmt.distinct {
8303            let only_the_order_column = stmt.items.len() == 1
8304                && match &stmt.items[0] {
8305                    SelectItem::Expr {
8306                        expr: Expr::Column(c),
8307                        ..
8308                    } => {
8309                        c.name.eq_ignore_ascii_case(&oc.name)
8310                            && match &c.qualifier {
8311                                Some(q) => q.eq_ignore_ascii_case(alias),
8312                                None => true,
8313                            }
8314                    }
8315                    _ => false,
8316                };
8317            if !only_the_order_column {
8318                return None;
8319            }
8320        }
8321        // r1046 — a nullable key no longer refuses the walk; it changes
8322        // what the walk has to do. A NULL key is not in the btree, so
8323        // walking alone would silently drop those rows — the r1020
8324        // defect, which shipped once. The walk emits them separately, at
8325        // the end SQL puts them.
8326        //
8327        // Refusing was costing every nullable indexed column a 3.4x:
8328        // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
8329        // 72.0 ms with the column nullable and 20.2 with the same data
8330        // under NOT NULL. `NOT NULL` is not the default, so that was the
8331        // common case paying for the uncommon one.
8332        let index = table.index_on(order_pos)?;
8333        if !matches!(index.kind, spg_storage::IndexKind::BTree(_))
8334            || index.expression.is_some()
8335            || index.partial_predicate.is_some()
8336        {
8337            return None;
8338        }
8339        Some((index.name.clone(), order_pos))
8340    }
8341
8342    fn try_index_order_stream<F>(
8343        &self,
8344        stmt: &SelectStatement,
8345        from: &FromClause,
8346        cancel: CancelToken<'_>,
8347        emit: &mut F,
8348    ) -> Result<Option<usize>, EngineError>
8349    where
8350        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8351    {
8352        // r1044 — the shape gate lives in `index_order_walk_target`, so
8353        // `EXPLAIN` answers the same question. What stays here is the
8354        // part that RAISES (an illegal ORDER BY has to keep erroring
8355        // from where it did) and the bindings the walk needs.
8356        crate::orderby::check_order_by_legality(stmt)?;
8357        crate::orderby::check_order_by_positions(stmt)?;
8358        crate::window::reject_window_in_row_clauses(stmt)?;
8359        let Some((_, order_pos)) = self.index_order_walk_target(stmt, from) else {
8360            return Ok(None);
8361        };
8362        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8363            return Ok(None);
8364        };
8365        let alias = from
8366            .primary
8367            .alias
8368            .as_deref()
8369            .unwrap_or(from.primary.name.as_str());
8370        let cols = table.schema().columns.clone();
8371        let order = &stmt.order_by[0];
8372        let Some(index) = table.index_on(order_pos) else {
8373            return Ok(None);
8374        };
8375
8376        let sess = self.dml_session();
8377        let ctx = EvalContext::new(&cols, Some(alias))
8378            .with_catalog(self.active_catalog())
8379            .with_session(&sess);
8380        let projection = build_projection(
8381            &stmt.items,
8382            &cols,
8383            alias,
8384            self.speaks_mysql,
8385            Some(self.active_catalog()),
8386        )?;
8387        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8388        emit(crate::StreamItem::Header(&columns))?;
8389        let bound_pos: Vec<Option<usize>> = projection
8390            .iter()
8391            .map(|p| match &p.expr {
8392                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8393                    Ok(Some(pos)) => Some(pos),
8394                    _ => None,
8395                },
8396                _ => None,
8397            })
8398            .collect();
8399
8400        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8401            .where_
8402            .as_ref()
8403            .filter(|w| crate::eval::fully_compilable(w))
8404            .map(|w| crate::eval::compile_expr(w, &ctx));
8405        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8406        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8407        let snapshot = self.current_snapshot();
8408
8409        // A btree holds one locator per row VERSION, so a row whose key was
8410        // updated can sit under two keys and a dead one can sit beside its
8411        // replacement. The visibility gate drops the dead; `seen` drops a
8412        // live row that the walk reaches twice, which would otherwise be a
8413        // duplicated output row rather than a slow one.
8414        let mut emitted_rows = alloc::vec![false; table.rows().len()];
8415
8416        // r1046 — the rows the index cannot hold.
8417        //
8418        // A NULL key is not in the btree, so the walk below never reaches
8419        // those rows; they are emitted here, at the end SQL puts them.
8420        // PG's default is NULLS LAST ascending and NULLS FIRST
8421        // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
8422        // the same rule `order_by_value_cmp_raw` applies to the sort this
8423        // replaces, so the two orders agree.
8424        //
8425        // Finding them costs one pass over the column. That pass is why
8426        // this is still worth doing: the sort it replaces encodes and
8427        // decodes every row, and the walk plus the pass measured 72.0 ms
8428        // down to about 22 on 400,000 rows.
8429        let nulls_first = order.nulls_first.unwrap_or(order.desc);
8430        // r1047 — under DISTINCT the walk emits the FIRST passing row of
8431        // each key group and skips the rest; the gate admits DISTINCT
8432        // only when the projection is the order column itself, so one
8433        // canonical key is one output row. NULL is one distinct value,
8434        // so the NULL pass stops at its first emit too.
8435        let distinct = stmt.distinct;
8436        let mut count = 0usize;
8437        let mut visited = 0usize;
8438        let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
8439                                  eval_stack: &mut Vec<Value<'static>>,
8440                                  values: &mut Vec<Value<'static>>,
8441                                  visited: &mut usize,
8442                                  emit: &mut F|
8443         -> Result<usize, EngineError> {
8444            if !cols[order_pos].nullable {
8445                return Ok(0);
8446            }
8447            let mut n = 0usize;
8448            for (ri, row) in table.rows().iter().enumerate() {
8449                if !matches!(row.values.get(order_pos), Some(Value::Null)) {
8450                    continue;
8451                }
8452                if emitted_rows.get(ri).copied().unwrap_or(true) {
8453                    continue;
8454                }
8455                if !table.is_row_visible(ri, &snapshot) {
8456                    continue;
8457                }
8458                *visited += 1;
8459                if visited.is_multiple_of(256) {
8460                    cancel.check()?;
8461                }
8462                emitted_rows[ri] = true;
8463                if Self::stream_project_row(
8464                    row,
8465                    stmt.where_.as_ref(),
8466                    compiled_where.as_ref(),
8467                    eval_stack,
8468                    &projection,
8469                    &bound_pos,
8470                    &ctx,
8471                    values,
8472                    emit,
8473                )? {
8474                    n += 1;
8475                    if distinct {
8476                        break;
8477                    }
8478                }
8479            }
8480            Ok(n)
8481        };
8482
8483        if nulls_first {
8484            count += emit_null_rows(
8485                &mut emitted_rows,
8486                &mut eval_stack,
8487                &mut values,
8488                &mut visited,
8489                emit,
8490            )?;
8491        }
8492
8493        let walker: alloc::boxed::Box<
8494            dyn Iterator<Item = (&spg_storage::IndexKey, &spg_storage::PostingList)>,
8495        > = if order.desc {
8496            alloc::boxed::Box::new(index.iter_desc())
8497        } else {
8498            alloc::boxed::Box::new(index.iter_asc())
8499        };
8500        for (_key, locators) in walker {
8501            for loc in locators {
8502                let spg_storage::RowLocator::Hot(ri) = *loc else {
8503                    continue;
8504                };
8505                if emitted_rows.get(ri).copied().unwrap_or(true) {
8506                    continue;
8507                }
8508                if !table.is_row_visible(ri, &snapshot) {
8509                    continue;
8510                }
8511                let Some(row) = table.rows().get(ri) else {
8512                    continue;
8513                };
8514                visited += 1;
8515                if visited.is_multiple_of(256) {
8516                    cancel.check()?;
8517                }
8518                emitted_rows[ri] = true;
8519                if Self::stream_project_row(
8520                    row,
8521                    stmt.where_.as_ref(),
8522                    compiled_where.as_ref(),
8523                    &mut eval_stack,
8524                    &projection,
8525                    &bound_pos,
8526                    &ctx,
8527                    &mut values,
8528                    emit,
8529                )? {
8530                    count += 1;
8531                    // One row per key group: the rest are the same value.
8532                    if distinct {
8533                        break;
8534                    }
8535                }
8536            }
8537        }
8538
8539        if !nulls_first {
8540            count += emit_null_rows(
8541                &mut emitted_rows,
8542                &mut eval_stack,
8543                &mut values,
8544                &mut visited,
8545                emit,
8546            )?;
8547        }
8548        Ok(Some(count))
8549    }
8550
8551    /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
8552    /// building an `OrderKey` vector per row.
8553    ///
8554    /// The row-returning sorted scan allocates twice per row: one
8555    /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
8556    /// projection. Counted over 400 k rows (r1030,
8557    /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
8558    /// allocations and 208 MB of traffic for an answer of four hundred
8559    /// thousand integers.
8560    ///
8561    /// The key half is pure ceremony on this shape.
8562    /// `sort_tagged_by_inline_int_key` already sorts indices rather than
8563    /// rows, so the per-row vector is built, has one integer taken out of
8564    /// it, and is then dragged through the permutation — it exists to carry
8565    /// a number the row's column already held. This lane carries the number
8566    /// instead, in a fixed-size array that lives inside the buffer element
8567    /// and allocates nothing. Same idea as the predicate VM's integer lane.
8568    ///
8569    /// Declines to `None` for anything it does not cover, and every caller
8570    /// falls through to the general path, so the gate list is the
8571    /// specification.
8572    ///
8573    /// Ties: equal keys keep scan order, as the stable sort on the general
8574    /// path does. Rows that tie on every ORDER BY term are entitled to any
8575    /// order among themselves either way — see `STABILITY.md`.
8576    fn try_int_key_sorted_stream<F>(
8577        &self,
8578        stmt: &SelectStatement,
8579        from: &FromClause,
8580        cancel: CancelToken<'_>,
8581        emit: &mut F,
8582    ) -> Result<Option<usize>, EngineError>
8583    where
8584        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8585    {
8586        /// Sort terms this lane carries inline. Four covers every ORDER BY
8587        /// in the endpoint sweep and in the dogfood corpus; wider ones fall
8588        /// through rather than growing the buffer element for everybody.
8589        const MAX_KEYS: usize = 4;
8590
8591        if stmt.order_by.is_empty()
8592            || stmt.order_by.len() > MAX_KEYS
8593            // v7.38.14 — DISTINCT is admitted when the projected set is
8594            // exactly the ORDER BY set, and only then. This lane sorts, and
8595            // when the sort key determines the projected row every duplicate
8596            // lands ADJACENT to its twin -- so the de-duplication is a
8597            // comparison with the previous row rather than a hash table, and
8598            // the reason this lane declined DISTINCT disappears with it. The
8599            // seen-set it could not offer held indices into a materialised
8600            // vector; there is no seen-set now.
8601            //
8602            // The gate is as narrow as the bare-GROUP-BY rewrite's for the
8603            // same reason: `ORDER BY a` over a projection of `a, b` does NOT
8604            // place duplicates of the PAIR adjacent, so set EQUALITY, never
8605            // overlap.
8606            || (stmt.distinct && !Self::distinct_is_adjacent_after_sort(stmt))
8607            || stmt.limit_with_ties
8608            || stmt.limit.is_some()
8609            || stmt.offset.is_some()
8610            || stmt.having.is_some()
8611            || stmt.group_by.is_some()
8612            || !stmt.unions.is_empty()
8613            || !from.joins.is_empty()
8614            || from.primary.lateral_subquery.is_some()
8615            || from.primary.unnest_expr.is_some()
8616            || from.primary.as_of_segment.is_some()
8617            || from.primary.generate_series_args.is_some()
8618            || select_has_window(stmt)
8619            || aggregate::uses_aggregate(stmt)
8620        {
8621            return Ok(None);
8622        }
8623        if stmt
8624            .items
8625            .iter()
8626            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8627        {
8628            return Ok(None);
8629        }
8630        crate::orderby::check_order_by_legality(stmt)?;
8631        crate::orderby::check_order_by_positions(stmt)?;
8632        crate::window::reject_window_in_row_clauses(stmt)?;
8633        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8634            return Ok(None);
8635        };
8636        if table.has_cold_rows_fast() {
8637            return Ok(None);
8638        }
8639        if !from.primary.only
8640            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8641        {
8642            return Ok(None);
8643        }
8644        let alias = from
8645            .primary
8646            .alias
8647            .as_deref()
8648            .unwrap_or(from.primary.name.as_str());
8649        let cols = table.schema().columns.clone();
8650
8651        // Every ORDER BY term must be a NOT NULL integer column of this
8652        // table. NOT NULL is what lets the key be a bare integer: with
8653        // NULLs the lane would have to carry their ordering too, and
8654        // getting that subtly wrong is the r1020 defect.
8655        let mut key_pos = [0usize; MAX_KEYS];
8656        let mut descs = [false; MAX_KEYS];
8657        // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
8658        // which the AST records as `None`; `unwrap_or(desc)` is how the
8659        // rest of the engine resolves it.
8660        let mut nulls_first = [false; MAX_KEYS];
8661        let n_keys = stmt.order_by.len();
8662        for (slot, order) in stmt.order_by.iter().enumerate() {
8663            let Expr::Column(oc) = &order.expr else {
8664                return Ok(None);
8665            };
8666            if let Some(q) = &oc.qualifier
8667                && !q.eq_ignore_ascii_case(alias)
8668            {
8669                return Ok(None);
8670            }
8671            let Some(pos) = cols
8672                .iter()
8673                .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
8674            else {
8675                return Ok(None);
8676            };
8677            if !matches!(
8678                cols[pos].ty,
8679                spg_storage::DataType::SmallInt
8680                    | spg_storage::DataType::Int
8681                    | spg_storage::DataType::BigInt
8682            ) {
8683                return Ok(None);
8684            }
8685            key_pos[slot] = pos;
8686            descs[slot] = order.desc;
8687            nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
8688        }
8689
8690        let sess = self.dml_session();
8691        let ctx = EvalContext::new(&cols, Some(alias))
8692            .with_catalog(self.active_catalog())
8693            .with_session(&sess);
8694        let projection = build_projection(
8695            &stmt.items,
8696            &cols,
8697            alias,
8698            self.speaks_mysql,
8699            Some(self.active_catalog()),
8700        )?;
8701        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8702        let bound_pos: Vec<Option<usize>> = projection
8703            .iter()
8704            .map(|p| match &p.expr {
8705                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8706                    Ok(Some(pos)) => Some(pos),
8707                    _ => None,
8708                },
8709                _ => None,
8710            })
8711            .collect();
8712        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8713            .where_
8714            .as_ref()
8715            .filter(|w| crate::eval::fully_compilable(w))
8716            .map(|w| crate::eval::compile_expr(w, &ctx));
8717
8718        // The same first-observable point the materialising planner fires,
8719        // placed after the gates so it fires exactly once: this lane runs
8720        // BEFORE that planner and would otherwise be a hole in the
8721        // panic-isolation and cancellation-race coverage rather than a
8722        // faster path through it.
8723        crate::injection_point!("planner_first_row_fetch", &stmt.from);
8724
8725        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8726        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8727        let mut budget = ByteBudget::new(self.max_query_bytes);
8728        let snapshot = self.current_snapshot();
8729        // Keys, a NULL bit per key slot, and the row. The bitmask keeps
8730        // the element small: a nullable key still costs one bit rather
8731        // than a second array.
8732        let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
8733
8734        for (ri, row) in table.rows().iter().enumerate() {
8735            if ri.is_multiple_of(256) {
8736                cancel.check()?;
8737            }
8738            if !table.is_row_visible(ri, &snapshot) {
8739                continue;
8740            }
8741            // The key comes from the STORED row, before projection: an
8742            // ORDER BY column need not appear in the select list.
8743            let mut keys = [0i64; MAX_KEYS];
8744            let mut nulls = 0u8;
8745            let mut keyed = true;
8746            for slot in 0..n_keys {
8747                match row.values.get(key_pos[slot]) {
8748                    Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
8749                    Some(Value::Int(v)) => keys[slot] = i64::from(*v),
8750                    Some(Value::BigInt(v)) => keys[slot] = *v,
8751                    Some(Value::Null) | None => nulls |= 1 << slot,
8752                    // An integer column holding something else is a row
8753                    // this lane cannot order; hand the whole query back
8754                    // rather than guess at it.
8755                    _ => {
8756                        keyed = false;
8757                        break;
8758                    }
8759                }
8760            }
8761            if !keyed {
8762                return Ok(None);
8763            }
8764            if !Self::stream_filter_project(
8765                row,
8766                stmt.where_.as_ref(),
8767                compiled_where.as_ref(),
8768                &mut eval_stack,
8769                &projection,
8770                &bound_pos,
8771                &ctx,
8772                &mut values,
8773            )? {
8774                continue;
8775            }
8776            budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
8777            sorted.push((keys, nulls, core::mem::take(&mut values)));
8778            values.reserve(projection.len());
8779        }
8780
8781        sorted.sort_by(|a, b| {
8782            use core::cmp::Ordering;
8783            for slot in 0..n_keys {
8784                let bit = 1u8 << slot;
8785                let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
8786                    (true, true) => Ordering::Equal,
8787                    // Where the NULLs go is already decided — `nulls_first`
8788                    // resolved DESC's default when it was read. Reversing
8789                    // this for DESC as well would apply the direction
8790                    // twice and put them at the wrong end.
8791                    (true, false) => {
8792                        if nulls_first[slot] {
8793                            Ordering::Less
8794                        } else {
8795                            Ordering::Greater
8796                        }
8797                    }
8798                    (false, true) => {
8799                        if nulls_first[slot] {
8800                            Ordering::Greater
8801                        } else {
8802                            Ordering::Less
8803                        }
8804                    }
8805                    (false, false) => {
8806                        let o = a.0[slot].cmp(&b.0[slot]);
8807                        if descs[slot] { o.reverse() } else { o }
8808                    }
8809                };
8810                if ord != Ordering::Equal {
8811                    return ord;
8812                }
8813            }
8814            Ordering::Equal
8815        });
8816
8817        emit(crate::StreamItem::Header(&columns))?;
8818        // v7.38.14 — DISTINCT, de-duplicated against the PREVIOUS row.
8819        //
8820        // The gate above only admits DISTINCT when the sort key determines
8821        // the projected row, so every duplicate is adjacent to its twin by
8822        // the time this loop runs and one comparison replaces a hash table
8823        // of every row seen. Equality is `values_eq_norm` with the same mask
8824        // the materialising path builds -- deliberately the same function,
8825        // because a de-duplication that disagreed with the one on the other
8826        // path would make the answer depend on which lane a query took.
8827        //
8828        // A query that did not ask for DISTINCT pays one already-false bool
8829        // test per row: the short-circuit means the comparison never runs
8830        // and `prev` is never written.
8831        let dedup_mask = fold_mask(&projection);
8832        let fold = FoldSpec::of(self.speaks_mysql, &dedup_mask);
8833        let mut count = 0usize;
8834        let mut prev: Option<&[Value<'static>]> = None;
8835        for (_, _, vals) in &sorted {
8836            if stmt.distinct
8837                && let Some(p) = prev
8838                && values_eq_norm(p, vals, fold)
8839            {
8840                continue;
8841            }
8842            emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
8843            count += 1;
8844            if stmt.distinct {
8845                prev = Some(vals);
8846            }
8847        }
8848        Ok(Some(count))
8849    }
8850
8851    /// v7.38.14 — would sorting place every duplicate next to its twin?
8852    ///
8853    /// True when the projected expressions and the ORDER BY expressions are the
8854    /// same SET. Then the sort key determines the projected row, so equal rows
8855    /// are adjacent afterwards and an adjacent comparison de-duplicates exactly
8856    /// as a hash would -- and, because both sort paths are stable, the survivor
8857    /// is the first-seen row, which is the one the hash keeps too.
8858    ///
8859    /// A wildcard's expansion is not known here, so it is not a set this can
8860    /// compare; an ordinal ORDER BY names a select-list position rather than a
8861    /// value and is left alone.
8862    fn distinct_is_adjacent_after_sort(stmt: &SelectStatement) -> bool {
8863        if stmt.order_by.is_empty() || !stmt.distinct_on.is_empty() {
8864            return false;
8865        }
8866        let mut projected: alloc::vec::Vec<&Expr> =
8867            alloc::vec::Vec::with_capacity(stmt.items.len());
8868        for item in &stmt.items {
8869            match item {
8870                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return false,
8871                SelectItem::Expr { expr, .. } => projected.push(expr),
8872            }
8873        }
8874        if projected.is_empty() {
8875            return false;
8876        }
8877        let keys: alloc::vec::Vec<&Expr> = stmt.order_by.iter().map(|o| &o.expr).collect();
8878        if keys
8879            .iter()
8880            .any(|k| matches!(k, Expr::Literal(spg_sql::ast::Literal::Integer(_))))
8881        {
8882            return false;
8883        }
8884        projected.iter().all(|p| keys.contains(p)) && keys.iter().all(|k| projected.contains(k))
8885    }
8886
8887    fn try_spill_sorted_stream<F>(
8888        &self,
8889        stmt: &SelectStatement,
8890        from: &FromClause,
8891        cancel: CancelToken<'_>,
8892        emit: &mut F,
8893    ) -> Result<Option<usize>, EngineError>
8894    where
8895        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8896    {
8897        // The shapes `try_spill_sorted_scan` declines, plus the ones the
8898        // streaming executor does not carry (a LIMIT is already bounded
8899        // by a partial sort; the rest need the answer addressable).
8900        if !self.can_spill()
8901            || stmt.order_by.is_empty()
8902            || stmt.distinct
8903            || stmt.limit_with_ties
8904            || stmt.limit.is_some()
8905            || stmt.offset.is_some()
8906            || stmt.having.is_some()
8907            || stmt.group_by.is_some()
8908            || !stmt.unions.is_empty()
8909            || !from.joins.is_empty()
8910            || from.primary.lateral_subquery.is_some()
8911            || from.primary.unnest_expr.is_some()
8912            || from.primary.as_of_segment.is_some()
8913            || from.primary.generate_series_args.is_some()
8914            || select_has_window(stmt)
8915            || aggregate::uses_aggregate(stmt)
8916        {
8917            return Ok(None);
8918        }
8919        if stmt
8920            .items
8921            .iter()
8922            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8923        {
8924            return Ok(None);
8925        }
8926        // Everything `exec_bare_select_cancel` does before it scans runs
8927        // BELOW this path, so a statement claimed here skips it. Three of
8928        // those were missed on the way in and each was caught by a
8929        // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
8930        // ORDER BY 2` sorted happily instead of raising 42P10), the
8931        // cancellation check by another, the partition fan-out by the
8932        // differential corpus. What is reconciled, item by item: with-ties
8933        // needs ORDER BY (gated above), USING/NATURAL and RLS join
8934        // rewrites (joins gated above), the single-table RLS predicate
8935        // (the dispatcher declines a policy-subject table before this is
8936        // reached), the meta-view dispatch (those names are not in the
8937        // catalog, so the lookup below declines). These three are calls,
8938        // so the message and SQLSTATE are the ones the fall-back gives —
8939        // `select_has_window` above reads the select list and ORDER BY but
8940        // not WHERE, which is the case the third one covers.
8941        crate::orderby::check_order_by_legality(stmt)?;
8942        crate::orderby::check_order_by_positions(stmt)?;
8943        crate::window::reject_window_in_row_clauses(stmt)?;
8944        // A parent's rows are its children's. These walks scan the named
8945        // relation alone, so a partitioned or inherited parent comes back
8946        // short — and silently: the corpus caught `SELECT id FROM pr
8947        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8948        // parent's own rows instead of the partitions'. `ONLY` is exactly
8949        // the case that does not fan out, so it stays, which is the test
8950        // the FROM-clause fan-out itself makes.
8951        if !from.primary.only
8952            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8953        {
8954            return Ok(None);
8955        }
8956        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8957            return Ok(None);
8958        };
8959        // Cold-tier rows live outside `rows()`; this walk would drop
8960        // them silently, the same reason round 831's walk declines.
8961        if table.has_cold_rows_fast() {
8962            return Ok(None);
8963        }
8964
8965        let alias = from
8966            .primary
8967            .alias
8968            .as_deref()
8969            .unwrap_or(from.primary.name.as_str());
8970        let cols = table.schema().columns.clone();
8971        let sess = self.dml_session();
8972        let ctx = EvalContext::new(&cols, Some(alias))
8973            .with_catalog(self.active_catalog())
8974            .with_session(&sess);
8975        let projection = build_projection(
8976            &stmt.items,
8977            &cols,
8978            alias,
8979            self.speaks_mysql,
8980            Some(self.active_catalog()),
8981        )?;
8982        let order_by = stmt.order_by.clone();
8983        // The same one-shot resolution the general path does (round
8984        // 582): each ORDER BY column is bound once, not once per row.
8985        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8986        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8987        // Resolved BEFORE the scan, because it now decides what the sort
8988        // STORES and not just what it decodes (round 995).
8989        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8990
8991        // v7.38.22 — resolved HERE, because this path did not resolve
8992        // them at all.
8993        //
8994        // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8995        // "en_US.utf8"` in BYTE order on this path — and swallowed an
8996        // unknown collation name rather than raising — because the sorter
8997        // below compared with an empty collation slice. The materialising
8998        // path honoured both. Which answer a query got depended on which
8999        // path the planner took, and this is the path a plain single-table
9000        // SELECT takes.
9001        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
9002        let mut sorter = crate::extsort::ExternalSorter::new(
9003            self.temp_run_factory,
9004            self.session_work_mem_bytes(),
9005            cols.clone(),
9006            &descs,
9007            &order_colls,
9008        )
9009        .with_stats(&self.spill_stats)
9010        .with_pruned(&needed);
9011        let snapshot = self.current_snapshot();
9012        // One key buffer for the whole scan: `push` drains it and leaves
9013        // the capacity behind.
9014        let mut keys: Vec<OrderKey> = Vec::new();
9015        // r1024 — compile the predicate once for the scan.
9016        //
9017        // These two sorted-spill scans are the paths a single-table SELECT
9018        // with an ORDER BY takes, and they were the last row-returning ones
9019        // still walking the expression tree per row. r1023 did the
9020        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
9021        // exactly this shape.
9022        //
9023        // Found from the profile's CALL TREE rather than its leaves. The
9024        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
9025        // 261, `mod_op` 178 — and two attempts at reasoning out which
9026        // function asked for it were both wrong. The tree names the caller
9027        // chain, and it named this one.
9028        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9029            .where_
9030            .as_ref()
9031            .filter(|w| crate::eval::fully_compilable(w))
9032            .map(|w| crate::eval::compile_expr(w, &ctx));
9033        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9034        for (i, row) in table.scan_visible_from(0, &snapshot) {
9035            if i.is_multiple_of(256) {
9036                cancel.check()?;
9037            }
9038            if let Some(c) = &compiled_where {
9039                if !crate::eval::compiled::eval_compiled_pred(
9040                    c,
9041                    row,
9042                    &ctx,
9043                    &mut eval_stack,
9044                    ctx.mysql_dialect,
9045                )? {
9046                    continue;
9047                }
9048            } else if let Some(w) = &stmt.where_ {
9049                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
9050                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9051                    continue;
9052                }
9053            }
9054            keys.clear();
9055            // The same collations the sorter compares with, and the
9056            // re-derivation below is handed the same ones. `finish`'s
9057            // contract is that a key comes back the way it was pushed;
9058            // a collation is part of the way it was pushed.
9059            crate::orderby::build_order_keys_bound(
9060                &order_by,
9061                &order_bound,
9062                &order_colls,
9063                row,
9064                &ctx,
9065                &mut keys,
9066            )?;
9067            sorter.push(&mut keys, row)?;
9068        }
9069
9070        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9071        emit(crate::StreamItem::Header(&columns))?;
9072
9073        let key_ctx = &ctx;
9074        let mut emitted_since_check = 0usize;
9075        let n = sorter.finish_each(
9076            |src, buf| {
9077                crate::orderby::build_order_keys_rederived(
9078                    &order_by,
9079                    &order_bound,
9080                    &order_colls,
9081                    src,
9082                    key_ctx,
9083                    buf,
9084                )
9085            },
9086            |src, values| {
9087                for p in &projection {
9088                    values.push(
9089                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
9090                    );
9091                }
9092                Ok(())
9093            },
9094            |cells| {
9095                // The merge is the long half of a big sort, and the scan's
9096                // check above stops running once it ends: a cancelled
9097                // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
9098                // anyway. Same stride as the scan.
9099                emitted_since_check += 1;
9100                if emitted_since_check >= 256 {
9101                    emitted_since_check = 0;
9102                    cancel.check()?;
9103                }
9104                emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
9105            },
9106        )?;
9107        Ok(Some(n))
9108    }
9109
9110    /// One row of the single-table streaming walk: the WHERE test, the
9111    /// projection, the emit. Returns whether a row was emitted.
9112    ///
9113    /// v7.39 (round 970) — factored out because the walk now has two ways
9114    /// to reach a row, the sequential scan and an index seek's candidate
9115    /// positions, and both must do IDENTICALLY this. A copy in each is how
9116    /// two paths for one job drift; this file already carries the cost of
9117    /// that lesson twice (rounds 823 and 961, both resolvers).
9118    ///
9119    /// `#[inline]` so the scan loop keeps the shape round 957 measured it
9120    /// in — a shared hot path pays for a new abstraction whether or not it
9121    /// uses it, and this one is on the scan.
9122    #[inline]
9123    #[allow(clippy::too_many_arguments)]
9124    fn stream_filter_project(
9125        row: &spg_storage::Row<'static>,
9126        where_: Option<&Expr>,
9127        // r1023 — the same WHERE, compiled once by the caller. `None` means
9128        // the expression did not qualify and `where_` is evaluated as before.
9129        compiled_where: Option<&crate::eval::CompiledExpr>,
9130        eval_stack: &mut Vec<Value<'static>>,
9131        projection: &[ProjectedItem],
9132        bound_pos: &[Option<usize>],
9133        ctx: &crate::eval::EvalContext<'_>,
9134        values: &mut Vec<Value<'static>>,
9135    ) -> Result<bool, EngineError> {
9136        // r1023 — this scan ran its predicate through the TREE INTERPRETER,
9137        // once per row, and it was the only row-returning path that did.
9138        // The aggregate path, `table_access`, and the PK walker all compile
9139        // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
9140        // server's live samples were `eval_expr` 99, `apply_binary` 81,
9141        // `mod_op` 29 — the interpreter, not delivery.
9142        //
9143        // The arithmetic accounted for it exactly. Over the wire, the same
9144        // filter costs 6.375 ms returning rows and 0.679 ms counting them;
9145        // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
9146        // which is what an interpreted predicate costs against the compiled
9147        // lane's 11.7. It was named "delivery after a filter" before this
9148        // profile, and it was never delivery.
9149        if let Some(c) = compiled_where {
9150            if !crate::eval::compiled::eval_compiled_pred(
9151                c,
9152                row,
9153                ctx,
9154                eval_stack,
9155                ctx.mysql_dialect,
9156            )? {
9157                return Ok(false);
9158            }
9159        } else if let Some(w) = where_ {
9160            let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
9161            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9162                return Ok(false);
9163            }
9164        }
9165        values.clear();
9166        for (p, bound) in projection.iter().zip(bound_pos) {
9167            values.push(match bound {
9168                Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
9169                None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
9170            });
9171        }
9172        Ok(true)
9173    }
9174
9175    /// The same filter and projection, then emit. Split from
9176    /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
9177    /// before it can emit them — a sort — runs the identical predicate and
9178    /// projection rather than a second copy of them.
9179    #[allow(clippy::too_many_arguments)]
9180    fn stream_project_row<F>(
9181        row: &spg_storage::Row<'static>,
9182        where_: Option<&Expr>,
9183        compiled_where: Option<&crate::eval::CompiledExpr>,
9184        eval_stack: &mut Vec<Value<'static>>,
9185        projection: &[ProjectedItem],
9186        bound_pos: &[Option<usize>],
9187        ctx: &crate::eval::EvalContext<'_>,
9188        values: &mut Vec<Value<'static>>,
9189        emit: &mut F,
9190    ) -> Result<bool, EngineError>
9191    where
9192        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9193    {
9194        if !Self::stream_filter_project(
9195            row,
9196            where_,
9197            compiled_where,
9198            eval_stack,
9199            projection,
9200            bound_pos,
9201            ctx,
9202            values,
9203        )? {
9204            return Ok(false);
9205        }
9206        emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
9207        Ok(true)
9208    }
9209
9210    fn try_stream_single_table<F>(
9211        &self,
9212        stmt: &SelectStatement,
9213        from: &FromClause,
9214        cancel: CancelToken<'_>,
9215        emit: &mut F,
9216    ) -> Result<Option<usize>, EngineError>
9217    where
9218        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9219    {
9220        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9221            return Ok(None);
9222        };
9223        // Cold-tier rows live outside `rows()`; the materialising fallback
9224        // covers both tiers and this walk would silently drop them.
9225        if table.has_cold_rows_fast() {
9226            return Ok(None);
9227        }
9228        let alias = from
9229            .primary
9230            .alias
9231            .as_deref()
9232            .unwrap_or(from.primary.name.as_str());
9233        let cols = table.schema().columns.clone();
9234        let sess = self.dml_session();
9235        let ctx = EvalContext::new(&cols, Some(alias))
9236            .with_catalog(self.active_catalog())
9237            .with_session(&sess);
9238        let projection = build_projection(
9239            &stmt.items,
9240            &cols,
9241            alias,
9242            self.speaks_mysql,
9243            Some(self.active_catalog()),
9244        )?;
9245
9246        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9247        emit(crate::StreamItem::Header(&columns))?;
9248
9249        // v7.37 (round 957) — resolve each bare-column projection ONCE
9250        // instead of once per row. `find_column_pos`-style resolution is a
9251        // linear walk of the schema comparing column-name strings, and the
9252        // row loop below ran it for every cell of every row: measured at
9253        // 400k rows, binding it out of the loop took `SELECT pad` from
9254        // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
9255        //
9256        // ORDER BY has bound its keys this way since round 582
9257        // (`order_by_bound_positions`); the projection never did.
9258        //
9259        // `locate_column` is the same resolution `resolve_column` performs,
9260        // returning the site instead of the value, so the two cannot drift
9261        // apart the way a second hand-written resolver would. Anything it
9262        // declines — an expression, a whole-row reference, a name that does
9263        // not resolve — binds to `None` and takes the general path below,
9264        // errors included, so an empty table still reports nothing rather
9265        // than raising at bind time.
9266        let bound_pos: Vec<Option<usize>> = projection
9267            .iter()
9268            .map(|p| match &p.expr {
9269                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9270                    Ok(Some(pos)) => Some(pos),
9271                    _ => None,
9272                },
9273                _ => None,
9274            })
9275            .collect();
9276
9277        // One snapshot for the whole scan, as the materialising path takes.
9278        let snapshot = self.current_snapshot();
9279
9280        // v7.39 (round 970) — ask the indices BEFORE walking the table.
9281        //
9282        // This walk had no index step at all, and it is preferred over the
9283        // materialising path, which does have one (`pick_indexed_rows` ->
9284        // `try_index_seek`). So a primary-key point lookup — the commonest
9285        // statement there is — read every row: measured on 500k rows,
9286        // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
9287        // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
9288        // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
9289        //
9290        // The control that named it: `... OFFSET 0` — semantically the same
9291        // query — answered in 0.159 ms, because OFFSET is one of the shape
9292        // gates that declines this walk and sends the statement to the path
9293        // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
9294        // no semantics in common; what they share is making this function
9295        // stand down.
9296        //
9297        // The seek only NARROWS: every candidate still goes through the
9298        // full WHERE below, exactly as the mutation paths use it, so a
9299        // partial index match cannot change an answer. Positions come back
9300        // already visibility-filtered and already capped at a quarter of the
9301        // table (round 490), so a seek can never cost more than the scan it
9302        // replaces, and `None` means "walk the table" as before.
9303        //
9304        // Sorted because the scan would have produced table order and the
9305        // index produces key order. Without an ORDER BY neither is promised,
9306        // but a walk that silently reorders its answer when an index happens
9307        // to exist is a difference nobody asked for.
9308        let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
9309            crate::index_access::try_index_seek_positions(
9310                w,
9311                &cols,
9312                table,
9313                alias,
9314                &snapshot,
9315                self.speaks_mysql,
9316            )
9317        });
9318
9319        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9320        // r1023 — compile the predicate once for the whole scan. Same gate
9321        // every other path uses: `fully_compilable` or keep the interpreter,
9322        // so a shape the VM cannot take answers exactly as it did before.
9323        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9324            .where_
9325            .as_ref()
9326            .filter(|w| crate::eval::fully_compilable(w))
9327            .map(|w| crate::eval::compile_expr(w, &ctx));
9328        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9329        let mut count: usize = 0;
9330        match seek_positions {
9331            Some(mut positions) => {
9332                positions.sort_unstable();
9333                for (n, pos) in positions.into_iter().enumerate() {
9334                    if n.is_multiple_of(256) {
9335                        cancel.check()?;
9336                    }
9337                    let Some(row) = table.rows().get(pos) else {
9338                        continue;
9339                    };
9340                    if Self::stream_project_row(
9341                        row,
9342                        stmt.where_.as_ref(),
9343                        compiled_where.as_ref(),
9344                        &mut eval_stack,
9345                        &projection,
9346                        &bound_pos,
9347                        &ctx,
9348                        &mut values,
9349                        emit,
9350                    )? {
9351                        count += 1;
9352                    }
9353                }
9354            }
9355            None => {
9356                // v7.38.11 — the streaming scan is the path a client
9357                // reaches over the wire, so it is the one that has to
9358                // ask the BRIN summary which slots can be skipped. The
9359                // predicate still runs on every row that survives.
9360                let slots = stmt
9361                    .where_
9362                    .as_ref()
9363                    .and_then(|w| crate::brin::candidate_slots(w, table))
9364                    .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
9365                for (i, row) in table.scan_visible_slots(slots, &snapshot) {
9366                    if i.is_multiple_of(256) {
9367                        cancel.check()?;
9368                    }
9369                    if Self::stream_project_row(
9370                        row,
9371                        stmt.where_.as_ref(),
9372                        compiled_where.as_ref(),
9373                        &mut eval_stack,
9374                        &projection,
9375                        &bound_pos,
9376                        &ctx,
9377                        &mut values,
9378                        emit,
9379                    )? {
9380                        count += 1;
9381                    }
9382                }
9383            }
9384        }
9385        Ok(Some(count))
9386    }
9387
9388    pub(crate) fn try_exec_joined_streaming<F>(
9389        &self,
9390        stmt: &SelectStatement,
9391        cancel: CancelToken<'_>,
9392        emit: &mut F,
9393    ) -> Result<Option<usize>, EngineError>
9394    where
9395        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9396    {
9397        // Shape gates — keep the streamable surface narrow on
9398        // purpose. The fall-back path still handles everything else.
9399        let Some(from) = &stmt.from else {
9400            return Ok(None);
9401        };
9402        // v7.37 (round 830) — decline anything a row-security policy binds
9403        // for this session. Policies are injected in
9404        // `exec_bare_select_cancel`, below this path, so a statement claimed
9405        // here would read the table unfiltered: measured, `SELECT val FROM
9406        // sec` returned all three rows to a session whose policy allows two,
9407        // while `SELECT upper(val) FROM sec` — declined by the shape gates
9408        // and so materialised — returned the correct two.
9409        //
9410        // Declining sends it to the path that enforces. Teaching this one to
9411        // inject the predicate itself would keep the streaming benefit for
9412        // RLS tables and is the better end state; it is not what a
9413        // correctness fix should carry, and the fall-back is exactly as
9414        // correct, only slower.
9415        if self.select_reads_policy_subject_table(stmt) {
9416            return Ok(None);
9417        }
9418        // r1058 — a WITH list this path never materialises: the CTE
9419        // name would be resolved as a physical relation and error
9420        // ("relation \"big\" does not exist" over the extended
9421        // protocol, caught by the perm-runner's wire legs). The
9422        // materialising fallback owns CTE execution.
9423        if !stmt.ctes.is_empty() {
9424            return Ok(None);
9425        }
9426        // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
9427        // tables` and kin) exist only as synth arms on the
9428        // materialising path; claiming one here errored "relation
9429        // does not exist" over the extended protocol for a query the
9430        // simple protocol answered. Prefix test only — a genuinely
9431        // missing relation must keep erroring in-path.
9432        if from.primary.name.starts_with("__spg_")
9433            || from
9434                .joins
9435                .iter()
9436                .any(|j| j.table.name.starts_with("__spg_"))
9437        {
9438            return Ok(None);
9439        }
9440        // r1058 — decline partitioned / inheritance parents, same
9441        // shape of bug as the RLS decline above: this path scans the
9442        // named table's own (empty) heap, so `SELECT id, region FROM
9443        // cust` on a partition parent streamed ZERO rows over the wire
9444        // while COUNT(*) — an aggregate, materialised below — said 3.
9445        // Caught by the perm-runner's server permutations; the
9446        // materialising fallback expands children correctly.
9447        if crate::partition::has_children(self.active_catalog(), &from.primary.name)
9448            || from
9449                .joins
9450                .iter()
9451                .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
9452        {
9453            return Ok(None);
9454        }
9455        // v7.39 (round 790) — single-table SELECTs stream too. This
9456        // gate said "joins only" because the path was written for
9457        // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
9458        // fell to the materialising fallback, which builds the whole
9459        // `Vec<Row<'static>>` and only then iterates it. Measured on
9460        // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
9461        // reached through a one-row JOIN — 2.6x, purely for lacking a
9462        // join. The deferred-join structure handles one source as the
9463        // degenerate stride-1 case, so the walk below is unchanged.
9464        let _single_table = from.joins.is_empty();
9465        // An ORDER BY that the bounded sort can serve streams; everything
9466        // else still falls to the materialising fallback below.
9467        // r1025 — an ordering the index already holds needs no sort at all.
9468        // Tried before the spill sort, which is the path it replaces.
9469        if !stmt.order_by.is_empty()
9470            && from.joins.is_empty()
9471            && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
9472        {
9473            return Ok(Some(n));
9474        }
9475        if !stmt.order_by.is_empty()
9476            && from.joins.is_empty()
9477            && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
9478        {
9479            return Ok(Some(n));
9480        }
9481        // r1031 — integer keys carried inline instead of an `OrderKey`
9482        // vector per row. Tried AFTER the spill sort on purpose: this lane
9483        // buffers the whole answer, so anything the spill path would take
9484        // must keep taking it rather than be turned back into an in-memory
9485        // sort that answers with a budget error.
9486        if !stmt.order_by.is_empty()
9487            && from.joins.is_empty()
9488            && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
9489        {
9490            return Ok(Some(n));
9491        }
9492        if !stmt.order_by.is_empty()
9493            || stmt.limit.is_some()
9494            || stmt.offset.is_some()
9495            || stmt.having.is_some()
9496            || stmt.group_by.is_some()
9497            || stmt.distinct
9498            || !stmt.unions.is_empty()
9499            || stmt.limit_with_ties
9500        {
9501            return Ok(None);
9502        }
9503        if aggregate::uses_aggregate(stmt) {
9504            return Ok(None);
9505        }
9506        // No window / SRF on the streaming path.
9507        if select_has_window(stmt) {
9508            return Ok(None);
9509        }
9510        if stmt
9511            .items
9512            .iter()
9513            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9514        {
9515            return Ok(None);
9516        }
9517        // v7.37 (round 831) — a joinless FROM over a plain stored table
9518        // never needs the deferred structure, and building one costs the
9519        // whole table. `materialise_table_ref_filtered` clones every row
9520        // into a `Vec<Row<'static>>` before anything is filtered or
9521        // projected, so peak cost tracks the TABLE, not the result:
9522        // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
9523        // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
9524        // projection saving nothing, while an arithmetic projection — which
9525        // the shape gates decline, so it materialises through the ordinary
9526        // executor — cost +21 MB.
9527        //
9528        // Scanning in batches and releasing each one is what `cursor_fill`
9529        // already does for a lazy cursor, and it is the same walk: resume
9530        // from a slot, take visible rows, evaluate, hand them over, drop
9531        // them. Round 800's finding stands and is why this reads rows OUT
9532        // rather than seeding the join by index — touching the stored
9533        // `PersistentVec` in place makes the whole table resident, which is
9534        // worse than the copy. Each batch is copied, then freed.
9535        if from.joins.is_empty()
9536            && from.primary.unnest_expr.is_none()
9537            && from.primary.lateral_subquery.is_none()
9538            && from.primary.as_of_segment.is_none()
9539            && from.primary.generate_series_args.is_none()
9540            && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
9541        {
9542            return Ok(Some(n));
9543        }
9544        // Build the deferred join under the regular byte budget.
9545        let mut budget = ByteBudget::new(self.max_query_bytes);
9546        let deferred = {
9547            let mut needed = alloc::collections::BTreeSet::new();
9548            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
9549            self.build_joined_filtered_rows(
9550                from,
9551                stmt.where_.as_ref(),
9552                cancel,
9553                if prunable { Some(&needed) } else { None },
9554                &mut budget,
9555            )?
9556        };
9557        let combined_schema = &deferred.combined_schema;
9558        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
9559        // `::regclass` / enum cast in a joined projection or HAVING needs it.
9560        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
9561        // the same predicate the unjoined shape carries.
9562        let joined_sess = self.dml_session();
9563        // v7.38.18 — and the DIALECT. This context carried the catalog and
9564        // the session and not the one field that decides how text
9565        // compares, so a joined row was evaluated in PostgreSQL
9566        // semantics inside a MySQL session.
9567        //
9568        // It showed up only where the two sides had DIFFERENT text types:
9569        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
9570        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
9571        // were fine and the same comparison inside one table was fine.
9572        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
9573        // so the wrong semantics were invisible until a CHAR's padding
9574        // had to be stripped and PostgreSQL's arm does not strip it.
9575        //
9576        // `with_engine` is what sets it; the next line already reaches
9577        // for `self.backslash_escapes`, so the dialect was in hand.
9578        let ctx = EvalContext::new(combined_schema, None)
9579            .with_catalog(self.active_catalog())
9580            .with_engine(self)
9581            .with_session(&joined_sess);
9582        let projection = build_projection(
9583            &stmt.items,
9584            combined_schema,
9585            "",
9586            self.speaks_mysql,
9587            Some(self.active_catalog()),
9588        )?;
9589        // Every projection item must be a bound qualified column —
9590        // anything that needs `eval_expr_with_correlated` keeps the
9591        // materialising path.
9592        let bound_pos = |e: &Expr| -> Option<usize> {
9593            match e {
9594                // v7.39 (round 822) — an UNQUALIFIED column resolves here
9595                // too. The `qualifier.is_some()` guard this replaces meant
9596                // `SELECT pad FROM big` — the commonest projection there is
9597                // — never reached the streaming walk: it fell out at this
9598                // gate and re-ran on the materialising path, after the
9599                // deferred join structure had already been built and paid
9600                // for. Measured (round 821, statement_timeout=120 over 400k
9601                // rows): `big.pad` and `b.pad` streamed and cancelled at
9602                // ~65k rows in 0.14 s, while bare `pad` ran to completion in
9603                // 0.80 s with the timeout never consulted. `find_column_pos`
9604                // has always handled the unqualified case (it falls through
9605                // to a by-name match), so the guard narrowed the gate for no
9606                // reason it recorded.
9607                Expr::Column(c) => eval::find_column_pos(c, &ctx),
9608                _ => None,
9609            }
9610        };
9611        let proj_decomposed: Vec<(usize, usize)> = {
9612            let mut out = Vec::with_capacity(projection.len());
9613            for p in &projection {
9614                let Some(abs) = bound_pos(&p.expr) else {
9615                    return Ok(None);
9616                };
9617                let Some(k) = deferred
9618                    .offsets
9619                    .partition_point(|&o| o <= abs)
9620                    .checked_sub(1)
9621                else {
9622                    return Ok(None);
9623                };
9624                out.push((k, abs - deferred.offsets[k]));
9625            }
9626            out
9627        };
9628        // Emit columns once.
9629        let columns: Vec<ColumnSchema> = projection
9630            .iter()
9631            // v7.39 (read01 round 54) — keep the column's enum identity through
9632            // the projection (it lives outside the DataType lattice), or a
9633            // derived table / UNION / windowed result forgets it and any outer
9634            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
9635            .map(|p| p.to_column_schema())
9636            .collect();
9637        emit(crate::StreamItem::Header(&columns))?;
9638        let sources_ref = &deferred.sources;
9639        let stride = deferred.stride;
9640        let survivors_ref = &deferred.survivors;
9641        let n_surv = if stride == 0 {
9642            0
9643        } else {
9644            survivors_ref.len() / stride
9645        };
9646        // Reused per-row cell-ref scratch — pushes are zero-alloc
9647        // after the first row.
9648        let null_value = Value::Null;
9649        let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
9650        let mut count: usize = 0;
9651        for surv_i in 0..n_surv {
9652            if surv_i.is_multiple_of(256) {
9653                cancel.check()?;
9654            }
9655            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9656            cell_refs.clear();
9657            for &(k, col_in_src) in &proj_decomposed {
9658                let ri = tuple[k];
9659                let v: &Value = if ri == usize::MAX {
9660                    &null_value
9661                } else {
9662                    sources_ref[k]
9663                        .get(ri)
9664                        .and_then(|r| r.values.get(col_in_src))
9665                        .unwrap_or(&null_value)
9666                };
9667                cell_refs.push(v);
9668            }
9669            emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
9670            count += 1;
9671        }
9672        Ok(Some(count))
9673    }
9674
9675    fn exec_joined_select(
9676        &self,
9677        stmt: &SelectStatement,
9678        from: &FromClause,
9679        cancel: CancelToken<'_>,
9680    ) -> Result<QueryResult, EngineError> {
9681        // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
9682        // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
9683        // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
9684        // FROM B WHERE B.k = A.k)` into
9685        //   SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
9686        //   WHERE B.k IS NULL
9687        // The general join executor builds a hash, probes every outer
9688        // tuple, materialises (left_padded_with_null) for every miss,
9689        // then runs the aggregate over the result set. For COUNT(*) we
9690        // only need the count — skip the tuple materialisation. Build
9691        // a HashSet of B's unique join values, scan A's PK index, and
9692        // increment the counter on each miss. PG's Merge Anti-Join
9693        // does roughly this; ours becomes a simple HashSet probe.
9694        if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
9695            return Ok(out);
9696        }
9697        // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
9698        // When ORDER BY is on an indexed primary column, walking the
9699        // btree in the requested direction lets the streamer break
9700        // after `LIMIT + OFFSET` survivors without ever materialising
9701        // the rest of the join — the 80 ms `mailrs_prod_not_exists`
9702        // plateau is exactly this shape.
9703        if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
9704            return Ok(out);
9705        }
9706        // v7.30.3 (mailrs round-26) — the bounded single-join path
9707        // first; peak memory scales with LIMIT instead of the table.
9708        if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
9709            return Ok(out);
9710        }
9711        // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
9712        // WHERE materialisation to the shared helper so the LATERAL
9713        // / UNNEST / regular-catalog paths route through one place.
9714        // (`build_joined_filtered_rows` carries LATERAL support as
9715        // of Phase 3.P0-41.) Downstream we still handle aggregate /
9716        // projection / ORDER BY / DISTINCT / LIMIT inline because
9717        // those depend on the SelectStatement's items list.
9718        let mut budget = ByteBudget::new(self.max_query_bytes);
9719        let deferred = {
9720            let mut needed = alloc::collections::BTreeSet::new();
9721            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
9722            self.build_joined_filtered_rows(
9723                from,
9724                stmt.where_.as_ref(),
9725                cancel,
9726                if prunable { Some(&needed) } else { None },
9727                &mut budget,
9728            )?
9729        };
9730        let combined_schema = &deferred.combined_schema;
9731        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
9732        // `::regclass` / enum cast in a joined projection or HAVING needs it.
9733        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
9734        // the same predicate the unjoined shape carries.
9735        let joined_sess = self.dml_session();
9736        // v7.38.18 — and the DIALECT. This context carried the catalog and
9737        // the session and not the one field that decides how text
9738        // compares, so a joined row was evaluated in PostgreSQL
9739        // semantics inside a MySQL session.
9740        //
9741        // It showed up only where the two sides had DIFFERENT text types:
9742        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
9743        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
9744        // were fine and the same comparison inside one table was fine.
9745        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
9746        // so the wrong semantics were invisible until a CHAR's padding
9747        // had to be stripped and PostgreSQL's arm does not strip it.
9748        //
9749        // `with_engine` is what sets it; the next line already reaches
9750        // for `self.backslash_escapes`, so the dialect was in hand.
9751        let ctx = EvalContext::new(combined_schema, None)
9752            .with_catalog(self.active_catalog())
9753            .with_engine(self)
9754            .with_session(&joined_sess);
9755        // Aggregate path: handle GROUP BY / aggregate calls over the
9756        // joined+filtered rows.
9757        if aggregate::uses_aggregate(stmt) {
9758            // v7.32 (P4 borrow channel, increment 2) — borrow each
9759            // surviving join tuple as a RowRef::Tuple; the aggregate
9760            // engine reads source cells by reference (bound fast path =
9761            // zero clone) instead of consuming materialised combined
9762            // Rows. This is where the +211k materialise_tuple_vals
9763            // clones disappear for the join+aggregate shape.
9764            let refs = deferred.row_refs();
9765            // v7.29 — a per-query memo so correlated scalar
9766            // subqueries batch-evaluate once (group map) instead of
9767            // executing per group.
9768            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
9769            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
9770                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
9771                    .map_err(|err| match err {
9772                        EngineError::Eval(ev) => ev,
9773                        other => eval::EvalError::TypeMismatch {
9774                            detail: alloc::format!("{other}"),
9775                        },
9776                    })
9777            };
9778            let agg = aggregate::run(
9779                stmt,
9780                crate::join::AggRows::Refs(&refs),
9781                combined_schema,
9782                None,
9783                Some(&agg_correlated),
9784                self.parallel_runner.0.as_deref(),
9785                Some(self.active_catalog()),
9786                Some(self),
9787            )?;
9788            return self.finish_agg_result(agg, stmt, cancel);
9789        }
9790
9791        let projection = build_projection(
9792            &stmt.items,
9793            combined_schema,
9794            "",
9795            self.speaks_mysql,
9796            Some(self.active_catalog()),
9797        )?;
9798        // v7.39 (round 734) — a set-returning projection over a JOIN.
9799        // This executor's projection loop treats every item as a scalar,
9800        // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
9801        // "function unnest(integer[]) does not exist" where PG expands
9802        // it. The row-set executor already carries the full SRF pipeline
9803        // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
9804        // sharding): materialise the joined survivors and hand over. The
9805        // WHERE is cleared — the join already applied it, and combined
9806        // columns resolve identically in both executors.
9807        if !self.srf_target_idxs(&projection).is_empty() {
9808            let refs = deferred.row_refs();
9809            let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
9810            let mut s2 = stmt.clone();
9811            s2.where_ = None;
9812            let schema = combined_schema.clone();
9813            return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
9814        }
9815        // v7.33 (P4 borrow channel, increment 3) — project directly off
9816        // the deferred row-index tuples instead of materialising an
9817        // intermediate combined Row per survivor. A bound qualified
9818        // column is read by reference (`RowRef::get` → `tuple_value`) and
9819        // cloned ONCE into the output row; the old `materialise()` (a full
9820        // combined Row plus a source→intermediate clone per referenced
9821        // cell, for every survivor) is gone. A row materialises on demand
9822        // only when a projection or ORDER BY expression needs the eval
9823        // path (subquery / function / arithmetic / unqualified column).
9824        // Same bind-once classification the aggregate input fast path uses
9825        // (`accumulate_groups`), reading the same `tuple_value` mapping the
9826        // differential gate already covers.
9827        let refs = deferred.row_refs();
9828        let bound_pos = |e: &Expr| -> Option<usize> {
9829            match e {
9830                Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
9831                _ => None,
9832            }
9833        };
9834        let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
9835        let all_proj_bound = proj_pos.iter().all(Option::is_some);
9836        // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
9837        // pre-decompose each bound projection position into
9838        // `(source_k, col_in_source)` so the per-row column read
9839        // skips the per-cell `tuple_value` partition_point + slice
9840        // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
9841        // calls) that walk dominated; this version reaches into
9842        // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
9843        let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
9844            .iter()
9845            .map(|p| {
9846                p.and_then(|abs| {
9847                    let k = deferred
9848                        .offsets
9849                        .partition_point(|&o| o <= abs)
9850                        .checked_sub(1)?;
9851                    Some((k, abs - deferred.offsets[k]))
9852                })
9853            })
9854            .collect();
9855        // v7.39 (round 962) — which projection items are whole-row
9856        // references, and to which join source. The test is
9857        // `locate_column` declining the name, which is the SAME resolver
9858        // the evaluation path uses, so this cannot drift from it: a real
9859        // column carrying an alias's name resolves to a position and is
9860        // not reported here. The source index comes from the alias
9861        // prefix, the way the combined schema names its columns.
9862        let whole_row_src: Vec<Option<usize>> = projection
9863            .iter()
9864            .map(|p| {
9865                let Expr::Column(c) = &p.expr else {
9866                    return None;
9867                };
9868                if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
9869                    return None;
9870                }
9871                let prefix = alloc::format!("{name}.", name = c.name);
9872                let abs = deferred
9873                    .combined_schema
9874                    .iter()
9875                    .position(|s| s.name.starts_with(&prefix))?;
9876                deferred
9877                    .offsets
9878                    .partition_point(|&o| o <= abs)
9879                    .checked_sub(1)
9880            })
9881            .collect();
9882        // ORDER BY (when present) still evaluates against a materialised
9883        // Row — keep the order-key encoder correct rather than fork it.
9884        let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
9885        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
9886        let mut proj_memo = memoize::MemoizeCache::default();
9887        let sources_ref = &deferred.sources;
9888        let stride = deferred.stride;
9889        let survivors_ref = &deferred.survivors;
9890        let n_surv = survivors_ref.len() / stride.max(1);
9891        // v7.38 (read01 B8) — streaming top-N budget (see the sibling
9892        // single-table path). Bounds this JOIN projection's accumulator
9893        // to O(keep) for `ORDER BY … LIMIT k`.
9894        let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
9895            && !stmt.distinct
9896            && !stmt.limit_with_ties
9897            && !self.env_cfg().disable_topk
9898        {
9899            stmt.limit_literal().and_then(|l| {
9900                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
9901                (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
9902            })
9903        } else {
9904            None
9905        };
9906        // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
9907        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9908            hashbrown::HashMap::new();
9909        let distinct_hb = hashbrown::DefaultHashBuilder::default();
9910        // v7.38.13 — which output positions must NOT fold. Built once per
9911        // scan from the projection, which carries the source column's
9912        // byte-wise-ness; see `FoldSpec`.
9913        let distinct_mask = fold_mask(&projection);
9914        for surv_i in 0..n_surv {
9915            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9916            let row = &refs[surv_i];
9917            let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
9918                Some(row.as_row())
9919            } else {
9920                None
9921            };
9922            let mut values = Vec::with_capacity(projection.len());
9923            for (i, p) in projection.iter().enumerate() {
9924                if let Some((k, col_in_src)) = proj_decomposed[i] {
9925                    // v7.36 — direct (source_k, col) lookup, no
9926                    // partition_point. tuple[k] is the row index in
9927                    // sources[k]; LEFT-NULL slots are `usize::MAX`.
9928                    let ri = tuple[k];
9929                    let v: Value<'static> = if ri == usize::MAX {
9930                        Value::Null
9931                    } else {
9932                        sources_ref[k]
9933                            .get(ri)
9934                            .and_then(|r| r.values.get(col_in_src))
9935                            .cloned()
9936                            .map(Value::into_owned)
9937                            .unwrap_or(Value::Null)
9938                    };
9939                    values.push(v);
9940                } else if let Some(pos) = proj_pos[i] {
9941                    // Bound but couldn't decompose (shouldn't normally
9942                    // happen — keep as a safe path).
9943                    values.push(
9944                        row.get(pos)
9945                            .cloned()
9946                            .map(Value::into_owned)
9947                            .unwrap_or(Value::Null),
9948                    );
9949                } else if let Some(k) = whole_row_src[i]
9950                    && tuple[k] == usize::MAX
9951                {
9952                    // v7.39 (round 962) — a whole-row reference to a side
9953                    // an OUTER join null-extended is NULL, not a
9954                    // composite whose fields are all NULL. PG18.4 answers
9955                    // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
9956                    // an empty cell; round 961 answered `(,)`.
9957                    //
9958                    // The evaluator below cannot tell the two apart: it
9959                    // reads the MATERIALISED combined row, where a
9960                    // null-extended side is indistinguishable from a real
9961                    // row whose every column is NULL — and that row is
9962                    // `(,)` in PG too, so guessing by "all fields NULL"
9963                    // would trade one wrong answer for another. The
9964                    // tuple, which is still in hand here, does know:
9965                    // `usize::MAX` is the sentinel the join writes for
9966                    // exactly this.
9967                    values.push(Value::Null);
9968                } else {
9969                    // Eval path — `materialised` is Some whenever any
9970                    // projection item is non-bound (need_eval_row true).
9971                    // v7.24 (round-16 B) — select-list subqueries under a
9972                    // JOIN go through the correlated-aware evaluator too.
9973                    let mrow = materialised.as_deref().expect("materialised for eval");
9974                    values.push(self.eval_expr_with_correlated(
9975                        &p.expr,
9976                        mrow,
9977                        &ctx,
9978                        cancel,
9979                        Some(&mut proj_memo),
9980                    )?);
9981                }
9982            }
9983            let out_row = Row::new(values);
9984            // v7.37.16 — streaming DISTINCT (see the scan-path twin):
9985            // probe on the projected row; duplicates skip the
9986            // build_order_keys eval and never enter `tagged`.
9987            if stmt.distinct {
9988                let bucket = seen_distinct
9989                    .entry(norm_hash_row(
9990                        &out_row,
9991                        &distinct_hb,
9992                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9993                    ))
9994                    .or_default();
9995                if bucket.iter().any(|i| {
9996                    row_eq_norm(
9997                        &tagged[i].1,
9998                        &out_row,
9999                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10000                    )
10001                }) {
10002                    continue;
10003                }
10004                bucket.push(tagged.len());
10005            }
10006            let order_keys = if stmt.order_by.is_empty() {
10007                Vec::new()
10008            } else {
10009                let mrow = materialised.as_deref().expect("materialised for order by");
10010                build_order_keys(&stmt.order_by, mrow, &ctx)?
10011            };
10012            budget.charge(approx_row_bytes(&out_row))?;
10013            tagged.push((order_keys, out_row));
10014            if let Some((k, descs)) = &topk_stream {
10015                topk_trim(&mut tagged, *k, descs);
10016            }
10017        }
10018        if !stmt.order_by.is_empty() {
10019            // v7.38 元机制 D acceptor — see other call site above.
10020            let keep = if self.env_cfg().disable_topk {
10021                None
10022            } else {
10023                stmt.limit_literal()
10024                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
10025            };
10026            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
10027            // v7.39 (round 688) — the join's ORDER BY resolves its keys
10028            // against `ctx`, which is built from `build_combined_schema`, so
10029            // this is where a declared collation reaches the sort. There was
10030            // exactly ONE resolver call in the engine before this — the
10031            // single-table scan's — which is why every other shape sorted by
10032            // bytes no matter what the schemas carried.
10033            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
10034            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
10035        }
10036        let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
10037        apply_offset_and_limit(
10038            &mut output_rows,
10039            stmt.offset_literal(),
10040            stmt.limit_literal(),
10041        );
10042        let columns: Vec<ColumnSchema> = projection
10043            .into_iter()
10044            .map(|p| p.to_column_schema())
10045            .collect();
10046        Ok(QueryResult::Rows {
10047            columns,
10048            rows: output_rows,
10049        })
10050    }
10051}
10052
10053impl Engine {
10054    /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
10055    /// by id, decodes each row body against the table's current
10056    /// schema, applies the SELECT's projection + optional WHERE +
10057    /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
10058    /// / ORDER BY are unsupported on this path (STABILITY carve-
10059    /// out); operators wanting them should restore the segment
10060    /// into a regular table first.
10061    fn exec_select_as_of_segment(
10062        &self,
10063        stmt: &SelectStatement,
10064        from: &spg_sql::ast::FromClause,
10065        segment_id: u32,
10066    ) -> Result<QueryResult, EngineError> {
10067        // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
10068        // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
10069        if !from.joins.is_empty()
10070            || stmt.group_by.is_some()
10071            || stmt.having.is_some()
10072            || !stmt.unions.is_empty()
10073            || !stmt.order_by.is_empty()
10074            || stmt.offset.is_some()
10075            || stmt.distinct
10076            || aggregate::uses_aggregate(stmt)
10077        {
10078            return Err(EngineError::Unsupported(
10079                "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
10080                 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
10081                    .into(),
10082            ));
10083        }
10084        let table = self
10085            .active_catalog()
10086            .get(&from.primary.name)
10087            .ok_or_else(|| StorageError::TableNotFound {
10088                name: from.primary.name.clone(),
10089            })?;
10090        let schema = table.schema().clone();
10091        let schema_cols = &schema.columns;
10092        let alias = from
10093            .primary
10094            .alias
10095            .as_deref()
10096            .unwrap_or(from.primary.name.as_str());
10097        let ctx = self.ev_ctx(schema_cols, Some(alias));
10098        let seg = self
10099            .active_catalog()
10100            .cold_segment(segment_id)
10101            .ok_or_else(|| {
10102                EngineError::Unsupported(alloc::format!(
10103                    "AS OF SEGMENT: cold segment {segment_id} not registered"
10104                ))
10105            })?;
10106        let mut out_rows: Vec<Row<'static>> = Vec::new();
10107        let mut limit_remaining: Option<usize> =
10108            stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
10109        for (_key, body) in seg.scan() {
10110            let (row, _consumed) =
10111                spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
10112                    .map_err(EngineError::Storage)?;
10113            if let Some(where_expr) = &stmt.where_ {
10114                let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
10115                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
10116                    continue;
10117                }
10118            }
10119            // Projection.
10120            let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
10121            out_rows.push(projected);
10122            if let Some(rem) = limit_remaining.as_mut() {
10123                if *rem == 0 {
10124                    out_rows.pop();
10125                    break;
10126                }
10127                *rem -= 1;
10128            }
10129        }
10130        // Output column schema: derive from SELECT items.
10131        let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
10132        Ok(QueryResult::Rows {
10133            columns,
10134            rows: out_rows,
10135        })
10136    }
10137
10138    /// v6.10.2 — simple-path WHERE eval that doesn't go through
10139    /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
10140    /// scan paths predicate against a snapshot frozen segment, no
10141    /// cross-row state.
10142    fn eval_expr_simple(
10143        &self,
10144        expr: &Expr,
10145        row: &Row<'static>,
10146        ctx: &EvalContext,
10147    ) -> Result<Value<'static>, EngineError> {
10148        let cancel = CancelToken::none();
10149        self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
10150    }
10151}
10152
10153// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
10154
10155/// One row-producing projection: an expression to evaluate, the resulting
10156/// column's user-visible name, its inferred type, and nullability.
10157#[derive(Debug, Clone)]
10158pub(crate) struct ProjectedItem {
10159    pub(crate) expr: Expr,
10160    pub(crate) output_name: String,
10161    pub(crate) ty: DataType,
10162    pub(crate) nullable: bool,
10163    /// v7.39 (read01 round 54) — a projected enum column keeps its enum
10164    /// identity. Enum-ness lives outside the DataType lattice (the value is a
10165    /// Text), so a projection that dropped this made the RESULT schema forget
10166    /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
10167    /// that schema, silently fell back to TEXT order instead of member order.
10168    pub(crate) user_enum_type: Option<String>,
10169    /// v7.39 (round 425) — a projected MySQL temporal column keeps its
10170    /// declared fractional-seconds precision, so the renderer can pad to
10171    /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
10172    /// a whole second). Like `user_enum_type` this lives outside the
10173    /// DataType lattice, so a projection that dropped it made the RESULT
10174    /// schema forget how wide the fraction should print.
10175    pub(crate) mysql_fsp: Option<u8>,
10176    /// v7.39 (round 688) — and its declared collation, the third thing to
10177    /// live outside the DataType lattice and the third to be lost the same
10178    /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
10179    /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
10180    /// projection rebuilt the output column and the ORDER BY resolves
10181    /// against THAT schema.
10182    pub(crate) collation_name: Option<String>,
10183    /// v7.38.13 — and whether this position must NOT fold when DISTINCT
10184    /// de-dups it. The fourth thing to live outside the DataType lattice
10185    /// and the fourth to be lost the same way: a column declared
10186    /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
10187    /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
10188    /// returns two.
10189    ///
10190    /// A BOOL rather than the `Collation` enum on purpose. The enum's
10191    /// storage default is `Binary`, but the FOLD default under MySQL is
10192    /// case-insensitive — carrying the enum would silently mean
10193    /// "exempt" for every projected expression that is not a column.
10194    /// This field states the question it answers.
10195    pub(crate) fold_exempt: bool,
10196    /// v7.38.18 — does this column's collation make trailing spaces
10197    /// insignificant? A separate question from `fold_exempt`:
10198    /// `utf8mb4_bin` is fold-exempt AND pads, `utf8mb4_0900_ai_ci`
10199    /// folds and does not. Read off the same column, at the same
10200    /// place, so the two masks cannot drift apart.
10201    pub(crate) pads: bool,
10202}
10203
10204impl ProjectedItem {
10205    /// v7.38.14 — the output column this projected item describes.
10206    ///
10207    /// There were TWENTY-ONE places converting a `ProjectedItem` into a
10208    /// `ColumnSchema`, each written as `ColumnSchema::new(..)` followed by a
10209    /// hand-picked list of attributes to copy after it, and the lists did not
10210    /// agree: six carried enum identity, the collation NAME and MySQL fsp; ten
10211    /// carried the first and last but not the name; five carried nothing at
10212    /// all. Not one carried `collation`, the enum every MySQL text comparison
10213    /// actually reads.
10214    ///
10215    /// That is how a declared collation vanished between a subquery and the
10216    /// query that selects from it: the inner SELECT's output schema claimed
10217    /// `ColumnSchema::new`'s default, which is `Binary` — a value downstream
10218    /// reads as "byte-wise ON PURPOSE" rather than as "unknown", so the loss
10219    /// presents as a deliberate declaration.
10220    ///
10221    /// One conversion, so a field added to either type has one place to be
10222    /// remembered instead of twenty-one.
10223    pub(crate) fn to_column_schema(&self) -> ColumnSchema {
10224        let mut c = ColumnSchema::new(self.output_name.clone(), self.ty, self.nullable);
10225        c.user_enum_type.clone_from(&self.user_enum_type);
10226        c.collation_name.clone_from(&self.collation_name);
10227        c.mysql_fsp = self.mysql_fsp;
10228        // `fold_exempt` is the projection's answer to the same question
10229        // `ColumnSchema::collation` answers downstream, and it was computed
10230        // from the source column. Keeping the two in step here is what stops
10231        // a de-duplication site further on from asking the schema and being
10232        // told the opposite of what the projection knew.
10233        c.collation = if self.fold_exempt {
10234            spg_storage::Collation::Binary
10235        } else {
10236            spg_storage::Collation::CaseInsensitive
10237        };
10238        c
10239    }
10240}
10241
10242/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
10243/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
10244/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
10245/// the spec's "two NULLs are not distinct"; the second is a tolerated
10246/// quirk for v1 (no NaN literals are reachable from the SQL surface).
10247/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
10248fn expr_is_aggregate_call(e: &Expr) -> bool {
10249    match e {
10250        Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
10251        Expr::AggregateOrdered { .. } => true,
10252        _ => false,
10253    }
10254}
10255
10256/// Collect distinct top-level aggregate call expressions (dedup by value). Does
10257/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
10258/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
10259/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
10260/// than today — never a regression on a working query).
10261fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
10262    if expr_is_aggregate_call(e) {
10263        if !out.iter().any(|x| x == e) {
10264            out.push(e.clone());
10265        }
10266        return;
10267    }
10268    match e {
10269        Expr::Binary { lhs, rhs, .. } => {
10270            collect_agg_exprs(lhs, out);
10271            collect_agg_exprs(rhs, out);
10272        }
10273        Expr::Unary { expr, .. }
10274        | Expr::Cast { expr, .. }
10275        | Expr::IsNull { expr, .. }
10276        | Expr::BoolTest { expr, .. }
10277        | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
10278        Expr::FunctionCall { args, .. } => {
10279            for a in args {
10280                collect_agg_exprs(a, out);
10281            }
10282        }
10283        Expr::Like { expr, pattern, .. } => {
10284            collect_agg_exprs(expr, out);
10285            collect_agg_exprs(pattern, out);
10286        }
10287        Expr::Extract { source, .. } => collect_agg_exprs(source, out),
10288        Expr::WindowFunction {
10289            args,
10290            partition_by,
10291            order_by,
10292            ..
10293        } => {
10294            for a in args {
10295                collect_agg_exprs(a, out);
10296            }
10297            for p in partition_by {
10298                collect_agg_exprs(p, out);
10299            }
10300            for (o, _, _) in order_by {
10301                collect_agg_exprs(o, out);
10302            }
10303        }
10304        _ => {}
10305    }
10306}
10307
10308/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
10309fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
10310    if expr_is_aggregate_call(e) {
10311        if let Some(idx) = aggs.iter().position(|x| x == e) {
10312            *e = Expr::Column(ColumnName {
10313                qualifier: None,
10314                name: alloc::format!("__agg{idx}"),
10315            });
10316        }
10317        return;
10318    }
10319    match e {
10320        Expr::Binary { lhs, rhs, .. } => {
10321            replace_agg_exprs(lhs, aggs);
10322            replace_agg_exprs(rhs, aggs);
10323        }
10324        Expr::Unary { expr, .. }
10325        | Expr::Cast { expr, .. }
10326        | Expr::IsNull { expr, .. }
10327        | Expr::BoolTest { expr, .. }
10328        | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
10329        Expr::FunctionCall { args, .. } => {
10330            for a in args {
10331                replace_agg_exprs(a, aggs);
10332            }
10333        }
10334        Expr::Like { expr, pattern, .. } => {
10335            replace_agg_exprs(expr, aggs);
10336            replace_agg_exprs(pattern, aggs);
10337        }
10338        Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
10339        Expr::WindowFunction {
10340            args,
10341            partition_by,
10342            order_by,
10343            ..
10344        } => {
10345            for a in args {
10346                replace_agg_exprs(a, aggs);
10347            }
10348            for p in partition_by {
10349                replace_agg_exprs(p, aggs);
10350            }
10351            for (o, _, _) in order_by {
10352                replace_agg_exprs(o, aggs);
10353            }
10354        }
10355        _ => {}
10356    }
10357}
10358
10359/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
10360/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
10361/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
10362/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
10363/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
10364/// Returns None outside the bounded subset (leaves current behaviour). Only fires
10365/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
10366/// window-only / aggregate-only queries.
10367fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
10368    if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
10369        return None;
10370    }
10371    // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
10372    if !stmt.unions.is_empty() {
10373        return None;
10374    }
10375    let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
10376    if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
10377        return None;
10378    }
10379    stmt.from.as_ref()?;
10380    // Collect the aggregate calls to hoist from projection + outer ORDER BY.
10381    let mut aggs: Vec<Expr> = Vec::new();
10382    for item in &stmt.items {
10383        if let SelectItem::Expr { expr, .. } = item {
10384            collect_agg_exprs(expr, &mut aggs);
10385        }
10386    }
10387    for ob in &stmt.order_by {
10388        collect_agg_exprs(&ob.expr, &mut aggs);
10389    }
10390    // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
10391    let mut inner_items: Vec<SelectItem> = Vec::new();
10392    for g in &group_cols {
10393        inner_items.push(SelectItem::Expr {
10394            expr: g.clone(),
10395            alias: None,
10396        });
10397    }
10398    for (i, a) in aggs.iter().enumerate() {
10399        inner_items.push(SelectItem::Expr {
10400            expr: a.clone(),
10401            alias: Some(alloc::format!("__agg{i}")),
10402        });
10403    }
10404    let inner = SelectStatement {
10405        items: inner_items,
10406        distinct: false,
10407        distinct_on: Vec::new(),
10408        unions: Vec::new(),
10409        order_by: Vec::new(),
10410        limit: None,
10411        offset: None,
10412        limit_with_ties: false,
10413        window_check_exprs: Vec::new(),
10414        ..stmt.clone()
10415    };
10416    let derived = TableRef {
10417        name: "__aggwin".into(),
10418        alias: Some("__aggwin".into()),
10419        only: false,
10420        as_of_segment: None,
10421        unnest_expr: None,
10422        unnest_column_aliases: Vec::new(),
10423        with_ordinality: false,
10424        generate_series_args: None,
10425        lateral_subquery: Some(alloc::boxed::Box::new(inner)),
10426        jsonb_each_text_arg: None,
10427        table_fn_call: None,
10428        rows_from: None,
10429        json_table: None,
10430        scalar_fn_item: false,
10431    };
10432    // Outer window query over the derived rows: aggregates → __aggN column refs.
10433    let mut outer_items = stmt.items.clone();
10434    for item in &mut outer_items {
10435        if let SelectItem::Expr { expr, alias } = item {
10436            // Preserve PG's column label for a bare aggregate projection.
10437            if alias.is_none()
10438                && let Expr::FunctionCall { name, .. } = expr
10439                && crate::aggregate::is_aggregate_name(name)
10440            {
10441                *alias = Some(name.to_ascii_lowercase());
10442            }
10443            replace_agg_exprs(expr, &aggs);
10444        }
10445    }
10446    let mut outer_order = stmt.order_by.clone();
10447    for ob in &mut outer_order {
10448        replace_agg_exprs(&mut ob.expr, &aggs);
10449    }
10450    let mut outer_distinct_on = stmt.distinct_on.clone();
10451    for e in &mut outer_distinct_on {
10452        replace_agg_exprs(e, &aggs);
10453    }
10454    Some(SelectStatement {
10455        locking: None,
10456        ctes: Vec::new(),
10457        distinct: stmt.distinct,
10458        distinct_on: outer_distinct_on,
10459        items: outer_items,
10460        from: Some(FromClause {
10461            primary: derived,
10462            joins: Vec::new(),
10463        }),
10464        where_: None,
10465        group_by: None,
10466        group_by_all: false,
10467        having: None,
10468        unions: Vec::new(),
10469        order_by: outer_order,
10470        limit: stmt.limit.clone(),
10471        offset: stmt.offset.clone(),
10472        limit_with_ties: stmt.limit_with_ties,
10473        window_check_exprs: Vec::new(),
10474    })
10475}
10476
10477/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
10478/// membership.
10479///
10480/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
10481/// there?", and all four answered by scanning the whole right side once per
10482/// left row. The cost was (left rows x right rows), which is why
10483/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
10484/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
10485/// row that does not pays for all of it. Over 100k left rows, raising the
10486/// right side from 100 to 10,000 took 35 ms to 2848.
10487///
10488/// This is the shape round 485 already solved for DISTINCT, and it reuses
10489/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
10490/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
10491/// every bucket with the exact comparator, so a collision costs time and
10492/// never an answer.
10493struct PeerIndex<'r> {
10494    bh: hashbrown::DefaultHashBuilder,
10495    buckets: hashbrown::HashMap<u64, Vec<usize>>,
10496    rows: &'r [Row<'static>],
10497    fold: FoldSpec<'r>,
10498}
10499
10500impl<'r> PeerIndex<'r> {
10501    fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
10502        // ONE hasher for the whole pass: the default builder is seeded per
10503        // instance, so a fresh one per row would put equal rows in different
10504        // buckets.
10505        let bh = hashbrown::DefaultHashBuilder::default();
10506        let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
10507            hashbrown::HashMap::with_capacity(rows.len());
10508        for (i, r) in rows.iter().enumerate() {
10509            buckets
10510                .entry(norm_hash_row(r, &bh, fold))
10511                .or_default()
10512                .push(i);
10513        }
10514        Self {
10515            bh,
10516            buckets,
10517            rows,
10518            fold,
10519        }
10520    }
10521
10522    fn contains(&self, r: &Row<'static>) -> bool {
10523        let h = norm_hash_row(r, &self.bh, self.fold);
10524        self.buckets
10525            .get(&h)
10526            .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
10527    }
10528
10529    /// Remove ONE occurrence, so the multiset forms cancel row for row the
10530    /// way the pool they replaced did.
10531    fn take_one(&mut self, r: &Row<'static>) -> bool {
10532        let h = norm_hash_row(r, &self.bh, self.fold);
10533        let Some(b) = self.buckets.get_mut(&h) else {
10534            return false;
10535        };
10536        let Some(pos) = b
10537            .iter()
10538            .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
10539        else {
10540            return false;
10541        };
10542        b.swap_remove(pos);
10543        true
10544    }
10545}
10546
10547pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
10548    dedup_by_row(rows, |r| r, fold)
10549}
10550
10551/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
10552/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
10553/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
10554/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
10555/// order is preserved, and correctness needs only the one-way guarantee
10556/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
10557/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
10558fn dedup_by_row<T>(
10559    items: Vec<T>,
10560    row_of: impl Fn(&T) -> &Row<'static>,
10561    fold: FoldSpec<'_>,
10562) -> Vec<T> {
10563    if items.len() <= 32 {
10564        let mut out: Vec<T> = Vec::with_capacity(items.len());
10565        for it in items {
10566            if !out
10567                .iter()
10568                .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
10569            {
10570                out.push(it);
10571            }
10572        }
10573        return out;
10574    }
10575    // ONE BuildHasher instance for the whole pass — the default builder
10576    // is randomly seeded PER INSTANCE, so a fresh one per row would give
10577    // equal rows different hashes and never dedup.
10578    let bh = hashbrown::DefaultHashBuilder::default();
10579    let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
10580    let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
10581        hashbrown::HashMap::with_capacity(items.len());
10582    for it in items {
10583        let h = norm_hash_row(row_of(&it), &bh, fold);
10584        let bucket = buckets.entry(h).or_default();
10585        if !bucket
10586            .iter()
10587            .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
10588        {
10589            bucket.push(out.len());
10590            out.push(it);
10591        }
10592    }
10593    out
10594}
10595
10596/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
10597/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
10598/// rows may collide (buckets are re-checked with the exact comparator).
10599///
10600/// Domain design mirrors `value_cmp`'s equivalence classes:
10601/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
10602///   shares one domain: a value that is an integer fitting i64 hashes the
10603///   i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
10604///   anything else hashes the f64 approximation computed by THE SAME
10605///   formula the value_cmp float arms use (`numeric_to_f64`), so
10606///   `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
10607///   hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
10608///   Known un-closable corner: an integer in [2^53, 2^63) can compare
10609///   Equal to a float via value_cmp's lossy f64 arm while hashing in the
10610///   exact-i64 domain — mixed int/float rows at that magnitude may miss a
10611///   dedup (PG itself compares int8↔float8 in the lossy float8 domain).
10612/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
10613///   compares them blank-insensitively; plain Text pairs that differ only
10614///   in trailing blanks merely collide and are separated exactly).
10615/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
10616///   hash their fields under a distinct tag.
10617/// - Everything value_cmp falls back to debug-format ordering for
10618///   (Json, arrays, vectors, geometry, ranges, …) shares one constant
10619///   bucket — degrades to the exact linear scan, never wrong.
10620fn norm_hash_row(
10621    row: &Row<'static>,
10622    bh: &hashbrown::DefaultHashBuilder,
10623    fold: FoldSpec<'_>,
10624) -> u64 {
10625    norm_hash_values(&row.values, bh, fold)
10626}
10627
10628/// v7.39 (round 485) — the same hash over a bare value slice, so the
10629/// DISTINCT probe can run against a reused buffer instead of demanding a
10630/// `Row` that has to be allocated first (see `values_eq_norm`).
10631fn norm_hash_values(
10632    values: &[Value<'static>],
10633    bh: &hashbrown::DefaultHashBuilder,
10634    fold: FoldSpec<'_>,
10635) -> u64 {
10636    use core::hash::{BuildHasher, Hash, Hasher};
10637    let mut h = bh.build_hasher();
10638    for (i, v) in values.iter().enumerate() {
10639        // v7.39 (round 410) — hash the folded key when the MySQL collation
10640        // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
10641        // `'A'` vs `'a '`) share a hash bucket.
10642        //
10643        // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
10644        // byte-wise column that folded here while the comparator did not
10645        // would scatter equal rows across buckets and stop de-duplicating
10646        // at all; the hash and the comparator have to read the same mask.
10647        if fold.folds(i)
10648            && let Some(folded) = mysql_dedup_fold(v, fold.pads_at(i))
10649        {
10650            folded.hash(&mut h);
10651            continue;
10652        }
10653        norm_hash_value(v, &mut h);
10654    }
10655    h.finish()
10656}
10657
10658/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
10659///
10660/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
10661const fn pow10_i128(p: u16) -> Option<i128> {
10662    const P: [i128; 39] = {
10663        let mut t = [1i128; 39];
10664        let mut i = 1;
10665        while i < 39 {
10666            t[i] = t[i - 1] * 10;
10667            i += 1;
10668        }
10669        t
10670    };
10671    if (p as usize) < P.len() {
10672        Some(P[p as usize])
10673    } else {
10674        None
10675    }
10676}
10677
10678fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
10679    const TAG_NULL: u8 = 0;
10680    const TAG_BOOL: u8 = 1;
10681    const TAG_NUM_I64: u8 = 2;
10682    const TAG_NUM_F64: u8 = 3;
10683    const TAG_TEXT: u8 = 4;
10684    const TAG_DATE: u8 = 6;
10685    const TAG_TIME: u8 = 7;
10686    const TAG_TIMESTAMP: u8 = 8;
10687    const TAG_TIMETZ: u8 = 10;
10688    const TAG_UUID: u8 = 11;
10689    const TAG_MONEY: u8 = 12;
10690    const TAG_BYTES: u8 = 13;
10691    const TAG_INTERVAL: u8 = 14;
10692    const TAG_CHAR1: u8 = 15;
10693    const TAG_OPAQUE: u8 = 255;
10694    // One shared writer for the numeric family: an integer value
10695    // representable as i64 goes exact (round-trip probe — no_std, so no
10696    // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
10697    // through 0i64, folding it into 0.0 as value_cmp requires.
10698    let num_f64 = |h: &mut H, x: f64| {
10699        if x.is_nan() {
10700            h.write_u8(TAG_NUM_F64);
10701            h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
10702            return;
10703        }
10704        const TWO63: f64 = 9_223_372_036_854_775_808.0;
10705        if (-TWO63..TWO63).contains(&x) {
10706            #[allow(clippy::cast_possible_truncation)]
10707            let n = x as i64;
10708            #[allow(clippy::cast_precision_loss)]
10709            if (n as f64) == x {
10710                h.write_u8(TAG_NUM_I64);
10711                h.write_i64(n);
10712                return;
10713            }
10714        }
10715        h.write_u8(TAG_NUM_F64);
10716        h.write_u64(x.to_bits());
10717    };
10718    match v {
10719        Value::Null => h.write_u8(TAG_NULL),
10720        Value::Bool(b) => {
10721            h.write_u8(TAG_BOOL);
10722            h.write_u8(u8::from(*b));
10723        }
10724        Value::SmallInt(n) => {
10725            h.write_u8(TAG_NUM_I64);
10726            h.write_i64(i64::from(*n));
10727        }
10728        Value::Int(n) => {
10729            h.write_u8(TAG_NUM_I64);
10730            h.write_i64(i64::from(*n));
10731        }
10732        Value::BigInt(n) => {
10733            h.write_u8(TAG_NUM_I64);
10734            h.write_i64(*n);
10735        }
10736        Value::Float(x) => num_f64(h, *x),
10737        Value::Numeric {
10738            scaled,
10739            scale,
10740            kind,
10741        } => match kind {
10742            spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
10743            spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
10744            spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
10745            spg_storage::NumericKind::Finite => {
10746                // Reduce trailing fractional zeros so 1.50 and 1.5 share a
10747                // representation, then: exact integers fitting i64 go to the
10748                // i64 domain; everything else uses numeric_to_f64 — the SAME
10749                // formula value_cmp's Numeric↔Float arm compares with.
10750                // r1044 — the reduction is required (`1.5` and `1.50` are
10751                // one value and must land in one bucket) and it used to
10752                // walk one digit at a time. That is O(scale), and scale
10753                // is not small in practice: `n / 100` on a NUMERIC
10754                // column stores `9.1900000000000000`, scale 16, so the
10755                // loop ran fourteen times PER ROW.
10756                //
10757                // Priced by ablation rather than guessed at — removing
10758                // the loop entirely took `SELECT DISTINCT n FROM t ORDER
10759                // BY n` over 400,000 rows from 52 ms to 14.8, against
10760                // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
10761                // tried first moved it not at all, which is why this one
10762                // was measured before it was written.
10763                //
10764                // Binary search over the same powers finds the whole
10765                // run of trailing zeros in at most six tests and one
10766                // division, instead of one test and one division per
10767                // digit.
10768                let (mut s, mut sc) = (*scaled, *scale);
10769                if sc > 0 && s != 0 {
10770                    let mut lo: u16 = 0;
10771                    let mut hi: u16 = sc;
10772                    while lo < hi {
10773                        let mid = (lo + hi).div_ceil(2);
10774                        match pow10_i128(mid) {
10775                            Some(p) if s % p == 0 => lo = mid,
10776                            _ => hi = mid - 1,
10777                        }
10778                    }
10779                    if lo > 0 {
10780                        if let Some(p) = pow10_i128(lo) {
10781                            s /= p;
10782                            sc -= lo;
10783                        }
10784                    }
10785                }
10786                if sc == 0 {
10787                    if let Ok(n) = i64::try_from(s) {
10788                        h.write_u8(TAG_NUM_I64);
10789                        h.write_i64(n);
10790                    } else {
10791                        num_f64(h, crate::orderby::numeric_to_f64(s, 0));
10792                    }
10793                } else {
10794                    num_f64(h, crate::orderby::numeric_to_f64(s, sc));
10795                }
10796            }
10797        },
10798        // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
10799        // value that also fits i128 reuses the Numeric path above so
10800        // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
10801        // any i128-representable value — constant bucket is safe.
10802        Value::NumericBig(b) => match b.to_i128() {
10803            Some(s) => norm_hash_value(
10804                &Value::Numeric {
10805                    scaled: s,
10806                    scale: b.scale(),
10807                    kind: spg_storage::NumericKind::Finite,
10808                },
10809                h,
10810            ),
10811            None => h.write_u8(TAG_OPAQUE),
10812        },
10813        // value_cmp compares Text↔BpChar blank-insensitively (both sides
10814        // trimmed), so both hash the trimmed bytes. Text pairs differing
10815        // only in trailing blanks collide and are split exactly in-bucket.
10816        Value::Text(s) | Value::BpChar(s) => {
10817            h.write_u8(TAG_TEXT);
10818            h.write(s.trim_end_matches(' ').as_bytes());
10819        }
10820        Value::Char1(c) => {
10821            h.write_u8(TAG_CHAR1);
10822            h.write_u8(*c);
10823        }
10824        Value::Date(d) => {
10825            h.write_u8(TAG_DATE);
10826            h.write_i32(*d);
10827        }
10828        Value::Time(t) => {
10829            h.write_u8(TAG_TIME);
10830            h.write_i64(*t);
10831        }
10832        Value::Timestamp(t) => {
10833            h.write_u8(TAG_TIMESTAMP);
10834            h.write_i64(*t);
10835        }
10836        Value::TimeTz { us, offset_secs } => {
10837            h.write_u8(TAG_TIMETZ);
10838            h.write_i64(*us);
10839            h.write_i32(*offset_secs);
10840        }
10841        Value::Uuid(u) => {
10842            h.write_u8(TAG_UUID);
10843            h.write(u);
10844        }
10845        Value::Money(c) => {
10846            h.write_u8(TAG_MONEY);
10847            h.write_i64(*c);
10848        }
10849        Value::Bytes(b) => {
10850            h.write_u8(TAG_BYTES);
10851            h.write(b.as_ref());
10852        }
10853        Value::Interval {
10854            months,
10855            days,
10856            micros,
10857            kind,
10858        } => {
10859            h.write_u8(TAG_INTERVAL);
10860            h.write_i32(*months);
10861            h.write_i32(*days);
10862            h.write_i64(*micros);
10863        }
10864        // v7.37.16 — REAL joined the numeric value_cmp family (widened
10865        // to f64, same formulas as the arms), so it hashes in the shared
10866        // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
10867        // f32→f64 is exact, so equal-under-cmp implies equal bits here.
10868        Value::Real(x) => num_f64(h, f64::from(*x)),
10869        // Json (structural equality), vector families (float rendering),
10870        // arrays / geometry / net / ranges / composites (debug-format
10871        // fallback): one constant bucket — exact linear within.
10872        _ => h.write_u8(TAG_OPAQUE),
10873    }
10874}
10875
10876/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
10877/// treats numerically-equal exact values as one regardless of type or scale
10878/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
10879/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
10880/// `Row` `==` would keep them distinct.
10881/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
10882/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
10883/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
10884/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
10885/// the folded comparison key for a text value, None for anything else (which
10886/// keeps the byte-exact `value_cmp` path).
10887fn mysql_dedup_fold(v: &Value, pads: bool) -> Option<String> {
10888    match v {
10889        // v7.38.17 — CHAR's trailing spaces are padding; TEXT's are
10890        // data. The comment above named `utf8mb4_uca1400_ai_ci`, which
10891        // is MariaDB's default and PAD SPACE. SPG advertises MySQL 8.0,
10892        // whose default is NO PAD, so `'alpha'` and `'alpha  '` are two
10893        // rows to a `SELECT DISTINCT` and one to `count(DISTINCT)` was
10894        // the same question answered twice.
10895        // v7.38.18 — a CHAR's padding is the TYPE's and never counts; a
10896        // TEXT's is the collation's, which `pads` carries per position.
10897        Value::BpChar(s) => Some(spg_storage::mysql_compare_fold_char(s)),
10898        Value::Text(s) if pads => Some(spg_storage::mysql_compare_fold_char(s)),
10899        Value::Text(s) => Some(spg_storage::mysql_compare_fold(s)),
10900        _ => None,
10901    }
10902}
10903
10904/// v7.39 (round 485) — how many projected rows the single-table scan
10905/// builds, and how many of those the DISTINCT probe throws away again.
10906///
10907/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
10908/// 21 % of all samples in malloc/free called straight from the scan
10909/// closure. The closure's one per-row allocation is the projected
10910/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
10911/// instructions later — but "most" is a guess until it is a number, so
10912/// these count it. (Round 480 was spent acting on an inference about a
10913/// branch that turned out never to run.)
10914/// v7.39 (round 488) — reachability counters for round 487's projection
10915/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
10916/// and a never-called-function probe rules out code layout — so the
10917/// question is whether that shape reaches this code at all, which is a
10918/// number, not an inference.
10919pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10920pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10921
10922pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10923pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
10924    core::sync::atomic::AtomicU64::new(0);
10925
10926/// v7.38.13 — how DISTINCT must compare one row of output.
10927///
10928/// The MySQL default collation folds case and trailing spaces when it
10929/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
10930/// must not fold — `e2e_mysql_collate_binary_round370` calls the
10931/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
10932/// one when the schema asked to keep them apart", and names DISTINCT as
10933/// one of the sites that has to honour it.
10934///
10935/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
10936/// value in a MySQL session, because a bool cannot see a column. The
10937/// GROUP BY path consults the schema and was right all along; the test
10938/// only ever exercised that spelling, so the DISTINCT hole was never
10939/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
10940///
10941/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
10942/// which is what a caller with no schema to offer gets.
10943#[derive(Clone, Copy)]
10944pub(crate) struct FoldSpec<'c> {
10945    mysql: bool,
10946    binary: &'c [bool],
10947    /// v7.38.18 — the padding mask, in lockstep with `binary`. Read the
10948    /// note on `folds`: a hash and its comparator must consult the same
10949    /// masks or equal rows scatter across buckets.
10950    pads: &'c [bool],
10951}
10952
10953impl<'c> FoldSpec<'c> {
10954    /// No column information — every Text position folds under MySQL.
10955    pub(crate) const fn dialect(mysql: bool) -> Self {
10956        Self {
10957            mysql,
10958            binary: &[],
10959            pads: &[],
10960        }
10961    }
10962
10963    /// The mask read off the output columns.
10964    pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
10965        Self {
10966            mysql,
10967            binary,
10968            pads: &[],
10969        }
10970    }
10971
10972    /// The masks read off the output columns — fold-exemption AND
10973    /// padding, which are different questions about the same collation.
10974    pub(crate) fn of_masks(mysql: bool, binary: &'c [bool], pads: &'c [bool]) -> Self {
10975        Self {
10976            mysql,
10977            binary,
10978            pads,
10979        }
10980    }
10981
10982    /// Does position `i` treat trailing spaces as insignificant?
10983    #[inline]
10984    fn pads_at(&self, i: usize) -> bool {
10985        self.pads.get(i).copied().unwrap_or(false)
10986    }
10987
10988    /// Does position `i` fold?
10989    #[inline]
10990    fn folds(&self, i: usize) -> bool {
10991        self.mysql && !self.binary.get(i).copied().unwrap_or(false)
10992    }
10993}
10994
10995/// The fold-exempt mask for a projection.
10996///
10997/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
10998/// projection rebuilds that schema through `ColumnSchema::new`, whose
10999/// collation default is `Binary` — a mask built from it would mark
11000/// EVERY column byte-wise and stop DISTINCT folding at all.
11001/// The padding mask for a projection, read off the same items as
11002/// [`fold_mask`] so the two cannot come from different places.
11003pub(crate) fn pad_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11004    projection.iter().map(|p| p.pads).collect()
11005}
11006
11007pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11008    projection.iter().map(|p| p.fold_exempt).collect()
11009}
11010
11011/// v7.38.14 — the same mask, from an OUTPUT SCHEMA instead of a
11012/// projection.
11013///
11014/// Some de-duplication sites hold `Vec<ColumnSchema>` and never see the
11015/// `ProjectedItem`s it came from. `ProjectedItem::fold_exempt` is built
11016/// from exactly this test (`select.rs`, `build_projection`), so the two
11017/// must keep answering identically -- a site that decided "byte-wise" one
11018/// way while its neighbour decided the other is how the answer came to
11019/// depend on which executor ran the query.
11020///
11021/// The direction matters: `Collation::Binary` is `ColumnSchema::new`'s
11022/// DEFAULT, so a schema rebuilt without carrying the field reads as
11023/// "byte-wise on purpose" here. That is a real trap and it has caught
11024/// five fields so far; it is why S4 of this release exists.
11025/// v7.38.18 — the padding mask from output columns, the sibling of
11026/// [`fold_mask_of_columns`]. Whether a column folds and whether it
11027/// pads are different questions about the same collation.
11028pub(crate) fn pad_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11029    columns
11030        .iter()
11031        .map(|c| crate::collate::pads_space(c.collation_name.as_deref()))
11032        .collect()
11033}
11034
11035pub(crate) fn fold_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11036    columns
11037        .iter()
11038        .map(|c| matches!(c.collation, spg_storage::Collation::Binary))
11039        .collect()
11040}
11041
11042pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
11043    values_eq_norm(&a.values, &b.values, fold)
11044}
11045
11046/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
11047/// DISTINCT probe can compare a reused projection buffer against a kept
11048/// row without building a `Row` for it.
11049pub(crate) fn values_eq_norm(
11050    a: &[Value<'static>],
11051    b: &[Value<'static>],
11052    fold: FoldSpec<'_>,
11053) -> bool {
11054    a.len() == b.len()
11055        && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
11056            if fold.folds(i)
11057                && let (Some(fx), Some(fy)) = (
11058                    mysql_dedup_fold(x, fold.pads_at(i)),
11059                    mysql_dedup_fold(y, fold.pads_at(i)),
11060                )
11061            {
11062                return fx == fy;
11063            }
11064            crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
11065        })
11066}
11067
11068/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
11069/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
11070/// order via the byte values; vectors are not sortable.
11071pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
11072    // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
11073    // so values sharing a ≥6-byte common prefix (`product_001` vs
11074    // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
11075    // order by their exact bytes instead of the old lossy f64 coarse key.
11076    // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
11077    // matches PG's default C / binary text collation. Every other type
11078    // keeps the lossless-enough `f64` fast path below.
11079    if let Value::Text(s) = v {
11080        return Ok(OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())));
11081    }
11082    // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
11083    // byte order (PG bpcharcmp under C collation), so mixed-pad values of
11084    // the same logical string order equal.
11085    if let Value::BpChar(s) = v {
11086        return Ok(OrderKey::Text(crate::orderby::CompactText::new(
11087            s.trim_end_matches(' '),
11088        )));
11089    }
11090    // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
11091    // carry the parsed value and compare it structurally (see
11092    // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
11093    if let Value::Json(s) = v {
11094        return Ok(match crate::json::parse(s) {
11095            Ok(jv) => OrderKey::Json(jv),
11096            Err(_) => OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())),
11097        });
11098    }
11099    // v7.37 — byte-orderable types PG sorts byte-wise but that have no
11100    // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
11101    // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
11102    // matching PG's network ordering.
11103    match v {
11104        Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
11105        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
11106        Value::NumericBig(b) => {
11107            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11108                spg_storage::NumericKey::from_big(b),
11109            )));
11110        }
11111        Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
11112        Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11113        Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11114        Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
11115        Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
11116            let mut key = alloc::vec::Vec::with_capacity(18);
11117            key.push(*family);
11118            key.extend_from_slice(addr);
11119            key.push(*bits);
11120            return Ok(OrderKey::Bytes(key));
11121        }
11122        _ => {}
11123    }
11124    // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
11125    // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
11126    // own OrderKey so integer arrays sort numerically; a NULL element rides to
11127    // the end via the +INF sentinel.
11128    let inf = || OrderKey::NullBig;
11129    let arr = match v {
11130        Value::IntArray(a) => Some(
11131            a.iter()
11132                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11133                .collect(),
11134        ),
11135        Value::SmallIntArray(a) => Some(
11136            a.iter()
11137                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11138                .collect(),
11139        ),
11140        Value::BigIntArray(a) => Some(
11141            a.iter()
11142                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11143                .collect(),
11144        ),
11145        Value::BoolArray(a) => Some(
11146            a.iter()
11147                .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
11148                .collect(),
11149        ),
11150        Value::TextArray(a) => Some(
11151            a.iter()
11152                .map(|o| {
11153                    o.as_ref()
11154                        .map_or_else(inf, |s| OrderKey::Text(crate::orderby::CompactText::new(s)))
11155                })
11156                .collect(),
11157        ),
11158        #[allow(clippy::cast_precision_loss)]
11159        Value::FloatArray(a) => Some(
11160            a.iter()
11161                .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
11162                .collect(),
11163        ),
11164        // r1040 — array elements take the same exact key their scalar
11165        // form does; an f64 projection here would order `{0.1}` against
11166        // `{0.1000000000000000001}` by luck.
11167        Value::NumericArray(a) => Some(
11168            a.iter()
11169                .map(|o| {
11170                    o.map_or_else(inf, |(m, s)| {
11171                        OrderKey::Numeric(alloc::boxed::Box::new(
11172                            spg_storage::NumericKey::from_numeric(
11173                                m,
11174                                s,
11175                                spg_storage::NumericKind::Finite,
11176                            ),
11177                        ))
11178                    })
11179                })
11180                .collect(),
11181        ),
11182        Value::DateArray(a) => Some(
11183            a.iter()
11184                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11185                .collect(),
11186        ),
11187        _ => None,
11188    };
11189    if let Some(elements) = arr {
11190        return Ok(OrderKey::Array(elements));
11191    }
11192    // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
11193    // right, which is exactly the lexicographic element order an Array key
11194    // already gives: `(2,'b') < (9,'a')` because the leading field decides.
11195    if let Value::Composite(fields) = v {
11196        let elements = fields
11197            .iter()
11198            .map(|(_, fv)| value_to_order_key(fv))
11199            .collect::<Result<alloc::vec::Vec<_>, _>>()?;
11200        return Ok(OrderKey::Array(elements));
11201    }
11202    // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
11203    // Projecting these to f64 (the historic path) silently collapses BigInt /
11204    // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
11205    // the wrong order for large ids and microsecond timestamps.
11206    match v {
11207        Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11208        Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
11209        Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11210        // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
11211        // integer (days / micros / cents / calendar year); TIMETZ by the
11212        // UTC-equivalent micros (local wall - offset) so the same physical
11213        // instant in different zones sorts equal.
11214        Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
11215        Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
11216        Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
11217        Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
11218        Value::TimeTz { us, offset_secs } => {
11219            return Ok(OrderKey::Int(
11220                i128::from(*us) - i128::from(*offset_secs) * 1_000_000,
11221            ));
11222        }
11223        Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
11224        _ => {}
11225    }
11226    let num = match v {
11227        // Callers without NULLS FIRST/LAST context (array elements,
11228        // histogram sampling) put NULL last, as before.
11229        Value::Null => return Ok(OrderKey::NullBig),
11230        // v7.17.0 Phase 3.P0-38 — range ordering is not supported
11231        // in v7.17.0 (needs lex-then-inclusivity tiebreak).
11232        Value::Range { .. } => {
11233            return Err(EngineError::Unsupported(
11234                "ORDER BY of a range value is not supported in v7.17.0".into(),
11235            ));
11236        }
11237        // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
11238        Value::Hstore(_) => {
11239            return Err(EngineError::Unsupported(
11240                "ORDER BY of a hstore value is not supported".into(),
11241            ));
11242        }
11243        // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
11244        Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
11245            return Err(EngineError::Unsupported(
11246                "ORDER BY of a 2D array is not supported in v7.17.0".into(),
11247            ));
11248        }
11249        // r1039/r1040 — the exact canonical key, not an f64 projection.
11250        //
11251        // r1039 fixed the three specials, which carry a canonical zero in
11252        // `scaled` and so all sorted as the number 0. The projection
11253        // itself was the rest of the defect: "precision losses here only
11254        // matter for tie-breaks well past 15 significant digits" was the
11255        // comment, and the measurement disagreed — f64 called
11256        // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
11257        // returned them in insertion order. Three of ten values came back
11258        // in the wrong place against PG18.4.
11259        Value::Numeric {
11260            scaled,
11261            scale,
11262            kind,
11263        } => {
11264            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11265                spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
11266            )));
11267        }
11268        Value::Float(x) => *x,
11269        // v7.37.16 — REAL sorts by its exact f64 widening (it had no
11270        // arm and fell through to the unsupported error).
11271        Value::Real(x) => f64::from(*x),
11272        Value::Bool(b) => {
11273            if *b {
11274                1.0
11275            } else {
11276                0.0
11277            }
11278        }
11279        Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
11280            return Err(EngineError::Unsupported(
11281                "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
11282            ));
11283        }
11284        // v7.37 — PG orders INTERVAL by its total time, treating a month as
11285        // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
11286        // f64 is exact for any interval under ~285 years, and only ORDER BY
11287        // tie-breaks past that magnitude lose precision. Matches the
11288        // min/max(interval) comparator in aggregate.rs.
11289        #[allow(clippy::cast_precision_loss)]
11290        Value::Interval {
11291            months,
11292            days,
11293            micros,
11294            kind,
11295        } => {
11296            let total = i128::from(*months) * 30 * 86_400_000_000
11297                + i128::from(*days) * 86_400_000_000
11298                + i128::from(*micros);
11299            total as f64
11300        }
11301        Value::Json(_) => {
11302            return Err(EngineError::Unsupported(
11303                "ORDER BY of a JSON value is not supported — cast the document to text first"
11304                    .into(),
11305            ));
11306        }
11307        // v7.5.0 — Value is #[non_exhaustive]; future variants need
11308        // an explicit ORDER BY mapping. Surface as Unsupported until
11309        // engine support is added.
11310        _ => {
11311            return Err(EngineError::Unsupported(
11312                "ORDER BY of this value type is not supported".into(),
11313            ));
11314        }
11315    };
11316    Ok(OrderKey::Num(num))
11317}
11318
11319/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
11320/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
11321/// `EngineError` so the projection-build path keeps `UnknownQualifier`
11322/// vs `ColumnNotFound` distinct.
11323/// PG's name for the physical row identity. It is reserved there — no table
11324/// can have a column called this — which is what lets `*` skip it by name.
11325pub(crate) const CTID_COLUMN: &str = "ctid";
11326
11327/// v7.39 (round 512) — PG's system columns, in the order they are appended.
11328/// All six are reserved names there, which is what lets `*` skip them and
11329/// lets a scan tell them from a user column without a flag.
11330pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
11331
11332/// Is this name one of them?
11333pub(crate) fn is_system_column(name: &str) -> bool {
11334    SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
11335}
11336
11337/// Where the scan's appended system columns begin, if this schema carries
11338/// them: the trailing six, named in order. A catalog view with a column of
11339/// its own called `xmin` does not match, which is the point.
11340fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
11341    let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
11342    cols[start..]
11343        .iter()
11344        .zip(SYSTEM_COLUMNS)
11345        .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
11346        .then_some(start)
11347}
11348
11349/// v7.39 (round 540) — which positions `*` must skip.
11350///
11351/// The rule stays round 512's — the synthetic columns are the trailing
11352/// six of a relation's block, matched by POSITION so a genuine `xmin`
11353/// column is not lost — but a JOINED schema names its columns
11354/// `alias.column` and lays the peers out end to end, so a peer's six sit
11355/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
11356/// "trailing six" test back on the block it was written for.
11357fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11358    let mut skip = alloc::vec![false; cols.len()];
11359    fn qualifier(n: &str) -> Option<&str> {
11360        n.rsplit_once('.').map(|(q, _)| q)
11361    }
11362    fn bare(n: &str) -> &str {
11363        n.rsplit('.').next().unwrap_or(n)
11364    }
11365    let mut i = 0;
11366    while i < cols.len() {
11367        let q = qualifier(&cols[i].name);
11368        let mut end = i;
11369        while end < cols.len() && qualifier(&cols[end].name) == q {
11370            end += 1;
11371        }
11372        if let Some(start) = (end - i)
11373            .checked_sub(SYSTEM_COLUMNS.len())
11374            .map(|off| i + off)
11375            && cols[start..end]
11376                .iter()
11377                .zip(SYSTEM_COLUMNS)
11378                .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
11379        {
11380            for s in skip.iter_mut().take(end).skip(start) {
11381                *s = true;
11382            }
11383        }
11384        i = end;
11385    }
11386    skip
11387}
11388
11389/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
11390/// read? Only then is the column materialised.
11391pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
11392    let mut found = false;
11393    crate::expr_analysis::visit_expr_columns_and_subqueries(
11394        e,
11395        &mut |c| {
11396            if is_system_column(&c.name) {
11397                found = true;
11398            }
11399        },
11400        &mut |_| {},
11401    );
11402    found
11403}
11404
11405fn references_ctid(stmt: &SelectStatement) -> bool {
11406    let in_expr = expr_references_ctid;
11407    stmt.items.iter().any(|i| match i {
11408        SelectItem::Expr { expr, .. } => in_expr(expr),
11409        _ => false,
11410    }) || stmt.where_.as_ref().is_some_and(in_expr)
11411        || stmt.order_by.iter().any(|o| in_expr(&o.expr))
11412        || stmt
11413            .group_by
11414            .as_ref()
11415            .is_some_and(|g| g.iter().any(in_expr))
11416        || stmt.having.as_ref().is_some_and(in_expr)
11417}
11418
11419/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
11420/// is a name the projection has to TYPE before any row exists.
11421///
11422/// Evaluation has answered this since round T9 (`resolve_column` builds a
11423/// `Value::Composite` of every column), but the typing side below had no
11424/// such branch and raised `column "t" does not exist` first — so the
11425/// feature was unreachable through a projection. Measured against PG18.4:
11426/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
11427///
11428/// The type is `Jsonb` + a composite marker, which is exactly how a
11429/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
11430/// the value travels as a `Value::Composite` and renders in the canonical
11431/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
11432/// so the marker names the alias and no rehydration keys off it — the
11433/// value arrives already built.
11434fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
11435    let mut s = ColumnSchema::new(
11436        alloc::string::String::from(alias),
11437        spg_storage::DataType::Jsonb,
11438        true,
11439    );
11440    s.user_composite_type = Some(alloc::string::String::from(alias));
11441    s
11442}
11443
11444/// v7.39.3 — `mysql` makes the name comparison case-INSENSITIVE, which
11445/// is MySQL's rule for column names (measured on 9.7.2: `mycol`,
11446/// `MYCOL` and a backquoted `MyCol` all resolve the same column).
11447///
11448/// SPG compared byte for byte and its lexer folds an UNQUOTED
11449/// identifier, so a table restored from a `mysqldump` — where every
11450/// identifier is backquoted and keeps its case — had every mixed-case
11451/// column unreachable from ordinary unquoted SQL. Same "two spellings,
11452/// two things" defect v7.39.1 closed for relation names.
11453pub(crate) fn resolve_projection_column<'a>(
11454    c: &ColumnName,
11455    schema_cols: &'a [ColumnSchema],
11456    table_alias: &str,
11457    mysql: bool,
11458) -> Result<Cow<'a, ColumnSchema>, EngineError> {
11459    let same = |a: &str, b: &str| {
11460        if mysql {
11461            a.eq_ignore_ascii_case(b)
11462        } else {
11463            a == b
11464        }
11465    };
11466    if let Some(q) = &c.qualifier {
11467        let composite = alloc::format!("{q}.{name}", name = c.name);
11468        if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &composite)) {
11469            return Ok(Cow::Borrowed(s));
11470        }
11471        // Single-table case: the qualifier may equal the active alias —
11472        // then look for the bare column name.
11473        if same(q, table_alias)
11474            && let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name))
11475        {
11476            return Ok(Cow::Borrowed(s));
11477        }
11478        // For multi-table schemas the qualifier is unknown only if no
11479        // column bears the "<q>." prefix. For single-table, the alias
11480        // mismatch alone is enough.
11481        let prefix = alloc::format!("{q}.");
11482        let qualifier_known =
11483            same(q, table_alias) || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
11484        if !qualifier_known {
11485            return Err(EngineError::Eval(EvalError::UnknownQualifier {
11486                qualifier: q.clone(),
11487                column: c.name.clone(),
11488            }));
11489        }
11490        return Err(EngineError::Eval(EvalError::ColumnNotFound {
11491            name: c.name.clone(),
11492        }));
11493    }
11494    if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name)) {
11495        return Ok(Cow::Borrowed(s));
11496    }
11497    let suffix = alloc::format!(".{name}", name = c.name);
11498    let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
11499    let first = matches.next();
11500    let extra = matches.next();
11501    match (first, extra) {
11502        (Some(s), None) => Ok(Cow::Borrowed(s)),
11503        (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
11504            detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
11505        })),
11506        // The whole-row reference, checked LAST so a real column carrying
11507        // the alias's name still wins — the same precedence
11508        // `resolve_column` applies on the evaluation side.
11509        //
11510        // Two schema shapes reach here. A single-table (or subquery, or
11511        // CTE) scan carries its alias and bare column names, so the name
11512        // has to equal the alias. A JOIN's combined schema carries no
11513        // alias at all and qualifies every column `alias.col`, so the
11514        // alias is identified by the prefix instead — which is exactly
11515        // how `whole_row_composite` picks the fields out on the
11516        // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
11517        // answers `(7,z)` on PG18.4 and errored here until this arm
11518        // covered the joined shape too.
11519        _ if !table_alias.is_empty() && c.name == table_alias => {
11520            Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
11521        }
11522        _ if table_alias.is_empty() && {
11523            let prefix = alloc::format!("{name}.", name = c.name);
11524            schema_cols.iter().any(|s| s.name.starts_with(&prefix))
11525        } =>
11526        {
11527            Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
11528        }
11529        _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
11530            name: c.name.clone(),
11531        })),
11532    }
11533}
11534
11535/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
11536/// parser to carry per-branch GROUPING() masks into a grouping-set query's
11537/// ORDER BY. They must never reach the output. No-op unless such a column is
11538/// present, so the common path is untouched.
11539/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
11540///
11541/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
11542/// a `LIMIT 2` that should have answered two groups answered one.
11543fn apply_deferred_limit(
11544    rows: alloc::vec::Vec<Row<'static>>,
11545    deferred: &(
11546        Option<spg_sql::ast::LimitExpr>,
11547        Option<spg_sql::ast::LimitExpr>,
11548    ),
11549) -> alloc::vec::Vec<Row<'static>> {
11550    let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
11551        Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
11552        _ => None,
11553    };
11554    let mut rows = rows;
11555    if let Some(off) = count(&deferred.1) {
11556        rows = rows.split_off(off.min(rows.len()));
11557    }
11558    if let Some(lim) = count(&deferred.0) {
11559        rows.truncate(lim);
11560    }
11561    rows
11562}
11563
11564fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
11565    let QueryResult::Rows { columns, rows } = result else {
11566        return result;
11567    };
11568    if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
11569        return QueryResult::Rows { columns, rows };
11570    }
11571    let keep: Vec<usize> = columns
11572        .iter()
11573        .enumerate()
11574        .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
11575        .map(|(i, _)| i)
11576        .collect();
11577    let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
11578    let new_rows: Vec<Row<'static>> = rows
11579        .into_iter()
11580        .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
11581        .collect();
11582    QueryResult::Rows {
11583        columns: new_cols,
11584        rows: new_rows,
11585    }
11586}
11587
11588/// v7.39 (round 487) — bind every projection item that is a bare column
11589/// reference to its position, once per query.
11590///
11591/// `#[inline(never)]` and out of line on purpose. Round 486 established
11592/// that adding code inside these scan bodies moves neighbouring hot
11593/// functions around under fat LTO: the first version of this had the loop
11594/// inline in `run_single_table_scan` and four aggregate shapes that never
11595/// touch that function — `full_agg`, `join_agg`, `group_500k`,
11596/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
11597/// the same machine. Keeping it out of line kept them still.
11598#[inline(never)]
11599fn bind_direct_columns(
11600    projection: &[ProjectedItem],
11601    ctx: &eval::EvalContext<'_>,
11602) -> Vec<Option<usize>> {
11603    projection
11604        .iter()
11605        .map(|p| match &p.expr {
11606            Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
11607                // Same exclusion `compile_into` makes: a composite column
11608                // has to be rehydrated from stored JSON, which is not a
11609                // cell read.
11610                ctx.columns
11611                    .get(*pos)
11612                    .is_none_or(|sc| sc.user_composite_type.is_none())
11613            }),
11614            _ => None,
11615        })
11616        .collect()
11617}
11618
11619/// v7.39 (round 505) — the name an un-aliased projected expression reports.
11620///
11621/// PG18 names a call for its function and everything else `?column?`;
11622/// measured with `\gdesc`. SPG used to print the parsed expression back
11623/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
11624/// name-keyed row access found nothing under `upper`.
11625///
11626/// The MySQL half is NOT this rule and is deliberately left alone here:
11627/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
11628/// which needs the parser to hand over spans the AST does not carry yet.
11629/// Until it does, a MySQL session keeps the printed form — closer to what
11630/// MariaDB answers than `?column?` would be.
11631pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
11632    if mysql {
11633        return expr.to_string();
11634    }
11635    spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
11636}
11637
11638pub(crate) fn build_projection(
11639    items: &[SelectItem],
11640    schema_cols: &[ColumnSchema],
11641    table_alias: &str,
11642    mysql: bool,
11643    cat: Option<&Catalog>,
11644) -> Result<Vec<ProjectedItem>, EngineError> {
11645    build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0, cat)
11646}
11647
11648/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
11649/// invisible to `*`.
11650///
11651/// The windowed-SELECT path appends a synthetic `__win_N` column per window
11652/// function so the rewritten projection can reference the computed values as
11653/// ordinary columns. `*` then expanded them too, and
11654/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
11655/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
11656/// silent one: the row simply had one more field than the client asked for.
11657///
11658/// Hidden by POSITION rather than by name, for the reason round 512 recorded
11659/// about the system columns: a name test looks safe until a real column
11660/// happens to carry the name. These are appended last, so the count is what
11661/// identifies them.
11662pub(crate) fn build_projection_hiding_tail(
11663    items: &[SelectItem],
11664    schema_cols: &[ColumnSchema],
11665    table_alias: &str,
11666    mysql: bool,
11667    hidden_tail: usize,
11668    // v7.38.19 — the catalog, so a user-defined function's DECLARED
11669    // return type reaches the projection. Without it `describe_expr`
11670    // cannot type `f_sql()` and the column falls back to text, which is
11671    // what psql reads to decide alignment: `SELECT 7::bigint, f_sql()`
11672    // right-aligned one cell and left-aligned the other while both held
11673    // a bigint. Reported by sentori against 7.38.18 (their §2.2), who
11674    // also established that the EXECUTOR was never confused -- CTAS off
11675    // the same expression gives a bigint column, and arithmetic on it
11676    // works. Only the type travelling in the RowDescription was wrong.
11677    cat: Option<&Catalog>,
11678) -> Result<Vec<ProjectedItem>, EngineError> {
11679    let visible = schema_cols.len().saturating_sub(hidden_tail);
11680    // v7.39 (round 462) — a join's combined schema qualifies every column
11681    // `alias.col` so the deferred-join cell lookups resolve by composite
11682    // name. That is an internal convention, and `*` was handing it to the
11683    // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
11684    // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
11685    // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
11686    // already learned this for `q.*`; plain `*` never got the same rule.
11687    //
11688    // The signal is the schema itself, not the call site: only a combined
11689    // join schema arrives with no table alias AND every column qualified.
11690    // A single-table schema carries its alias, an empty schema has nothing
11691    // to strip, and a synthetic schema's names carry no dot.
11692    let joined_schema = table_alias.is_empty()
11693        && !schema_cols.is_empty()
11694        && schema_cols.iter().all(|c| c.name.contains('.'));
11695    let bare_name = |name: &str| -> String {
11696        if !joined_schema {
11697            return name.to_string();
11698        }
11699        match name.split_once('.') {
11700            Some((_, rest)) if !rest.is_empty() => rest.to_string(),
11701            _ => name.to_string(),
11702        }
11703    };
11704    let mut out = Vec::new();
11705    for item in items {
11706        match item {
11707            SelectItem::Wildcard => {
11708                // v7.39 (round 511) — `*` never expands a system column, as
11709                // PG's does not. They join the schema only when the statement
11710                // asked for them, so this matters for the mixed shape
11711                // `SELECT *, ctid FROM t`.
11712                //
11713                // v7.39 (round 512) — by POSITION, not by name. Matching on
11714                // the name alone looked safe because PG reserves them, and it
11715                // is not: `pg_replication_slots` genuinely has a column called
11716                // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
11717                // Only the trailing six, in the order the scan appends them,
11718                // are the synthetic ones.
11719                let sys_skip = synthetic_system_positions(schema_cols);
11720                for (idx, col) in schema_cols.iter().enumerate() {
11721                    if sys_skip[idx] || idx >= visible {
11722                        continue;
11723                    }
11724                    out.push(ProjectedItem {
11725                        expr: Expr::Column(ColumnName {
11726                            qualifier: None,
11727                            name: col.name.clone(),
11728                        }),
11729                        output_name: bare_name(&col.name),
11730                        ty: col.ty,
11731                        nullable: col.nullable,
11732                        user_enum_type: col.user_enum_type.clone(),
11733                        mysql_fsp: col.mysql_fsp,
11734                        collation_name: col.collation_name.clone(),
11735                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
11736                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
11737                    });
11738                }
11739            }
11740            // v7.39 (round 128) — `q.*` expands to every column belonging to
11741            // the qualifier `q`. Single-table schemas carry bare column names
11742            // reachable via `table_alias`; a join's combined schema carries
11743            // `alias.col` names, so a column belongs to `q` when its name has
11744            // the `q.` prefix. PG labels the expanded columns by their bare
11745            // name, so the `alias.` prefix is stripped from the output name.
11746            SelectItem::QualifiedWildcard(q) => {
11747                let prefix = alloc::format!("{q}.");
11748                let single_table = !table_alias.is_empty() && q == table_alias;
11749                let mut matched = 0usize;
11750                for col in &schema_cols[..visible] {
11751                    let belongs =
11752                        col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
11753                    if !belongs {
11754                        continue;
11755                    }
11756                    matched += 1;
11757                    let output_name = col
11758                        .name
11759                        .strip_prefix(&prefix)
11760                        .unwrap_or(&col.name)
11761                        .to_string();
11762                    out.push(ProjectedItem {
11763                        expr: Expr::Column(ColumnName {
11764                            qualifier: None,
11765                            name: col.name.clone(),
11766                        }),
11767                        output_name,
11768                        ty: col.ty,
11769                        nullable: col.nullable,
11770                        user_enum_type: col.user_enum_type.clone(),
11771                        mysql_fsp: col.mysql_fsp,
11772                        collation_name: col.collation_name.clone(),
11773                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
11774                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
11775                    });
11776                }
11777                if matched == 0 {
11778                    // `q.*` names no column, so the reference IS the star.
11779                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
11780                        qualifier: q.clone(),
11781                        column: alloc::string::String::from("*"),
11782                    }));
11783                }
11784            }
11785            SelectItem::Expr { expr, alias } => {
11786                // Plain column ref keeps full schema info (real type +
11787                // nullability). For compound expressions try the
11788                // describe-side function-return-type table first
11789                // (e.g. `SELECT now()` → Timestamptz, `SELECT
11790                // concat(…)` → Text). Falls back to nullable Text
11791                // for shapes the describe path can't resolve.
11792                if let Expr::Column(c) = expr {
11793                    let sch = resolve_projection_column(c, schema_cols, table_alias, mysql)?;
11794                    let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
11795                    out.push(ProjectedItem {
11796                        expr: expr.clone(),
11797                        output_name,
11798                        ty: sch.ty,
11799                        nullable: sch.nullable,
11800                        // v7.39 (read01 round 54) — a bare enum column keeps
11801                        // its enum identity through the projection.
11802                        user_enum_type: sch.user_enum_type.clone(),
11803                        mysql_fsp: sch.mysql_fsp,
11804                        collation_name: sch.collation_name.clone(),
11805                        // v7.38.13 — and its byte-wise-ness. This is the
11806                        // site `SELECT DISTINCT t FROM t` arrives at.
11807                        fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
11808                        pads: crate::collate::pads_space(sch.collation_name.as_deref()),
11809                    });
11810                } else if let Some(shape) = describe::describe_expr_in(expr, schema_cols, cat) {
11811                    let output_name = alias
11812                        .clone()
11813                        .unwrap_or_else(|| default_output_name(expr, mysql));
11814                    out.push(ProjectedItem {
11815                        expr: expr.clone(),
11816                        // v7.38.18 — a projected EXPRESSION has no column collation
11817                        // to read, so it takes the session default, which is MySQL
11818                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
11819                        pads: false,
11820                        output_name,
11821                        ty: shape.ty,
11822                        // v7.39 (round 258) — a projected EXPRESSION keeps its
11823                        // enum identity too, not just a bare column. `FROM
11824                        // (VALUES ('happy'::mood), …) t(m)` lowers to constant
11825                        // SELECTs, so the derived column arrived here as a cast
11826                        // and lost the enum — making the outer ORDER BY / min /
11827                        // max / array_agg sort by the label's TEXT.
11828                        nullable: shape.nullable,
11829                        user_enum_type: None,
11830                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11831                        // A bare column reference keeps its collation; any
11832                        // other expression produces a new value and has none.
11833                        collation_name: match expr {
11834                            Expr::Column(c) => schema_cols
11835                                .iter()
11836                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11837                                .and_then(|sc| sc.collation_name.clone()),
11838                            _ => None,
11839                        },
11840                        fold_exempt: match expr {
11841                            Expr::Column(c) => schema_cols
11842                                .iter()
11843                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11844                                .is_some_and(|sc| {
11845                                    matches!(sc.collation, spg_storage::Collation::Binary)
11846                                }),
11847                            // Not a column: no declared collation to honour,
11848                            // so the session default applies and it folds.
11849                            _ => false,
11850                        },
11851                    });
11852                } else {
11853                    let output_name = alias
11854                        .clone()
11855                        .unwrap_or_else(|| default_output_name(expr, mysql));
11856                    out.push(ProjectedItem {
11857                        expr: expr.clone(),
11858                        // v7.38.18 — a projected EXPRESSION has no column collation
11859                        // to read, so it takes the session default, which is MySQL
11860                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
11861                        pads: false,
11862                        output_name,
11863                        // A user ENUM has no DataType of its own, so
11864                        // `describe_expr` cannot type `'ok'::mood` and the
11865                        // item lands HERE, defaulting to text — which is why
11866                        // pg_typeof answered `text` and a derived table sorted
11867                        // enum values by their label.
11868                        ty: DataType::Text,
11869                        nullable: true,
11870                        user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
11871                            .map(alloc::string::String::from),
11872                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11873                        collation_name: match expr {
11874                            Expr::Column(c) => schema_cols
11875                                .iter()
11876                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11877                                .and_then(|sc| sc.collation_name.clone()),
11878                            _ => None,
11879                        },
11880                        fold_exempt: match expr {
11881                            Expr::Column(c) => schema_cols
11882                                .iter()
11883                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11884                                .is_some_and(|sc| {
11885                                    matches!(sc.collation, spg_storage::Collation::Binary)
11886                                }),
11887                            // Not a column: no declared collation to honour,
11888                            // so the session default applies and it folds.
11889                            _ => false,
11890                        },
11891                    });
11892                }
11893            }
11894        }
11895    }
11896    Ok(out)
11897}
11898
11899// ---- v4.12 window-function helpers ----
11900// The (partition-key, order-key, original-index) tuple shape used
11901// across these helpers is intrinsic to the planner. Factoring it
11902// into a typedef adds indirection without making the code clearer,
11903// so several lints are allowed inline on the affected functions
11904// rather than module-wide.
11905
11906/// v4.22: pick more specific column types from observed rows when
11907/// the projection builder defaulted to Text (the v1.x behavior for
11908/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
11909/// land an Int column in the CTE storage table rather than failing
11910/// the insert with "expected TEXT, got INT".
11911pub(crate) fn infer_column_types(
11912    columns: &[ColumnSchema],
11913    rows: &[Row<'static>],
11914) -> Vec<ColumnSchema> {
11915    let mut out = columns.to_vec();
11916    for (col_idx, col) in out.iter_mut().enumerate() {
11917        if col.ty != DataType::Text {
11918            continue;
11919        }
11920        let mut inferred: Option<DataType> = None;
11921        let mut all_null = true;
11922        for row in rows {
11923            let Some(v) = row.values.get(col_idx) else {
11924                continue;
11925            };
11926            let ty = match v {
11927                Value::Null => continue,
11928                Value::SmallInt(_) => DataType::SmallInt,
11929                Value::Int(_) => DataType::Int,
11930                Value::BigInt(_) => DataType::BigInt,
11931                Value::Float(_) => DataType::Float,
11932                Value::Bool(_) => DataType::Bool,
11933                Value::Vector(_) => DataType::Vector {
11934                    dim: 0,
11935                    encoding: VecEncoding::F32,
11936                },
11937                // v7.38 (read01 U16) — carry array values through with an
11938                // array type so a recursive CTE that projects an array
11939                // (e.g. a SEARCH/CYCLE ord / path column) types the working
11940                // column as an array, not Text.
11941                Value::TextArray(_) => DataType::TextArray,
11942                Value::IntArray(_) => DataType::IntArray,
11943                Value::BigIntArray(_) => DataType::BigIntArray,
11944                Value::SmallIntArray(_) => DataType::SmallIntArray,
11945                Value::FloatArray(_) => DataType::FloatArray,
11946                Value::BoolArray(_) => DataType::BoolArray,
11947                // v7.39 (GUC knife 2) — an interval projection describes
11948                // as INTERVAL (typed drivers read the RowDescription OID).
11949                Value::Interval { .. } => DataType::Interval,
11950                _ => DataType::Text,
11951            };
11952            all_null = false;
11953            inferred = Some(match inferred {
11954                None => ty,
11955                Some(prev) if prev == ty => prev,
11956                Some(_) => DataType::Text,
11957            });
11958        }
11959        if let Some(t) = inferred {
11960            col.ty = t;
11961            col.nullable = true;
11962        } else if all_null {
11963            col.nullable = true;
11964        }
11965    }
11966    out
11967}
11968
11969/// Numeric widening rank for UNION type resolution (higher = wider).
11970fn numeric_rank(t: DataType) -> Option<u8> {
11971    match t {
11972        DataType::SmallInt => Some(1),
11973        DataType::Int => Some(2),
11974        DataType::BigInt => Some(3),
11975        DataType::Numeric { .. } => Some(4),
11976        DataType::Float => Some(5),
11977        _ => None,
11978    }
11979}
11980
11981/// Resolve the common result type for a UNION / VALUES column from the
11982/// set of concrete (non-NULL) branch types, following the safe subset
11983/// of PG's type resolution:
11984///   * all-numeric  → the widest numeric (int ∪ bigint → bigint, … ∪
11985///     numeric → numeric, … ∪ float → float);
11986///   * DATE ∪ TIMESTAMP → TIMESTAMP;
11987///   * exactly one concrete non-TEXT type mixed with TEXT literals →
11988///     that concrete type (the TEXT cells get parsed into it).
11989/// Returns `None` for anything ambiguous, so the caller leaves the
11990/// column untouched rather than risk a wrong or failing coercion.
11991fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
11992    // NB: types are collected from RUNTIME values, which are coarser
11993    // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
11994    // a single-concrete-type fast path must NOT overwrite the column
11995    // type — it would downgrade tstz to ts. NULL-only unification (PG:
11996    // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
11997    // row's pg_typeof) needs schema-level resolution — recorded, not
11998    // attempted here.
11999    if types.len() < 2 {
12000        return None;
12001    }
12002    if types.iter().all(|t| numeric_rank(*t).is_some()) {
12003        return types
12004            .iter()
12005            .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
12006            .copied();
12007    }
12008    let non_text: Vec<&DataType> = types
12009        .iter()
12010        .filter(|t| !matches!(t, DataType::Text))
12011        .collect();
12012    // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
12013    // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
12014    // if any is timestamp the result is timestamp (ts ∪ date). All values are
12015    // the same UTC-micros instant, so widening date/ts to tstz is lossless.
12016    if non_text.iter().all(|t| {
12017        matches!(
12018            t,
12019            DataType::Date | DataType::Timestamp | DataType::Timestamptz
12020        )
12021    }) && non_text
12022        .iter()
12023        .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
12024    {
12025        if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
12026            return Some(DataType::Timestamptz);
12027        }
12028        return Some(DataType::Timestamp);
12029    }
12030    // A single concrete non-TEXT type mixed with TEXT literals.
12031    if non_text.len() == 1 {
12032        return Some(*non_text[0]);
12033    }
12034    // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
12035    // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
12036    // text): resolve the concrete set first (PG treats the unknown-
12037    // typed string literals as castable to whatever the knowns
12038    // resolve to), then the TEXT cells parse into that target — the
12039    // caller's coercion dry-run still abandons the column if any
12040    // literal doesn't parse.
12041    if !non_text.is_empty() && non_text.len() < types.len() {
12042        let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
12043        return resolve_union_common_type(&concrete);
12044    }
12045    None
12046}
12047
12048/// Coerce every cell of a UNION / VALUES result column to one common
12049/// type (see [`resolve_union_common_type`]). Conservative: a column
12050/// whose branches already agree, or whose types don't resolve, or where
12051/// any cell fails to coerce, is left exactly as it was — this never
12052/// turns a previously-working query into an error.
12053fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
12054    for col_idx in 0..columns.len() {
12055        let mut seen: Vec<DataType> = Vec::new();
12056        for row in rows.iter() {
12057            if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
12058                if !seen.contains(&dt) {
12059                    seen.push(dt);
12060                }
12061            }
12062        }
12063        // v7.37.16 — a single concrete runtime type under a TEXT-typed
12064        // column means the column type came off a NULL (or unknown-text)
12065        // branch: NULL literals describe as TEXT (`L::Null → Text`), so
12066        // `VALUES (NULL),(1.5)` left the column "text" while every
12067        // non-NULL cell is numeric. Adopt the concrete type — schema
12068        // only, no cell changes. tstz-safe by construction: a real
12069        // timestamptz column's schema type is Timestamptz, not Text, so
12070        // the coarser runtime type (Value::Timestamp) can't downgrade it
12071        // through this arm; and a real text column's non-NULL cells are
12072        // Text, which keeps seen == [Text] and skips it.
12073        if seen.len() == 1
12074            && matches!(columns[col_idx].ty, DataType::Text)
12075            && !matches!(seen[0], DataType::Text)
12076        {
12077            columns[col_idx].ty = seen[0];
12078            continue;
12079        }
12080        let Some(target) = resolve_union_common_type(&seen) else {
12081            continue;
12082        };
12083        // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
12084        // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
12085        // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
12086        // existing numeric cell untouched and only promote integers (to scale 0)
12087        // rather than rescaling everything to the widest scale.
12088        let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
12089        // Dry-run the coercion; abandon the whole column if any fails.
12090        let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
12091        let mut ok = true;
12092        for row in rows.iter() {
12093            match row.values.get(col_idx) {
12094                Some(Value::Numeric { .. }) if scale_preserving_numeric => {
12095                    coerced.push(Some(row.values[col_idx].clone()));
12096                }
12097                Some(v) => {
12098                    let cell_target = if scale_preserving_numeric {
12099                        DataType::Numeric {
12100                            precision: 0,
12101                            scale: 0,
12102                        }
12103                    } else {
12104                        target
12105                    };
12106                    match crate::conversions::coerce_value(
12107                        v.clone(),
12108                        cell_target,
12109                        &columns[col_idx].name,
12110                        col_idx,
12111                    ) {
12112                        Ok(cv) => coerced.push(Some(cv)),
12113                        Err(_) => {
12114                            ok = false;
12115                            break;
12116                        }
12117                    }
12118                }
12119                None => coerced.push(None),
12120            }
12121        }
12122        if !ok {
12123            continue;
12124        }
12125        for (row, cv) in rows.iter_mut().zip(coerced) {
12126            if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
12127                *slot = nv;
12128            }
12129        }
12130        columns[col_idx].ty = target;
12131    }
12132}
12133
12134/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
12135/// dedup inside the recursive iteration. Crude but deterministic
12136/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
12137fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
12138    let mut out = Vec::new();
12139    for v in &row.values {
12140        // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
12141        // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
12142        // like PG (and like GROUP BY, which already normalizes). The old
12143        // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
12144        // the exact-decimal family through one scale-stripped canonical form.
12145        match v {
12146            Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12147            Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12148            Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12149            Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
12150            other => {
12151                let s = alloc::format!("{other:?}|");
12152                out.extend_from_slice(s.as_bytes());
12153            }
12154        }
12155    }
12156    out
12157}
12158
12159/// Append a scale-independent canonical key for an exact-decimal value: strip
12160/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
12161/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
12162fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
12163    while scale > 0 && scaled % 10 == 0 {
12164        scaled /= 10;
12165        scale -= 1;
12166    }
12167    let s = alloc::format!("\u{1}{scaled}e-{scale}|");
12168    out.extend_from_slice(s.as_bytes());
12169}
12170
12171/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
12172/// (uncorrelated; outer refs were substituted upstream), then zip
12173/// them in parallel, NULL-padding shorter arrays to the longest
12174/// (PG's ROWS FROM shorthand). Shared by the primary-position
12175/// executor and the join-position materialiser, which both detect
12176/// the parser's `__unnest_zip` marker call.
12177pub(crate) fn unnest_zip_rows(
12178    args: &[Expr],
12179) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
12180    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12181    let ctx = EvalContext::new(&empty_schema, None);
12182    let dummy_row = Row::new(alloc::vec::Vec::new());
12183    let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
12184    let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
12185        alloc::vec::Vec::with_capacity(args.len());
12186    for a in args {
12187        let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
12188        let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = match v {
12189            Value::Null => (DataType::Text, alloc::vec::Vec::new()),
12190            Value::TextArray(xs) => (
12191                DataType::Text,
12192                xs.into_iter()
12193                    .map(|x| x.map(Value::text).unwrap_or(Value::Null))
12194                    .collect(),
12195            ),
12196            Value::IntArray(xs) => (
12197                DataType::Int,
12198                xs.into_iter()
12199                    .map(|x| x.map(Value::Int).unwrap_or(Value::Null))
12200                    .collect(),
12201            ),
12202            Value::BigIntArray(xs) => (
12203                DataType::BigInt,
12204                xs.into_iter()
12205                    .map(|x| x.map(Value::BigInt).unwrap_or(Value::Null))
12206                    .collect(),
12207            ),
12208            other => {
12209                return Err(EngineError::Unsupported(alloc::format!(
12210                    "unnest() expects array arguments, got {}",
12211                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
12212                )));
12213            }
12214        };
12215        dtypes.push(dt);
12216        columns.push(items);
12217    }
12218    let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
12219    let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
12220    for i in 0..max_len {
12221        let vals: alloc::vec::Vec<Value<'static>> = columns
12222            .iter()
12223            .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
12224            .collect();
12225        rows.push(Row::new(vals));
12226    }
12227    Ok((dtypes, rows))
12228}
12229
12230/// Detect the parser's multi-arg unnest marker on an unnest_expr.
12231pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
12232    match expr {
12233        Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
12234        _ => None,
12235    }
12236}
12237
12238/// Evaluate generate_series arguments (uncorrelated — outer refs
12239/// were substituted upstream where applicable) and build the row
12240/// stream. Dispatches on the start value's shape and rejects
12241/// mixed-shape calls early (e.g. start = timestamp, stop =
12242/// integer) so the caller gets a clean error rather than a panic.
12243/// Shared by the primary-position executor and the join-position
12244/// materialiser.
12245pub(crate) fn generate_series_rows(
12246    args: &[Expr],
12247    cancel: &CancelToken<'_>,
12248) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12249    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12250    let ctx = EvalContext::new(&empty_schema, None);
12251    let dummy_row = Row::new(alloc::vec::Vec::new());
12252    let mut arg_values: alloc::vec::Vec<Value<'static>> =
12253        alloc::vec::Vec::with_capacity(args.len());
12254    for a in args {
12255        arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
12256    }
12257    generate_series_from_values(arg_values, args, cancel)
12258}
12259
12260/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
12261/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
12262/// full integer / numeric / timestamp overload set with the FROM-clause path.
12263/// Before this split the target-list arm reimplemented only the integer case,
12264/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
12265/// NULL for the timestamp column instead of the series. `arg_values` are the
12266/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
12267/// timestamp type resolution (it inspects the argument expressions' types).
12268pub(crate) fn generate_series_from_values(
12269    mut arg_values: alloc::vec::Vec<Value<'static>>,
12270    args: &[Expr],
12271    cancel: &CancelToken<'_>,
12272) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12273    // PG: a NULL bound or step yields zero rows (also keeps the
12274    // NULL-padded lateral probe alive — schema without data).
12275    if arg_values.iter().any(|v| matches!(v, Value::Null)) {
12276        return Ok((DataType::BigInt, alloc::vec::Vec::new()));
12277    }
12278    // PG resolves `generate_series(date, date, interval)` to the
12279    // timestamp/timestamptz overload by implicitly casting each date
12280    // bound up to a timestamp at midnight (verified vs live PG18.4:
12281    // date args yield rows anchored at 00:00:00). SPG's TZ-naive
12282    // timestamp model renders the same instants, so fold any Date
12283    // bound to its midnight Timestamp (canonical `days *
12284    // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
12285    // the shape match so the existing timestamp arm drives the walk.
12286    // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
12287    // `generate_series(date, date, interval)` has no date overload, and among
12288    // the two candidates PG prefers the timestamptz one (timestamptz is the
12289    // preferred type of the datetime category), so the column comes back
12290    // `timestamp with time zone` — the rows render with a `+00` offset. A
12291    // timestamptz bound obviously lands there too. Only genuinely
12292    // timestamp-typed bounds keep the TZ-naive result type.
12293    let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12294    let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
12295        || args.iter().any(|a| {
12296            crate::describe::describe_expr(a, &empty_cols)
12297                .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
12298        });
12299    for v in &mut arg_values {
12300        if let Value::Date(d) = *v {
12301            *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
12302        }
12303    }
12304    match arg_values.as_slice() {
12305        [Value::Timestamp(start), Value::Timestamp(stop), step] => {
12306            let interval_step = match step {
12307                Value::Interval { .. } => step.clone(),
12308                // v7.38 (read01) — PG resolves an unknown-type string step
12309                // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
12310                // a bare text step by parsing it the same way `::interval` does.
12311                Value::Text(s) => crate::conversions::coerce_value(
12312                    Value::text(s.as_ref()),
12313                    DataType::Interval,
12314                    "",
12315                    0,
12316                )
12317                .map_err(|_| {
12318                    EngineError::Unsupported(alloc::format!(
12319                        "generate_series(timestamp, timestamp, …): \
12320                         could not parse step {s:?} as INTERVAL"
12321                    ))
12322                })?,
12323                other => {
12324                    return Err(EngineError::Unsupported(alloc::format!(
12325                        "generate_series(timestamp, timestamp, …): \
12326                         step must be INTERVAL, got {}",
12327                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
12328                    )));
12329                }
12330            };
12331            let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
12332            Ok((
12333                if tz {
12334                    DataType::Timestamptz
12335                } else {
12336                    DataType::Timestamp
12337                },
12338                rows,
12339            ))
12340        }
12341        [start, stop, step]
12342            if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
12343        {
12344            let s = value_to_i64(start);
12345            let e = value_to_i64(stop);
12346            let st = value_to_i64(step);
12347            // PG types the series by the argument type: int4 args → int4
12348            // elements, int8 (bigint) args → int8. Any BigInt operand widens.
12349            let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
12350            let rows = generate_series_integers(s, e, st, wide, cancel)?;
12351            Ok((
12352                if wide {
12353                    DataType::BigInt
12354                } else {
12355                    DataType::Int
12356                },
12357                rows,
12358            ))
12359        }
12360        [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
12361            let s = value_to_i64(start);
12362            let e = value_to_i64(stop);
12363            let wide = value_is_bigint(start) || value_is_bigint(stop);
12364            let rows = generate_series_integers(s, e, 1, wide, cancel)?;
12365            Ok((
12366                if wide {
12367                    DataType::BigInt
12368                } else {
12369                    DataType::Int
12370                },
12371                rows,
12372            ))
12373        }
12374        // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
12375        // series in exact numeric arithmetic; NaN / infinity bounds and a
12376        // zero step get dedicated wordings, and a mixed int/numeric call
12377        // resolves here via the implicit int→numeric cast.
12378        [_, _] | [_, _, _]
12379            if arg_values
12380                .iter()
12381                .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
12382                && arg_values.iter().all(|v| {
12383                    matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
12384                }) =>
12385        {
12386            use spg_storage::NumericKind as K;
12387            let words: [(&str, &str); 3] = [
12388                (
12389                    "start value cannot be NaN",
12390                    "start value cannot be infinity",
12391                ),
12392                ("stop value cannot be NaN", "stop value cannot be infinity"),
12393                ("step size cannot be NaN", "step size cannot be infinity"),
12394            ];
12395            for (i, v) in arg_values.iter().enumerate() {
12396                if let Value::Numeric { kind, .. } = v {
12397                    if *kind != K::Finite {
12398                        let (nan_w, inf_w) = words[i];
12399                        return Err(EngineError::Unsupported(
12400                            if *kind == K::NaN { nan_w } else { inf_w }.into(),
12401                        ));
12402                    }
12403                }
12404            }
12405            let big =
12406                |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
12407            let start = big(&arg_values[0]);
12408            let stop = big(&arg_values[1]);
12409            let step = if arg_values.len() == 3 {
12410                big(&arg_values[2])
12411            } else {
12412                spg_storage::bignum::BigNumeric::from_i128(1, 0)
12413            };
12414            if step.is_zero() {
12415                return Err(EngineError::Unsupported(
12416                    "step size cannot equal zero".into(),
12417                ));
12418            }
12419            let descending = step.parts().0;
12420            let mut rows = alloc::vec::Vec::new();
12421            let mut cur = start;
12422            const MAX_ROWS: usize = 10_000_000;
12423            loop {
12424                cancel.check()?;
12425                let c = cur.cmp(&stop);
12426                if descending {
12427                    if c == core::cmp::Ordering::Less {
12428                        break;
12429                    }
12430                } else if c == core::cmp::Ordering::Greater {
12431                    break;
12432                }
12433                if rows.len() >= MAX_ROWS {
12434                    return Err(EngineError::Unsupported(alloc::format!(
12435                        "generate_series() result exceeds {MAX_ROWS} rows"
12436                    )));
12437                }
12438                rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
12439                    cur.clone()
12440                )]));
12441                cur = cur.add(&step);
12442            }
12443            Ok((
12444                DataType::Numeric {
12445                    precision: 0,
12446                    scale: 0,
12447                },
12448                rows,
12449            ))
12450        }
12451        _ => Err(EngineError::Unsupported(alloc::format!(
12452            "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
12453             argument shapes; got {}",
12454            arg_values
12455                .iter()
12456                .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
12457                .collect::<alloc::vec::Vec<_>>()
12458                .join(", ")
12459        ))),
12460    }
12461}
12462
12463/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
12464/// Step direction follows the sign: positive step iterates upward
12465/// (stops when current > stop); negative iterates downward; zero
12466/// errors. Caller-facing row stream is `BigInt`-typed so a single
12467/// projection schema covers SmallInt / Int / BigInt callers.
12468fn generate_series_integers(
12469    start: i64,
12470    stop: i64,
12471    step: i64,
12472    wide: bool,
12473    cancel: &CancelToken<'_>,
12474) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
12475    if step == 0 {
12476        return Err(EngineError::Unsupported(
12477            "step size cannot equal zero".into(),
12478        ));
12479    }
12480    let mut out = alloc::vec::Vec::new();
12481    let mut cur = start;
12482    // Hard cap to keep a runaway call from eating all memory. PG
12483    // has no such cap but does honour query timeout; SPG's cancel
12484    // token will fire too — this is a defense-in-depth backstop.
12485    const MAX_ROWS: usize = 10_000_000;
12486    loop {
12487        cancel.check()?;
12488        if step > 0 && cur > stop {
12489            break;
12490        }
12491        if step < 0 && cur < stop {
12492            break;
12493        }
12494        out.push(Row::new(alloc::vec![if wide {
12495            Value::BigInt(cur)
12496        } else {
12497            Value::Int(cur as i32)
12498        }]));
12499        if out.len() > MAX_ROWS {
12500            return Err(EngineError::Unsupported(alloc::format!(
12501                "generate_series(): exceeded {MAX_ROWS} rows; \
12502                 narrow start/stop or use a larger step"
12503            )));
12504        }
12505        cur = match cur.checked_add(step) {
12506            Some(n) => n,
12507            None => break,
12508        };
12509    }
12510    Ok(out)
12511}
12512
12513/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
12514/// `Value::Interval { months, micros }` per the caller's guard;
12515/// each iteration adds the interval via `apply_binary_interval`
12516/// so month-shifting handles short-month rollover (PG semantics).
12517fn generate_series_timestamps(
12518    start: i64,
12519    stop: i64,
12520    step: Value,
12521    cancel: &CancelToken<'_>,
12522) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
12523    let (months, days, micros) = match &step {
12524        Value::Interval {
12525            months,
12526            days,
12527            micros,
12528            kind,
12529        } => (*months, *days, *micros),
12530        _ => unreachable!("caller guards step.is_interval"),
12531    };
12532    if months == 0 && days == 0 && micros == 0 {
12533        return Err(EngineError::Unsupported(
12534            "generate_series(): INTERVAL step cannot be zero".into(),
12535        ));
12536    }
12537    let ascending = months > 0 || days > 0 || micros > 0;
12538    let mut out = alloc::vec::Vec::new();
12539    let mut cur = Value::Timestamp(start);
12540    const MAX_ROWS: usize = 10_000_000;
12541    loop {
12542        cancel.check()?;
12543        let cur_t = match cur {
12544            Value::Timestamp(t) => t,
12545            _ => unreachable!("loop invariant: cur is Timestamp"),
12546        };
12547        if ascending && cur_t > stop {
12548            break;
12549        }
12550        if !ascending && cur_t < stop {
12551            break;
12552        }
12553        out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
12554        if out.len() > MAX_ROWS {
12555            return Err(EngineError::Unsupported(alloc::format!(
12556                "generate_series(): exceeded {MAX_ROWS} rows; \
12557                 narrow start/stop or use a larger step"
12558            )));
12559        }
12560        let next = eval::apply_binary_interval(
12561            spg_sql::ast::BinOp::Add,
12562            &cur,
12563            &Value::Interval {
12564                months,
12565                days,
12566                micros,
12567                kind: spg_storage::IntervalKind::Finite,
12568            },
12569        )
12570        .map_err(EngineError::Eval)?;
12571        cur = match next {
12572            Some(v) => v,
12573            None => break,
12574        };
12575    }
12576    Ok(out)
12577}
12578
12579/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
12580/// WITH TIES` requires an `ORDER BY`. Without one, there's no
12581/// way to identify "ties" deterministically, so PG errors at
12582/// plan time. SPG mirrors that surface so the same DDL / app
12583/// behaviour holds on cutover.
12584fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
12585    if stmt.limit_with_ties && stmt.order_by.is_empty() {
12586        return Err(EngineError::Unsupported(alloc::string::String::from(
12587            "WITH TIES cannot be specified without ORDER BY clause",
12588        )));
12589    }
12590    Ok(())
12591}
12592
12593/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
12594/// (case-insensitive). Used by `exec_select_cancel`'s
12595/// projection loop to detect Set-Returning-Function rows that
12596/// need per-row expansion. Only the top-level call counts —
12597/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
12598/// projection's perspective; it would surface as an "unknown
12599/// function" mismatch downstream, which is what we want
12600/// (multi-SRF / nested SRF is documented carve-out for v7.19).
12601fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
12602    top_level_srf_kind(expr).is_some()
12603}
12604
12605/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
12606/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
12607/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
12608/// source row.
12609#[derive(Clone, Copy, PartialEq, Eq)]
12610pub(crate) enum SrfKind {
12611    Unnest,
12612    /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
12613    /// list. It used to be handled ONLY by the parser's lift into FROM, so a
12614    /// second one in the same list came back as "unknown function".
12615    GenerateSeries,
12616    GenerateSubscripts,
12617    /// `_text` variants unwrap scalars to their lexeme; the plain forms render
12618    /// every value as compact JSON text.
12619    ArrayElements {
12620        as_text: bool,
12621    },
12622    PathQuery,
12623    RegexpMatches,
12624    Each {
12625        as_text: bool,
12626    },
12627    ObjectKeys,
12628}
12629
12630/// Case-insensitive match against any of `names`.
12631fn name_is(name: &str, names: &[&str]) -> bool {
12632    names.iter().any(|n| name.eq_ignore_ascii_case(n))
12633}
12634
12635pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
12636    let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
12637        return None;
12638    };
12639    let n = args.len();
12640    // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
12641    // SELECT list (it returned an array there before) and shares the unnest
12642    // expansion machinery.
12643    if n == 1 && name.eq_ignore_ascii_case("unnest") {
12644        return Some(SrfKind::Unnest);
12645    }
12646    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
12647        return Some(SrfKind::GenerateSeries);
12648    }
12649    if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
12650        return Some(SrfKind::GenerateSubscripts);
12651    }
12652    // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
12653    // per element / match in the SELECT list; they collapsed to a single row
12654    // (a TextArray, or an "unknown function" error for `each`) before.
12655    if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
12656        return Some(SrfKind::ArrayElements { as_text: false });
12657    }
12658    if n == 1
12659        && name_is(
12660            name,
12661            &["jsonb_array_elements_text", "json_array_elements_text"],
12662        )
12663    {
12664        return Some(SrfKind::ArrayElements { as_text: true });
12665    }
12666    // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
12667    if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
12668        return Some(SrfKind::PathQuery);
12669    }
12670    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
12671        return Some(SrfKind::RegexpMatches);
12672    }
12673    if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
12674        return Some(SrfKind::Each { as_text: false });
12675    }
12676    if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
12677        return Some(SrfKind::Each { as_text: true });
12678    }
12679    if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
12680        return Some(SrfKind::ObjectKeys);
12681    }
12682    None
12683}
12684
12685/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
12686/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
12687/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
12688/// rows, as in PG).
12689pub(crate) fn top_level_srf_output(
12690    expr: &spg_sql::ast::Expr,
12691    row: &Row<'static>,
12692    ctx: &EvalContext<'_>,
12693) -> Result<Vec<Value<'static>>, EngineError> {
12694    let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
12695        (top_level_srf_kind(expr), expr)
12696    else {
12697        return Err(EngineError::Unsupported(
12698            "expected a SELECT-list SRF call".into(),
12699        ));
12700    };
12701    match kind {
12702        SrfKind::Unnest => {
12703            // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
12704            // the elements DIRECTLY: the old path built the whole
12705            // Value::Array (one eval + a clone per element) only for
12706            // array_value_to_elements to clone every element back out.
12707            // Any other argument shape (a column, a function result)
12708            // keeps the build-then-split path.
12709            if let spg_sql::ast::Expr::Array(items) = &args[0] {
12710                return items
12711                    .iter()
12712                    .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
12713                    .collect();
12714            }
12715            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12716            array_value_to_elements(&arr)
12717        }
12718        SrfKind::GenerateSeries => {
12719            // v7.39 (read01 round 96) — evaluate the args against the actual
12720            // row, then hand off to the shared core so the numeric and
12721            // timestamp/timestamptz overloads work here too (this arm used to
12722            // handle only integers, silently NULLing a temporal/numeric series
12723            // when it shared a target list with another SRF).
12724            let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
12725            for a in args {
12726                arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
12727            }
12728            let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
12729            Ok(rows
12730                .into_iter()
12731                .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
12732                .collect())
12733        }
12734        SrfKind::GenerateSubscripts => {
12735            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12736            let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
12737            if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
12738                return Ok(Vec::new());
12739            }
12740            let len = array_value_to_elements(&arr)?.len();
12741            Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
12742        }
12743        // One Value per array element (`_text` → text / SQL NULL, plain → the
12744        // element's compact JSON text) — the element list the FROM-clause form
12745        // materialises.
12746        SrfKind::ArrayElements { as_text } => {
12747            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12748            if matches!(arg, Value::Null) {
12749                return Ok(Vec::new());
12750            }
12751            let items =
12752                crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
12753            Ok(items
12754                .into_iter()
12755                .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
12756                .collect())
12757        }
12758        // The scalar form already yields a TextArray of the keys (or errors on
12759        // a non-object, like PG); expand it into rows.
12760        SrfKind::ObjectKeys => {
12761            let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
12762            array_value_to_elements(&v)
12763        }
12764        // One row per match, each a text[] of the pattern's capture groups.
12765        SrfKind::RegexpMatches => {
12766            let vals: Vec<Value<'static>> = args
12767                .iter()
12768                .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
12769                .collect::<Result<_, _>>()?;
12770            crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
12771        }
12772        // One composite `(key, value)` row per object member (plain → jsonb
12773        // value, `_text` → text / SQL NULL).
12774        SrfKind::Each { as_text } => {
12775            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12776            if matches!(arg, Value::Null) {
12777                return Ok(Vec::new());
12778            }
12779            let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
12780            Ok(pairs
12781                .into_iter()
12782                .map(|(k, v)| {
12783                    let val = if as_text {
12784                        v.map(Value::text).unwrap_or(Value::Null)
12785                    } else {
12786                        v.map(Value::json).unwrap_or(Value::Null)
12787                    };
12788                    Value::Composite(alloc::vec![
12789                        ("key".to_string(), Value::text(k)),
12790                        ("value".to_string(), val),
12791                    ])
12792                })
12793                .collect())
12794        }
12795        // One Value per matched JSON value.
12796        SrfKind::PathQuery => {
12797            let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12798            let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
12799            // v7.39 — optional vars document (3rd arg).
12800            let vars = match args.get(2) {
12801                Some(a) => {
12802                    let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
12803                    crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
12804                }
12805                None => None,
12806            };
12807            match crate::json::path_query_vars(&doc, &path, vars.as_ref())
12808                .map_err(EngineError::Eval)?
12809            {
12810                Value::Null => Ok(Vec::new()),
12811                Value::TextArray(items) => Ok(items
12812                    .into_iter()
12813                    .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
12814                    .collect()),
12815                other => Ok(alloc::vec![other]),
12816            }
12817        }
12818    }
12819}
12820
12821/// v7.19 P5 — turn an array-typed `Value` into the element list
12822/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
12823/// = (no rows)`). Non-array values fall through to a type-mismatch
12824/// error.
12825pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
12826    // v7.39 (round 236) — PG unnests a multidimensional array into its
12827    // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
12828    // rows). SPG stores 2-D arrays as their own variants, which fell
12829    // through to the type-mismatch arm below.
12830    if let Some(flat) = crate::eval::values::flatten_2d(v) {
12831        return array_value_to_elements(&flat);
12832    }
12833    match v {
12834        Value::Null => Ok(Vec::new()),
12835        Value::TextArray(items) => Ok(items
12836            .iter()
12837            .map(|opt| {
12838                opt.as_ref()
12839                    .map(|s| Value::text(s.clone()))
12840                    .unwrap_or(Value::Null)
12841            })
12842            .collect()),
12843        Value::IntArray(items) => Ok(items
12844            .iter()
12845            .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
12846            .collect()),
12847        Value::BigIntArray(items) => Ok(items
12848            .iter()
12849            .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
12850            .collect()),
12851        // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
12852        // range per canonical span.
12853        Value::Multirange { kind, ranges } => Ok(ranges
12854            .iter()
12855            .map(|s| Value::Range {
12856                kind: *kind,
12857                lower: s.lower.clone(),
12858                upper: s.upper.clone(),
12859                lower_inc: s.lower_inc,
12860                upper_inc: s.upper_inc,
12861                empty: false,
12862            })
12863            .collect()),
12864        other => Err(EngineError::Eval(EvalError::TypeMismatch {
12865            detail: alloc::format!(
12866                "unnest() expects an array argument, got {}",
12867                crate::conversions::pg_type_name_for_error_opt(other.data_type())
12868            ),
12869        })),
12870    }
12871}
12872
12873impl Engine {
12874    /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
12875    /// the SELECT's FROM / JOIN graph, re-parse each view's body
12876    /// source, and prepend it as a synthetic CTE on the
12877    /// returned SelectStatement. Returns `None` when no view
12878    /// references are found (caller proceeds with the original
12879    /// statement); returns `Some(rewritten)` otherwise (caller
12880    /// re-runs exec_select_cancel on the rewritten form so the
12881    /// regular CTE materialiser handles it).
12882    fn expand_views_in_select(
12883        &self,
12884        stmt: &SelectStatement,
12885    ) -> Result<Option<SelectStatement>, EngineError> {
12886        let cat = self.active_catalog();
12887        let mut referenced: Vec<String> = Vec::new();
12888        if let Some(from) = &stmt.from {
12889            collect_view_refs(&from.primary, cat, &mut referenced);
12890            for j in &from.joins {
12891                collect_view_refs(&j.table, cat, &mut referenced);
12892            }
12893        }
12894        // Don't expand a view name that's already shadowed by a
12895        // CTE on the same SELECT — the CTE wins per PG.
12896        referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
12897        if referenced.is_empty() {
12898            return Ok(None);
12899        }
12900        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
12901        for name in &referenced {
12902            let view = cat.view(name).ok_or_else(|| {
12903                EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
12904                    "view {name:?} disappeared mid-expansion"
12905                )))
12906            })?;
12907            let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
12908                EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
12909            })?;
12910            let Statement::Select(body) = parsed else {
12911                return Err(EngineError::Unsupported(alloc::format!(
12912                    "view {name:?} body is not a SELECT (catalog corruption)"
12913                )));
12914            };
12915            new_ctes.push(spg_sql::ast::Cte {
12916                name: name.clone(),
12917                body: spg_sql::ast::CteBody::Select(body),
12918                recursive: false,
12919                column_overrides: view.columns.clone(),
12920                search: None,
12921                cycle: None,
12922            });
12923        }
12924        let mut out = stmt.clone();
12925        // Prepend so view CTEs are visible to caller-supplied CTEs.
12926        new_ctes.extend(out.ctes);
12927        out.ctes = new_ctes;
12928        Ok(Some(out))
12929    }
12930
12931    /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
12932    /// any partition-parent table, rewrite the SELECT so each parent
12933    /// reference resolves to a CTE whose body is a `UNION ALL` over the
12934    /// children that pass the WHERE-derived partition-key range. Returns
12935    /// `None`(no rewrite needed)when no parent is referenced or all
12936    /// references are shadowed by a same-name CTE.
12937    ///
12938    /// Pruning vocabulary at v7.37.6-B:
12939    ///   * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
12940    ///     and `<key> BETWEEN literal AND literal`.
12941    ///   * Anything outside that(OR / nested IN / function call on the
12942    ///     key)defaults to "no pruning" — every child + DEFAULT lands
12943    ///     in the UNION. Correctness is preserved; only the plan size
12944    ///     widens.
12945    fn expand_partition_parents_in_select(
12946        &self,
12947        stmt: &SelectStatement,
12948    ) -> Result<Option<SelectStatement>, EngineError> {
12949        let cat = self.active_catalog();
12950        let Some(from) = &stmt.from else {
12951            return Ok(None);
12952        };
12953        let mut parent_refs: Vec<String> = Vec::new();
12954        collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
12955        for j in &from.joins {
12956            collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
12957        }
12958        // Drop names shadowed by a CTE on the same SELECT(PG semantics
12959        // — same as view expansion above).
12960        parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
12961        if parent_refs.is_empty() {
12962            return Ok(None);
12963        }
12964        // Synthesise a CTE name per parent so the existing
12965        // "CTE shadows a real table" guard doesn't fire (the parent
12966        // IS a real table in the catalog, unlike VIEW expansion's
12967        // case). The FROM-clause TableRef walker below rewrites
12968        // every parent reference to point at the synthetic CTE.
12969        let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
12970        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
12971        let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
12972        for parent_name in &parent_refs {
12973            // No children = no rewrite. The parent itself is a real
12974            // (empty-rows) table — the regular FROM-resolution path
12975            // will scan it and return 0 rows, matching the
12976            // "partition parent with no children" plan. Skipping the
12977            // CTE here also avoids `SELECT * FROM parent` re-entering
12978            // this rewrite on the synthetic body (infinite recursion).
12979            let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
12980                continue;
12981            };
12982            new_ctes.push(spg_sql::ast::Cte {
12983                name: synth_name(parent_name),
12984                body: spg_sql::ast::CteBody::Select(body),
12985                recursive: false,
12986                column_overrides: Vec::new(),
12987                search: None,
12988                cycle: None,
12989            });
12990            expanded_parents.push(parent_name.clone());
12991        }
12992        if expanded_parents.is_empty() {
12993            return Ok(None);
12994        }
12995        let mut out = stmt.clone();
12996        if let Some(from) = out.from.as_mut() {
12997            rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
12998            for j in &mut from.joins {
12999                rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
13000            }
13001        }
13002        new_ctes.extend(out.ctes);
13003        out.ctes = new_ctes;
13004        Ok(Some(out))
13005    }
13006
13007    /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
13008    /// Children include every overlap-hit `Range` plus(always)the
13009    /// `Default` child(if any). Returns `Ok(None)` when no children
13010    /// would survive — caller skips the CTE injection and lets the
13011    /// parent fall through to the regular(empty-rows)scan path,
13012    /// avoiding the infinite recursion that an empty-body CTE
13013    /// referencing the parent name would trigger.
13014    /// v7.37.16 (16.10) — public helper invoked from explain.rs to
13015    /// surface "which children survive the WHERE-clause prune" in
13016    /// EXPLAIN output. Returns `None` when `parent_name` isn't
13017    /// actually a partition parent; otherwise returns the list of
13018    /// children the planner would scan (same algorithm as
13019    /// [`Self::build_partition_parent_union_body`] but without the
13020    /// SQL re-parse).
13021    /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
13022    /// expression (the PG-shaped EXPLAIN's scan builder has no full
13023    /// SelectStatement in hand). Wraps the original by synthesising a
13024    /// minimal statement carrying just the predicate.
13025    pub(crate) fn explain_partition_kept_children_by_where(
13026        &self,
13027        parent_name: &str,
13028        where_: Option<&spg_sql::ast::Expr>,
13029    ) -> Option<Vec<alloc::string::String>> {
13030        let mut synth = SelectStatement::default();
13031        synth.where_ = where_.cloned();
13032        self.explain_partition_kept_children(parent_name, &synth)
13033    }
13034
13035    pub(crate) fn explain_partition_kept_children(
13036        &self,
13037        parent_name: &str,
13038        outer: &SelectStatement,
13039    ) -> Option<Vec<alloc::string::String>> {
13040        use spg_storage::PartitionRole;
13041        let cat = self.active_catalog();
13042        let parent = cat.get(parent_name)?;
13043        let (key_position, parent_kind) = match &parent.schema().partition_role {
13044            Some(PartitionRole::Parent {
13045                key_column_positions,
13046                kind,
13047                ..
13048            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13049            _ => return None,
13050        };
13051        let key_col_name = parent.schema().columns[key_position].name.clone();
13052        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13053            Some(expr) => extract_key_range(expr, &key_col_name),
13054            None => (None, None),
13055        };
13056        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13057            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13058            None => None,
13059        };
13060        let children = crate::partition::children_of_parent(cat, parent_name);
13061        let mut kept: Vec<alloc::string::String> = Vec::new();
13062        let mut default_child: Option<alloc::string::String> = None;
13063        for child_name in &children {
13064            let Some(child) = cat.get(child_name) else {
13065                continue;
13066            };
13067            match &child.schema().partition_role {
13068                Some(PartitionRole::Range { lower, upper, .. }) => {
13069                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13070                        kept.push(child_name.clone());
13071                    }
13072                }
13073                Some(PartitionRole::List { values, .. }) => match &eq_value {
13074                    Some(v) => {
13075                        if values.iter().any(|b| b.equals_value(v)) {
13076                            kept.push(child_name.clone());
13077                        }
13078                    }
13079                    None => kept.push(child_name.clone()),
13080                },
13081                Some(PartitionRole::Hash {
13082                    modulus, remainder, ..
13083                }) => match &eq_value {
13084                    Some(v) => {
13085                        let h = crate::partition::pg_compatible_hash(v);
13086                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13087                            kept.push(child_name.clone());
13088                        }
13089                    }
13090                    None => kept.push(child_name.clone()),
13091                },
13092                Some(PartitionRole::Default { .. }) => {
13093                    default_child = Some(child_name.clone());
13094                }
13095                _ => {}
13096            }
13097        }
13098        let _ = parent_kind;
13099        if let Some(d) = default_child {
13100            if kept.is_empty() || eq_value.is_none() {
13101                kept.push(d);
13102            }
13103        }
13104        Some(kept)
13105    }
13106
13107    fn build_partition_parent_union_body(
13108        &self,
13109        parent_name: &str,
13110        outer: &SelectStatement,
13111    ) -> Result<Option<SelectStatement>, EngineError> {
13112        use spg_storage::PartitionRole;
13113        let cat = self.active_catalog();
13114        let parent = cat.get(parent_name).ok_or_else(|| {
13115            EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13116                "partition parent {parent_name:?} disappeared mid-expansion"
13117            )))
13118        })?;
13119        let (key_position, parent_kind) = match &parent.schema().partition_role {
13120            Some(PartitionRole::Parent {
13121                key_column_positions,
13122                kind,
13123                ..
13124            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13125            // v7.39 (round 645) — an INHERITANCE parent, which has no
13126            // role of its own: the relationship is recorded only in the
13127            // children. Three things differ from a partition parent and
13128            // all three are in this body.
13129            //
13130            //   * The parent HOLDS ROWS, so it is a term of the union —
13131            //     `FROM ONLY`, or expanding it would recurse.
13132            //   * There is no partition key, so there is nothing to
13133            //     prune: every child is a term.
13134            //   * A child may declare columns of its own, so the terms
13135            //     name the PARENT's columns rather than `*`. PG's
13136            //     `SELECT * FROM parent` returns the parent's shape.
13137            //
13138            // Answered from this match rather than a branch before it —
13139            // round 644 measured what an extra early return beside an
13140            // existing test costs in this file.
13141            _ if crate::partition::has_inheritance_children(cat, parent_name) => {
13142                let cols = parent
13143                    .schema()
13144                    .columns
13145                    .iter()
13146                    .map(|c| quote_ident_for_sql(&c.name))
13147                    .collect::<Vec<_>>()
13148                    .join(", ");
13149                let carry_sys = references_ctid(outer);
13150                let sys = if carry_sys {
13151                    let mut t = alloc::string::String::new();
13152                    for s in SYSTEM_COLUMNS {
13153                        t.push_str(", ");
13154                        t.push_str(s);
13155                    }
13156                    t
13157                } else {
13158                    alloc::string::String::new()
13159                };
13160                let mut body = alloc::format!(
13161                    "SELECT {cols}{sys} FROM ONLY {}",
13162                    quote_ident_for_sql(parent_name)
13163                );
13164                for child in crate::partition::children_of_parent(cat, parent_name) {
13165                    body.push_str(&alloc::format!(
13166                        " UNION ALL SELECT {cols}{sys} FROM {}",
13167                        quote_ident_for_sql(&child)
13168                    ));
13169                }
13170                return parse_select_or_corrupt(&body).map(Some);
13171            }
13172            _ => {
13173                return Err(EngineError::Unsupported(alloc::format!(
13174                    "partition expansion: {parent_name:?} is not a parent"
13175                )));
13176            }
13177        };
13178        let key_col_name = parent.schema().columns[key_position].name.clone();
13179        // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
13180        // off the WHERE; for LIST / HASH we extract a single `=`
13181        // literal (and the rest of the planner falls back to "keep
13182        // every child" — same conservative path as 16.1/16.2).
13183        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13184            Some(expr) => extract_key_range(expr, &key_col_name),
13185            None => (None, None),
13186        };
13187        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13188            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13189            None => None,
13190        };
13191        let children = crate::partition::children_of_parent(cat, parent_name);
13192        let mut kept: Vec<String> = Vec::new();
13193        let mut default_child: Option<String> = None;
13194        // First pass — apply per-strategy gates, defer DEFAULT until
13195        // we know whether some non-DEFAULT child matched.
13196        for child_name in &children {
13197            let Some(child) = cat.get(child_name) else {
13198                continue;
13199            };
13200            match &child.schema().partition_role {
13201                Some(PartitionRole::Range { lower, upper, .. }) => {
13202                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13203                        kept.push(child_name.clone());
13204                    }
13205                }
13206                // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
13207                // = <lit>`, only the child whose values contain that
13208                // literal survives. Otherwise (no equality predicate
13209                // or planner couldn't extract one) keep the child
13210                // conservatively.
13211                Some(PartitionRole::List { values, .. }) => match &eq_value {
13212                    Some(v) => {
13213                        if values.iter().any(|b| b.equals_value(v)) {
13214                            kept.push(child_name.clone());
13215                        }
13216                    }
13217                    None => kept.push(child_name.clone()),
13218                },
13219                // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
13220                // we know the residue class deterministically, so
13221                // only the matching REMAINDER child survives.
13222                Some(PartitionRole::Hash {
13223                    modulus, remainder, ..
13224                }) => match &eq_value {
13225                    Some(v) => {
13226                        let h = crate::partition::pg_compatible_hash(v);
13227                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13228                            kept.push(child_name.clone());
13229                        }
13230                    }
13231                    None => kept.push(child_name.clone()),
13232                },
13233                Some(PartitionRole::Default { .. }) => {
13234                    default_child = Some(child_name.clone());
13235                }
13236                _ => {}
13237            }
13238        }
13239        // PG-style DEFAULT semantics: the DEFAULT child must be
13240        // scanned iff some row could fall outside every concrete
13241        // child's bound predicate. We approximate that as "no
13242        // concrete child matched" (== full prune) — strictly
13243        // conservative for LIST / HASH (DEFAULT also catches rows
13244        // outside the union of value-sets / residues), and matches
13245        // PG for the equality case where we *do* know the routing
13246        // outcome.
13247        let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
13248        if let Some(d) = default_child {
13249            if kept.is_empty() {
13250                kept.push(d);
13251            } else if eq_value.is_none() {
13252                // Without an equality literal, the DEFAULT child may
13253                // still hold matching rows (e.g. LIKE on TEXT keys
13254                // for which a LIST partition exists). Keep it.
13255                kept.push(d);
13256            }
13257        }
13258        // Build the UNION ALL body text and re-parse — keeps the
13259        // rewrite expressible in surface SQL so the engine's existing
13260        // parser path handles the AST shape uniformly.
13261        if kept.is_empty() {
13262            // No children survive — caller falls back to scanning the
13263            // (empty) parent table. Returning None here is what
13264            // prevents the synthetic CTE from referring back to the
13265            // parent name and re-entering this rewrite pass.
13266            let _ = parent_name;
13267            return Ok(None);
13268        }
13269        // v7.39 (round 622, S05a) — the system columns of the CHILD the row
13270        // actually lives in.
13271        //
13272        // The parent is read through a synthetic CTE, so a `tableoid` on it
13273        // resolved against that CTE: every row of every child reported
13274        // `__spg_partition_pm`, an internal name no user ever typed, where
13275        // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
13276        // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
13277        // one asks "which partition is this row in", answering 0 rows where
13278        // PG answers 1. `ctid` had the same shape: it numbered the CTE's
13279        // output, so rows in different children got distinct ctids instead
13280        // of each child's own physical position.
13281        //
13282        // Naming them in the term is what carries them: the child scan
13283        // materialises its own six because the statement now references
13284        // them, and they land in SYSTEM_COLUMNS order right after the user
13285        // columns — the exact layout the positional `*` skip already
13286        // expects. Only done when the outer statement asks for one, so a
13287        // plain `SELECT * FROM parent` scans exactly what it scanned.
13288        let carry_sys = references_ctid(outer);
13289        let mut body = alloc::string::String::new();
13290        for (i, child_name) in kept.iter().enumerate() {
13291            if i > 0 {
13292                body.push_str(" UNION ALL ");
13293            }
13294            body.push_str("SELECT *");
13295            if carry_sys {
13296                for sys in SYSTEM_COLUMNS {
13297                    body.push_str(", ");
13298                    body.push_str(sys);
13299                }
13300            }
13301            body.push_str(" FROM ");
13302            body.push_str(&quote_ident_for_sql(child_name));
13303        }
13304        parse_select_or_corrupt(&body).map(Some)
13305    }
13306}
13307
13308/// Rewrite a `TableRef` pointing at a partition parent so it
13309/// references the synthetic CTE created by the expansion. If the
13310/// original ref had no alias, preserve the parent name as an alias
13311/// so column references like `events_partitioned.received_at`
13312/// keep resolving.
13313fn rewrite_partition_parent_table_ref(
13314    t: &mut spg_sql::ast::TableRef,
13315    parents: &[alloc::string::String],
13316    synth_name: &impl Fn(&str) -> alloc::string::String,
13317) {
13318    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13319        return;
13320    }
13321    // v7.39 (round 644) — an ONLY reference stays pointed at the parent
13322    // itself. The rewrite is keyed on the NAME, so in
13323    // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
13324    // parent list and this then rewrote BOTH — including the one that
13325    // asked not to descend. PG answers 0 for that join; SPG answered 2.
13326    // Folded into the existing test — see the note in
13327    // `collect_partition_parent_refs` for what a separate one cost.
13328    if t.only || !parents.iter().any(|p| p == &t.name) {
13329        return;
13330    }
13331    if t.alias.is_none() {
13332        t.alias = Some(t.name.clone());
13333    }
13334    t.name = synth_name(&t.name);
13335}
13336
13337/// Walk a `TableRef` and push its `name` if it resolves to a partition
13338/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
13339/// `generate_series_args` references — those aren't catalog tables.
13340fn collect_partition_parent_refs(
13341    t: &spg_sql::ast::TableRef,
13342    cat: &spg_storage::Catalog,
13343    out: &mut Vec<alloc::string::String>,
13344) {
13345    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13346        return;
13347    }
13348    // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
13349    // The keyword used to be absorbed at parse time, so this fanned out
13350    // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
13351    // answered 2 where PG answers 0.
13352    //
13353    // Folded into the existing test rather than given an early return of
13354    // its own: as two extra lines in this function's body it cost
13355    // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
13356    // outside the panel. Rounds 641 and 643 met the same wall from the
13357    // other two directions — adding to a hot function and taking away
13358    // from a cold one. What goes in a body near the row loop is a
13359    // codegen decision whatever its shape.
13360    if !t.only && crate::partition::has_children(cat, &t.name) {
13361        out.push(t.name.clone());
13362    }
13363}
13364
13365/// v7.37.6-B partition-key range derived from a WHERE expression.
13366/// `i64` microseconds since epoch with the same sign convention as
13367/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
13368/// / `=`),`false` ⇒ exclusive(`>` / `<`).
13369#[derive(Debug, Clone, Copy)]
13370pub(crate) struct PartitionFilterBound {
13371    pub micros: i64,
13372    pub inclusive: bool,
13373}
13374
13375/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
13376/// shapes; tighten the running lo / hi as we go. Anything outside that
13377/// (OR / nested calls / non-key columns)is ignored — caller treats
13378/// `None` as "no constraint on that side."
13379fn extract_key_range(
13380    expr: &spg_sql::ast::Expr,
13381    key_col: &str,
13382) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
13383    let mut lo: Option<PartitionFilterBound> = None;
13384    let mut hi: Option<PartitionFilterBound> = None;
13385    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
13386    while let Some(e) = stack.pop() {
13387        match e {
13388            spg_sql::ast::Expr::Binary {
13389                lhs,
13390                op: spg_sql::ast::BinOp::And,
13391                rhs,
13392            } => {
13393                stack.push(lhs);
13394                stack.push(rhs);
13395            }
13396            // BETWEEN is desugared at parse time into `lhs >= low AND
13397            // lhs <= high`, so it lands here as two regular Binary
13398            // arms via the AND walker above.
13399            spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
13400                let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
13401                    (Some(lhs.as_ref()), rhs.as_ref(), false)
13402                } else if is_column_ref(rhs, key_col) {
13403                    (Some(rhs.as_ref()), lhs.as_ref(), true)
13404                } else {
13405                    (None, lhs.as_ref(), false)
13406                };
13407                if col_ref.is_none() {
13408                    continue;
13409                }
13410                let Some(lit) = literal_to_micros(lit_side) else {
13411                    continue;
13412                };
13413                use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
13414                let effective_op = if swapped {
13415                    match op {
13416                        Lt => Gt,
13417                        LtEq => GtEq,
13418                        Gt => Lt,
13419                        GtEq => LtEq,
13420                        other => *other,
13421                    }
13422                } else {
13423                    *op
13424                };
13425                match effective_op {
13426                    Eq => {
13427                        tighten_lo(
13428                            &mut lo,
13429                            PartitionFilterBound {
13430                                micros: lit,
13431                                inclusive: true,
13432                            },
13433                        );
13434                        tighten_hi(
13435                            &mut hi,
13436                            PartitionFilterBound {
13437                                micros: lit,
13438                                inclusive: true,
13439                            },
13440                        );
13441                    }
13442                    GtEq => {
13443                        tighten_lo(
13444                            &mut lo,
13445                            PartitionFilterBound {
13446                                micros: lit,
13447                                inclusive: true,
13448                            },
13449                        );
13450                    }
13451                    Gt => {
13452                        tighten_lo(
13453                            &mut lo,
13454                            PartitionFilterBound {
13455                                micros: lit,
13456                                inclusive: false,
13457                            },
13458                        );
13459                    }
13460                    LtEq => {
13461                        tighten_hi(
13462                            &mut hi,
13463                            PartitionFilterBound {
13464                                micros: lit,
13465                                inclusive: true,
13466                            },
13467                        );
13468                    }
13469                    Lt => {
13470                        tighten_hi(
13471                            &mut hi,
13472                            PartitionFilterBound {
13473                                micros: lit,
13474                                inclusive: false,
13475                            },
13476                        );
13477                    }
13478                    _ => {}
13479                }
13480            }
13481            _ => {}
13482        }
13483    }
13484    (lo, hi)
13485}
13486
13487fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
13488    match slot {
13489        None => *slot = Some(new),
13490        Some(cur) => {
13491            if new.micros > cur.micros
13492                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
13493            {
13494                *slot = Some(new);
13495            }
13496        }
13497    }
13498}
13499
13500fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
13501    match slot {
13502        None => *slot = Some(new),
13503        Some(cur) => {
13504            if new.micros < cur.micros
13505                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
13506            {
13507                *slot = Some(new);
13508            }
13509        }
13510    }
13511}
13512
13513fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
13514    if let spg_sql::ast::Expr::Column(c) = e {
13515        c.name.eq_ignore_ascii_case(key_col)
13516    } else {
13517        false
13518    }
13519}
13520
13521/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
13522/// `key_col = <literal>` predicate out for LIST/HASH partition
13523/// pruning. Returns `None` when no equality literal can be lifted
13524/// (planner then keeps every child — correctness preserved). The
13525/// returned `Value<'static>` is an owned coercion so the caller can
13526/// outlive any AST node it was extracted from.
13527pub(crate) fn extract_key_eq_value(
13528    expr: &spg_sql::ast::Expr,
13529    key_col: &str,
13530) -> Option<spg_storage::Value<'static>> {
13531    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
13532    while let Some(e) = stack.pop() {
13533        match e {
13534            spg_sql::ast::Expr::Binary {
13535                lhs,
13536                op: spg_sql::ast::BinOp::And,
13537                rhs,
13538            } => {
13539                stack.push(lhs);
13540                stack.push(rhs);
13541            }
13542            spg_sql::ast::Expr::Binary {
13543                lhs,
13544                op: spg_sql::ast::BinOp::Eq,
13545                rhs,
13546            } => {
13547                let lit_side = if is_column_ref(lhs, key_col) {
13548                    rhs.as_ref()
13549                } else if is_column_ref(rhs, key_col) {
13550                    lhs.as_ref()
13551                } else {
13552                    continue;
13553                };
13554                let cloned = lit_side.clone();
13555                let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
13556                    continue;
13557                };
13558                // Coerce to an owned Value<'static> so the caller
13559                // can hold it past the WHERE expression's lifetime.
13560                let owned: spg_storage::Value<'static> = match v {
13561                    spg_storage::Value::Text(s) => {
13562                        spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
13563                    }
13564                    spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
13565                    spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
13566                    spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
13567                    spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
13568                    spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
13569                    spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
13570                    spg_storage::Value::Null => spg_storage::Value::Null,
13571                    // Anything else (Vector / Json / Bytes / Numeric /
13572                    // arrays / interval / …) isn't a current partition
13573                    // key type; skip without pruning.
13574                    _ => continue,
13575                };
13576                return Some(owned);
13577            }
13578            _ => {}
13579        }
13580    }
13581    None
13582}
13583
13584/// Coerce a literal Expr(after the parser folded sequence calls etc.)
13585/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
13586/// pruning and routing agree on the literal vocabulary. Returns
13587/// `None` when the literal isn't recognised(planner then skips
13588/// pruning on that branch — correctness preserved).
13589fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
13590    let cloned = e.clone();
13591    let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
13592    match value {
13593        spg_storage::Value::Timestamp(m) => Some(m),
13594        spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
13595        spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
13596        _ => None,
13597    }
13598}
13599
13600/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
13601/// satisfying the WHERE-derived filter range. PG-style half-open:
13602/// child upper exclusive. Filter inclusivity is honoured per-bound.
13603fn range_satisfies_filter(
13604    range_lo: &spg_storage::PartitionBound,
13605    range_hi: &spg_storage::PartitionBound,
13606    filter_lo: Option<&PartitionFilterBound>,
13607    filter_hi: Option<&PartitionFilterBound>,
13608) -> bool {
13609    use spg_storage::PartitionBound;
13610    // For each filter side, reject children that can't host any row
13611    // matching the predicate.
13612    if let Some(lo) = filter_lo {
13613        // child upper bound vs filter lower:
13614        //   if filter is x >= L, child rejects iff child.hi <= L
13615        //   if filter is x  > L, child rejects iff child.hi <= L
13616        //   (child.hi exclusive, so equality with L still rejects)
13617        match range_hi {
13618            PartitionBound::MinValue => return false,
13619            PartitionBound::MaxValue => {}
13620            PartitionBound::TimestampTz(hi) => {
13621                if *hi <= lo.micros {
13622                    return false;
13623                }
13624            }
13625            // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
13626            // matched against TIMESTAMPTZ filters here; keep child
13627            // (conservative: don't prune).
13628            PartitionBound::BigInt(_)
13629            | PartitionBound::Int(_)
13630            | PartitionBound::SmallInt(_)
13631            | PartitionBound::Date(_)
13632            | PartitionBound::Text(_) => {}
13633        }
13634    }
13635    if let Some(hi) = filter_hi {
13636        // child lower bound vs filter upper:
13637        //   if filter is x <= U, child rejects iff child.lo > U
13638        //   if filter is x  < U, child rejects iff child.lo >= U
13639        match range_lo {
13640            PartitionBound::MaxValue => return false,
13641            PartitionBound::MinValue => {}
13642            PartitionBound::TimestampTz(lo) => {
13643                let rejects = if hi.inclusive {
13644                    *lo > hi.micros
13645                } else {
13646                    *lo >= hi.micros
13647                };
13648                if rejects {
13649                    return false;
13650                }
13651            }
13652            PartitionBound::BigInt(_)
13653            | PartitionBound::Int(_)
13654            | PartitionBound::SmallInt(_)
13655            | PartitionBound::Date(_)
13656            | PartitionBound::Text(_) => {}
13657        }
13658    }
13659    true
13660}
13661
13662fn quote_ident_for_sql(name: &str) -> alloc::string::String {
13663    // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
13664    // identifier, otherwise quoted). Conservative: always quote so
13665    // children with reserved names round-trip safely through the
13666    // CTE-body parse.
13667    let mut out = alloc::string::String::with_capacity(name.len() + 2);
13668    out.push('"');
13669    for c in name.chars() {
13670        if c == '"' {
13671            out.push('"');
13672        }
13673        out.push(c);
13674    }
13675    out.push('"');
13676    out
13677}
13678
13679fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
13680    let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
13681        EngineError::Unsupported(alloc::format!(
13682            "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
13683        ))
13684    })?;
13685    let Statement::Select(body) = parsed else {
13686        return Err(EngineError::Unsupported(alloc::format!(
13687            "partition expansion: generated SQL {sql:?} is not a SELECT"
13688        )));
13689    };
13690    Ok(body)
13691}
13692
13693/// v7.39 (read01 round 65/66) — the column shape a set-returning function
13694/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
13695/// yields ONE column named after the call's alias when there is one (`FROM
13696/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
13697/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
13698fn setof_column_shape_from(
13699    declared: &str,
13700    name: &str,
13701    alias: Option<&str>,
13702    got: &[ColumnSchema],
13703) -> alloc::vec::Vec<ColumnSchema> {
13704    let upper = declared.to_ascii_uppercase();
13705    if upper.starts_with("TABLE(") {
13706        let raw = &declared["TABLE(".len()..declared.len() - 1];
13707        return raw
13708            .split(',')
13709            .zip(got.iter())
13710            .map(|(decl, g)| {
13711                let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
13712                ColumnSchema::new(cname.to_string(), g.ty, true)
13713            })
13714            .collect();
13715    }
13716    let cname = alias.unwrap_or(name);
13717    got.first()
13718        .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
13719        .unwrap_or_default()
13720}
13721
13722/// The plpgsql twin: the interpreter hands back raw value rows, so the types
13723/// come off the first row.
13724fn setof_column_shape(
13725    declared: &str,
13726    name: &str,
13727    alias: Option<&str>,
13728    first_row: Option<&alloc::vec::Vec<Value<'static>>>,
13729) -> alloc::vec::Vec<ColumnSchema> {
13730    let got: alloc::vec::Vec<ColumnSchema> = first_row
13731        .map(|r| {
13732            r.iter()
13733                .enumerate()
13734                .map(|(i, v)| {
13735                    ColumnSchema::new(
13736                        alloc::format!("col{i}"),
13737                        v.data_type().unwrap_or(DataType::Text),
13738                        true,
13739                    )
13740                })
13741                .collect()
13742        })
13743        .unwrap_or_default();
13744    setof_column_shape_from(declared, name, alias, &got)
13745}
13746
13747/// v7.39 (read01 round 67) — expand every set-returning call in a target list
13748/// for ONE input row, PG's ProjectSet semantics.
13749///
13750/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
13751/// output has as many rows as the LONGEST of them, and a shorter one is padded
13752/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
13753/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
13754/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
13755/// is zero rows, not one NULL row.
13756///
13757/// Non-SRF items repeat, evaluated once per output row from the same input row.
13758/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
13759/// used to reach the scalar function dispatcher, which reported the aggregate as
13760/// an *unknown function* — the same "symptom two layers above the cause" shape
13761/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
13762/// sees a call, not the clause it came from. The statement knows.
13763/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
13764/// clause may appear.
13765///
13766/// PG rejects `FOR UPDATE` on exactly the shapes that have no
13767/// identifiable base row to lock, each with its own wording. SPG
13768/// accepted all of them and locked nothing, so a query that PG refuses
13769/// outright came back looking like it had taken locks.
13770///
13771/// Every wording read off live PG 18.4.
13772impl crate::Engine {
13773    /// v7.39.2 — a column name in WHERE / ORDER BY / GROUP BY / HAVING
13774    /// that names nothing is refused before the scan, not when a row
13775    /// reaches it.
13776    ///
13777    /// The projection resolves its names eagerly; a predicate only meets
13778    /// them per row. So on an EMPTY table `SELECT a FROM t WHERE nosuch
13779    /// = 1` answered zero rows and no error, and the same statement over
13780    /// a table with one row raised. Measured on PostgreSQL 18.6 and
13781    /// MySQL 9.7.2: both refuse it whatever the row count. A typo in a
13782    /// predicate therefore passed a test written against an empty
13783    /// fixture and failed in production — or, worse, ran nightly over an
13784    /// empty window and reported nothing.
13785    ///
13786    /// Deliberately narrow: ONE plain base table, nothing else. A join,
13787    /// a CTE, a set operation, a lateral or function source, or a
13788    /// subquery in the clause all bring a second scope into which a name
13789    /// may legitimately resolve, and refusing one of those would be a
13790    /// worse defect than the one this closes. Those shapes keep the
13791    /// old behaviour; the walk below does not descend into a subquery
13792    /// for the same reason.
13793    /// v7.39.2 — refuse a call whose argument count no overload accepts,
13794    /// BEFORE the scan rather than per row.
13795    ///
13796    /// `SELECT lower(t, n) FROM t` answered zero rows and no error over
13797    /// an EMPTY table and raised the moment the table had one row in it,
13798    /// because the arity check lives inside the row-time dispatch. It is
13799    /// the same shape as the unknown-column-in-a-predicate defect closed
13800    /// earlier in this release, and it hides in the same place: a query
13801    /// written against an empty fixture passes its test.
13802    ///
13803    /// The accepted counts come from `eval::arity::REFUSED_ARITIES`,
13804    /// which is derived by asking the dispatch itself offline and can
13805    /// only ever UNDER-refuse — see that file for why the two other
13806    /// candidate oracles were refuted.
13807    /// v7.39.3 — MySQL's column names are case-insensitive; PostgreSQL's
13808    /// quoted ones are not. See `EvalContext::col_eq`.
13809    fn col_name_eq(&self, a: &str, b: &str) -> bool {
13810        if self.speaks_mysql {
13811            a.eq_ignore_ascii_case(b)
13812        } else {
13813            a == b
13814        }
13815    }
13816
13817    pub(crate) fn validate_function_arity(
13818        &self,
13819        stmt: &SelectStatement,
13820    ) -> Result<(), EngineError> {
13821        let mut calls: Vec<(alloc::string::String, Vec<Expr>)> = Vec::new();
13822        for it in &stmt.items {
13823            if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
13824                collect_function_calls(expr, &mut calls);
13825            }
13826        }
13827        if let Some(w) = &stmt.where_ {
13828            collect_function_calls(w, &mut calls);
13829        }
13830        for o in &stmt.order_by {
13831            collect_function_calls(&o.expr, &mut calls);
13832        }
13833        // The columns a name in this statement could resolve to. Only
13834        // plain base tables; anything else and the types are not
13835        // statically knowable, so nothing is refused early.
13836        let cat = self.active_catalog();
13837        let mut cols: Vec<ColumnSchema> = Vec::new();
13838        if let Some(from) = &stmt.from {
13839            for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
13840                if let Some(table) = cat.get(&t.name) {
13841                    cols.extend(table.schema().columns.iter().cloned());
13842                }
13843            }
13844        }
13845        for (name, args) in calls {
13846            let Ok(i) = crate::eval::arity::REFUSED_ARITIES
13847                .binary_search_by(|(n, _)| (*n).cmp(name.as_str()))
13848            else {
13849                continue;
13850            };
13851            if !crate::eval::arity::REFUSED_ARITIES[i]
13852                .1
13853                .contains(&args.len())
13854            {
13855                continue;
13856            }
13857            // v7.39.2 — PostgreSQL names the SIGNATURE it could not
13858            // match, and before the scan there are no values to read a
13859            // type from. Where every argument's type is knowable
13860            // statically — a column of a source table, or a literal —
13861            // the sentence is PostgreSQL's exactly; where one is not,
13862            // this leaves the call to the row-time raise, which has the
13863            // values. Refusing early with a WORSE message would trade
13864            // one defect for another.
13865            let mut types: Vec<alloc::string::String> = Vec::new();
13866            for a in &args {
13867                let Some(t) = static_arg_type(a, &cols) else {
13868                    types.clear();
13869                    break;
13870                };
13871                types.push(t);
13872            }
13873            if types.len() != args.len() {
13874                continue;
13875            }
13876            return Err(EngineError::Eval(EvalError::WrongArity {
13877                name,
13878                types: types.join(", "),
13879            }));
13880        }
13881        Ok(())
13882    }
13883
13884    pub(crate) fn validate_clause_columns(
13885        &self,
13886        stmt: &SelectStatement,
13887    ) -> Result<(), EngineError> {
13888        let Some(from) = &stmt.from else {
13889            return Ok(());
13890        };
13891        if !stmt.ctes.is_empty() {
13892            return Ok(());
13893        }
13894        // v7.39.2 — every source, not just the first. A join is checkable
13895        // for the same reason one table is: with no CTE and no
13896        // subquery-shaped source, a bare name has to come from one of
13897        // them. Refusing the check for joins left `SELECT … FROM a JOIN b
13898        // … WHERE nosuch = 1` labelled `'field list'` where MySQL 9.7.2
13899        // says `'where clause'`.
13900        let plain = |t: &spg_sql::ast::TableRef| -> bool {
13901            t.unnest_expr.is_none()
13902                && t.generate_series_args.is_none()
13903                && t.lateral_subquery.is_none()
13904                && t.jsonb_each_text_arg.is_none()
13905                && t.table_fn_call.is_none()
13906                && t.rows_from.is_none()
13907                && t.json_table.is_none()
13908                && !t.scalar_fn_item
13909        };
13910        let cat = self.active_catalog();
13911        let mut sources: Vec<(String, &spg_storage::Table)> = Vec::new();
13912        for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
13913            if !plain(t) {
13914                return Ok(());
13915            }
13916            let Some(table) = cat.get(&t.name) else {
13917                return Ok(());
13918            };
13919            sources.push((t.alias.clone().unwrap_or_else(|| t.name.clone()), table));
13920        }
13921        let known = |c: &spg_sql::ast::ColumnName| -> bool {
13922            // A system column is not in a table's list and is a perfectly
13923            // good predicate: `WHERE ctid = '(0,4)'::tid` and `WHERE
13924            // tableoid::regclass::text = 'pm_a'` are both real, and the
13925            // first draft of this check refused them. The e2e suite said
13926            // so immediately, which is what it is for.
13927            if is_system_column(&c.name) {
13928                return true;
13929            }
13930            if let Some(q) = &c.qualifier {
13931                // A qualifier must name one of this statement's sources,
13932                // and that source must carry the column. An alias
13933                // REPLACES the written name, which is PostgreSQL's rule
13934                // and MySQL's: `FROM pg_cast c WHERE pg_cast.oid <> 0`
13935                // is an error on both.
13936                return match sources.iter().find(|(a, _)| a == q) {
13937                    Some((_, t)) => t
13938                        .schema()
13939                        .columns
13940                        .iter()
13941                        .any(|sc| self.col_name_eq(&sc.name, &c.name)),
13942                    None => false,
13943                };
13944            }
13945            sources
13946                .iter()
13947                .any(|(_, t)| {
13948                    t.schema()
13949                        .columns
13950                        .iter()
13951                        .any(|sc| self.col_name_eq(&sc.name, &c.name))
13952                })
13953                // An output name the statement itself defines: ORDER BY,
13954                // GROUP BY and HAVING may all name one.
13955                || stmt.items.iter().any(|it| match it {
13956                    SelectItem::Expr { expr, alias } => {
13957                        alias.as_deref() == Some(c.name.as_str())
13958                            || matches!(expr, Expr::Column(pc) if pc.name == c.name)
13959                    }
13960                    _ => false,
13961                })
13962        };
13963        // v7.39.2 — the CLAUSE travels with the reference, because MySQL
13964        // names it: `Unknown column 'x' in 'where clause'`, `'order
13965        // clause'`, `'group statement'`, `'having clause'`. Measured on
13966        // 9.7.2, and a driver's error handling reads the sentence as well
13967        // as the number. PostgreSQL says only `column "x" does not
13968        // exist`, with no clause, so its wording is unchanged.
13969        //
13970        // This walk is the only place the clause is still known: by the
13971        // time a row-time resolver meets the name, the expression has
13972        // been detached from the statement that held it.
13973        let mut refs: Vec<(spg_sql::ast::ColumnName, &'static str)> = Vec::new();
13974        let mut push = |e: &Expr, ctx: &'static str, out: &mut Vec<_>| {
13975            let mut here: Vec<spg_sql::ast::ColumnName> = Vec::new();
13976            collect_plain_column_refs(e, &mut here);
13977            out.extend(here.into_iter().map(|c| (c, ctx)));
13978        };
13979        if let Some(w) = &stmt.where_ {
13980            push(w, "where clause", &mut refs);
13981        }
13982        if let Some(g) = &stmt.group_by {
13983            for e in g {
13984                push(e, "group statement", &mut refs);
13985            }
13986        }
13987        if let Some(h) = &stmt.having {
13988            push(h, "having clause", &mut refs);
13989        }
13990        for o in &stmt.order_by {
13991            push(&o.expr, "order clause", &mut refs);
13992        }
13993        // v7.39.2 — and the join predicates, which MySQL calls the `on
13994        // clause`. Measured on 9.7.2: `Unknown column 'j1.nosuch' in 'on
13995        // clause'`, qualifier and all.
13996        for j in &from.joins {
13997            if let Some(on) = &j.on {
13998                push(on, "on clause", &mut refs);
13999            }
14000        }
14001        for (c, ctx) in &refs {
14002            if !known(c) {
14003                if self.speaks_mysql {
14004                    // The QUALIFIER travels with it: MySQL 9.7.2 answers
14005                    // `Unknown column 'j1.nosuch' in 'on clause'`, not the
14006                    // bare name. Measured.
14007                    let shown = match &c.qualifier {
14008                        Some(q) => alloc::format!("{q}.{}", c.name),
14009                        None => c.name.clone(),
14010                    };
14011                    return Err(EngineError::Eval(EvalError::TypeMismatch {
14012                        detail: alloc::format!("Unknown column '{shown}' in '{ctx}'"),
14013                    }));
14014                }
14015                // PostgreSQL 18.6 names the missing TABLE when the
14016                // qualifier is the part that resolves to nothing
14017                // (`missing FROM-clause entry for table "pg_cast"`) and
14018                // the COLUMN otherwise. Raising the column error for both
14019                // dropped the table name a caller matches on.
14020                if let Some(q) = &c.qualifier
14021                    && !sources.iter().any(|(a, _)| a == q)
14022                {
14023                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
14024                        qualifier: q.clone(),
14025                        column: c.name.clone(),
14026                    }));
14027                }
14028                // v7.39.2 — and a qualified reference whose qualifier
14029                // DOES resolve prints the whole thing, unquoted:
14030                // `column ea.no_such does not exist` (measured on PG
14031                // 18.6). The bare `column "no_such" does not exist` drops
14032                // the alias a caller matches on, which is what the
14033                // sqlx round-20 pin says.
14034                if let Some(q) = &c.qualifier {
14035                    return Err(EngineError::Eval(EvalError::QualifiedColumnNotFound {
14036                        qualifier: q.clone(),
14037                        column: c.name.clone(),
14038                    }));
14039                }
14040                return Err(EngineError::Eval(EvalError::ColumnNotFound {
14041                    name: c.name.clone(),
14042                }));
14043            }
14044        }
14045        Ok(())
14046    }
14047}
14048
14049/// v7.39.2 — the column references of an expression, NOT descending into
14050/// a subquery.
14051///
14052/// A correlated subquery resolves its names against an outer scope this
14053/// walk cannot see, so descending would refuse valid queries. Missing a
14054/// typo inside one is the safe direction; refusing a good query is not.
14055/// v7.39.2 — the type PostgreSQL would name for an argument, when it
14056/// can be known without a row: a column of a source table, or a
14057/// literal. `None` for anything else, which is what keeps the pre-scan
14058/// refusal from printing a worse sentence than the row-time one.
14059pub(crate) fn static_arg_type(e: &Expr, cols: &[ColumnSchema]) -> Option<alloc::string::String> {
14060    use spg_sql::ast::Literal as L;
14061    match e {
14062        Expr::Column(c) => cols
14063            .iter()
14064            .find(|s| s.name.eq_ignore_ascii_case(&c.name))
14065            .map(|s| crate::conversions::pg_type_name_for_error(s.ty)),
14066        // A bare literal has no type yet on PostgreSQL — it names it
14067        // `unknown` in this very sentence — except where the lexeme
14068        // fixes one.
14069        Expr::Literal(L::String(_)) | Expr::Literal(L::Null) => {
14070            Some(alloc::string::String::from("unknown"))
14071        }
14072        Expr::Literal(L::Integer(_)) => Some(alloc::string::String::from("integer")),
14073        Expr::Literal(L::Bool(_)) => Some(alloc::string::String::from("boolean")),
14074        _ => None,
14075    }
14076}
14077
14078/// v7.39.2 — the function calls of an expression, name and argument
14079/// count, NOT descending into a subquery (its scope is its own).
14080fn collect_function_calls(e: &Expr, out: &mut Vec<(alloc::string::String, Vec<Expr>)>) {
14081    match e {
14082        Expr::FunctionCall { name, args } => {
14083            out.push((name.to_ascii_lowercase(), args.clone()));
14084            for a in args {
14085                collect_function_calls(a, out);
14086            }
14087        }
14088        Expr::Binary { lhs, rhs, .. } => {
14089            collect_function_calls(lhs, out);
14090            collect_function_calls(rhs, out);
14091        }
14092        Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14093            collect_function_calls(expr, out);
14094        }
14095        _ => {}
14096    }
14097}
14098
14099fn collect_plain_column_refs(e: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
14100    match e {
14101        Expr::Column(c) => out.push(c.clone()),
14102        Expr::Binary { lhs, rhs, .. } => {
14103            collect_plain_column_refs(lhs, out);
14104            collect_plain_column_refs(rhs, out);
14105        }
14106        Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14107            collect_plain_column_refs(expr, out);
14108        }
14109        Expr::FunctionCall { args, .. } => {
14110            for a in args {
14111                collect_plain_column_refs(a, out);
14112            }
14113        }
14114        _ => {}
14115    }
14116}
14117
14118fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
14119    let Some(lock) = &stmt.locking else {
14120        return Ok(());
14121    };
14122    let verb = lock_clause_verb(lock.strength);
14123    let refuse = |what: &str| {
14124        Err(EngineError::Unsupported(alloc::format!(
14125            "{verb} is not allowed with {what}"
14126        )))
14127    };
14128    if !stmt.unions.is_empty() {
14129        return refuse("UNION/INTERSECT/EXCEPT");
14130    }
14131    if stmt.distinct || !stmt.distinct_on.is_empty() {
14132        return refuse("DISTINCT clause");
14133    }
14134    if stmt.group_by.is_some() || stmt.group_by_all {
14135        return refuse("GROUP BY clause");
14136    }
14137    let has_agg = stmt.items.iter().any(|it| match it {
14138        spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
14139        _ => false,
14140    });
14141    if has_agg {
14142        return refuse("aggregate functions");
14143    }
14144    // `FOR UPDATE OF t` must name a relation that is actually in FROM.
14145    for want in &lock.of_tables {
14146        if !locking_from_names(stmt)
14147            .iter()
14148            .any(|n| n.eq_ignore_ascii_case(want))
14149        {
14150            return Err(EngineError::Unsupported(alloc::format!(
14151                "relation \"{want}\" in {verb} clause not found in FROM clause"
14152            )));
14153        }
14154    }
14155    Ok(())
14156}
14157
14158/// How PG names the clause in its diagnostics.
14159const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
14160    use spg_sql::ast::LockStrength as LS;
14161    match s {
14162        LS::Update => "FOR UPDATE",
14163        LS::NoKeyUpdate => "FOR NO KEY UPDATE",
14164        LS::Share => "FOR SHARE",
14165        LS::KeyShare => "FOR KEY SHARE",
14166    }
14167}
14168
14169/// Every relation name (or alias) the FROM clause exposes.
14170fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
14171    let mut out = alloc::vec::Vec::new();
14172    if let Some(f) = &stmt.from {
14173        let mut push = |t: &spg_sql::ast::TableRef| {
14174            if let Some(a) = &t.alias {
14175                out.push(a.clone());
14176            }
14177            out.push(t.name.clone());
14178        };
14179        push(&f.primary);
14180        for j in &f.joins {
14181            push(&j.table);
14182        }
14183    }
14184    out
14185}
14186
14187fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
14188    use spg_sql::ast::Expr;
14189    if let Some(w) = &stmt.where_
14190        && aggregate::contains_aggregate(w)
14191    {
14192        return Err(EngineError::Unsupported(
14193            "aggregate functions are not allowed in WHERE".into(),
14194        ));
14195    }
14196    let mut nested = false;
14197    let mut check = |e: &Expr| {
14198        let mut probe = e.clone();
14199        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14200            let args = match n {
14201                Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
14202                _ => return false,
14203            };
14204            if args.iter().any(aggregate::contains_aggregate) {
14205                nested = true;
14206            }
14207            false
14208        });
14209    };
14210    for it in &stmt.items {
14211        if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14212            check(expr);
14213        }
14214    }
14215    if let Some(h) = &stmt.having {
14216        check(h);
14217    }
14218    for o in &stmt.order_by {
14219        check(&o.expr);
14220    }
14221    if nested {
14222        return Err(EngineError::Unsupported(
14223            "aggregate function calls cannot be nested".into(),
14224        ));
14225    }
14226    Ok(())
14227}
14228
14229/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
14230/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
14231/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
14232/// to a set and then applies the enclosing expression once per element. SPG only
14233/// ever recognised an SRF that WAS the item, so everything above died on
14234/// "unknown function unnest" — the set-returning call, wrapped in anything at
14235/// all, fell through to the scalar function dispatcher which has no such name.
14236///
14237/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
14238/// rewritten to read that column, and the rewritten expression is evaluated once
14239/// per output row against the input row extended with the lifted values. The
14240/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
14241/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
14242/// executors (the single-table scan, the synthetic-table pipeline, and the
14243/// unnest FROM path) each evaluated the key as an ordinary expression, where the
14244/// literal `n` is just the constant n — the same sort key for every row. The
14245/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
14246/// back in input order, not in a wrong order. Statement prep resolves the common
14247/// case, but only when the SELECT item is an expression — a `*` is not one, and
14248/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
14249/// spelling landed on exactly the shape prep could not resolve.
14250///
14251/// A set-returning item is left alone: copying it into ORDER BY would make the
14252/// key "the whole set", evaluated once per INPUT row.
14253fn resolve_positional_order_by(
14254    order_by: &[spg_sql::ast::OrderBy],
14255    projection: &[ProjectedItem],
14256) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
14257    order_by
14258        .iter()
14259        .filter_map(|o| {
14260            let mut o = o.clone();
14261            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
14262                && *n >= 1
14263                && let Ok(idx) = usize::try_from(*n - 1)
14264                && let Some(item) = projection.get(idx)
14265                && !expr_contains_builtin_srf(&item.expr)
14266            {
14267                // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
14268                // item is itself an integer LITERAL must not be
14269                // substituted textually: the literal would read as an
14270                // ordinal again downstream, and `SELECT 10 … ORDER BY
14271                // 1` died with "position 10 is not in select list"
14272                // where PG happily returns the rows. Ordering by a
14273                // constant orders nothing, so the key drops.
14274                if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
14275                    return None;
14276                }
14277                o.expr = item.expr.clone();
14278            }
14279            Some(o)
14280        })
14281        .collect()
14282}
14283
14284/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
14285/// this expression? Statement preparation (`resolve_order_by_position`) runs
14286/// before any catalog is in hand, and it only needs to know "is this item's value
14287/// a set", which the builtin SRFs answer syntactically.
14288pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
14289    let mut found = false;
14290    let mut probe = e.clone();
14291    crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14292        if is_top_level_unnest(n) {
14293            found = true;
14294            return true;
14295        }
14296        false
14297    });
14298    found
14299}
14300
14301/// v7.39 (round 599) — everything about a target-list SRF that does not
14302/// depend on the row.
14303///
14304/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
14305/// each SRF-bearing projection expression, walked and rewrote the tree,
14306/// formatted a `__srf_N` name per node, and copied the whole column schema.
14307/// A counting allocator put the path at 24 allocations per input row for a
14308/// single-element `unnest`, against 0 for the same scan without one — 211 MB
14309/// where the plain scan took 4.3 — and the shape held whatever the array
14310/// contained, which is what invariant work looks like.
14311struct SrfPlan {
14312    /// The lifted SRF calls, in slot order.
14313    nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
14314    /// Per projection position, the expression with its SRF calls replaced
14315    /// by `__srf_N` column references. `None` means the item has none.
14316    rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
14317    /// The input schema followed by one column per slot. Only the slots'
14318    /// TYPES vary per row, and they are patched in place.
14319    ext_cols: alloc::vec::Vec<ColumnSchema>,
14320    /// v7.39 (round 743) — the rewritten projection COMPILED against the
14321    /// extended schema, once per plan. The per-output-row evaluation ran
14322    /// the interpreter (~560 ns/row on the unnest panel cell); the Step
14323    /// VM reads the `__srf_N` slots as plain columns. `None` = that item
14324    /// is not fully compilable and keeps the interpreter.
14325    compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
14326    base_cols: usize,
14327}
14328
14329fn build_srf_plan(
14330    engine: &Engine,
14331    projection: &[ProjectedItem],
14332    srf_idxs: &[usize],
14333    ctx: &EvalContext<'_>,
14334) -> Result<SrfPlan, EngineError> {
14335    // Lift every SRF node out of every item that contains one.
14336    let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
14337    let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
14338    let mut reject: Option<EngineError> = None;
14339    for &i in srf_idxs {
14340        let mut e = projection[i].expr.clone();
14341        crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
14342            if reject.is_some() {
14343                return true;
14344            }
14345            // PG refuses a set-returning function inside a conditional: the set
14346            // would have to be produced before anyone knows whether the branch
14347            // is even taken.
14348            let conditional = match n {
14349                spg_sql::ast::Expr::Case { .. } => Some("CASE"),
14350                spg_sql::ast::Expr::FunctionCall { name, .. }
14351                    if name.eq_ignore_ascii_case("coalesce") =>
14352                {
14353                    Some("COALESCE")
14354                }
14355                _ => None,
14356            };
14357            if let Some(kind) = conditional
14358                && engine.expr_contains_srf(n)
14359            {
14360                reject = Some(EngineError::Unsupported(alloc::format!(
14361                    "set-returning functions are not allowed in {kind}"
14362                )));
14363                return true;
14364            }
14365            if !engine.is_srf_node(n) {
14366                return false;
14367            }
14368            let slot = nodes.len();
14369            nodes.push(n.clone());
14370            *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
14371                qualifier: None,
14372                name: alloc::format!("__srf_{slot}"),
14373            });
14374            true
14375        });
14376        rewritten[i] = Some(e);
14377    }
14378    if let Some(err) = reject {
14379        return Err(err);
14380    }
14381    let base_cols = ctx.columns.len();
14382    let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
14383    for slot in 0..nodes.len() {
14384        ext_cols.push(ColumnSchema::new(
14385            alloc::format!("__srf_{slot}"),
14386            DataType::Text,
14387            true,
14388        ));
14389    }
14390    // v7.39 (round 743) — compile the rewritten items against the
14391    // EXTENDED schema. The slot columns' declared type is a per-row
14392    // patched detail the compiled column read does not consult.
14393    let compiled: Vec<Option<eval::CompiledExpr>> = {
14394        let mut ext_ctx = ctx.clone();
14395        ext_ctx.columns = &ext_cols;
14396        projection
14397            .iter()
14398            .enumerate()
14399            .map(|(i, p)| {
14400                let e = rewritten[i].as_ref().unwrap_or(&p.expr);
14401                if eval::fully_compilable(e) {
14402                    Some(eval::compile_expr(e, &ext_ctx))
14403                } else {
14404                    None
14405                }
14406            })
14407            .collect()
14408    };
14409    Ok(SrfPlan {
14410        nodes,
14411        rewritten,
14412        ext_cols,
14413        compiled,
14414        base_cols,
14415    })
14416}
14417
14418/// One input row expanded through a plan built once for the whole scan.
14419/// v7.39 (round 621) — expand a projection whose target list contains
14420/// set-returning items, remembering which INPUT row each output row came from.
14421///
14422/// The three materialised-source tails — `FROM unnest(…)`, `FROM
14423/// generate_series(…)`, and the one that serves VALUES / a derived table /
14424/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
14425/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
14426/// v(x)` answered `function unnest(integer[]) does not exist` on all the
14427/// others, for a query PG answers. Sharing the expansion is the point: a
14428/// fourth copy would have been the fourth place to forget.
14429fn expand_projection_srfs(
14430    engine: &Engine,
14431    projection: &[ProjectedItem],
14432    srf_idxs: &[usize],
14433    filtered: &[Row<'static>],
14434    ctx: &EvalContext<'_>,
14435) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
14436    let mut out = alloc::vec::Vec::with_capacity(filtered.len());
14437    let mut src = alloc::vec::Vec::with_capacity(filtered.len());
14438    // v7.39 (round 726) — ONE plan for the whole scan. The per-row
14439    // spelling rebuilt it for every input row: a full clone of the
14440    // rewritten projection trees and the extended schema, 50k times on
14441    // the panel's unnest cell.
14442    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
14443    // v7.39 (round 733) — shard the expansion. Each shard clones the
14444    // plan (its ext_cols slot types are per-row mutable) and builds a
14445    // MINIMAL context — EvalContext is not Sync — which is sound only
14446    // when every expression involved is pure: the whole projection and
14447    // every SRF argument must be fully_compilable, or the row loop
14448    // stays serial with the full session context.
14449    // The projection is judged in its REWRITTEN form — the SRF call
14450    // itself is never compilable, but after the lift it is a plain
14451    // `__srf_N` column reference.
14452    let all_pure = projection
14453        .iter()
14454        .enumerate()
14455        .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
14456        && plan.nodes.iter().all(|n| match n {
14457            Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
14458            other => eval::fully_compilable(other),
14459        });
14460    if all_pure
14461        && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
14462        && let Some(r) = engine.parallel_runner.0.as_deref()
14463    {
14464        let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
14465        let chunk = filtered.len().div_ceil(n_shards);
14466        type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
14467        let schema_cols = ctx.columns;
14468        let alias = ctx.table_alias;
14469        let mysql = ctx.mysql_dialect;
14470        let style = ctx.render_style;
14471        let plan_ref = &plan;
14472        let results = r.run_shards(n_shards, &|si| {
14473            let lo = si * chunk;
14474            let hi = ((si + 1) * chunk).min(filtered.len());
14475            let mut sctx = eval::EvalContext::new(schema_cols, alias);
14476            sctx.mysql_dialect = mysql;
14477            sctx.render_style = style;
14478            // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
14479            // compiled programs); each shard rebuilds it, which also
14480            // recompiles against the shard's own context. Build errors
14481            // were already surfaced by the outer build above.
14482            let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
14483                Ok(p) => p,
14484                Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
14485            };
14486            let mut run = || -> ShardOut {
14487                let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
14488                let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
14489                for (i, row) in filtered[lo..hi].iter().enumerate() {
14490                    let expanded =
14491                        expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
14492                    sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
14493                    o.extend(expanded);
14494                }
14495                Ok((o, sidx))
14496            };
14497            alloc::boxed::Box::new(run())
14498        });
14499        for boxed in results {
14500            let shard = boxed
14501                .downcast::<ShardOut>()
14502                .expect("runner echoes the closure's box");
14503            let (o, sidx) = (*shard)?;
14504            out.extend(o);
14505            src.extend(sidx);
14506        }
14507        return Ok((out, src));
14508    }
14509    for (i, row) in filtered.iter().enumerate() {
14510        let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
14511        src.extend(core::iter::repeat_n(i, expanded.len()));
14512        out.extend(expanded);
14513    }
14514    Ok((out, src))
14515}
14516
14517/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
14518///
14519/// A key that names a select-list item reads it out of the EXPANDED row,
14520/// because PG sorts after the expansion. A key that names a source column the
14521/// query does not project is evaluated against the input row that output row
14522/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
14523fn srf_order_key(
14524    ob: &spg_sql::ast::OrderBy,
14525    out_col: Option<usize>,
14526    out: &Row<'static>,
14527    src: &Row<'static>,
14528    ctx: &EvalContext<'_>,
14529) -> Result<Value<'static>, EngineError> {
14530    match out_col {
14531        Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
14532        None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
14533    }
14534}
14535
14536fn expand_srf_row_with(
14537    engine: &Engine,
14538    plan: &mut SrfPlan,
14539    projection: &[ProjectedItem],
14540    row: &Row<'static>,
14541    ctx: &EvalContext<'_>,
14542) -> Result<Vec<Row<'static>>, EngineError> {
14543    let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
14544    for n in &plan.nodes {
14545        lists.push(engine.srf_values(n, row, ctx)?);
14546    }
14547    let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
14548    // Only the slots' element types depend on the row; the names and the
14549    // input schema around them do not.
14550    for (slot, list) in lists.iter().enumerate() {
14551        plan.ext_cols[plan.base_cols + slot].ty = list
14552            .iter()
14553            .find_map(|v| v.data_type())
14554            .unwrap_or(DataType::Text);
14555    }
14556    let mut ext_ctx = ctx.clone();
14557    ext_ctx.columns = &plan.ext_cols;
14558    let mut out = Vec::with_capacity(n_rows);
14559    // v7.39 (round 726) — the base columns are the SAME for every
14560    // expanded row; clone them once and rewrite only the SRF slots per
14561    // k. The old form cloned the whole input row per OUTPUT row — for
14562    // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
14563    // TEXT column the projection never reads.
14564    let base_len = row.values.len();
14565    let mut ext_vals = row.values.clone();
14566    ext_vals.resize(base_len + lists.len(), Value::Null);
14567    let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
14568    for k in 0..n_rows {
14569        for (slot, list) in lists.iter().enumerate() {
14570            // Past the end of THIS srf's rows → NULL (PG pads).
14571            ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
14572        }
14573        let ext_row = Row::new(core::mem::take(&mut ext_vals));
14574        let mut vals = Vec::with_capacity(projection.len());
14575        for (i, p) in projection.iter().enumerate() {
14576            // v7.39 (round 743) — compiled when possible; the
14577            // interpreter for the rest, with its exact wording.
14578            vals.push(match &plan.compiled[i] {
14579                Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
14580                    .map_err(EngineError::Eval)?,
14581                None => {
14582                    let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
14583                    eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
14584                }
14585            });
14586        }
14587        ext_vals = ext_row.values;
14588        out.push(Row::new(vals));
14589    }
14590    Ok(out)
14591}
14592
14593/// The one-shot spelling, for the callers that expand a single row.
14594/// v7.39 (round 600) — which output column each ORDER BY key names, for a
14595/// query whose target list contains a set-returning function.
14596///
14597/// The keys used to be built from the INPUT row, before the SRF expanded, so
14598/// anything that named the SRF's own output was evaluated as a scalar call:
14599/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
14600/// "function unnest(integer[]) does not exist", and so did the spellings that
14601/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
14602/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
14603/// back in input order. PG sorts AFTER the expansion, so a key that names a
14604/// select-list item reads that item's value out of the expanded row.
14605///
14606/// `None` keeps the key on the input row, which is where an ORDER BY naming
14607/// a column the query does not project has to be evaluated.
14608/// v7.38.19 — the output column an ORDER BY term reads, when reading it
14609/// is provably the same as building a key from the input row.
14610///
14611/// A sort key is a COPY of the sort column, made because the source row
14612/// is gone by the time the sort runs — only the projection survives. On
14613/// `SELECT s_long FROM t ORDER BY s_long` that copy is of data the
14614/// projected row already holds, and on 400,000 rows of 192-character
14615/// text it is 400,000 allocations, 400,000 frees and 77 MB of copying.
14616/// A profile of that cell put the allocator at 2,025 leaf samples of the
14617/// working set, second only to the comparison chain.
14618///
14619/// The condition is narrow on purpose. `srf_order_output_cols` resolves
14620/// an ORDER BY term the way SQL does — a positional ordinal, or a name
14621/// matching the select list — and SQL resolves against the select list
14622/// BEFORE the input columns. The key path resolves against the INPUT
14623/// columns. For `SELECT g AS id … ORDER BY id` on a table that also has
14624/// an `id`, those are different columns, and swapping one for the other
14625/// would change answers rather than timings.
14626///
14627/// So this takes only the case where the two cannot disagree: a bare
14628/// unqualified column name, matching exactly one output item, whose own
14629/// expression is that same column. The projected cell then IS the input
14630/// cell, and the key would have been its copy.
14631/// True when comparing two of this column's VALUES gives the same order
14632/// as comparing the sort KEYS built from them.
14633///
14634/// It does not hold widely. A user ENUM stores its label as text but
14635/// orders by DECLARATION position; an array orders element-wise; a
14636/// domain or composite carries its own rules. For those the two paths
14637/// answer differently, and a sort that skipped the key would silently
14638/// reorder the result. This is the short list where they agree.
14639fn value_order_is_key_order(col: &ColumnSchema) -> bool {
14640    use spg_storage::DataType as T;
14641    col.user_enum_type.is_none()
14642        && col.user_domain_type.is_none()
14643        && col.user_composite_type.is_none()
14644        && col.collation_name.is_none()
14645        && col.collation == spg_storage::Collation::Binary
14646        && matches!(
14647            col.ty,
14648            T::SmallInt | T::Int | T::BigInt | T::Text | T::Varchar(_) | T::Bool | T::Uuid
14649        )
14650}
14651
14652/// The full ORDER BY comparison between two rows, named by index.
14653///
14654/// v7.38.19 — what a permutation sort falls back to when its key ties.
14655fn row_cmp_by_index(
14656    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14657    terms: &[(usize, bool, Option<bool>)],
14658    colls: &[Option<crate::collate::Collated>],
14659    mysql: bool,
14660    ia: u32,
14661    ib: u32,
14662) -> core::cmp::Ordering {
14663    let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
14664    for (i, (col, desc, nf)) in terms.iter().enumerate() {
14665        let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col)) else {
14666            continue;
14667        };
14668        let ord = match (va, vb) {
14669            (Value::Text(x), Value::Text(y)) => match colls.get(i).and_then(Option::as_ref) {
14670                Some(c) => {
14671                    let o = c.compare(x, y);
14672                    if *desc { o.reverse() } else { o }
14673                }
14674                None if !mysql => {
14675                    let o = crate::orderby::str_cmp_prefix_first(x, y);
14676                    if *desc { o.reverse() } else { o }
14677                }
14678                None => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
14679            },
14680            _ => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
14681        };
14682        if ord != core::cmp::Ordering::Equal {
14683            return ord;
14684        }
14685    }
14686    core::cmp::Ordering::Equal
14687}
14688
14689/// Whether ordering these rows by BYTES is what the collation in force
14690/// would have answered anyway.
14691///
14692/// v7.38.19 — a collated sort used to be shut out of the keyed path
14693/// entirely, and the cost of that showed up the moment the byte path
14694/// got fast: on the same fixture, the same binary took 92 ms under `C`
14695/// and 371 ms under `en_US`, so declaring a collation had become a
14696/// four-fold tax on a query that sorts md5 hex.
14697///
14698/// It need not be. For several locales `[0-9a-z]` orders exactly as
14699/// bytes do -- `collate::ascii_byte_order` carries that fact, and the
14700/// test beside it re-derives the whole allowlist by sorting a corpus
14701/// twice rather than asserting it. So when the collation is one of
14702/// those AND every value in every sort column is drawn from that
14703/// alphabet, the byte answer IS the collated answer.
14704///
14705/// Both halves are required. A collation outside the list can put `z`
14706/// between `s` and `t`; a value outside the alphabet can be `Ápple`,
14707/// which no locale in the list orders by its bytes. Either one and this
14708/// returns false, and the sort takes the collator's own path.
14709fn byte_order_answers_the_collation(
14710    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14711    terms: &[(usize, bool, Option<bool>)],
14712    colls: &[Option<crate::collate::Collated>],
14713) -> bool {
14714    if colls.iter().all(Option::is_none) {
14715        return true;
14716    }
14717    if !colls
14718        .iter()
14719        .flatten()
14720        .all(crate::collate::Collated::ascii_byte_order)
14721    {
14722        return false;
14723    }
14724    tagged.iter().all(|(_, row)| {
14725        terms.iter().all(|(col, _, _)| match row.values.get(*col) {
14726            // Only TEXT is collation-sensitive; a number or a NULL
14727            // orders the same under every collation there is.
14728            Some(Value::Text(t)) => crate::collate::is_ascii_alnum_lower(t),
14729            _ => true,
14730        })
14731    })
14732}
14733
14734/// An eight-byte key for each row's sort column, paired with the row's
14735/// index — or `None` when the column cannot give one on every row.
14736///
14737/// v7.38.19 — the pair is what the sort array holds instead of the row.
14738/// Two kinds of column can supply it:
14739///
14740///   * an INTEGER, whose whole value fits. Flipping the sign bit maps
14741///     the signed order onto the unsigned one, so the key is EXACT and
14742///     a comparison never has to look at the row at all.
14743///   * TEXT, as the first eight bytes big-endian, zero-padded. That
14744///     orders the same as the string — two that differ inside those
14745///     bytes differ at the same index either way, and one shorter than
14746///     eight pads with zeros exactly where `[u8]`'s own comparison runs
14747///     out — but it is a PREFIX, so equal keys must still ask the full
14748///     comparator.
14749///
14750/// The `None` is the safety of it: a NULL or any other type has no
14751/// faithful eight-byte key, so such a column takes the ordinary path
14752/// rather than being given a made-up one.
14753fn sort_keys_of(
14754    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14755    col: usize,
14756) -> Option<(Vec<(u64, u32)>, bool)> {
14757    let n = u32::try_from(tagged.len()).ok()?;
14758    let mut out: Vec<(u64, u32)> = Vec::with_capacity(tagged.len());
14759    let exact = match tagged.first()?.1.values.get(col)? {
14760        Value::Text(_) => false,
14761        Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => true,
14762        _ => return None,
14763    };
14764    for (i, row) in (0..n).zip(tagged.iter()) {
14765        let key = match row.1.values.get(col) {
14766            Some(Value::Text(t)) if !exact => {
14767                let mut k = [0u8; 8];
14768                let bytes = t.as_bytes();
14769                let take = bytes.len().min(8);
14770                k[..take].copy_from_slice(&bytes[..take]);
14771                u64::from_be_bytes(k)
14772            }
14773            Some(Value::SmallInt(v)) if exact => (i64::from(*v) as u64) ^ (1 << 63),
14774            Some(Value::Int(v)) if exact => (i64::from(*v) as u64) ^ (1 << 63),
14775            Some(Value::BigInt(v)) if exact => (*v as u64) ^ (1 << 63),
14776            _ => return None,
14777        };
14778        out.push((key, i));
14779    }
14780    Some((out, exact))
14781}
14782
14783/// Whether a PREFIX key is worth sorting a permutation on.
14784///
14785/// v7.38.19 — it is not always, and the panel says so in one cell. The
14786/// `text (26 values)` fixture is two hundred identical characters drawn
14787/// from twenty-six letters, so every eight-byte prefix inside a letter
14788/// is the same and 15,000 rows tie on it. Each tie then pays the prefix
14789/// compare, a two-hundred-byte comparison, AND a random read into a
14790/// 400,000-element array — while sorting the rows in place keeps the
14791/// partition contiguous. Measured: 160 ms sorting rows, 247 ms sorting
14792/// the permutation, on the very fixture built to be degenerate.
14793///
14794/// So the permutation is taken when the key DECIDES, and a sample says
14795/// whether it does. An exact key always decides; a prefix has to earn
14796/// it.
14797fn key_discriminates(keys: &[(u64, u32)]) -> bool {
14798    const SAMPLE: usize = 1024;
14799    let step = (keys.len() / SAMPLE).max(1);
14800    let mut seen: Vec<u64> = keys
14801        .iter()
14802        .step_by(step)
14803        .take(SAMPLE)
14804        .map(|&(k, _)| k)
14805        .collect();
14806    let taken = seen.len();
14807    if taken < 8 {
14808        return true;
14809    }
14810    seen.sort_unstable();
14811    seen.dedup();
14812    seen.len() * 2 >= taken
14813}
14814
14815fn order_by_output_cols_if_identical(
14816    order_by: &[spg_sql::ast::OrderBy],
14817    projection: &[ProjectedItem],
14818    schema_cols: &[ColumnSchema],
14819) -> Option<Vec<usize>> {
14820    if order_by.is_empty() {
14821        return None;
14822    }
14823    let mut out = Vec::with_capacity(order_by.len());
14824    for ob in order_by {
14825        let Expr::Column(c) = &ob.expr else {
14826            return None;
14827        };
14828        if c.qualifier.is_some() {
14829            return None;
14830        }
14831        let mut hit = None;
14832        for (i, p) in projection.iter().enumerate() {
14833            if !p.output_name.eq_ignore_ascii_case(&c.name) {
14834                continue;
14835            }
14836            if hit.is_some() {
14837                return None; // ambiguous — SQL would reject it too
14838            }
14839            // The item must BE that column, not merely be named for it.
14840            let Expr::Column(pc) = &p.expr else {
14841                return None;
14842            };
14843            if !pc.name.eq_ignore_ascii_case(&c.name) {
14844                return None;
14845            }
14846            let sc = schema_cols
14847                .iter()
14848                .find(|s| s.name.eq_ignore_ascii_case(&c.name))?;
14849            if !value_order_is_key_order(sc) {
14850                return None;
14851            }
14852            hit = Some(i);
14853        }
14854        out.push(hit?);
14855    }
14856    Some(out)
14857}
14858
14859fn srf_order_output_cols(
14860    order_by: &[spg_sql::ast::OrderBy],
14861    projection: &[ProjectedItem],
14862) -> Vec<Option<usize>> {
14863    order_by
14864        .iter()
14865        .map(|ob| {
14866            // A positive ordinal is the Nth output column, directly.
14867            // `resolve_positional_order_by` deliberately leaves an ordinal
14868            // pointing at a set-returning item alone — copying the call into
14869            // ORDER BY would have made the key "the whole set" back when keys
14870            // came from the input row. Reading the expanded row's column is
14871            // what it should have meant, and is what this does.
14872            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
14873                && *n >= 1
14874                && let Ok(idx) = usize::try_from(*n - 1)
14875                && idx < projection.len()
14876            {
14877                return Some(idx);
14878            }
14879            // An unqualified name matching exactly one output name. SQL
14880            // resolves ORDER BY against the select list first, so this wins
14881            // over an input column of the same name — which is the whole
14882            // point of `SELECT g AS id … ORDER BY id`.
14883            if let Expr::Column(c) = &ob.expr
14884                && c.qualifier.is_none()
14885            {
14886                let mut hit = None;
14887                for (i, p) in projection.iter().enumerate() {
14888                    if p.output_name.eq_ignore_ascii_case(&c.name) {
14889                        if hit.is_some() {
14890                            hit = None;
14891                            break;
14892                        }
14893                        hit = Some(i);
14894                    }
14895                }
14896                if hit.is_some() {
14897                    return hit;
14898                }
14899            }
14900            // Or the same expression as a select-list item — which is what
14901            // `ORDER BY 1` becomes once `resolve_positional_order_by` has
14902            // run, and what a repeated `ORDER BY unnest(…)` is.
14903            projection.iter().position(|p| p.expr == ob.expr)
14904        })
14905        .collect()
14906}
14907
14908fn expand_srf_row(
14909    engine: &Engine,
14910    projection: &[ProjectedItem],
14911    srf_idxs: &[usize],
14912    row: &Row<'static>,
14913    ctx: &EvalContext<'_>,
14914) -> Result<Vec<Row<'static>>, EngineError> {
14915    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
14916    expand_srf_row_with(engine, &mut plan, projection, row, ctx)
14917}
14918
14919impl Engine {
14920    /// The rows one target-list SRF yields for an input row. `None` from
14921    /// `srf_target_idxs` means the expression is not set-returning at all.
14922    fn srf_values(
14923        &self,
14924        expr: &spg_sql::ast::Expr,
14925        row: &Row<'static>,
14926        ctx: &EvalContext<'_>,
14927    ) -> Result<Vec<Value<'static>>, EngineError> {
14928        if top_level_srf_kind(expr).is_some() {
14929            return top_level_srf_output(expr, row, ctx);
14930        }
14931        // A user set-returning function. Its body runs through the real
14932        // executor, like every function body since round 63.
14933        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
14934            return Err(EngineError::Unsupported(
14935                "expected a SELECT-list SRF call".into(),
14936            ));
14937        };
14938        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
14939        for a in args {
14940            vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
14941        }
14942        let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
14943        // v7.39 (read01 round 68) — in a target list a multi-column function is
14944        // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
14945        // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
14946        // what it is for. A single-column function contributes its bare value.
14947        Ok(rows
14948            .into_iter()
14949            .map(|r| {
14950                if r.values.len() == 1 {
14951                    r.values.into_iter().next().unwrap_or(Value::Null)
14952                } else {
14953                    Value::Composite(
14954                        cols.iter()
14955                            .map(|c| c.name.clone())
14956                            .zip(r.values)
14957                            .collect::<alloc::vec::Vec<_>>(),
14958                    )
14959                }
14960            })
14961            .collect())
14962    }
14963
14964    /// Is THIS node a set-returning call: one of the builtin kinds, or a user
14965    /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
14966    fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
14967        if is_top_level_unnest(e) {
14968            return true;
14969        }
14970        let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
14971            return false;
14972        };
14973        self.active_catalog().functions_named(name).iter().any(|f| {
14974            let r = f.returns.trim().to_ascii_uppercase();
14975            r.starts_with("SETOF") || r.starts_with("TABLE(")
14976        })
14977    }
14978
14979    /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
14980    fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
14981        let mut found = false;
14982        let mut probe = e.clone();
14983        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14984            if self.is_srf_node(n) {
14985                found = true;
14986                return true;
14987            }
14988            false
14989        });
14990        found
14991    }
14992
14993    /// Which projection items CONTAIN a set-returning call. Before round 78 this
14994    /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
14995    /// ordinary scalar call all the way down to the function dispatcher, which
14996    /// then reported `unnest` as an unknown function.
14997    fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
14998        projection
14999            .iter()
15000            .enumerate()
15001            .filter(|(_, p)| self.expr_contains_srf(&p.expr))
15002            .map(|(i, _)| i)
15003            .collect()
15004    }
15005}
15006
15007impl Engine {
15008    /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
15009    /// no `(f(args)).*` item.
15010    fn lower_record_expansion(
15011        &self,
15012        stmt: &SelectStatement,
15013    ) -> Result<Option<SelectStatement>, EngineError> {
15014        use spg_sql::ast::{Expr, SelectItem};
15015        let is_marker = |it: &SelectItem| {
15016            matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
15017                if name == "__record_expand")
15018        };
15019        if !stmt.items.iter().any(is_marker) {
15020            return Ok(None);
15021        }
15022        let mut out = stmt.clone();
15023        let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
15024        let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
15025        for (n, item) in stmt.items.iter().enumerate() {
15026            if !is_marker(item) {
15027                items.push(item.clone());
15028                continue;
15029            }
15030            let SelectItem::Expr {
15031                expr: Expr::FunctionCall { args, .. },
15032                ..
15033            } = item
15034            else {
15035                unreachable!("checked by is_marker");
15036            };
15037            let Some(Expr::FunctionCall {
15038                name: fname,
15039                args: fargs,
15040            }) = args.first()
15041            else {
15042                return Err(EngineError::Unsupported(
15043                    "(<expr>).* expands a function's record — it needs a function call".into(),
15044                ));
15045            };
15046            let cols = self.setof_declared_columns(fname)?;
15047            let alias = alloc::format!("__rec{n}");
15048            let mut tref = bare_table_ref_named(&alias);
15049            tref.table_fn_call = Some(alloc::boxed::Box::new((
15050                fname.to_ascii_lowercase(),
15051                fargs.clone(),
15052            )));
15053            tref.alias = Some(alias.clone());
15054            lateral_refs.push(tref);
15055            for c in cols {
15056                items.push(SelectItem::Expr {
15057                    expr: Expr::Column(spg_sql::ast::ColumnName {
15058                        qualifier: Some(alias.clone()),
15059                        name: c,
15060                    }),
15061                    alias: None,
15062                });
15063            }
15064        }
15065        out.items = items;
15066        // The function joins the FROM. With no FROM it BECOMES the FROM; with one
15067        // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
15068        // (the arguments may reference the outer row — the round-69 correlation).
15069        for tref in lateral_refs {
15070            match &mut out.from {
15071                None => {
15072                    out.from = Some(spg_sql::ast::FromClause {
15073                        primary: tref,
15074                        joins: alloc::vec::Vec::new(),
15075                    });
15076                }
15077                Some(from) => from.joins.push(spg_sql::ast::FromJoin {
15078                    kind: spg_sql::ast::JoinKind::Cross,
15079                    table: tref,
15080                    on: None,
15081                    using_cols: None,
15082                    natural: false,
15083                }),
15084            }
15085        }
15086        Ok(Some(out))
15087    }
15088
15089    /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
15090    /// v text)` names them; a `SETOF <scalar>` is one column named after the
15091    /// function.
15092    fn setof_declared_columns(
15093        &self,
15094        name: &str,
15095    ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
15096        let cat = self.active_catalog();
15097        let overloads = cat.functions_named(name);
15098        let def = overloads.first().ok_or_else(|| {
15099            EngineError::Unsupported(alloc::format!("function {name} does not exist"))
15100        })?;
15101        let declared = def.returns.trim();
15102        let upper = declared.to_ascii_uppercase();
15103        if upper.starts_with("TABLE(") {
15104            let raw = &declared["TABLE(".len()..declared.len() - 1];
15105            return Ok(raw
15106                .split(',')
15107                .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
15108                .collect());
15109        }
15110        Ok(alloc::vec![name.to_string()])
15111    }
15112}
15113
15114/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
15115/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
15116/// COLUMNS list (data-independent), NESTED children inlined in
15117/// declaration order (PG's flattened output shape).
15118/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
15119/// correlated JSON_TABLE's static schema without evaluating its doc.
15120pub(crate) fn json_table_schema_pub(
15121    cols: &[spg_sql::ast::JsonTableColumn],
15122) -> alloc::vec::Vec<ColumnSchema> {
15123    json_table_schema(cols)
15124}
15125
15126fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
15127    use spg_sql::ast::JsonTableColumn as C;
15128    let mut out = alloc::vec::Vec::new();
15129    for c in cols {
15130        match c {
15131            C::Ordinality { name } => {
15132                out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
15133            }
15134            C::Regular {
15135                name, ty, exists, ..
15136            } => {
15137                let dt = if *exists {
15138                    DataType::Bool
15139                } else {
15140                    crate::conversions::column_type_to_data_type(*ty)
15141                };
15142                out.push(ColumnSchema::new(name.clone(), dt, true));
15143            }
15144            C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
15145        }
15146    }
15147    out
15148}
15149
15150/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
15151/// JSON_TABLE column's declared type (the DEFAULT expr may be a
15152/// string literal like `'none'` that must land as the column type).
15153fn coerce_json_table_default(
15154    v: Value<'static>,
15155    ty: spg_sql::ast::ColumnTypeName,
15156    name: &str,
15157) -> Result<Value<'static>, EngineError> {
15158    if v.is_null() {
15159        return Ok(Value::Null);
15160    }
15161    let dt = crate::conversions::column_type_to_data_type(ty);
15162    crate::conversions::coerce_value(v, dt, name, 0)
15163}
15164
15165/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
15166fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
15167    use crate::json::JsonValue as J;
15168    match v {
15169        Value::Null => J::Null,
15170        Value::Bool(b) => J::Bool(*b),
15171        Value::SmallInt(n) => J::Number(f64::from(*n)),
15172        Value::Int(n) => J::Number(f64::from(*n)),
15173        Value::BigInt(n) => J::Number(*n as f64),
15174        Value::Float(x) => J::Number(*x),
15175        Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
15176        other => J::String(crate::eval::value_to_text(other)),
15177    }
15178}
15179
15180fn bare_table_ref_named(name: &str) -> TableRef {
15181    TableRef {
15182        name: name.to_string(),
15183        alias: None,
15184        only: false,
15185        as_of_segment: None,
15186        unnest_expr: None,
15187        unnest_column_aliases: alloc::vec::Vec::new(),
15188        with_ordinality: false,
15189        generate_series_args: None,
15190        lateral_subquery: None,
15191        jsonb_each_text_arg: None,
15192        table_fn_call: None,
15193        rows_from: None,
15194        json_table: None,
15195        scalar_fn_item: false,
15196    }
15197}
15198
15199impl Engine {
15200    /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
15201    /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
15202    /// entries are the array-able SRFs, already lowered by the parser into their
15203    /// scalar array form.
15204    fn rows_from_rows(
15205        &self,
15206        primary: &TableRef,
15207    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
15208        let entries = primary
15209            .rows_from
15210            .as_ref()
15211            .expect("caller guards rows_from.is_some()");
15212        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
15213        let ctx = self.ev_ctx(&empty, None);
15214        let dummy = Row::new(alloc::vec::Vec::new());
15215        let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
15216        let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
15217        for (name, args) in entries {
15218            let (vals, colname) = if name == "__array" {
15219                // The parser lowered this one to `<array expr>`; its rows are the
15220                // array's elements.
15221                let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
15222                (
15223                    array_value_to_elements(&arr)?,
15224                    alloc::string::String::from("unnest"),
15225                )
15226            } else {
15227                let call = spg_sql::ast::Expr::FunctionCall {
15228                    name: name.clone(),
15229                    args: args.clone(),
15230                };
15231                (self.srf_values(&call, &dummy, &ctx)?, name.clone())
15232            };
15233            let ty = vals
15234                .first()
15235                .and_then(spg_storage::Value::data_type)
15236                .unwrap_or(DataType::Text);
15237            cols.push(ColumnSchema::new(colname, ty, true));
15238            lists.push(vals);
15239        }
15240        let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
15241        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
15242        for k in 0..n {
15243            let mut vals: alloc::vec::Vec<Value<'static>> =
15244                alloc::vec::Vec::with_capacity(lists.len() + 1);
15245            for l in &lists {
15246                vals.push(l.get(k).cloned().unwrap_or(Value::Null));
15247            }
15248            rows.push(Row::new(vals));
15249        }
15250        if primary.with_ordinality {
15251            cols.push(ColumnSchema::new(
15252                "ordinality".to_string(),
15253                DataType::BigInt,
15254                false,
15255            ));
15256            rows = rows
15257                .into_iter()
15258                .enumerate()
15259                .map(|(i, r)| {
15260                    let mut v = r.values;
15261                    v.push(Value::BigInt(i as i64 + 1));
15262                    Row::new(v)
15263                })
15264                .collect();
15265        }
15266        Ok((rows, cols))
15267    }
15268}
15269
15270/// v7.39 (round 232) — PG names the offending set operation in its
15271/// arity / type-mismatch messages ("each UNION query must have the same
15272/// number of columns"). `UNION ALL` is still spelled UNION there.
15273fn set_op_name(kind: UnionKind) -> &'static str {
15274    match kind {
15275        UnionKind::All | UnionKind::Distinct => "UNION",
15276        UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
15277        UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
15278    }
15279}
15280
15281/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
15282/// type: a bare string or NULL literal that no context has typed yet. SPG
15283/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
15284/// be the syntax. A wildcard or a non-literal expression is never unknown.
15285/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
15286/// LABELS as text (the wire render) but the value is an oid-carrying
15287/// dual, so a UNION with a numeric column must not be refused on the
15288/// label (pg_dump: `SELECT classid … UNION ALL SELECT
15289/// 'pg_opfamily'::regclass …`).
15290fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
15291    fn is_regcast(e: &Expr) -> bool {
15292        matches!(
15293            e,
15294            Expr::Cast {
15295                target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
15296                ..
15297            }
15298        )
15299    }
15300    stmt.items
15301        .iter()
15302        .map(|item| match item {
15303            SelectItem::Expr { expr, .. } => is_regcast(expr),
15304            _ => false,
15305        })
15306        .collect()
15307}
15308
15309fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
15310    stmt.items
15311        .iter()
15312        .map(|item| match item {
15313            SelectItem::Expr { expr, .. } => matches!(
15314                expr,
15315                Expr::Literal(spg_sql::ast::Literal::String(_))
15316                    | Expr::Literal(spg_sql::ast::Literal::Null)
15317            ),
15318            _ => false,
15319        })
15320        .collect()
15321}
15322
15323/// v7.39 (round 233) — retype one branch column's cells, reporting the
15324/// conversion failure the way PG does rather than leaving the column
15325/// half-converted. Used when the other branch typed an untyped literal.
15326fn coerce_branch_column(
15327    rows: &mut [Row<'static>],
15328    col_idx: usize,
15329    target: DataType,
15330    col_name: &str,
15331) -> Result<(), EngineError> {
15332    for row in rows.iter_mut() {
15333        let Some(slot) = row.values.get_mut(col_idx) else {
15334            continue;
15335        };
15336        if matches!(slot, Value::Null) {
15337            continue;
15338        }
15339        *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
15340    }
15341    Ok(())
15342}
15343
15344/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
15345/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
15346/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
15347/// reference to q's output columns substituted by the underlying column.
15348///
15349/// Admission is deliberately narrow — anything that changes cardinality,
15350/// order, or scope stays on the materialising path:
15351/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
15352///   FROM with no ordinality or positional column aliases, and no
15353///   subquery anywhere its expressions (an inner scope could reference
15354///   q too — descending is a later knife);
15355/// * inner: one stored table, bare-column projection only, no
15356///   CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
15357/// * every outer column reference must resolve inside q's output list —
15358///   a name that does not is an ERROR today, and flattening would
15359///   silently legalise it against the base table.
15360fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
15361    use spg_sql::ast::SelectItem;
15362    let inner = primary.lateral_subquery.as_deref()?;
15363    // Outer shape.
15364    if !stmt.ctes.is_empty()
15365        || !stmt.unions.is_empty()
15366        || stmt.distinct
15367        || !stmt.distinct_on.is_empty()
15368        || !stmt.window_check_exprs.is_empty()
15369        || stmt.locking.is_some()
15370        || primary.with_ordinality
15371        || !primary.unnest_column_aliases.is_empty()
15372    {
15373        return None;
15374    }
15375    // Inner shape.
15376    if !inner.ctes.is_empty()
15377        || !inner.unions.is_empty()
15378        || inner.distinct
15379        || !inner.distinct_on.is_empty()
15380        || inner.group_by.is_some()
15381        || inner.group_by_all
15382        || inner.having.is_some()
15383        || !inner.order_by.is_empty()
15384        || inner.limit.is_some()
15385        || inner.offset.is_some()
15386        || !inner.window_check_exprs.is_empty()
15387        || inner.locking.is_some()
15388    {
15389        return None;
15390    }
15391    let ifrom = inner.from.as_ref()?;
15392    let it = &ifrom.primary;
15393    if !ifrom.joins.is_empty()
15394        || it.name.is_empty()
15395        || it.lateral_subquery.is_some()
15396        || it.unnest_expr.is_some()
15397        || it.generate_series_args.is_some()
15398        || it.as_of_segment.is_some()
15399        || it.jsonb_each_text_arg.is_some()
15400        || it.table_fn_call.is_some()
15401        || it.rows_from.is_some()
15402        || it.json_table.is_some()
15403        || it.with_ordinality
15404        || !it.unnest_column_aliases.is_empty()
15405    {
15406        return None;
15407    }
15408    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
15409        return None;
15410    }
15411    // The output map: q's visible name -> the underlying column.
15412    let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
15413    let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
15414        alloc::collections::BTreeMap::new();
15415    for item in &inner.items {
15416        let SelectItem::Expr { expr, alias } = item else {
15417            return None;
15418        };
15419        let Expr::Column(c) = expr else {
15420            return None;
15421        };
15422        if let Some(q) = c.qualifier.as_deref()
15423            && !q.eq_ignore_ascii_case(&inner_alias)
15424        {
15425            return None;
15426        }
15427        let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
15428        // A duplicated output name would make substitution ambiguous.
15429        if map
15430            .insert(out_name.to_ascii_lowercase(), c.clone())
15431            .is_some()
15432        {
15433            return None;
15434        }
15435    }
15436    if map.is_empty() {
15437        return None;
15438    }
15439    let derived_alias = primary
15440        .alias
15441        .clone()
15442        .unwrap_or_else(|| primary.name.clone())
15443        .to_ascii_lowercase();
15444    // Substitute in a clone; bail (None) on the first reference the map
15445    // cannot answer.
15446    let mut out = stmt.clone();
15447    let ok = core::cell::Cell::new(true);
15448    let mut subst = |e: &mut Expr| -> bool {
15449        match e {
15450            Expr::Column(c) => {
15451                match c.qualifier.as_deref() {
15452                    Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
15453                    None => {}
15454                    Some(_) => {
15455                        ok.set(false);
15456                        return true;
15457                    }
15458                }
15459                match map.get(&c.name.to_ascii_lowercase()) {
15460                    Some(target) => *c = target.clone(),
15461                    None => ok.set(false),
15462                }
15463                true
15464            }
15465            // Any subquery could reference q from its own scope;
15466            // descending is a later knife — bail for now.
15467            Expr::ScalarSubquery(_)
15468            | Expr::Exists { .. }
15469            | Expr::InSubquery { .. }
15470            | Expr::RowInSubquery { .. }
15471            | Expr::RowCmpSubquery { .. } => {
15472                ok.set(false);
15473                true
15474            }
15475            _ => false,
15476        }
15477    };
15478    for item in &mut out.items {
15479        match item {
15480            SelectItem::Expr { expr, .. } => {
15481                crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
15482            }
15483            // `SELECT * FROM (…) q` means q's columns, in q's order.
15484            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
15485        }
15486    }
15487    if let Some(w) = &mut out.where_ {
15488        crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
15489    }
15490    if let Some(gs) = &mut out.group_by {
15491        for g in gs {
15492            crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
15493        }
15494    }
15495    if let Some(h) = &mut out.having {
15496        crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
15497    }
15498    for o in &mut out.order_by {
15499        crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
15500    }
15501    for d in &mut out.distinct_on {
15502        crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
15503    }
15504    if !ok.get() {
15505        return None;
15506    }
15507    // FROM becomes the stored table; the filters conjoin.
15508    out.from = Some(spg_sql::ast::FromClause {
15509        primary: it.clone(),
15510        joins: Vec::new(),
15511    });
15512    out.where_ = match (inner.where_.clone(), out.where_.take()) {
15513        (Some(a), Some(b)) => Some(Expr::Binary {
15514            lhs: alloc::boxed::Box::new(a),
15515            op: spg_sql::ast::BinOp::And,
15516            rhs: alloc::boxed::Box::new(b),
15517        }),
15518        (Some(a), None) => Some(a),
15519        (None, b) => b,
15520    };
15521    Some(out)
15522}
15523
15524/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
15525/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
15526/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
15527/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
15528/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
15529/// DISTINCT, an SRF, or an unprovable inner shape stays put.
15530fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
15531    use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
15532    let inner = primary.lateral_subquery.as_deref()?;
15533    // Outer: exactly `SELECT count(*)`, nothing else.
15534    if !stmt.ctes.is_empty()
15535        || !stmt.unions.is_empty()
15536        || stmt.distinct
15537        || !stmt.distinct_on.is_empty()
15538        || stmt.where_.is_some()
15539        || stmt.group_by.is_some()
15540        || stmt.having.is_some()
15541        || !stmt.order_by.is_empty()
15542        || stmt.limit.is_some()
15543        || stmt.offset.is_some()
15544        || stmt.items.len() != 1
15545    {
15546        return None;
15547    }
15548    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
15549        return None;
15550    };
15551    let E::FunctionCall { name, args } = expr else {
15552        return None;
15553    };
15554    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
15555        return None;
15556    }
15557    // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
15558    let Some(LimitExpr::Literal(k)) = &inner.offset else {
15559        return None;
15560    };
15561    let k = i64::from(*k);
15562    if inner.limit.is_some() || inner.order_by.is_empty() {
15563        return None;
15564    }
15565    let mut counted = inner.clone();
15566    counted.order_by = Vec::new();
15567    counted.offset = None;
15568    // The stripped inner must now be a provable simple shape (its
15569    // items become irrelevant — count(*) reads none of them — but an
15570    // SRF item would change the row count, so the flatten predicate's
15571    // scrutiny still applies).
15572    let base = matview_flatten_probe(&counted)?;
15573    let mut out = stmt.clone();
15574    out.items = alloc::vec![SelectItem::Expr {
15575        expr: E::FunctionCall {
15576            name: String::from("greatest"),
15577            args: alloc::vec![
15578                E::Binary {
15579                    lhs: alloc::boxed::Box::new(E::FunctionCall {
15580                        name: String::from("count_star"),
15581                        args: alloc::vec![],
15582                    }),
15583                    op: spg_sql::ast::BinOp::Sub,
15584                    rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
15585                },
15586                E::Literal(spg_sql::ast::Literal::Integer(0)),
15587            ],
15588        },
15589        alias: Some(String::from("count")),
15590    }];
15591    out.from = Some(spg_sql::ast::FromClause {
15592        primary: base,
15593        joins: Vec::new(),
15594    });
15595    out.where_ = counted.where_.clone();
15596    Some(out)
15597}
15598
15599/// The inner-shape probe `try_count_over_offset` shares with the
15600/// flatten: single stored table, no modifiers, no subqueries, no SRF
15601/// items. Returns the base TableRef.
15602fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
15603    use spg_sql::ast::SelectItem;
15604    if !inner.ctes.is_empty()
15605        || !inner.unions.is_empty()
15606        || inner.distinct
15607        || !inner.distinct_on.is_empty()
15608        || inner.group_by.is_some()
15609        || inner.group_by_all
15610        || inner.having.is_some()
15611        || !inner.order_by.is_empty()
15612        || inner.limit.is_some()
15613        || inner.offset.is_some()
15614        || !inner.window_check_exprs.is_empty()
15615        || inner.locking.is_some()
15616    {
15617        return None;
15618    }
15619    let ifrom = inner.from.as_ref()?;
15620    let it = &ifrom.primary;
15621    if !ifrom.joins.is_empty()
15622        || it.name.is_empty()
15623        || it.lateral_subquery.is_some()
15624        || it.unnest_expr.is_some()
15625        || it.generate_series_args.is_some()
15626        || it.as_of_segment.is_some()
15627        || it.jsonb_each_text_arg.is_some()
15628        || it.table_fn_call.is_some()
15629        || it.rows_from.is_some()
15630        || it.json_table.is_some()
15631        || it.with_ordinality
15632    {
15633        return None;
15634    }
15635    for item in &inner.items {
15636        match item {
15637            SelectItem::Expr { expr, .. } => {
15638                if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
15639                    return None;
15640                }
15641            }
15642            SelectItem::Wildcard => {}
15643            SelectItem::QualifiedWildcard(_) => return None,
15644        }
15645    }
15646    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
15647        return None;
15648    }
15649    Some(it.clone())
15650}
15651
15652/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
15653/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
15654/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
15655/// constant-LENGTH array literal unnests to exactly k rows per input
15656/// row (NULL elements are rows too). One SRF item only, elements
15657/// subquery-free, and the stripped inner must pass the same probe the
15658/// count-over-offset rewrite uses.
15659fn try_count_over_const_unnest(
15660    stmt: &SelectStatement,
15661    primary: &TableRef,
15662) -> Option<SelectStatement> {
15663    use spg_sql::ast::{Expr as E, SelectItem};
15664    let inner = primary.lateral_subquery.as_deref()?;
15665    if !stmt.ctes.is_empty()
15666        || !stmt.unions.is_empty()
15667        || stmt.distinct
15668        || !stmt.distinct_on.is_empty()
15669        || stmt.where_.is_some()
15670        || stmt.group_by.is_some()
15671        || stmt.having.is_some()
15672        || !stmt.order_by.is_empty()
15673        || stmt.limit.is_some()
15674        || stmt.offset.is_some()
15675        || stmt.items.len() != 1
15676    {
15677        return None;
15678    }
15679    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
15680        return None;
15681    };
15682    let E::FunctionCall { name, args } = expr else {
15683        return None;
15684    };
15685    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
15686        return None;
15687    }
15688    // Inner: exactly one item, and it is unnest(ARRAY[...]).
15689    if inner.items.len() != 1
15690        || !inner.order_by.is_empty()
15691        || inner.limit.is_some()
15692        || inner.offset.is_some()
15693    {
15694        return None;
15695    }
15696    let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
15697        return None;
15698    };
15699    let E::FunctionCall {
15700        name: fname,
15701        args: fargs,
15702    } = item
15703    else {
15704        return None;
15705    };
15706    if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
15707        return None;
15708    }
15709    let E::Array(elems) = &fargs[0] else {
15710        return None;
15711    };
15712    if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
15713        return None;
15714    }
15715    let k = elems.len() as i64;
15716    // The stripped inner (the SRF item replaced by a plain constant)
15717    // must be the provable simple shape.
15718    let mut counted = inner.clone();
15719    counted.items = alloc::vec![SelectItem::Expr {
15720        expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
15721        alias: None,
15722    }];
15723    let base = matview_flatten_probe(&counted)?;
15724    let mut out = stmt.clone();
15725    out.items = alloc::vec![SelectItem::Expr {
15726        expr: E::Binary {
15727            lhs: alloc::boxed::Box::new(E::FunctionCall {
15728                name: String::from("count_star"),
15729                args: alloc::vec![],
15730            }),
15731            op: spg_sql::ast::BinOp::Mul,
15732            rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
15733        },
15734        alias: Some(String::from("count")),
15735    }];
15736    out.from = Some(spg_sql::ast::FromClause {
15737        primary: base,
15738        joins: Vec::new(),
15739    });
15740    out.where_ = counted.where_.clone();
15741    Some(out)
15742}