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, wnode) in window_nodes.iter().enumerate() {
688            // v7.39.12 — the synthetic column carries the window call's
689            // TYPE.
690            //
691            // The comment here said "type doesn't matter for projection
692            // eval", and for the eval it does not — the values are
693            // already computed. It is the type that travels in the
694            // RowDescription, and psql aligns a column by that: on
695            // `SELECT count(*) AS plaincnt, count(*) OVER () AS wincnt`
696            // PostgreSQL right-aligns both and SPG left-aligned the
697            // second, because the first was bigint and the second was
698            // this `Text`. `\gdesc` — which asks the extended
699            // protocol's Describe — reported the right type for both,
700            // so the two descriptions of one column disagreed.
701            //
702            // Reported by sentori against 7.39.11, found by the
703            // alignment. Text stays as the fallback for a call whose
704            // type this build cannot name, which is what it was.
705            let ty =
706                crate::describe::describe_expr_type(wnode, schema_cols).unwrap_or(DataType::Text);
707            ext_cols.push(ColumnSchema::new(alloc::format!("__win_{i}"), ty, true));
708        }
709        // 6) Rewrite the projection: WindowFunction nodes → Column(__win_N).
710        let mut rewritten_items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
711        for item in &stmt.items {
712            let new_item = match item {
713                SelectItem::Wildcard => SelectItem::Wildcard,
714                SelectItem::QualifiedWildcard(q) => SelectItem::QualifiedWildcard(q.clone()),
715                SelectItem::Expr { expr, alias } => {
716                    let mut e = expr.clone();
717                    rewrite_window_to_columns(&mut e, &window_nodes);
718                    // The rewrite swaps the window call for a synthetic
719                    // `__win_N` column, and the projection then reported
720                    // THAT as the column name — `SELECT count(*) OVER ()`
721                    // answered `__win_0`, an internal name, where PG18
722                    // answers `count`. Pin the name while the call the
723                    // column is named for is still in hand.
724                    let alias = if alias.is_none() && e != *expr {
725                        Some(default_output_name(expr, self.speaks_mysql))
726                    } else {
727                        alias.clone()
728                    };
729                    SelectItem::Expr { expr: e, alias }
730                }
731            };
732            rewritten_items.push(new_item);
733        }
734
735        // 7) Project into final rows. JOIN case uses None so the
736        // qualifier check in `resolve_column` falls through to the
737        // composite `alias.col` schema lookup; single-table case
738        // keeps the bare alias so `bare_col` resolution still
739        // works for the projection's per-row column references.
740        // v7.39 (read01 round 54) — build through `ev_ctx`, the canonical
741        // constructor: it threads the catalog (plus render style / tz / GUCs)
742        // that a bare `EvalContext::new` drops. Without the catalog the OUTER
743        // `ORDER BY <enum col>` of a windowed query sorted by TEXT — the
744        // window values were right, the row order silently was not.
745        let ext_ctx = self.ev_ctx(&ext_cols, alias_opt);
746        let projection = build_projection_hiding_tail(
747            &rewritten_items,
748            &ext_cols,
749            alias,
750            self.speaks_mysql,
751            window_nodes.len(),
752            Some(self.active_catalog()),
753        )?;
754        let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(n_rows);
755        // v7.39 (round 592) — the extended row (input columns plus the window
756        // values) used to be materialised for EVERY input row and kept until
757        // the projection had run: the input values cloned into a fresh Vec,
758        // then grown once to take the window columns. A counting allocator put
759        // the window path at 4 allocations a row where a plain derived table
760        // takes 1, and named all four — the input row, the clone, the growth,
761        // and the projected row. Only the last has to exist afterwards, so the
762        // extended row is one buffer refilled per row.
763        let mut ext_row: Row<'static> =
764            Row::new(Vec::with_capacity(schema_cols.len() + window_nodes.len()));
765        for i in 0..n_rows {
766            if i.is_multiple_of(256) {
767                cancel.check()?;
768            }
769            ext_row.values.clear();
770            ext_row.values.extend(filtered[i].values.iter().cloned());
771            for w in 0..window_nodes.len() {
772                ext_row.values.push(win_vals[w][i].clone());
773            }
774            let row = &ext_row;
775            let mut values = Vec::with_capacity(projection.len());
776            for p in &projection {
777                values.push(eval::eval_expr(&p.expr, row, &ext_ctx)?);
778            }
779            let order_keys = if stmt.order_by.is_empty() {
780                Vec::new()
781            } else {
782                let mut keys = Vec::with_capacity(stmt.order_by.len());
783                for o in &stmt.order_by {
784                    let mut e = o.expr.clone();
785                    rewrite_window_to_columns(&mut e, &window_nodes);
786                    let key = eval::eval_expr(&e, row, &ext_ctx)?;
787                    // v7.39 (read01 round 54) — this path builds its order keys
788                    // itself instead of going through `build_order_keys`, so it
789                    // skipped the enum-ordinal substitution: the OUTER
790                    // `ORDER BY <enum col>` of a windowed query sorted by the
791                    // label's TEXT, not by member order. The window values were
792                    // right and only the row order was wrong — silently.
793                    match crate::orderby::enum_order_ordinal(&e, &key, &ext_ctx) {
794                        Some(ord) => keys.push(value_to_order_key(&Value::Float(ord))?),
795                        None => keys.push(value_to_order_key(&key)?),
796                    }
797                }
798                keys
799            };
800            tagged.push((order_keys, Row::new(values)));
801        }
802        // ORDER BY + LIMIT/OFFSET on the projected rows.
803        if !stmt.order_by.is_empty() {
804            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
805            // v7.39.11 — and the collation, which this path was sorting
806            // without.
807            //
808            // Reported by sentori against 7.39.10: `SELECT t, count(*)
809            // OVER () FROM t ORDER BY t` answered `A B a b` on a
810            // database collating `en_US.utf8` where the same query
811            // without the window function answers `a A b B`. No row is
812            // wrong and nothing raises; only the order changes.
813            //
814            // Same cause as the enum-ordinal defect the comment above
815            // records: this branch builds its order keys itself instead
816            // of going through `build_order_keys`, so anything that
817            // path resolves has to be resolved again here, and the
818            // collation was not. `order_by_collations` is the one place
819            // that answers it — explicit `COLLATE` first, then the
820            // column's declaration, then the database's — so calling it
821            // here cannot disagree with the ungrouped path.
822            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
823            crate::orderby::sort_by_keys_in(
824                &mut tagged,
825                &descs,
826                &colls,
827                self.session_parallel_workers(),
828            );
829        }
830        let mut out_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
831        // v7.37 D.41 — `SELECT DISTINCT` over a window projection: the window
832        // pipeline builds one output row per input row, so DISTINCT must dedup the
833        // projected rows (PG evaluates window functions before DISTINCT). Applied
834        // after ORDER BY (duplicate rows share sort keys, so order is preserved)
835        // and before LIMIT.
836        if stmt.distinct {
837            // v7.38.14 — see the synthetic-source sites below: the mask was
838            // always available here, from the same projection this function
839            // already built.
840            out_rows = dedup_rows(
841                out_rows,
842                FoldSpec::of_masks(
843                    self.speaks_mysql,
844                    &fold_mask(&projection),
845                    &pad_mask(&projection),
846                ),
847            );
848        }
849        apply_offset_and_limit(&mut out_rows, stmt.offset_literal(), stmt.limit_literal());
850        let final_cols: Vec<ColumnSchema> = projection
851            .into_iter()
852            .map(|p| p.to_column_schema())
853            .collect();
854        Ok(QueryResult::Rows {
855            columns: final_cols,
856            rows: out_rows,
857        })
858    }
859
860    /// v4.11: materialise each CTE into a temp table inside a
861    /// cloned catalog, then run the body SELECT against a fresh
862    /// engine instance that owns the enriched catalog. The clone
863    /// is moderately expensive — only paid by CTE-bearing queries.
864    /// Subqueries inside CTE bodies / the main body resolve as
865    /// usual; `clock_fn` is propagated so `NOW()` lines up.
866    /// v7.16.2 — mailrs round-10 A.3. Materialise the
867    /// `information_schema.*` / `pg_catalog.*` virtual views
868    /// the SELECT references, then re-execute the SELECT
869    /// against an enriched catalog where those views are real
870    /// tables. Same pattern as `exec_with_ctes`. The temp
871    /// engine carries `meta_views_materialised = true` so its
872    /// own meta-dispatch short-circuits — without that we'd
873    /// infinite-recurse since the temp catalog's view name
874    /// still starts with `__spg_info_` and re-triggers the
875    /// check.
876    pub(crate) fn exec_select_with_meta_views(
877        &self,
878        stmt: &SelectStatement,
879        cancel: CancelToken<'_>,
880    ) -> Result<QueryResult, EngineError> {
881        let catalog = self.meta_view_catalog(stmt)?;
882        let mut temp = Engine::restore(catalog);
883        if let Some(c) = self.clock {
884            temp = temp.with_clock(c);
885        }
886        if let Some(f) = self.salt_fn {
887            temp = temp.with_salt_fn(f);
888        }
889        // v7.39 (round 522) — the temp engine holds the materialised
890        // catalog and, until now, nothing of the SESSION. So every
891        // session-scoped answer changed the moment a system view
892        // appeared in the FROM clause: `SELECT current_user` said
893        // `unmei` and `SELECT current_user FROM pg_class` said `admin`;
894        // `current_setting('work_mem')` fell back to the boot default
895        // after a SET; `application_name` read empty. A privilege check
896        // written against a catalog join was reading a different
897        // identity than the same check written without one.
898        //
899        // Carry what a session can be observed through — its parameters
900        // (which is also where the session user lives), the role store
901        // the privilege builtins read, the dialect, and the rendering
902        // settings a timestamp is spelled with.
903        temp.session_params.clone_from(&self.session_params);
904        temp.users.clone_from(&self.users);
905        temp.backslash_escapes = self.backslash_escapes;
906        temp.speaks_mysql = self.speaks_mysql;
907        temp.mysql_strict = self.mysql_strict;
908        temp.render_style = self.render_style;
909        temp.tz_offset_fn = self.tz_offset_fn;
910        temp.tz_localize_fn = self.tz_localize_fn;
911        temp.tz_abbrev_fn = self.tz_abbrev_fn;
912        temp.meta_views_materialised = true;
913        temp.exec_select_cancel(stmt, cancel)
914    }
915
916    /// v7.39 (round 462) — the catalog a meta-view SELECT resolves
917    /// against: this engine's catalog with every `__spg_*` view the
918    /// statement references materialised into it.
919    ///
920    /// Split out of `exec_select_with_meta_views` so Describe can reach
921    /// the same shapes execution reaches. Describe used to look the FROM
922    /// relation up in the plain catalog, where a system view does not
923    /// exist, and reported "no columns" for every one of them — so an
924    /// extended-protocol client reading `pg_stat_user_tables` got rows
925    /// with no column metadata. Sharing the materialisation means a
926    /// view added here is described correctly the day it is added.
927    pub(crate) fn meta_view_catalog(&self, stmt: &SelectStatement) -> Result<Catalog, EngineError> {
928        let mut needed: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
929        collect_meta_view_names(stmt, &mut needed);
930        let mut catalog = self.active_catalog().clone();
931        for view in &needed {
932            if catalog.get(view).is_some() {
933                continue;
934            }
935            match view.as_str() {
936                "__spg_info_columns" => {
937                    let (schema, rows) = synth_information_schema_columns(
938                        self.active_catalog(),
939                        self.speaks_mysql,
940                        &self.mysql_schema_name(),
941                    );
942                    materialise_meta_view(&mut catalog, view, schema, rows)?;
943                }
944                "__spg_info_tables" => {
945                    let (schema, rows) = synth_information_schema_tables(
946                        self.active_catalog(),
947                        self.speaks_mysql,
948                        &self.mysql_schema_name(),
949                    );
950                    materialise_meta_view(&mut catalog, view, schema, rows)?;
951                }
952                "__spg_pg_class" => {
953                    let (schema, rows) = synth_pg_class(
954                        self.active_catalog(),
955                        i64::try_from(self.vacuum_oldest_active()).unwrap_or(i64::MAX),
956                    );
957                    materialise_meta_view(&mut catalog, view, schema, rows)?;
958                }
959                "__spg_pg_attribute" => {
960                    let (schema, rows) = synth_pg_attribute(self.active_catalog());
961                    materialise_meta_view(&mut catalog, view, schema, rows)?;
962                }
963                // v7.17.0 Phase 3.P0-50 — pg_catalog.pg_type for
964                // sqlx / SQLAlchemy / Diesel / pgAdmin lookups.
965                "__spg_pg_type" => {
966                    let (schema, rows) = synth_pg_type(self.active_catalog());
967                    materialise_meta_view(&mut catalog, view, schema, rows)?;
968                }
969                // v7.39 (round 621) — pg_catalog.pg_operator, which did not
970                // exist at all.
971                "__spg_pg_operator" => {
972                    let (schema, rows) = synth_pg_operator(self.active_catalog());
973                    materialise_meta_view(&mut catalog, view, schema, rows)?;
974                }
975                // v7.17.0 Phase 3.P0-51 — pg_catalog.pg_proc for
976                // function-name introspection (ORM / pgAdmin).
977                "__spg_pg_proc" => {
978                    let (schema, rows) = synth_pg_proc(self.active_catalog());
979                    materialise_meta_view(&mut catalog, view, schema, rows)?;
980                }
981                // v7.24 (round-16 D) — pg_catalog.pg_trigger. The
982                // round-16 "why doesn't prod fire the trigger"
983                // question was unanswerable because triggers had NO
984                // introspection surface; tgname/tgenabled plus the
985                // pragmatic relname/timing/events/function columns
986                // make "is it registered and enabled" a one-liner.
987                "__spg_pg_trigger" => {
988                    let (schema, rows) = synth_pg_trigger(self.active_catalog());
989                    materialise_meta_view(&mut catalog, view, schema, rows)?;
990                }
991                // v7.17.0 Phase 3.P0-52 — pg_catalog.pg_namespace
992                // (schema list for admin tools' tree views).
993                "__spg_pg_namespace" => {
994                    let (schema, rows) = synth_pg_namespace(self.active_catalog());
995                    materialise_meta_view(&mut catalog, view, schema, rows)?;
996                }
997                // v7.39 — pg_tables convenience view (was a pgwire
998                // canned response that ignored projections).
999                "__spg_pg_tables" => {
1000                    let (schema, rows) =
1001                        crate::system_catalog::synth_pg_tables(self.active_catalog());
1002                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1003                }
1004                // v7.37.24 (24.1) — pg_catalog.pg_enum (label list
1005                // for ENUM types; sqlx / ORM enum codecs read this).
1006                "__spg_pg_enum" => {
1007                    let (schema, rows) =
1008                        crate::system_catalog::synth_pg_enum(self.active_catalog());
1009                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1010                }
1011                // v7.37.21 (21.13) — pg_catalog.pg_replication_slots
1012                // (shape-stable empty until 21.12 persists slot state).
1013                // v7.39 (round 277) — session-scoped prepared statements.
1014                "__spg_pg_prepared_statements" => {
1015                    let (schema, rows) = crate::system_catalog::synth_pg_prepared_statements(
1016                        &self.prepared_statements,
1017                    );
1018                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1019                }
1020                "__spg_pg_replication_slots" => {
1021                    let (schema, rows) =
1022                        crate::system_catalog::synth_pg_replication_slots(self.active_catalog());
1023                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1024                }
1025                // v7.37.21 (21.13-b) — pg_catalog.pg_publication
1026                // (one row per CREATE PUBLICATION).
1027                "__spg_pg_publication" => {
1028                    let (schema, rows) = crate::system_catalog::synth_pg_publication(self);
1029                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1030                }
1031                // v7.37.21 (21.13-c) — pg_catalog.pg_subscription
1032                // (one row per CREATE SUBSCRIPTION; subconninfo
1033                // redacted so dashboards can't leak credentials).
1034                "__spg_pg_subscription" => {
1035                    let (schema, rows) = crate::system_catalog::synth_pg_subscription(self);
1036                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1037                }
1038                // v7.37.22 (22.x-stat-db) — pg_catalog.pg_stat_database
1039                // (one row for SPG's single database; counters are
1040                // shape-stable 0 until wiring lands).
1041                "__spg_pg_stat_database" => {
1042                    let (schema, rows) = crate::system_catalog::synth_pg_stat_database(
1043                        self,
1044                        self.stat_tup_inserted,
1045                        self.stat_tup_updated,
1046                        self.stat_tup_deleted,
1047                    );
1048                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1049                }
1050                // v7.37.22 (22.14) — pg_catalog.pg_stat_user_tables
1051                // (per-table churn counters; live_tup = row count).
1052                "__spg_pg_stat_user_tables" => {
1053                    // r192 — DML counters come from the engine-side
1054                    // non-transactional map, not the (tx-shadowed)
1055                    // catalog tables.
1056                    let (schema, rows) = crate::system_catalog::synth_pg_stat_user_tables(
1057                        self.active_catalog(),
1058                        &self.table_write_stats,
1059                    );
1060                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1061                }
1062                // v7.37.22 (22.15) — pg_catalog.pg_stat_user_indexes
1063                // (per-index usage counters; flag unused indexes).
1064                "__spg_pg_stat_user_indexes" => {
1065                    let (schema, rows) =
1066                        crate::system_catalog::synth_pg_stat_user_indexes(self.active_catalog());
1067                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1068                }
1069                // v7.37.22 (22.16) — pg_catalog.pg_stat_bgwriter.
1070                "__spg_pg_stat_bgwriter" => {
1071                    let (schema, rows) =
1072                        crate::system_catalog::synth_pg_stat_bgwriter(self.active_catalog());
1073                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1074                }
1075                // v7.38 (read01 P3.14) — pg_catalog.pg_stat_checkpointer /
1076                // pg_stat_wal shell views (shape-stable, counters pending).
1077                "__spg_pg_stat_checkpointer" => {
1078                    let (schema, rows) =
1079                        crate::system_catalog::synth_pg_stat_checkpointer(self.active_catalog());
1080                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1081                }
1082                "__spg_pg_stat_wal" => {
1083                    let (schema, rows) =
1084                        crate::system_catalog::synth_pg_stat_wal(self.active_catalog());
1085                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1086                }
1087                // v7.38 (read01 P3.15) — pg_catalog.pg_stat_slru /
1088                // pg_stat_subscription_stats shell views.
1089                "__spg_pg_stat_slru" => {
1090                    let (schema, rows) =
1091                        crate::system_catalog::synth_pg_stat_slru(self.active_catalog());
1092                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1093                }
1094                "__spg_pg_stat_subscription_stats" => {
1095                    let (schema, rows) = crate::system_catalog::synth_pg_stat_subscription_stats(
1096                        self.active_catalog(),
1097                    );
1098                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1099                }
1100                // v7.37.22 (22.17) — pg_catalog.pg_stat_archiver.
1101                "__spg_pg_stat_archiver" => {
1102                    let (schema, rows) =
1103                        crate::system_catalog::synth_pg_stat_archiver(self.active_catalog());
1104                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1105                }
1106                // v7.37.21 (21.13-d) — pg_catalog.pg_stat_replication.
1107                "__spg_pg_stat_replication" => {
1108                    let (schema, rows) =
1109                        crate::system_catalog::synth_pg_stat_replication(self.active_catalog());
1110                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1111                }
1112                // v7.37.24 (24.13) — pg_catalog.pg_am.
1113                "__spg_pg_am" => {
1114                    let (schema, rows) = crate::system_catalog::synth_pg_am(self.active_catalog());
1115                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1116                }
1117                // v7.37.22 (22.18) — pg_catalog.pg_stat_io (PG 16+).
1118                "__spg_pg_stat_io" => {
1119                    let (schema, rows) =
1120                        crate::system_catalog::synth_pg_stat_io(self.active_catalog());
1121                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1122                }
1123                // v7.37.22 (22.19) — pg_catalog.pg_stat_user_functions.
1124                "__spg_pg_stat_user_functions" => {
1125                    let (schema, rows) =
1126                        crate::system_catalog::synth_pg_stat_user_functions(self.active_catalog());
1127                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1128                }
1129                // v7.39 (round 287) — pg_catalog.pg_largeobject{,_metadata}.
1130                "__spg_pg_largeobject" => {
1131                    let (schema, rows) =
1132                        crate::system_catalog::synth_pg_largeobject(self.active_catalog());
1133                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1134                }
1135                "__spg_pg_largeobject_metadata" => {
1136                    let (schema, rows) =
1137                        crate::system_catalog::synth_pg_largeobject_metadata(self.active_catalog());
1138                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1139                }
1140                // v7.37.23 (23.7-a) — pg_catalog.pg_statistic_ext.
1141                "__spg_pg_statistic_ext" => {
1142                    let (schema, rows) =
1143                        crate::system_catalog::synth_pg_statistic_ext(self.active_catalog());
1144                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1145                }
1146                // v7.38.18 — pg_catalog.pg_stats, the readable view.
1147                "__spg_pg_stats" => {
1148                    let (schema, rows) = crate::system_catalog::synth_pg_stats(
1149                        self.active_catalog(),
1150                        &self.statistics,
1151                    );
1152                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1153                }
1154                // v7.37.24 (24.15) — pg_catalog.pg_statistic.
1155                "__spg_pg_statistic" => {
1156                    let (schema, rows) = crate::system_catalog::synth_pg_statistic(
1157                        self.active_catalog(),
1158                        &self.statistics,
1159                    );
1160                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1161                }
1162                // v7.37.22 (22.20) — pg_catalog.pg_stat_progress_vacuum.
1163                "__spg_pg_stat_progress_vacuum" => {
1164                    let (schema, rows) =
1165                        crate::system_catalog::synth_pg_stat_progress_vacuum(self.active_catalog());
1166                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1167                }
1168                // v7.37.22 (22.21) — pg_catalog.pg_stat_progress_create_index.
1169                "__spg_pg_stat_progress_create_index" => {
1170                    let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_create_index(
1171                        self.active_catalog(),
1172                    );
1173                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1174                }
1175                // v7.37.22 (22.22) — pg_catalog.pg_stat_progress_analyze.
1176                "__spg_pg_stat_progress_analyze" => {
1177                    let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_analyze(
1178                        self.active_catalog(),
1179                    );
1180                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1181                }
1182                // v7.37.24 (24.16) — pg_catalog.pg_inherits
1183                // (partition parent → child OID mapping).
1184                "__spg_pg_inherits" => {
1185                    let (schema, rows) =
1186                        crate::system_catalog::synth_pg_inherits(self.active_catalog());
1187                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1188                }
1189                // v7.39 (round 650) — the text-search catalogs, filled
1190                // with what SPG actually has rather than PG's thirty.
1191                "__spg_pg_ts_config_map" => {
1192                    let (schema, rows) =
1193                        crate::system_catalog::synth_pg_ts_config_map(self.active_catalog());
1194                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1195                }
1196                "__spg_pg_ts_config" => {
1197                    let (schema, rows) =
1198                        crate::system_catalog::synth_pg_ts_config(self.active_catalog());
1199                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1200                }
1201                "__spg_pg_ts_dict" => {
1202                    let (schema, rows) =
1203                        crate::system_catalog::synth_pg_ts_dict(self.active_catalog());
1204                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1205                }
1206                "__spg_pg_ts_parser" => {
1207                    let (schema, rows) =
1208                        crate::system_catalog::synth_pg_ts_parser(self.active_catalog());
1209                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1210                }
1211                "__spg_pg_ts_template" => {
1212                    let (schema, rows) =
1213                        crate::system_catalog::synth_pg_ts_template(self.active_catalog());
1214                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1215                }
1216                // v7.37.24 (24.17) — pg_catalog.pg_depend
1217                // (dependency graph; shape-stable empty since
1218                // SPG's drop enforcement is per-kind, not per-object).
1219                "__spg_pg_depend" => {
1220                    let (schema, rows) =
1221                        crate::system_catalog::synth_pg_depend(self.active_catalog());
1222                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1223                }
1224                // 7.38.1 S5.1 — pg_catalog.pg_opclass (pg_dump wall #1).
1225                "__spg_pg_opclass" => {
1226                    let (schema, rows) =
1227                        crate::system_catalog::synth_pg_opclass(self.active_catalog());
1228                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1229                }
1230                "__spg_pg_opfamily" => {
1231                    let (schema, rows) =
1232                        crate::system_catalog::synth_pg_opfamily(self.active_catalog());
1233                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1234                }
1235                "__spg_pg_amop" => {
1236                    let (schema, rows) =
1237                        crate::system_catalog::synth_pg_amop(self.active_catalog());
1238                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1239                }
1240                "__spg_pg_amproc" => {
1241                    let (schema, rows) =
1242                        crate::system_catalog::synth_pg_amproc(self.active_catalog());
1243                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1244                }
1245                // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1246                // ORM reflection + pg_dump read the deparsed default text).
1247                "__spg_pg_attrdef" => {
1248                    let (schema, rows) =
1249                        crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1250                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1251                }
1252                // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1253                "__spg_pg_policy" => {
1254                    let (schema, rows) =
1255                        crate::system_catalog::synth_pg_policy(self.active_catalog());
1256                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1257                }
1258                "__spg_pg_policies" => {
1259                    let (schema, rows) =
1260                        crate::system_catalog::synth_pg_policies(self.active_catalog());
1261                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1262                }
1263                // v7.37.24 (24.14) — pg_catalog.pg_collation.
1264                "__spg_pg_collation" => {
1265                    let (schema, rows) =
1266                        crate::system_catalog::synth_pg_collation(self.active_catalog());
1267                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1268                }
1269                // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1270                "__spg_pg_tablespace" => {
1271                    let (schema, rows) =
1272                        crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1273                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1274                }
1275                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1276                // for pgAdmin / DataGrip "indexes per table" listings.
1277                "__spg_pg_indexes" => {
1278                    let (schema, rows) = synth_pg_indexes(self.active_catalog());
1279                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1280                }
1281                // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1282                // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1283                "__spg_pg_description" => {
1284                    let (schema, rows) =
1285                        crate::system_catalog::synth_pg_description(self.active_catalog());
1286                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1287                }
1288                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1289                // for index introspection by ORM compilers.
1290                "__spg_pg_index" => {
1291                    let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1292                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1293                }
1294                // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1295                // for FK / UNIQUE / PK / CHECK introspection.
1296                "__spg_pg_constraint" => {
1297                    let (schema, rows) = synth_pg_constraint(self.active_catalog());
1298                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1299                }
1300                // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1301                // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1302                "__spg_pg_sequence" => {
1303                    let (schema, rows) = synth_pg_sequence(self.active_catalog());
1304                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1305                }
1306                // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1307                // pg_roles / pg_user. SPG is single-database so
1308                // pg_database surfaces just `postgres`; pg_roles
1309                // / pg_user walk the engine's UserStore.
1310                "__spg_pg_database" => {
1311                    let (schema, rows) = synth_pg_database(self);
1312                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1313                }
1314                "__spg_pg_roles" => {
1315                    let (schema, rows) = synth_pg_roles(self);
1316                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1317                }
1318                // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1319                // same roles, with PG's own `use*` column names. It used to
1320                // publish pg_roles' columns under this name.
1321                "__spg_pg_user" => {
1322                    let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1323                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1324                }
1325                // v7.39 (read01 round 58) — role membership.
1326                "__spg_pg_auth_members" => {
1327                    let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1328                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1329                }
1330                // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1331                // pg_views surfaces every CREATE VIEW result; SPG
1332                // ships one row per declared view from the catalog.
1333                "__spg_pg_views" => {
1334                    let (schema, rows) = synth_pg_views(self.active_catalog());
1335                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1336                }
1337                // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1338                // catalogued query-rewrite RULE.
1339                "__spg_pg_rules" => {
1340                    let (schema, rows) =
1341                        crate::system_catalog::synth_pg_rules(self.active_catalog());
1342                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1343                }
1344                // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1345                // catalogue `pg_get_ruledef(oid)` resolves against.
1346                "__spg_pg_rewrite" => {
1347                    let (schema, rows) =
1348                        crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1349                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1350                }
1351                // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1352                // and PG's own column names.
1353                "__spg_pg_matviews" => {
1354                    let (schema, rows) =
1355                        crate::system_catalog::synth_pg_matviews(self.active_catalog());
1356                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1357                }
1358                // pg_catalog.pg_extension — native capability list
1359                // (mailrs embed round-12).
1360                // v7.39 (round 546) — the catalogs SPG has real content
1361                // for, from the facts it already holds.
1362                "__spg_pg_db_role_setting" => {
1363                    let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1364                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1365                }
1366                "__spg_pg_language" => {
1367                    let (schema, rows) = crate::system_catalog::synth_pg_language();
1368                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1369                }
1370                "__spg_pg_sequences" => {
1371                    let (schema, rows) =
1372                        crate::system_catalog::synth_pg_sequences(self.active_catalog());
1373                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1374                }
1375                "__spg_pg_range" => {
1376                    let (schema, rows) = crate::system_catalog::synth_pg_range();
1377                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1378                }
1379                "__spg_pg_partitioned_table" => {
1380                    let (schema, rows) =
1381                        crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1382                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1383                }
1384                "__spg_pg_authid" => {
1385                    let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1386                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1387                }
1388                "__spg_pg_group" => {
1389                    let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1390                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1391                }
1392                "__spg_pg_shadow" => {
1393                    let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1394                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1395                }
1396                // v7.39 (round 544) — pg_cast, probed from the real
1397                // cast implementation.
1398                "__spg_pg_cast" => {
1399                    let (schema, rows) = crate::system_catalog::synth_pg_cast();
1400                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1401                }
1402                // v7.39 (round 541) — an empty catalog that exists.
1403                "__spg_pg_foreign_table" => {
1404                    let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1405                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1406                }
1407                "__spg_pg_extension" => {
1408                    let (schema, rows) = synth_pg_extension();
1409                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1410                }
1411                // v7.39 (round 502) — the timezone catalogues.
1412                "__spg_pg_timezone_names" => {
1413                    let (schema, rows) = synth_pg_timezone_names(self);
1414                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1415                }
1416                "__spg_pg_timezone_abbrevs" => {
1417                    let (schema, rows) = synth_pg_timezone_abbrevs(self);
1418                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1419                }
1420                // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1421                "__spg_pg_settings" => {
1422                    let (schema, rows) = synth_pg_settings(self);
1423                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1424                }
1425                // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1426                // v7.39 (read01 round 51) — information_schema.role_table_grants
1427                // and .table_privileges. Both report the owner's seven implicit
1428                // table privileges; SPG's single role owns everything.
1429                // v7.39 (read01 round 59) — information_schema.column_privileges.
1430                "__spg_info_column_privileges" => {
1431                    let (schema, rows) =
1432                        crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1433                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1434                }
1435                "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1436                    let grantee = self.current_role().to_string();
1437                    let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1438                        self.active_catalog(),
1439                        &grantee,
1440                    );
1441                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1442                }
1443                "__spg_info_key_column_usage" => {
1444                    // v7.39.11 — the session's dialect decides the
1445                    // column list; see the synthesiser.
1446                    let mysql = self.in_mysql_dialect();
1447                    let (schema, rows) = synth_info_key_column_usage(self.active_catalog(), mysql);
1448                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1449                }
1450                // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1451                "__spg_info_referential_constraints" => {
1452                    let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1453                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1454                }
1455                // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1456                "__spg_info_statistics" => {
1457                    let (schema, rows) = synth_info_statistics(self.active_catalog());
1458                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1459                }
1460                // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1461                "__spg_info_routines" => {
1462                    let (schema, rows) = synth_info_routines();
1463                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1464                }
1465                // v7.37.24 (24.3) — information_schema.attributes.
1466                "__spg_info_attributes" => {
1467                    let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1468                        self.active_catalog(),
1469                    );
1470                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1471                }
1472                // v7.37.24 (24.2) — information_schema.domains.
1473                "__spg_info_domains" => {
1474                    let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1475                        self.active_catalog(),
1476                    );
1477                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1478                }
1479                // v7.37.24 (24.9) — information_schema.schemata.
1480                "__spg_info_schemata" => {
1481                    let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1482                        self.active_catalog(),
1483                        self.speaks_mysql,
1484                        &self.listed_database_names(),
1485                    );
1486                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1487                }
1488                // v7.37.24 (24.9) — information_schema.views.
1489                "__spg_info_views" => {
1490                    let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1491                        self.active_catalog(),
1492                    );
1493                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1494                }
1495                // v7.37.24 (24.9) — information_schema.table_constraints.
1496                "__spg_info_table_constraints" => {
1497                    let (schema, rows) =
1498                        crate::system_catalog::synth_information_schema_table_constraints(
1499                            self.active_catalog(),
1500                            self.speaks_mysql,
1501                        );
1502                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1503                }
1504                // v7.37.17 — information_schema.constraint_column_usage.
1505                "__spg_info_constraint_column_usage" => {
1506                    let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1507                        self.active_catalog(),
1508                    );
1509                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1510                }
1511                // v7.37.17 — information_schema.triggers.
1512                "__spg_info_triggers" => {
1513                    let (schema, rows) =
1514                        crate::system_catalog::synth_info_triggers(self.active_catalog());
1515                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1516                }
1517                // v7.37.17 — information_schema.check_constraints.
1518                "__spg_info_check_constraints" => {
1519                    let (schema, rows) =
1520                        crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1521                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1522                }
1523                // v7.37.17 — information_schema.sequences.
1524                "__spg_info_sequences" => {
1525                    let (schema, rows) =
1526                        crate::system_catalog::synth_info_sequences(self.active_catalog());
1527                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1528                }
1529                // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1530                "__spg_mysql_user" => {
1531                    let (schema, rows) = synth_mysql_user(self);
1532                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1533                }
1534                "__spg_mysql_db" => {
1535                    let (schema, rows) = synth_mysql_db();
1536                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1537                }
1538                // v7.39 (round 541) — the catalogs PG has that SPG is
1539                // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1540                other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1541                    let (schema, rows) =
1542                        crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1543                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1544                }
1545                _ => {
1546                    return Err(EngineError::Unsupported(alloc::format!(
1547                        "meta view {view:?} is not yet materialisable; \
1548                         v7.16.2 covers information_schema.columns / .tables \
1549                         and pg_catalog.pg_class / pg_attribute; \
1550                         v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1551                         pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1552                         pg_user / pg_views / pg_matviews / pg_settings"
1553                    )));
1554                }
1555            }
1556        }
1557        Ok(catalog)
1558    }
1559
1560    pub(crate) fn exec_with_ctes(
1561        &self,
1562        stmt: &SelectStatement,
1563        cancel: CancelToken<'_>,
1564    ) -> Result<QueryResult, EngineError> {
1565        cancel.check()?;
1566        // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1567        // bodies are supported here. Writable CTEs on a SELECT
1568        // outer require `&mut self` and route through the
1569        // top-level `exec_select_cancel_mut` entry; sentori
1570        // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1571        // INSERT, not a SELECT, so this restriction is harmless
1572        // in practice.
1573        if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1574            // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1575            // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1576            // of a statement, not nested inside a subquery; this path is
1577            // reached exactly when one is nested. The old text described SPG's
1578            // own executor plumbing ("the top-level mutable entry"), which
1579            // means nothing to a client.
1580            return Err(EngineError::Unsupported(
1581                "WITH clause containing a data-modifying statement must be at the top level".into(),
1582            ));
1583        }
1584        let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1585        // Strip CTEs from the body before running on the temp engine
1586        // so we don't recurse forever.
1587        let mut body = stmt.clone();
1588        body.ctes = Vec::new();
1589        let mut temp = Engine::restore(catalog);
1590        if let Some(c) = self.clock {
1591            temp = temp.with_clock(c);
1592        }
1593        if let Some(f) = self.salt_fn {
1594            temp = temp.with_salt_fn(f);
1595        }
1596        temp.exec_select_cancel(&body, cancel)
1597    }
1598
1599    /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1600    /// `&self` SELECT path. Caller guarantees no modifying CTE
1601    /// bodies are present.
1602    pub(crate) fn materialise_ctes_readonly(
1603        &self,
1604        ctes: &[spg_sql::ast::Cte],
1605        cancel: CancelToken<'_>,
1606    ) -> Result<crate::Catalog, EngineError> {
1607        cancel.check()?;
1608        let mut catalog = self.active_catalog().clone();
1609        for cte in ctes {
1610            let body_select = cte.body.as_select().ok_or_else(|| {
1611                EngineError::Unsupported(alloc::format!(
1612                    "data-modifying CTE not supported on this SELECT entry"
1613                ))
1614            })?;
1615            // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1616            // (PG scoping: the WITH name wins for the outer query and later
1617            // CTEs, while THIS body still sees the real table — a
1618            // non-recursive body's self-name is the table, probe P2). This
1619            // materialiser works on a CLONE, so the shadow is simply: run
1620            // the body against the untouched clone, then drop the real
1621            // table from the clone before installing the CTE's temp. A
1622            // RECURSIVE self-reference is the CTE itself (P6), so there the
1623            // drop happens before the iterating materialiser runs.
1624            let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1625                let synthetic = spg_sql::ast::Cte {
1626                    name: cte.name.clone(),
1627                    body: spg_sql::ast::CteBody::Select(body_select.clone()),
1628                    recursive: true,
1629                    column_overrides: cte.column_overrides.clone(),
1630                    search: None,
1631                    cycle: None,
1632                };
1633                if catalog.get(&cte.name).is_some() {
1634                    let _ = catalog.drop_table(&cte.name);
1635                }
1636                self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1637            } else {
1638                let mut cte_engine = Engine::restore(catalog.clone());
1639                if let Some(c) = self.clock {
1640                    cte_engine = cte_engine.with_clock(c);
1641                }
1642                if let Some(f) = self.salt_fn {
1643                    cte_engine = cte_engine.with_salt_fn(f);
1644                }
1645                let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1646                let QueryResult::Rows { columns, rows } = body_result else {
1647                    return Err(EngineError::Unsupported(alloc::format!(
1648                        "CTE {:?} body did not return rows",
1649                        cte.name
1650                    )));
1651                };
1652                (columns, rows)
1653            };
1654            let inferred = infer_column_types(&columns, &rows);
1655            let mut columns = inferred;
1656            if !cte.column_overrides.is_empty() {
1657                if cte.column_overrides.len() != columns.len() {
1658                    return Err(EngineError::Unsupported(alloc::format!(
1659                        "CTE {:?} column list has {} names but body returns {} columns",
1660                        cte.name,
1661                        cte.column_overrides.len(),
1662                        columns.len()
1663                    )));
1664                }
1665                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1666                    col.name.clone_from(name);
1667                }
1668            }
1669            let schema = TableSchema::new(cte.name.clone(), columns);
1670            // v7.39 (round 156) — the body ran against the untouched clone;
1671            // from here on the CTE name resolves to the temp (PG scoping).
1672            if catalog.get(&cte.name).is_some() {
1673                let _ = catalog.drop_table(&cte.name);
1674            }
1675            catalog.create_table(schema).map_err(EngineError::Storage)?;
1676            let table = catalog
1677                .get_mut(&cte.name)
1678                .expect("just-created CTE table must exist");
1679            for row in rows {
1680                table.insert(row).map_err(EngineError::Storage)?;
1681            }
1682        }
1683        Ok(catalog)
1684    }
1685
1686    /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1687    /// Retained for non-DML callers; the DML path (writable CTE on
1688    /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1689    /// `dml.rs` which installs the CTE temps directly on the
1690    /// active catalog so the outer statement's writes hit real
1691    /// tables.
1692    #[allow(dead_code)]
1693    pub(crate) fn materialise_ctes(
1694        &mut self,
1695        ctes: &[spg_sql::ast::Cte],
1696        cancel: CancelToken<'_>,
1697    ) -> Result<crate::Catalog, EngineError> {
1698        cancel.check()?;
1699        // v7.37.43-T4.4 — modifying CTEs need to write through the
1700        // SAME catalog as the outer statement, not a clone (PG's
1701        // writable CTE puts all modifications in one transaction).
1702        // For the read-only case the original logic cloned, but
1703        // since the outer statement also goes through the cloned
1704        // engine and ALL writes must converge, we now drive the
1705        // accumulator off `self.active_catalog().clone()` and
1706        // commit the modifying writes directly to `self`'s active
1707        // catalog so the surface is consistent.
1708        let mut catalog = self.active_catalog().clone();
1709        // v7.39 (round 149) — a modifying CTE body's target must be a
1710        // real relation, never a sibling CTE (PG: relation does not
1711        // exist); checked before any alias lands in the accumulator.
1712        for cte in ctes {
1713            let body_target = match &cte.body {
1714                spg_sql::ast::CteBody::Select(_) => None,
1715                spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1716                spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1717                spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1718                spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1719            };
1720            if let Some(t) = body_target
1721                && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1722                && catalog.get(t).is_none()
1723            {
1724                return Err(EngineError::Storage(
1725                    spg_storage::StorageError::TableNotFound { name: t.into() },
1726                ));
1727            }
1728        }
1729        for cte in ctes {
1730            if catalog.get(&cte.name).is_some() {
1731                return Err(EngineError::Unsupported(alloc::format!(
1732                    "CTE name {:?} shadows an existing table; rename the CTE",
1733                    cte.name
1734                )));
1735            }
1736            let (columns, rows) = match &cte.body {
1737                // v7.39 (round 145) — see the sibling site: only a body that
1738                // truly self-references takes the iterating materialiser.
1739                spg_sql::ast::CteBody::Select(body)
1740                    if cte.recursive && select_refers_to(body, &cte.name) =>
1741                {
1742                    // Recursive CTE — the existing helper takes a
1743                    // SELECT body and the snapshot catalog.
1744                    let synthetic = spg_sql::ast::Cte {
1745                        name: cte.name.clone(),
1746                        body: spg_sql::ast::CteBody::Select(body.clone()),
1747                        recursive: true,
1748                        column_overrides: cte.column_overrides.clone(),
1749                        search: None,
1750                        cycle: None,
1751                    };
1752                    self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1753                }
1754                spg_sql::ast::CteBody::Select(body) => {
1755                    // v7.25 (round-17) — run against the accumulated
1756                    // catalog so later CTEs can reference earlier
1757                    // ones in the same WITH clause.
1758                    let mut cte_engine = Engine::restore(catalog.clone());
1759                    if let Some(c) = self.clock {
1760                        cte_engine = cte_engine.with_clock(c);
1761                    }
1762                    if let Some(f) = self.salt_fn {
1763                        cte_engine = cte_engine.with_salt_fn(f);
1764                    }
1765                    let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1766                    let QueryResult::Rows { columns, rows } = body_result else {
1767                        return Err(EngineError::Unsupported(alloc::format!(
1768                            "CTE {:?} body did not return rows",
1769                            cte.name
1770                        )));
1771                    };
1772                    (columns, rows)
1773                }
1774                spg_sql::ast::CteBody::Insert(body) => {
1775                    self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1776                }
1777                spg_sql::ast::CteBody::Update(body) => {
1778                    self.exec_modifying_cte_update(&cte.name, body, cancel)?
1779                }
1780                spg_sql::ast::CteBody::Delete(body) => {
1781                    self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1782                }
1783                spg_sql::ast::CteBody::Merge(body) => {
1784                    self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1785                }
1786            };
1787            // v4.22: the projection builder labels any non-column
1788            // expression as Text — including literal SELECT 1.
1789            // Promote each column's type to whatever the rows
1790            // actually carry so the CTE storage table accepts them.
1791            let inferred = infer_column_types(&columns, &rows);
1792            let mut columns = inferred;
1793            if !cte.column_overrides.is_empty() {
1794                if cte.column_overrides.len() != columns.len() {
1795                    return Err(EngineError::Unsupported(alloc::format!(
1796                        "CTE {:?} column list has {} names but body returns {} columns",
1797                        cte.name,
1798                        cte.column_overrides.len(),
1799                        columns.len()
1800                    )));
1801                }
1802                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1803                    col.name.clone_from(name);
1804                }
1805            }
1806            let schema = TableSchema::new(cte.name.clone(), columns);
1807            catalog.create_table(schema).map_err(EngineError::Storage)?;
1808            let table = catalog
1809                .get_mut(&cte.name)
1810                .expect("just-created CTE table must exist");
1811            for row in rows {
1812                table.insert(row).map_err(EngineError::Storage)?;
1813            }
1814        }
1815        Ok(catalog)
1816    }
1817
1818    /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1819    /// against `self` (so the mutation lands in the active catalog
1820    /// inside the current transaction) and captures the RETURNING
1821    /// projection — column schema + rows — to materialise as the
1822    /// CTE alias's table. An INSERT without RETURNING produces a
1823    /// 0-row table with a synthetic single-column placeholder
1824    /// (matches PG: the CTE alias is still defined, but referencing
1825    /// it from the outer query without RETURNING raises a
1826    /// column-resolution error at scan time).
1827    fn exec_modifying_cte_insert(
1828        &mut self,
1829        cte_name: &str,
1830        body: &spg_sql::ast::InsertStatement,
1831        _cancel: CancelToken<'_>,
1832    ) -> Result<
1833        (
1834            Vec<spg_storage::ColumnSchema>,
1835            Vec<spg_storage::Row<'static>>,
1836        ),
1837        EngineError,
1838    > {
1839        // round 151 — a WITH-headed body keeps its own ctes; the body
1840        // statement routes through its writable-CTE entry (outer CTEs
1841        // are never copied into bodies, so no recursion risk).
1842        let body = body.clone();
1843        let result = self.exec_insert(body)?;
1844        match result {
1845            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1846            QueryResult::CommandOk { .. } => {
1847                // No RETURNING — emit a sentinel single-column
1848                // schema with zero rows so the alias is defined.
1849                let placeholder = spg_storage::ColumnSchema::new(
1850                    alloc::format!("{cte_name}_returning_absent"),
1851                    spg_storage::DataType::Text,
1852                    true,
1853                );
1854                Ok((alloc::vec![placeholder], Vec::new()))
1855            }
1856        }
1857    }
1858
1859    /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1860    /// as INSERT above.
1861    fn exec_modifying_cte_update(
1862        &mut self,
1863        cte_name: &str,
1864        body: &spg_sql::ast::UpdateStatement,
1865        cancel: CancelToken<'_>,
1866    ) -> Result<
1867        (
1868            Vec<spg_storage::ColumnSchema>,
1869            Vec<spg_storage::Row<'static>>,
1870        ),
1871        EngineError,
1872    > {
1873        let body = body.clone();
1874        let result = self.exec_update_cancel(&body, cancel)?;
1875        match result {
1876            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1877            QueryResult::CommandOk { .. } => {
1878                let placeholder = spg_storage::ColumnSchema::new(
1879                    alloc::format!("{cte_name}_returning_absent"),
1880                    spg_storage::DataType::Text,
1881                    true,
1882                );
1883                Ok((alloc::vec![placeholder], Vec::new()))
1884            }
1885        }
1886    }
1887
1888    /// v7.37.43-T4.4 — execute a DELETE CTE body.
1889    fn exec_modifying_cte_delete(
1890        &mut self,
1891        cte_name: &str,
1892        body: &spg_sql::ast::DeleteStatement,
1893        cancel: CancelToken<'_>,
1894    ) -> Result<
1895        (
1896            Vec<spg_storage::ColumnSchema>,
1897            Vec<spg_storage::Row<'static>>,
1898        ),
1899        EngineError,
1900    > {
1901        let body = body.clone();
1902        let result = self.exec_delete_cancel(&body, cancel)?;
1903        match result {
1904            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1905            QueryResult::CommandOk { .. } => {
1906                let placeholder = spg_storage::ColumnSchema::new(
1907                    alloc::format!("{cte_name}_returning_absent"),
1908                    spg_storage::DataType::Text,
1909                    true,
1910                );
1911                Ok((alloc::vec![placeholder], Vec::new()))
1912            }
1913        }
1914    }
1915
1916    /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1917    fn exec_modifying_cte_merge(
1918        &mut self,
1919        cte_name: &str,
1920        body: &spg_sql::ast::MergeStatement,
1921        cancel: CancelToken<'_>,
1922    ) -> Result<
1923        (
1924            Vec<spg_storage::ColumnSchema>,
1925            Vec<spg_storage::Row<'static>>,
1926        ),
1927        EngineError,
1928    > {
1929        let body = body.clone();
1930        let result = self.exec_merge_cancel(&body, cancel)?;
1931        match result {
1932            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1933            QueryResult::CommandOk { .. } => {
1934                let placeholder = spg_storage::ColumnSchema::new(
1935                    alloc::format!("{cte_name}_returning_absent"),
1936                    spg_storage::DataType::Text,
1937                    true,
1938                );
1939                Ok((alloc::vec![placeholder], Vec::new()))
1940            }
1941        }
1942    }
1943
1944    /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1945    /// UNION (or UNION ALL) of an anchor that does not reference
1946    /// the CTE name, and one or more recursive terms that do. The
1947    /// anchor runs first; each subsequent iteration runs the
1948    /// recursive term against a temp catalog where the CTE name is
1949    /// bound to the *previous* iteration's output. Iteration stops
1950    /// when the recursive term yields no rows; UNION (DISTINCT)
1951    /// deduplicates against the accumulated result, UNION ALL does
1952    /// not. A hard cap on total rows prevents runaway queries.
1953    #[allow(clippy::too_many_lines)]
1954    pub(crate) fn materialise_recursive_cte(
1955        &self,
1956        cte: &spg_sql::ast::Cte,
1957        base_catalog: &Catalog,
1958        cancel: CancelToken<'_>,
1959    ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1960        const MAX_TOTAL_ROWS: usize = 1_000_000;
1961        const MAX_ITERATIONS: usize = 100_000;
1962        cancel.check()?;
1963        // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1964        // a modifying recursive CTE is parser-rejectable but we
1965        // guard here defensively.
1966        let body_select = cte.body.as_select().ok_or_else(|| {
1967            EngineError::Unsupported(alloc::format!(
1968                "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1969                cte.name
1970            ))
1971        })?;
1972        if body_select.unions.is_empty() {
1973            return Err(EngineError::Unsupported(alloc::format!(
1974                "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1975                cte.name
1976            )));
1977        }
1978        // Anchor: the body's leading SELECT, with unions stripped.
1979        let mut anchor = body_select.clone();
1980        let all_union_terms = core::mem::take(&mut anchor.unions);
1981        anchor.ctes = Vec::new();
1982        // v7.37 D.42 — split the UNION members: those that do NOT reference the
1983        // CTE are additional ANCHOR terms, only the ones that do recurse. A
1984        // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1985        // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1986        // treating the non-recursive `SELECT r2` as a recursive term made it
1987        // re-emit its constant row every iteration → runaway loop.
1988        let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1989            .into_iter()
1990            .partition(|(_, t)| !select_refers_to(t, &cte.name));
1991        let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1992        let QueryResult::Rows {
1993            columns: anchor_cols,
1994            rows: mut anchor_rows,
1995        } = anchor_result
1996        else {
1997            return Err(EngineError::Unsupported(alloc::format!(
1998                "WITH RECURSIVE {:?}: anchor did not return rows",
1999                cte.name
2000            )));
2001        };
2002        // Append every non-recursive UNION member's rows to the anchor set.
2003        for (_, term) in &anchor_terms {
2004            let mut term = term.clone();
2005            term.ctes = Vec::new();
2006            if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
2007                anchor_rows.extend(rows);
2008            }
2009        }
2010        // The projection builder labels non-column expressions Text;
2011        // refine column types from the anchor's actual values so the
2012        // intermediate iter-catalog tables accept them.
2013        let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
2014        if !cte.column_overrides.is_empty() {
2015            if cte.column_overrides.len() != columns.len() {
2016                return Err(EngineError::Unsupported(alloc::format!(
2017                    "CTE {:?} column list has {} names but anchor returns {} columns",
2018                    cte.name,
2019                    cte.column_overrides.len(),
2020                    columns.len()
2021                )));
2022            }
2023            for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
2024                col.name.clone_from(name);
2025            }
2026        }
2027        let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
2028        let mut working_set: Vec<Row<'static>> = anchor_rows;
2029        let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
2030        // Track at least one "all UNION ALL" flag — if every union
2031        // kind is ALL we skip the dedup step (faster + matches PG).
2032        let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
2033        if !all_union_all {
2034            for r in &all_rows {
2035                seen.insert(encode_row_key(r));
2036            }
2037        }
2038        // v7.39 (round 598) — the engine and its catalog are built ONCE.
2039        // Each iteration used to clone the catalog, create the CTE table,
2040        // and construct a whole `Engine` — which initialises 82 fields — to
2041        // hold that round's working set. A counting allocator put the loop
2042        // at 63 allocations and 104 kB per iteration, or 1 GB for a
2043        // 10,000-row recursive CTE, and none of it varied with how much
2044        // else was in the catalog: the per-round rebuild WAS the cost. The
2045        // table is emptied and refilled instead.
2046        let mut iter_catalog = base_catalog.clone();
2047        let schema = TableSchema::new(cte.name.clone(), columns.clone());
2048        iter_catalog
2049            .create_table(schema)
2050            .map_err(EngineError::Storage)?;
2051        let mut iter_engine = Engine::restore(iter_catalog);
2052        if let Some(c) = self.clock {
2053            iter_engine = iter_engine.with_clock(c);
2054        }
2055        if let Some(f) = self.salt_fn {
2056            iter_engine = iter_engine.with_salt_fn(f);
2057        }
2058        // The recursive terms are cloned once too — the clone stripped the
2059        // CTE list off each of them, per term per iteration.
2060        let recursive_terms: Vec<SelectStatement> = union_terms
2061            .iter()
2062            .map(|(_, t)| {
2063                let mut t = t.clone();
2064                t.ctes = Vec::new();
2065                t
2066            })
2067            .collect();
2068        // v7.39 (round 618) — plan every recursive term once. Taken only if
2069        // ALL of them plan, so a query never runs half on each path.
2070        let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2071            .iter()
2072            .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2073            .collect();
2074        let fast_ctx = term_plans.as_ref().map(|plans| {
2075            let alias = plans[0].alias.clone();
2076            (alias, ())
2077        });
2078        for iter in 0..MAX_ITERATIONS {
2079            cancel.check()?;
2080            if working_set.is_empty() {
2081                break;
2082            }
2083            if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2084                // The worktable IS the working set: no table to empty and
2085                // refill, and no query execution per round.
2086                let mut next_set: Vec<Row<'static>> = Vec::new();
2087                for plan in plans {
2088                    let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2089                    for row in &working_set {
2090                        cancel.check()?;
2091                        if let Some(w) = plan.where_ {
2092                            let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2093                            if !matches!(v, Value::Bool(true)) {
2094                                continue;
2095                            }
2096                        }
2097                        let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2098                        for it in &plan.items {
2099                            vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2100                        }
2101                        let out = Row::new(vals);
2102                        if !all_union_all {
2103                            let key = encode_row_key(&out);
2104                            if !seen.insert(key) {
2105                                continue;
2106                            }
2107                        }
2108                        next_set.push(out);
2109                    }
2110                }
2111                if next_set.is_empty() {
2112                    break;
2113                }
2114                all_rows.extend(next_set.iter().cloned());
2115                working_set = next_set;
2116                if all_rows.len() > MAX_TOTAL_ROWS {
2117                    return Err(EngineError::Unsupported(alloc::format!(
2118                        "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2119                        cte.name
2120                    )));
2121                }
2122                if iter + 1 == MAX_ITERATIONS {
2123                    return Err(EngineError::Unsupported(alloc::format!(
2124                        "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2125                        cte.name
2126                    )));
2127                }
2128                continue;
2129            }
2130            {
2131                // Truncated rather than dropped and recreated: the table's
2132                // own structure is what dropping it throws away, and it is
2133                // identical every round.
2134                let cat = iter_engine.base_catalog_mut();
2135                let table = cat.get_mut(&cte.name).expect("created above");
2136                table.truncate();
2137                for row in &working_set {
2138                    table.insert(row.clone()).map_err(EngineError::Storage)?;
2139                }
2140            }
2141            // Run each recursive term in sequence and collect new rows.
2142            let mut next_set: Vec<Row<'static>> = Vec::new();
2143            for term in &recursive_terms {
2144                let r = iter_engine.exec_select_cancel(term, cancel)?;
2145                let QueryResult::Rows {
2146                    columns: rc,
2147                    rows: rs,
2148                } = r
2149                else {
2150                    return Err(EngineError::Unsupported(alloc::format!(
2151                        "WITH RECURSIVE {:?}: recursive term did not return rows",
2152                        cte.name
2153                    )));
2154                };
2155                if rc.len() != columns.len() {
2156                    return Err(EngineError::Unsupported(alloc::format!(
2157                        "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2158                        cte.name,
2159                        rc.len(),
2160                        columns.len()
2161                    )));
2162                }
2163                for row in rs {
2164                    if !all_union_all {
2165                        let key = encode_row_key(&row);
2166                        if !seen.insert(key) {
2167                            continue;
2168                        }
2169                    }
2170                    next_set.push(row);
2171                }
2172            }
2173            if next_set.is_empty() {
2174                break;
2175            }
2176            all_rows.extend(next_set.iter().cloned());
2177            working_set = next_set;
2178            if all_rows.len() > MAX_TOTAL_ROWS {
2179                return Err(EngineError::Unsupported(alloc::format!(
2180                    "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2181                    cte.name
2182                )));
2183            }
2184            if iter + 1 == MAX_ITERATIONS {
2185                return Err(EngineError::Unsupported(alloc::format!(
2186                    "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2187                    cte.name
2188                )));
2189            }
2190        }
2191        Ok((columns, all_rows))
2192    }
2193
2194    pub(crate) fn resolve_select_subqueries(
2195        &self,
2196        stmt: &mut SelectStatement,
2197        cancel: CancelToken<'_>,
2198    ) -> Result<(), EngineError> {
2199        for item in &mut stmt.items {
2200            if let SelectItem::Expr { expr, alias } = item {
2201                // An UNCORRELATED subquery is replaced by its value right
2202                // here, and the shape the column was named for goes with
2203                // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2204                // boolean literal, so SPG answered `?column?` where PG18
2205                // answers `exists`. Only a subquery at the TOP of the item
2206                // loses its name this way — one nested inside a call still
2207                // reports the call.
2208                if alias.is_none()
2209                    && matches!(
2210                        expr,
2211                        Expr::ScalarSubquery(_)
2212                            | Expr::Exists { .. }
2213                            | Expr::InSubquery { .. }
2214                            | Expr::RowInSubquery { .. }
2215                            | Expr::RowCmpSubquery { .. }
2216                    )
2217                {
2218                    *alias = Some(default_output_name(expr, self.speaks_mysql));
2219                }
2220                self.resolve_expr_subqueries(expr, cancel)?;
2221            }
2222        }
2223        if let Some(w) = &mut stmt.where_ {
2224            self.resolve_expr_subqueries(w, cancel)?;
2225        }
2226        // v7.24.1 — JOIN ON conditions can carry subqueries too;
2227        // they were never walked, so even an UNCORRELATED subquery
2228        // in ON hit "subquery reached row eval".
2229        if let Some(from) = &mut stmt.from {
2230            for j in &mut from.joins {
2231                if let Some(on) = &mut j.on {
2232                    self.resolve_expr_subqueries(on, cancel)?;
2233                }
2234            }
2235        }
2236        if let Some(gs) = &mut stmt.group_by {
2237            for g in gs {
2238                self.resolve_expr_subqueries(g, cancel)?;
2239            }
2240        }
2241        if let Some(h) = &mut stmt.having {
2242            self.resolve_expr_subqueries(h, cancel)?;
2243        }
2244        for o in &mut stmt.order_by {
2245            self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2246        }
2247        for (_, peer) in &mut stmt.unions {
2248            self.resolve_select_subqueries(peer, cancel)?;
2249        }
2250        Ok(())
2251    }
2252
2253    #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2254    pub(crate) fn resolve_expr_subqueries(
2255        &self,
2256        e: &mut Expr,
2257        cancel: CancelToken<'_>,
2258    ) -> Result<(), EngineError> {
2259        // Replace-on-this-node cases first.
2260        if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2261            *e = replacement;
2262            return Ok(());
2263        }
2264        match e {
2265            Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } => {
2266                self.resolve_expr_subqueries(expr, cancel)?
2267            }
2268            Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2269            Expr::AggregateOrdered { call, order_by, .. } => {
2270                self.resolve_expr_subqueries(call, cancel)?;
2271                for o in order_by.iter_mut() {
2272                    self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2273                }
2274            }
2275            Expr::Binary { lhs, rhs, .. } => {
2276                self.resolve_expr_subqueries(lhs, cancel)?;
2277                self.resolve_expr_subqueries(rhs, cancel)?;
2278            }
2279            Expr::Unary { expr, .. }
2280            | Expr::Cast { expr, .. }
2281            | Expr::IsNull { expr, .. }
2282            | Expr::BoolTest { expr, .. }
2283            | Expr::FieldAccess { base: expr, .. } => {
2284                self.resolve_expr_subqueries(expr, cancel)?;
2285            }
2286            Expr::FunctionCall { args, .. } => {
2287                for a in args {
2288                    self.resolve_expr_subqueries(a, cancel)?;
2289                }
2290            }
2291            Expr::Like { expr, pattern, .. } => {
2292                self.resolve_expr_subqueries(expr, cancel)?;
2293                self.resolve_expr_subqueries(pattern, cancel)?;
2294            }
2295            Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2296            // v4.12 window functions — recurse into args + ORDER BY
2297            // + PARTITION BY in case they carry inner subqueries.
2298            Expr::WindowFunction {
2299                args,
2300                partition_by,
2301                order_by,
2302                ..
2303            } => {
2304                for a in args {
2305                    self.resolve_expr_subqueries(a, cancel)?;
2306                }
2307                for p in partition_by {
2308                    self.resolve_expr_subqueries(p, cancel)?;
2309                }
2310                for (e, _, _) in order_by {
2311                    self.resolve_expr_subqueries(e, cancel)?;
2312                }
2313            }
2314            // Subquery nodes are handled in subquery_replacement
2315            // (which returned None — defensive no-op); Literal /
2316            // Column are leaves.
2317            Expr::ScalarSubquery(_)
2318            | Expr::Exists { .. }
2319            | Expr::InSubquery { .. }
2320            | Expr::RowInSubquery { .. }
2321            | Expr::RowCmpSubquery { .. }
2322            | Expr::Literal(_)
2323            | Expr::Placeholder(_)
2324            | Expr::Column(_) => {}
2325            // v7.30.2 — list elements can carry scalar subqueries
2326            // (`x IN (1, (SELECT …))`).
2327            Expr::InList { expr, list, .. } => {
2328                self.resolve_expr_subqueries(expr, cancel)?;
2329                for item in list {
2330                    self.resolve_expr_subqueries(item, cancel)?;
2331                }
2332            }
2333            // v7.10.10 — recurse children.
2334            Expr::Array(items) => {
2335                for elem in items {
2336                    self.resolve_expr_subqueries(elem, cancel)?;
2337                }
2338            }
2339            Expr::ArraySubscript { target, index } => {
2340                self.resolve_expr_subqueries(target, cancel)?;
2341                self.resolve_expr_subqueries(index, cancel)?;
2342            }
2343            Expr::ArraySlice { target, lo, hi } => {
2344                self.resolve_expr_subqueries(target, cancel)?;
2345                if let Some(l) = lo {
2346                    self.resolve_expr_subqueries(l, cancel)?;
2347                }
2348                if let Some(h) = hi {
2349                    self.resolve_expr_subqueries(h, cancel)?;
2350                }
2351            }
2352            Expr::AnyAll { expr, array, .. } => {
2353                self.resolve_expr_subqueries(expr, cancel)?;
2354                // Quantified subquery — an uncorrelated one
2355                // materialises up front; a correlated one stays for
2356                // the per-row resolver.
2357                if let Expr::ScalarSubquery(inner) = array.as_mut() {
2358                    if !crate::subquery::select_is_correlated(inner) {
2359                        let s = (**inner).clone();
2360                        **array = self.materialize_quantified_rows(&s, cancel)?;
2361                    }
2362                } else {
2363                    self.resolve_expr_subqueries(array, cancel)?;
2364                }
2365            }
2366            Expr::Case {
2367                operand,
2368                branches,
2369                else_branch,
2370            } => {
2371                if let Some(o) = operand {
2372                    self.resolve_expr_subqueries(o, cancel)?;
2373                }
2374                for (w, t) in branches {
2375                    self.resolve_expr_subqueries(w, cancel)?;
2376                    self.resolve_expr_subqueries(t, cancel)?;
2377                }
2378                if let Some(e) = else_branch {
2379                    self.resolve_expr_subqueries(e, cancel)?;
2380                }
2381            }
2382        }
2383        Ok(())
2384    }
2385}
2386
2387impl Engine {
2388    /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2389    /// `SelectItem::Wildcard` to all schema columns and
2390    /// `SelectItem::Expr` via the regular eval path.
2391    pub(crate) fn project_row_simple(
2392        &self,
2393        row: &Row<'static>,
2394        items: &[SelectItem],
2395        schema_cols: &[ColumnSchema],
2396        alias: &str,
2397    ) -> Result<Row<'static>, EngineError> {
2398        let ctx = self.ev_ctx(schema_cols, Some(alias));
2399        let cancel = CancelToken::none();
2400        let mut out_vals = Vec::new();
2401        for item in items {
2402            match item {
2403                // In a single-table projection (AS OF SEGMENT / RETURNING) a
2404                // qualified `t.*` covers exactly the same columns as a bare `*`.
2405                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2406                    out_vals.extend(row.values.iter().cloned());
2407                }
2408                SelectItem::Expr { expr, .. } => {
2409                    let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2410                    out_vals.push(v);
2411                }
2412            }
2413        }
2414        Ok(Row::new(out_vals))
2415    }
2416
2417    /// v6.10.2 — derive the output `ColumnSchema` list for an
2418    /// AS OF SEGMENT projection. Wildcards take the full schema;
2419    /// expressions take the alias if present or a synthetic
2420    /// `?column?` (PG convention) otherwise.
2421    pub(crate) fn derive_output_columns(
2422        &self,
2423        items: &[SelectItem],
2424        schema_cols: &[ColumnSchema],
2425        table_alias: &str,
2426    ) -> Vec<ColumnSchema> {
2427        let mut out = Vec::new();
2428        for item in items {
2429            match item {
2430                // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2431                // a single-table projection.
2432                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2433                    out.extend(schema_cols.iter().cloned());
2434                }
2435                SelectItem::Expr { expr, alias } => {
2436                    // Bare column references inherit the schema
2437                    // column's name + type — PG names `RETURNING id`
2438                    // "id" and types it BIGINT, and the sqlx embed
2439                    // path type-checks RowDescription against the
2440                    // Rust target (mailrs embed round-12).
2441                    if let Expr::Column(col) = expr
2442                        && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2443                    {
2444                        let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2445                        let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2446                        // v7.39 (read01 round 54) — carry the enum identity:
2447                        // it lives outside the DataType lattice, so a derived
2448                        // table built from this schema otherwise forgets it and
2449                        // the OUTER `ORDER BY <enum col>` silently sorts by the
2450                        // label's TEXT instead of member order.
2451                        c.user_enum_type = sc.user_enum_type.clone();
2452                        out.push(c);
2453                        continue;
2454                    }
2455                    let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2456                    // v7.30.4 (mailrs round-27, P0) — type the
2457                    // expression with the same inference the SELECT
2458                    // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2459                    // The old Text default broke every typed decode
2460                    // of `RETURNING uidnext - 1 AS uid`: four days
2461                    // of inbound mail indexed nowhere. Inference
2462                    // failure keeps the old Text fallback rather
2463                    // than inventing new error paths here.
2464                    // v7.39 (round 258) — take the enum identity from the
2465                    // same projection build, not just the type: a constant
2466                    // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2467                    // VALUES row lowers to) is an EXPRESSION, so it landed
2468                    // here and the derived table forgot the enum.
2469                    let (ty, nullable) = build_projection(
2470                        core::slice::from_ref(item),
2471                        schema_cols,
2472                        table_alias,
2473                        self.speaks_mysql,
2474                        Some(self.active_catalog()),
2475                    )
2476                    .ok()
2477                    .and_then(|p| p.into_iter().next())
2478                    .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2479                    out.push(ColumnSchema::new(name, ty, nullable));
2480                }
2481            }
2482        }
2483        out
2484    }
2485
2486    /// v4.5: SELECT with cooperative cancellation. The token is
2487    /// honoured between UNION peers and inside the bare-SELECT row
2488    /// loop; HNSW kNN graph walks and the aggregate executor don't
2489    /// honour it yet (deferred — those paths bound their work
2490    /// internally by `LIMIT k` and `GROUP BY` cardinality).
2491    /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2492    /// its (lowercased) name, or None if the name isn't a virtual view.
2493    /// Callers decide whether to return it directly (`SELECT *`) or stage
2494    /// it as a temp table for the full query pipeline.
2495    fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2496        Some(match name {
2497            "spg_statistic" => self.exec_spg_statistic(),
2498            "spg_stat_replication" => self.exec_spg_stat_replication(),
2499            "spg_stat_segment" => self.exec_spg_stat_segment(),
2500            "spg_memory_stats" => self.exec_spg_memory_stats(),
2501            "spg_stat_query" => self.exec_spg_stat_query(),
2502            "pg_stat_statements" => self.exec_pg_stat_statements(),
2503            "spg_stat_activity" => self.exec_spg_stat_activity(),
2504            "pg_stat_activity" => self.exec_pg_stat_activity(),
2505            "pg_locks" => self.exec_pg_locks(),
2506            "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2507            "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2508            "spg_partition_health" => self.exec_spg_partition_health(),
2509            "spg_audit_chain" => self.exec_spg_audit_chain(),
2510            "spg_audit_verify" => self.exec_spg_audit_verify(),
2511            "spg_table_ddl" => self.exec_spg_table_ddl(),
2512            "spg_role_ddl" => self.exec_spg_role_ddl(),
2513            "spg_database_ddl" => self.exec_spg_database_ddl(),
2514            _ => return None,
2515        })
2516    }
2517
2518    /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2519    /// describes against: this engine's catalog with the view staged as a
2520    /// table, exactly as `exec_select_cancel_as` stages it for a
2521    /// non-bare query.
2522    ///
2523    /// These views never reach the catalog — each is a fixed row set built
2524    /// inside its own `exec_*` — so Describe reported no columns for all
2525    /// seventeen of them. Rows are deliberately not inserted: Describe
2526    /// only needs the shape, and `infer_column_types` reads the rows we
2527    /// already have in hand.
2528    pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2529        let from = stmt.from.as_ref()?;
2530        if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2531            return None;
2532        }
2533        let lower = from.primary.name.to_ascii_lowercase();
2534        let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2535            return None;
2536        };
2537        let mut catalog = self.active_catalog().clone();
2538        let cols = infer_column_types(&columns, &rows);
2539        catalog
2540            .create_table(TableSchema::new(from.primary.name.clone(), cols))
2541            .ok()?;
2542        Some(catalog)
2543    }
2544
2545    pub(crate) fn exec_select_cancel(
2546        &self,
2547        stmt: &SelectStatement,
2548        cancel: CancelToken<'_>,
2549    ) -> Result<QueryResult, EngineError> {
2550        self.exec_select_cancel_as(stmt, cancel, None)
2551    }
2552
2553    /// v7.39 (round 334, V55) — the same read core, authorised as
2554    /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2555    /// function's OWNER: that is the entire point of the form, and without
2556    /// it every definer function failed with "permission denied" on the
2557    /// very table it exists to expose.
2558    /// v7.39 (round 559) — see the call site. `None` for anything but
2559    /// the bare shape, so every other query keeps its old path.
2560    fn try_bare_count_star(
2561        &self,
2562        stmt: &SelectStatement,
2563        as_role: Option<&str>,
2564    ) -> Result<Option<QueryResult>, EngineError> {
2565        use spg_sql::ast::SelectItem;
2566        if as_role.is_some()
2567            || !stmt.ctes.is_empty()
2568            || !stmt.unions.is_empty()
2569            || stmt.where_.is_some()
2570            || stmt.group_by.is_some()
2571            || stmt.having.is_some()
2572            || stmt.distinct
2573            || !stmt.order_by.is_empty()
2574            || stmt.limit.is_some()
2575            || stmt.offset.is_some()
2576            || stmt.items.len() != 1
2577        {
2578            return Ok(None);
2579        }
2580        let Some(from) = &stmt.from else {
2581            return Ok(None);
2582        };
2583        if !from.joins.is_empty()
2584            || stmt.locking.is_some()
2585            || from.primary.lateral_subquery.is_some()
2586            || from.primary.unnest_expr.is_some()
2587            || from.primary.generate_series_args.is_some()
2588            || from.primary.name.is_empty()
2589            || from.primary.name.starts_with("__spg_")
2590        {
2591            return Ok(None);
2592        }
2593        // A partition PARENT holds no rows of its own — they live in the
2594        // children — so its header count is 0 and the ordinary path has
2595        // to fan out. Caught by the partition conformance cases.
2596        //
2597        // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2598        // of them, which is worse: its header count is a real number,
2599        // just not the answer. `SELECT count(*) FROM par` returned 1
2600        // where PG returns 2, because this shortcut fired before the
2601        // fan-out could. The question is "does anything descend from
2602        // this", not "was it declared a partition parent".
2603        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2604            return Ok(None);
2605        }
2606        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2607            return Ok(None);
2608        };
2609        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2610            return Ok(None);
2611        };
2612        if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2613            return Ok(None);
2614        }
2615        // A row-security policy filters rows, so the header count is not
2616        // the answer; the ordinary path applies the policy.
2617        let Some(table) = self.active_catalog().get(&from.primary.name) else {
2618            return Ok(None);
2619        };
2620        if table.schema().row_security {
2621            return Ok(None);
2622        }
2623        // Rows frozen to the cold tier are not in `headers`, so the
2624        // header count would miss them. Caught by the cold-tier e2e.
2625        if table.has_cold_rows_fast() {
2626            return Ok(None);
2627        }
2628        let n = table.count_visible(&self.current_snapshot());
2629        let col = alias.clone().unwrap_or_else(|| String::from("count"));
2630        Ok(Some(QueryResult::Rows {
2631            columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2632            rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2633                i64::try_from(n).unwrap_or(i64::MAX)
2634            )])],
2635        }))
2636    }
2637
2638    /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2639    /// that col>` served from the index, never reading a row.
2640    ///
2641    /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2642    /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2643    /// count (2x at 1k). PG needs its visibility map for this — a heap
2644    /// tuple carries its own visibility, so an index entry alone cannot
2645    /// say whether the row is live, and PG reads the heap for any page
2646    /// the map does not mark all-visible. SPG keeps a header array
2647    /// beside the rows, so the locator answers it directly and there is
2648    /// no map to be stale.
2649    /// v7.39 (round 564) — the shape test, once, for both the
2650    /// materialising scan and the streaming one.
2651    ///
2652    /// Two callers asking the same question in two places is how a fact
2653    /// starts drifting; the answer here is the single copy. Returns the
2654    /// table, the alias the predicate is written against, the projected
2655    /// column's position, and the name the single output column takes.
2656    pub(crate) fn index_only_shape<'s>(
2657        &'s self,
2658        stmt: &'s SelectStatement,
2659    ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2660        use spg_sql::ast::SelectItem;
2661        if !stmt.ctes.is_empty()
2662            || !stmt.unions.is_empty()
2663            || stmt.group_by.is_some()
2664            || stmt.having.is_some()
2665            || stmt.distinct
2666            || stmt.locking.is_some()
2667            || !stmt.order_by.is_empty()
2668            || stmt.limit.is_some()
2669            || stmt.offset.is_some()
2670            || stmt.items.len() != 1
2671        {
2672            return None;
2673        }
2674        let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2675            return None;
2676        };
2677        if !from.joins.is_empty()
2678            || from.primary.lateral_subquery.is_some()
2679            || from.primary.unnest_expr.is_some()
2680            || from.primary.generate_series_args.is_some()
2681            || from.primary.name.is_empty()
2682            || from.primary.name.starts_with("__spg_")
2683        {
2684            return None;
2685        }
2686        // v7.39 (round 645) — see the note on the sibling shortcut above:
2687        // an inheritance parent's own header count is not the answer.
2688        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2689            return None;
2690        }
2691        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2692            return None;
2693        };
2694        let spg_sql::ast::Expr::Column(c) = expr else {
2695            return None;
2696        };
2697        let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2698        if let Some(q) = c.qualifier.as_deref()
2699            && !q.eq_ignore_ascii_case(alias_name)
2700        {
2701            return None;
2702        }
2703        let table = self.active_catalog().get(&from.primary.name)?;
2704        if table.schema().row_security {
2705            return None;
2706        }
2707        let cols = &table.schema().columns;
2708        let pos = cols
2709            .iter()
2710            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2711        let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2712        Some((table, alias_name, pos, out))
2713    }
2714
2715    /// v7.39 (round 565) — would this statement be answered out of the
2716    /// index alone?
2717    ///
2718    /// EXPLAIN has to name the node the executor will actually run, and
2719    /// the only honest way to know is to ask the same two questions the
2720    /// executor asks: the statement's shape, and everything decidable
2721    /// about the scan before it walks. Neither is re-stated here.
2722    pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2723        let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2724            return false;
2725        };
2726        let Some(where_) = stmt.where_.as_ref() else {
2727            return false;
2728        };
2729        crate::index_access::index_only_precheck(
2730            where_,
2731            &table.schema().columns,
2732            table,
2733            alias_name,
2734            pos,
2735            self.speaks_mysql,
2736        )
2737        .is_some()
2738    }
2739
2740    fn try_index_only_scan(
2741        &self,
2742        stmt: &SelectStatement,
2743    ) -> Result<Option<QueryResult>, EngineError> {
2744        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2745            return Ok(None);
2746        };
2747        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2748        // are not materialised here, and a partition parent's own
2749        // heap/indexes are empty (its rows live in the children).
2750        if !stmt.ctes.is_empty() {
2751            return Ok(None);
2752        }
2753        if let Some(from) = &stmt.from
2754            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2755        {
2756            return Ok(None);
2757        }
2758        let where_ = stmt.where_.as_ref().expect("shape checked it");
2759        let cols = &table.schema().columns;
2760        let Some(values) = crate::index_access::try_index_only_range(
2761            where_,
2762            cols,
2763            table,
2764            alias_name,
2765            &self.current_snapshot(),
2766            pos,
2767            self.speaks_mysql,
2768        ) else {
2769            return Ok(None);
2770        };
2771        let schema = alloc::vec![ColumnSchema::new(
2772            out_name,
2773            cols[pos].ty,
2774            cols[pos].nullable
2775        )];
2776        Ok(Some(QueryResult::Rows {
2777            columns: schema,
2778            rows: values
2779                .into_iter()
2780                .map(|v| Row::new(alloc::vec![v]))
2781                .collect(),
2782        }))
2783    }
2784
2785    /// v7.39 (round 564) — the same scan, emitting each value instead of
2786    /// building a `Vec<Row>` for the encoder to walk once and drop.
2787    ///
2788    /// A profile of the server serving a 50k-row range put 10.2% of the
2789    /// connection thread's CPU on BUILDING that vector and another 9.7%
2790    /// on dropping it — a fifth of the query, spent allocating and
2791    /// freeing one single-element `Vec` per output row so that the wire
2792    /// encoder could borrow each value for a few nanoseconds. The
2793    /// streaming interface it then hands them to takes `&[Value]`
2794    /// already.
2795    ///
2796    /// Returns `None` when the shape does not apply, so the caller falls
2797    /// back before anything has been emitted.
2798    pub(crate) fn try_index_only_stream<F>(
2799        &self,
2800        stmt: &SelectStatement,
2801        emit: &mut F,
2802    ) -> Result<Option<usize>, EngineError>
2803    where
2804        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2805    {
2806        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2807            return Ok(None);
2808        };
2809        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2810        // are not materialised here, and a partition parent's own
2811        // heap/indexes are empty (its rows live in the children).
2812        if !stmt.ctes.is_empty() {
2813            return Ok(None);
2814        }
2815        if let Some(from) = &stmt.from
2816            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2817        {
2818            return Ok(None);
2819        }
2820        let where_ = stmt.where_.as_ref().expect("shape checked it");
2821        let cols = &table.schema().columns;
2822        let schema = alloc::vec![ColumnSchema::new(
2823            out_name,
2824            cols[pos].ty,
2825            cols[pos].nullable
2826        )];
2827        let snapshot = self.current_snapshot();
2828        // The header goes out only once the walk has agreed to run — a
2829        // shape rejection after it would leave the client with a
2830        // RowDescription for a result that never comes.
2831        let mut wrote_header = false;
2832        let counted = crate::index_access::index_only_range_each(
2833            where_,
2834            cols,
2835            table,
2836            alias_name,
2837            &snapshot,
2838            pos,
2839            self.speaks_mysql,
2840            &mut |v: spg_storage::Value<'_>| {
2841                if !wrote_header {
2842                    emit(crate::StreamItem::Header(&schema))?;
2843                    wrote_header = true;
2844                }
2845                emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2846            },
2847        );
2848        match counted {
2849            None => Ok(None),
2850            Some(Err(e)) => Err(e),
2851            Some(Ok(n)) => {
2852                if !wrote_header {
2853                    emit(crate::StreamItem::Header(&schema))?;
2854                }
2855                Ok(Some(n))
2856            }
2857        }
2858    }
2859
2860    /// `DISTINCT ON`'s de-duplication, which runs after the inner
2861    /// SELECT has produced its rows.
2862    ///
2863    /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2864    /// reason round 848 established: a debug build gives every branch's
2865    /// locals a slot in the frame whichever branch runs, and this one is
2866    /// eighty lines of hashing, key slicing and survivor sorting that a
2867    /// statement without `DISTINCT ON` never touches. Round 867
2868    /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2869    /// reaches none of it — the segment that had been blamed on
2870    /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2871    #[inline(never)]
2872    fn apply_distinct_on(
2873        &self,
2874        result: QueryResult,
2875        don_hidden: usize,
2876        don_limit: &(
2877            Option<spg_sql::ast::LimitExpr>,
2878            Option<spg_sql::ast::LimitExpr>,
2879        ),
2880        don_top1: usize,
2881        orig_order_by: &[spg_sql::ast::OrderBy],
2882    ) -> Result<QueryResult, EngineError> {
2883        let QueryResult::Rows { columns, rows } = result else {
2884            return Ok(result);
2885        };
2886        // The keys are the hidden trailing columns appended above.
2887        // v7.39 (round 729) — top-1 mode: the trailing columns are the
2888        // DON keys plus the ORDER tail; keep each group's best in one
2889        // hash pass, then sort the SURVIVORS with the original spec.
2890        let mut kept: alloc::vec::Vec<Row<'static>>;
2891        let key_start;
2892        if don_top1 > 0 {
2893            let tail = don_top1 - 1;
2894            key_start = columns.len().saturating_sub(don_hidden + tail);
2895            let ord_start = key_start + don_hidden;
2896            let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2897                .iter()
2898                .map(|o| (o.desc, o.nulls_first))
2899                .collect();
2900            let mysql = self.speaks_mysql;
2901            let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2902                for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2903                    let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2904                    let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2905                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2906                        core::cmp::Ordering::Less => return true,
2907                        core::cmp::Ordering::Greater => return false,
2908                        core::cmp::Ordering::Equal => {}
2909                    }
2910                }
2911                false
2912            };
2913            let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2914            let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2915            let mut keybuf = String::new();
2916            for row in rows {
2917                keybuf.clear();
2918                for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2919                    aggregate::push_canonical_key(&mut keybuf, v);
2920                }
2921                match slot.get(keybuf.as_str()) {
2922                    Some(&i) => {
2923                        if better(&row, &best[i]) {
2924                            best[i] = row;
2925                        }
2926                    }
2927                    None => {
2928                        slot.insert(keybuf.clone(), best.len());
2929                        best.push(row);
2930                    }
2931                }
2932            }
2933            // Survivors sort with the FULL original spec (keys are still
2934            // aboard as hidden columns).
2935            let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2936                .iter()
2937                .map(|o| (o.desc, o.nulls_first))
2938                .collect();
2939            best.sort_by(|a, b| {
2940                for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2941                    let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2942                    let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2943                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2944                        core::cmp::Ordering::Equal => {}
2945                        o => return o,
2946                    }
2947                }
2948                core::cmp::Ordering::Equal
2949            });
2950            for r in &mut best {
2951                r.values.truncate(key_start);
2952            }
2953            kept = best;
2954        } else {
2955            key_start = columns.len().saturating_sub(don_hidden);
2956            let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2957            kept = alloc::vec::Vec::new();
2958            for mut row in rows {
2959                let key: alloc::vec::Vec<Value<'static>> =
2960                    row.values.get(key_start..).unwrap_or(&[]).to_vec();
2961                if seen.iter().any(|k| k == &key) {
2962                    continue;
2963                }
2964                seen.push(key);
2965                row.values.truncate(key_start);
2966                kept.push(row);
2967            }
2968        }
2969        let mut columns = columns;
2970        columns.truncate(key_start);
2971        // PG limits what DISTINCT ON left, not what fed it.
2972        let kept = apply_deferred_limit(kept, don_limit);
2973        Ok(QueryResult::Rows {
2974            columns,
2975            rows: kept,
2976        })
2977    }
2978
2979    pub(crate) fn exec_select_cancel_as(
2980        &self,
2981        stmt: &SelectStatement,
2982        cancel: CancelToken<'_>,
2983        as_role: Option<&str>,
2984    ) -> Result<QueryResult, EngineError> {
2985        // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2986        // <all columns>` is legal PG (the wildcard expands to grouped
2987        // columns); SPG refused the whole shape. Expand the wildcard
2988        // into explicit column refs up front — the aggregate layer's
2989        // existing "must appear in the GROUP BY clause" validation
2990        // then answers PG's sentence for any non-grouped column.
2991        if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2992            return self.exec_select_cancel_as(&expanded, cancel, as_role);
2993        }
2994        // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2995        // a row.
2996        //
2997        // The aggregate layer already short-circuits this to
2998        // `rows.len()`, so the O(1) part was never the problem — the
2999        // cost is UPSTREAM, materialising every visible row so that
3000        // layer can take its length. Measured over pgwire on 500k rows:
3001        // PG18 8.2 ms with two parallel workers, 10.3 ms with
3002        // parallelism off, SPG 16.5 ms — 1.6x slower than a
3003        // single-threaded PG on the commonest aggregate there is, and no
3004        // ledger entry recorded it.
3005        //
3006        // Counting visible HEADERS needs no row at all. PG cannot do
3007        // this: its visibility lives in the heap tuples themselves, so
3008        // it has to read them (that is why its own count(*) is a full
3009        // scan, parallel or not).
3010        // v7.39 (read01 round 57) — the table-privilege gate on the common
3011        // read core. A superuser session returns from it immediately.
3012        // v7.39 (round 529) — resolve an ORDER BY that names an output
3013        // ALIAS. The statement-level pass never reached a SELECT nested in
3014        // a FROM clause, a CTE or a scalar subquery, so the same query
3015        // worked on its own and failed the moment anything wrapped it —
3016        // which is what generated SQL does constantly.
3017        let aliased;
3018        let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
3019            let mut s = stmt.clone();
3020            crate::orderby::resolve_order_by_position(&mut s);
3021            aliased = s;
3022            &aliased
3023        } else {
3024            stmt
3025        };
3026        // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
3027        //
3028        // Its keys were evaluated against the PROJECTED row, so a key that
3029        // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
3030        // ORDER BY g, v DESC`, the canonical "latest row per group" — could
3031        // not be read at all and the query failed. PG evaluates them on the
3032        // input. They are projected as hidden columns here and stripped
3033        // again below, the same way the grouping-set ordering columns
3034        // already travel.
3035        //
3036        // And the dedup ran AFTER the inner statement's LIMIT, so
3037        // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
3038        // PG answers two: the limit had already taken two rows of the same
3039        // group before anything deduplicated them. A paginated DISTINCT ON
3040        // returned short pages, with no error. The limit is deferred to
3041        // after the dedup, which is PG's order.
3042        let don_stmt;
3043        // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
3044        // order spec (the rewritten stmt's is emptied).
3045        let orig_order_by = stmt.order_by.clone();
3046        let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
3047            (stmt, 0, (None, None), 0usize)
3048        } else {
3049            let mut s = stmt.clone();
3050            let hidden = s.distinct_on.len();
3051            for (i, e) in stmt.distinct_on.iter().enumerate() {
3052                s.items.push(SelectItem::Expr {
3053                    expr: e.clone(),
3054                    alias: Some(alloc::format!("__distinct_on_{i}")),
3055                });
3056            }
3057            // v7.39 (round 729) — group-top-1 short circuit. When the
3058            // DISTINCT ON keys are exactly the ORDER BY's leading keys,
3059            // the answer is "per group, the row that wins the remaining
3060            // order" — a single O(n) hash pass. The old path sorted the
3061            // ENTIRE input first (500k rows, ~180 ms on the panel cell)
3062            // to keep 100. The inner query runs UNSORTED with every
3063            // order key appended as a hidden column; the dedup below
3064            // keeps each group's best, then sorts the SURVIVORS.
3065            // Declared-collation order keys stay on the sorting path
3066            // (the value comparator here is collation-blind).
3067            let prefix_matches = s.order_by.len() >= hidden
3068                && stmt
3069                    .distinct_on
3070                    .iter()
3071                    .zip(s.order_by.iter())
3072                    .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3073            let colls_plain =
3074                crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3075                    .map(|cs| cs.iter().all(Option::is_none))
3076                    .unwrap_or(false);
3077            let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3078                let tail = s.order_by.len() - hidden;
3079                for (j, o) in s.order_by[hidden..].iter().enumerate() {
3080                    s.items.push(SelectItem::Expr {
3081                        expr: o.expr.clone(),
3082                        alias: Some(alloc::format!("__don_ord_{j}")),
3083                    });
3084                }
3085                // Carry the tail's direction flags through the aliases'
3086                // ORDER; the survivors re-sort below with the full spec.
3087                s.order_by = Vec::new();
3088                tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3089            } else {
3090                0
3091            };
3092            // Only a folded literal is deferred; a placeholder or an
3093            // expression keeps the path it has today rather than being
3094            // resolved a second way here.
3095            let deferrable = matches!(
3096                (&s.limit, &s.offset),
3097                (
3098                    None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3099                    None | Some(spg_sql::ast::LimitExpr::Literal(_))
3100                )
3101            );
3102            let deferred = if deferrable {
3103                (s.limit.take(), s.offset.take())
3104            } else {
3105                (None, None)
3106            };
3107            don_stmt = s;
3108            (&don_stmt, hidden, deferred, top1_tail)
3109        };
3110        self.acl_check_select_as(stmt, as_role)?;
3111        validate_aggregate_placement(stmt)?;
3112        // BEFORE the fast paths below, not after: a name that resolves to
3113        // nothing is not a question the count fast path or the index-only
3114        // scan should get to answer first. Placed after them at first,
3115        // and the two of them swallowed `WHERE` and `ORDER BY` while
3116        // `GROUP BY` and `HAVING`, which cannot take those routes, raised
3117        // — the same statement answering two ways depending on the plan.
3118        self.validate_clause_columns(stmt)?;
3119        self.validate_function_arity(stmt)?;
3120        // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3121        // privilege gate above. Placed before it at first, and the
3122        // security-definer e2e caught it immediately: a SECURITY INVOKER
3123        // function whose body is `SELECT count(*) FROM t` answered
3124        // instead of being refused, because the fast path never reached
3125        // the check.
3126        if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3127            return Ok(r);
3128        }
3129        // v7.39 (round 560) — an index-only range scan. Same placement
3130        // reasoning as the count above: after the privilege gate.
3131        if let Some(r) = self.try_index_only_scan(stmt)? {
3132            return Ok(r);
3133        }
3134        validate_locking_clause(stmt)?;
3135        let result = self.exec_select_cancel_inner(stmt, cancel)?;
3136        // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3137        // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3138        // They carry the per-branch mask through the UNION-ALL sort and must not
3139        // appear in the output. Stripped per SELECT level (grouping-set queries
3140        // are often wrapped in a derived subquery), before DISTINCT ON.
3141        let result = strip_synthetic_order_cols(result);
3142        // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3143        // rows arrive here already ORDER BY'd; keep the FIRST row of
3144        // each group the expressions define (PG semantics). The
3145        // expressions evaluate against the projected schema — an
3146        // expression that isn't in the select list errors honestly.
3147        if stmt.distinct_on.is_empty() {
3148            return Ok(result);
3149        }
3150        self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3151    }
3152
3153    /// The UNION chain: execute the head as a bare block, then fold each
3154    /// peer in with left-associative dedup.
3155    ///
3156    /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3157    /// reason round 848 established. A statement with no unions returns
3158    /// one line above the call — and every nested subquery on a deep
3159    /// path is such a statement, so each level of the recursion carried
3160    /// 170 lines of locals it could not reach. Round 867 measured that
3161    /// frame at 34,800 bytes, the largest single one on the descent,
3162    /// after two earlier attributions had blamed its caller and then its
3163    /// callee: the gap between two marks is the frame of everything
3164    /// BETWEEN them, and this function had no mark of its own.
3165    #[inline(never)]
3166    fn exec_union_chain(
3167        &self,
3168        stmt_ref: &SelectStatement,
3169        stmt: &SelectStatement,
3170        cancel: CancelToken<'_>,
3171    ) -> Result<QueryResult, EngineError> {
3172        // UNION path: clone-strip the head into a bare block (its own
3173        // DISTINCT and any inner ORDER BY are dropped by parser rule —
3174        // the wrapper SelectStatement carries them), execute, then chain
3175        // peers with left-associative dedup semantics.
3176        // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3177        // output columns; a position past their count is PG's 42P10.
3178        crate::orderby::check_order_by_positions(stmt_ref)?;
3179        let mut head_unknown = branch_unknown_mask(stmt_ref);
3180        let head_regcast = branch_regcast_mask(stmt_ref);
3181        let mut head = stmt_ref.clone();
3182        head.unions = Vec::new();
3183        head.order_by = Vec::new();
3184        head.limit = None;
3185        let QueryResult::Rows {
3186            mut columns,
3187            mut rows,
3188        } = self.exec_bare_select_cancel(&head, cancel)?
3189        else {
3190            unreachable!("bare SELECT cannot return CommandOk")
3191        };
3192        for (kind, peer) in &stmt_ref.unions {
3193            // v7.37.17 (17.6 siblings) — a peer carrying its own
3194            // unions is a nested INTERSECT group (the parser's
3195            // precedence regrouping); recurse through the
3196            // union-aware wrapper for it.
3197            let peer_result = if peer.unions.is_empty() {
3198                self.exec_bare_select_cancel(peer, cancel)?
3199            } else {
3200                self.exec_select_cancel(peer, cancel)?
3201            };
3202            let QueryResult::Rows {
3203                columns: peer_cols,
3204                rows: mut peer_rows,
3205            } = peer_result
3206            else {
3207                unreachable!("bare SELECT cannot return CommandOk")
3208            };
3209            if peer_cols.len() != columns.len() {
3210                // v7.39 (round 232) — PG's wording, which clients match on.
3211                return Err(EngineError::Unsupported(alloc::format!(
3212                    "each {} query must have the same number of columns",
3213                    set_op_name(*kind)
3214                )));
3215            }
3216            // v7.39 (round 232+233) — PG resolves each result column to one
3217            // type before it merges anything, and refuses the query when the
3218            // two branches have no common type. SPG's unifier
3219            // (`unify_union_columns`) is value-driven and deliberately
3220            // conservative — "a column where any cell fails to coerce is left
3221            // exactly as it was" — so a mismatch produced a column holding
3222            // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3223            // back with integers and text interleaved) instead of an error.
3224            //
3225            // The check has to read the branch ASTs, not just their schemas:
3226            // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3227            // as TEXT and is indistinguishable from a real text column by
3228            // schema alone — yet PG treats the two completely differently
3229            // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3230            // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3231            let peer_unknown = branch_unknown_mask(peer);
3232            let peer_regcast = branch_regcast_mask(peer);
3233            for i in 0..columns.len() {
3234                let hu = head_unknown.get(i).copied().unwrap_or(false);
3235                let pu = peer_unknown.get(i).copied().unwrap_or(false);
3236                let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3237                let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3238                    || head_regcast.get(i).copied().unwrap_or(false);
3239                match (hu, pu) {
3240                    // Both sides carry a real type: they must share a category.
3241                    (false, false) => {
3242                        if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3243                            return Err(EngineError::Unsupported(alloc::format!(
3244                                "{} types {} and {} cannot be matched",
3245                                set_op_name(*kind),
3246                                crate::conversions::pg_type_name_for_error(ht),
3247                                crate::conversions::pg_type_name_for_error(pt),
3248                            )));
3249                        }
3250                    }
3251                    // One side is an untyped literal: it takes the other's
3252                    // type, and failing to convert is the error PG reports.
3253                    (true, false) => {
3254                        coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3255                        columns[i].ty = pt;
3256                        head_unknown[i] = false;
3257                    }
3258                    (false, true) => {
3259                        coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3260                    }
3261                    // Both untyped — nothing to resolve against yet.
3262                    (true, true) => {}
3263                }
3264            }
3265            // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3266            // nullable (PG semantics). Previously the result kept only the head's
3267            // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3268            // non-null `1`) wrongly reported the column NOT NULL, which let
3269            // `count(col)`'s NOT-NULL fast-path count the NULL row.
3270            for (i, pc) in peer_cols.iter().enumerate() {
3271                if pc.nullable {
3272                    columns[i].nullable = true;
3273                }
3274            }
3275            // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3276            // text by the session collation (CI + accent + PAD SPACE), like
3277            // GROUP BY. PG stays byte-exact.
3278            let mysql = self.speaks_mysql;
3279            // v7.38.14 — the mask, which 7.38.13 recorded as impossible here
3280            // and was wrong about. `columns` and `peer_cols` are both in
3281            // scope; what was actually missing is that the branches' output
3282            // schemas did not CARRY the collation, so a mask built from them
3283            // would have marked every column byte-wise. Unifying the
3284            // projection-to-schema conversion fixed the supply side, and the
3285            // mask is now buildable from what was always there.
3286            //
3287            // Either side byte-wise keeps the position byte-wise, mirroring
3288            // `eval::resolve::mysql_text_fold_applies`: a set operation
3289            // between a folding column and a declared-binary one must not
3290            // quietly fold the binary one's values away.
3291            let set_mask: alloc::vec::Vec<bool> = columns
3292                .iter()
3293                .zip(peer_cols.iter())
3294                .map(|(l, r)| {
3295                    matches!(l.collation, spg_storage::Collation::Binary)
3296                        || matches!(r.collation, spg_storage::Collation::Binary)
3297                })
3298                .collect();
3299            let fold = FoldSpec::of(mysql, &set_mask);
3300            match kind {
3301                UnionKind::All => rows.extend(peer_rows),
3302                UnionKind::Distinct => {
3303                    rows.extend(peer_rows);
3304                    rows = dedup_rows(rows, fold);
3305                }
3306                // v7.37.17 (17.6 siblings) — PG set semantics.
3307                // v7.39 (round 591) — all four ask the same question of the
3308                // right side, and all four used to answer it by scanning it
3309                // once per left row. `PeerIndex` buckets it by the hash
3310                // DISTINCT already uses, so the answer is a lookup.
3311                // INTERSECT: distinct rows present on both sides.
3312                UnionKind::Intersect => {
3313                    let idx = PeerIndex::build(&peer_rows, fold);
3314                    rows = dedup_rows(rows, fold)
3315                        .into_iter()
3316                        .filter(|r| idx.contains(r))
3317                        .collect();
3318                }
3319                // INTERSECT ALL: multiset intersection — each row
3320                // keeps min(left count, right count) occurrences.
3321                UnionKind::IntersectAll => {
3322                    let mut idx = PeerIndex::build(&peer_rows, fold);
3323                    let mut kept: Vec<Row<'static>> = Vec::new();
3324                    for r in rows {
3325                        if idx.take_one(&r) {
3326                            kept.push(r);
3327                        }
3328                    }
3329                    rows = kept;
3330                }
3331                // EXCEPT: distinct left rows absent from the right.
3332                UnionKind::Except => {
3333                    let idx = PeerIndex::build(&peer_rows, fold);
3334                    rows = dedup_rows(rows, fold)
3335                        .into_iter()
3336                        .filter(|r| !idx.contains(r))
3337                        .collect();
3338                }
3339                // EXCEPT ALL: multiset subtraction — each right
3340                // occurrence cancels one left occurrence.
3341                UnionKind::ExceptAll => {
3342                    let mut idx = PeerIndex::build(&peer_rows, fold);
3343                    let mut kept: Vec<Row<'static>> = Vec::new();
3344                    for r in rows {
3345                        if !idx.take_one(&r) {
3346                            kept.push(r);
3347                        }
3348                    }
3349                    rows = kept;
3350                }
3351            }
3352        }
3353        // PG resolves a UNION / VALUES result column to one common type
3354        // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3355        // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3356        // built each branch independently, leaving mixed-type columns
3357        // that broke ORDER BY, comparisons, and value-based window
3358        // frames. Unify + coerce before the combined ORDER BY sees them.
3359        unify_union_columns(&mut columns, &mut rows);
3360        // ORDER BY at the top of a UNION applies to the combined result.
3361        // Eval against the projected schema (NOT the source table).
3362        if !stmt.order_by.is_empty() {
3363            // v7.39 (read01 round 54) — the combined-result ctx must carry the
3364            // catalog, and the projected columns must keep their enum identity
3365            // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3366            // by TEXT instead of member order — silently wrong rows, not an
3367            // error. (Same shape as the enum-order knife's GROUP BY fix.)
3368            let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3369            // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3370            // survive to here when the head projects a Wildcard (the
3371            // group-tail wrapper shape): map them onto the Nth
3372            // projected column so the combined sort works.
3373            let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3374                .order_by
3375                .iter()
3376                .map(|o| {
3377                    let mut o = o.clone();
3378                    if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3379                        && *n >= 1
3380                        && let Ok(idx) = usize::try_from(*n - 1)
3381                        && idx < columns.len()
3382                    {
3383                        o.expr = Expr::Column(spg_sql::ast::ColumnName {
3384                            qualifier: None,
3385                            name: columns[idx].name.clone(),
3386                        });
3387                    }
3388                    o
3389                })
3390                .collect();
3391            let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3392            let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3393            for r in rows {
3394                // v7.39.12 — a correlated subquery in ORDER BY is resolved
3395                // for this row before the key is built; see
3396                // `Engine::order_by_resolved_for_row`.
3397                let per_row =
3398                    self.order_by_resolved_for_row(&resolved_order, &r, &synth_ctx, cancel)?;
3399                let keys = build_order_keys(
3400                    per_row.as_deref().unwrap_or(&resolved_order),
3401                    &r,
3402                    &synth_ctx,
3403                )?;
3404                tagged.push((keys, r));
3405            }
3406            sort_by_keys(&mut tagged, &descs, self.session_parallel_workers());
3407            rows = tagged.into_iter().map(|(_, r)| r).collect();
3408        }
3409        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3410        Ok(QueryResult::Rows { columns, rows })
3411    }
3412
3413    fn exec_select_cancel_inner(
3414        &self,
3415        stmt: &SelectStatement,
3416        cancel: CancelToken<'_>,
3417    ) -> Result<QueryResult, EngineError> {
3418        cancel.check()?;
3419        // v7.38 P0 元机制 A — first observable point inside the
3420        // planner / executor. Tests use this to inject a delay or
3421        // a cancellation race before any row is produced. Release
3422        // build expands to `let _ = (...);` — zero cost.
3423        crate::injection_point!("planner_first_row_fetch", &stmt.from);
3424        // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3425        // PG analyses every definition, referenced or not, so `SELECT i FROM
3426        // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3427        // succeeded here (the parser used to drop the unreferenced defs
3428        // whole). The check is the CREATE VIEW check's shape (round 700): a
3429        // LIMIT-0 run of the same FROM with the definitions' key
3430        // expressions as the projection — it cannot disagree with what a
3431        // referencing window would have done, because it resolves the same
3432        // names the same way. Zero cost for the ordinary statement: the
3433        // list is empty unless a WINDOW clause left unreferenced defs.
3434        if !stmt.window_check_exprs.is_empty() {
3435            let mut probe = stmt.clone();
3436            probe.items = stmt
3437                .window_check_exprs
3438                .iter()
3439                .map(|e| spg_sql::ast::SelectItem::Expr {
3440                    expr: e.clone(),
3441                    alias: None,
3442                })
3443                .collect();
3444            probe.window_check_exprs = Vec::new();
3445            probe.distinct = false;
3446            probe.distinct_on = Vec::new();
3447            probe.group_by = None;
3448            probe.group_by_all = false;
3449            probe.having = None;
3450            probe.unions = Vec::new();
3451            probe.order_by = Vec::new();
3452            probe.locking = None;
3453            probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3454            probe.offset = None;
3455            probe.limit_with_ties = false;
3456            self.exec_select_cancel_inner(&probe, cancel)?;
3457        }
3458        // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3459        // takes the catalog, so the parser leaves a marker and the rewrite lands
3460        // here: the call moves into a LATERAL FROM item and the item becomes one
3461        // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3462        // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3463        // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3464        // second one.
3465        if let Some(lowered) = self.lower_record_expansion(stmt)? {
3466            return self.exec_select_cancel_inner(&lowered, cancel);
3467        }
3468        // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3469        // FROM / JOIN graph references any catalogued view name,
3470        // re-parse the view body and prepend it as a synthetic
3471        // CTE. Recurses on views-in-views via the regular CTE
3472        // dispatch below. Fast-path: skip the walker entirely when
3473        // the catalog has no views (the typical OLTP load).
3474        if !self.active_catalog().views_all().is_empty() {
3475            if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3476                return self.exec_select_cancel(&rewritten, cancel);
3477            }
3478        }
3479        // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3480        // gets rewritten to a UNION-ALL over the children that overlap
3481        // the WHERE-derived key range. Uses the same CTE-injection
3482        // trick as VIEW expansion above so downstream resolution
3483        // doesn't need a partition-aware code path.
3484        if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3485            return self.exec_select_cancel(&rewritten, cancel);
3486        }
3487        // v7.16.2 — information_schema / pg_catalog virtual
3488        // views (mailrs round-10 A.3). If the SELECT touches a
3489        // synthetic meta-table name (`__spg_info_*` /
3490        // `__spg_pg_*` — produced by the parser for
3491        // `information_schema.X` / `pg_catalog.X`), clone the
3492        // catalog, materialise the requested view as a real
3493        // temporary table, and re-execute against an enriched
3494        // engine. Same pattern as `exec_with_ctes` for CTEs.
3495        if !self.meta_views_materialised && select_references_meta_view(stmt) {
3496            return self.exec_select_with_meta_views(stmt, cancel);
3497        }
3498        // v6.10.2 — cold-tier time-travel short-circuit. When the
3499        // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3500        // dedicated cold-segment scan instead of the regular
3501        // hot+index path. The scope is intentionally narrow for
3502        // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3503        // optionally with a single-column-equality WHERE. JOINs /
3504        // aggregates / ORDER BY / subqueries on top of a time-
3505        // travelled scan are STABILITY § "Out of v6.10".
3506        if let Some(from) = &stmt.from
3507            && let Some(seg_id) = from.primary.as_of_segment
3508        {
3509            return self.exec_select_as_of_segment(stmt, from, seg_id);
3510        }
3511        // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3512        // pre-CTE because they don't read from the catalog and
3513        // shouldn't participate in regular FROM resolution.
3514        // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3515        // short-circuits. A meta-view FROM materialises to a fixed row
3516        // set. For a bare `SELECT *` we return it directly; otherwise we
3517        // stage it as a temp table and run the normal pipeline, so
3518        // projection / WHERE / ORDER BY / aggregates work over these views
3519        // (they were `SELECT *`-only before). A real table shadowing the
3520        // name wins (checked first), which also stops the staged re-run
3521        // from recursing back into meta-view detection.
3522        if let Some(from) = &stmt.from
3523            && from.joins.is_empty()
3524            && self.active_catalog().get(&from.primary.name).is_none()
3525        {
3526            let lower = from.primary.name.to_ascii_lowercase();
3527            if let Some(result) = self.meta_view_result(&lower) {
3528                let bare = stmt.where_.is_none()
3529                    && stmt.group_by.is_none()
3530                    && stmt.having.is_none()
3531                    && stmt.unions.is_empty()
3532                    && stmt.order_by.is_empty()
3533                    && stmt.limit.is_none()
3534                    && stmt.offset.is_none()
3535                    && !stmt.distinct
3536                    && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3537                if bare {
3538                    return Ok(result);
3539                }
3540                if let QueryResult::Rows { columns, rows } = result {
3541                    let mut catalog = self.active_catalog().clone();
3542                    let cols = infer_column_types(&columns, &rows);
3543                    let schema = TableSchema::new(from.primary.name.clone(), cols);
3544                    catalog.create_table(schema).map_err(EngineError::Storage)?;
3545                    let t = catalog
3546                        .get_mut(&from.primary.name)
3547                        .expect("just-created meta-view table must exist");
3548                    for row in rows {
3549                        t.insert(row).map_err(EngineError::Storage)?;
3550                    }
3551                    let mut eng = Engine::restore(catalog);
3552                    if let Some(c) = self.clock {
3553                        eng = eng.with_clock(c);
3554                    }
3555                    if let Some(f) = self.salt_fn {
3556                        eng = eng.with_salt_fn(f);
3557                    }
3558                    // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3559                    // connection identity so `WHERE pid = pg_backend_pid()`
3560                    // matches inside the staged meta-view run.
3561                    if let Some(f) = self.backend_pid_fn {
3562                        eng.set_backend_pid_fn(f);
3563                    }
3564                    return eng.exec_select_cancel(stmt, cancel);
3565                }
3566                return Ok(result);
3567            }
3568        }
3569        // v4.11: CTEs materialise into a temporary enriched catalog
3570        // *before* anything else — the body SELECT can then refer
3571        // to CTE names via the regular FROM-clause resolution.
3572        // Uncorrelated only: each CTE body runs once against the
3573        // current catalog, not against later CTEs' results (left-
3574        // to-right materialisation would relax this, but we keep
3575        // it simple for v4.11 MVP).
3576        if !stmt.ctes.is_empty() {
3577            return self.exec_with_ctes(stmt, cancel);
3578        }
3579        // v4.10: subqueries (uncorrelated) are resolved here, before
3580        // the executor sees the row loop. We clone the statement so
3581        // we can mutate without disturbing the caller's AST — most
3582        // queries pass through with no subquery nodes and the clone
3583        // is cheap; with subqueries the materialisation cost
3584        // dominates anyway.
3585        let mut stmt_owned;
3586        let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3587            stmt_owned = stmt.clone();
3588            // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3589            // aggregate-wrapped correlated scalar subquery whose
3590            // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3591            // executor streams one join instead of splicing a per-row
3592            // subplan. Runs before the per-row/batch resolver, which then
3593            // only sees the subqueries the pull-up left behind.
3594            self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3595            // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3596            // the "per-key latest" scalar subquery shape (inbox / feed
3597            // / timeline applications) becomes a CTE + LEFT JOIN
3598            // against a GROUP BY pre-aggregation that reuses the v7.33
3599            // first_ordered argmax executor. Runs AFTER unique-key
3600            // pull-up (so the unique-key fast path still wins for
3601            // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3602            // Phase 1 (this commit) is skeleton only — no-op pass.
3603            self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3604            // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3605            // sublink pull-up to semi/anti-join, before the resolver gets
3606            // a chance to walk per-row.
3607            self.pull_up_exists_sublinks(&mut stmt_owned);
3608            // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3609            // exec_with_ctes so they materialise once before the body
3610            // SELECT runs. exec_with_ctes strips ctes from the body
3611            // clone, then re-enters select.
3612            if !stmt_owned.ctes.is_empty() {
3613                return self.exec_with_ctes(&stmt_owned, cancel);
3614            }
3615            // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3616            //   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3617            // BEFORE `resolve_select_subqueries` materialises the inner
3618            // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3619            // INSUBQ benchmark). Run the inner once, collect the result
3620            // values into a `HashSet<i64>` directly, then probe A.pk per
3621            // value and tally. Returns `Some` when the shape matches.
3622            if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3623                return Ok(out);
3624            }
3625            self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3626            &stmt_owned
3627        } else {
3628            stmt
3629        };
3630        if stmt_ref.unions.is_empty() {
3631            return self.exec_bare_select_cancel(stmt_ref, cancel);
3632        }
3633        self.exec_union_chain(stmt_ref, stmt, cancel)
3634    }
3635
3636    #[allow(clippy::too_many_lines)]
3637    #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3638    /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3639    /// Synthesises a single-column virtual table whose column type
3640    /// is TEXT and whose rows are the array elements. Routes
3641    /// through the regular projection / WHERE / ORDER BY / LIMIT
3642    /// machinery so set-returning UNNEST composes naturally with
3643    /// the rest of the SELECT surface.
3644    fn exec_select_unnest(
3645        &self,
3646        stmt: &SelectStatement,
3647        primary: &TableRef,
3648        cancel: CancelToken<'_>,
3649    ) -> Result<QueryResult, EngineError> {
3650        let expr = primary
3651            .unnest_expr
3652            .as_deref()
3653            .expect("caller guards unnest_expr.is_some()");
3654        // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3655        // N value columns instead of one; the shared builder does
3656        // the work and the tail below (WHERE / agg / projection)
3657        // runs against the wider schema.
3658        let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3659            match unnest_zip_args(expr) {
3660                Some(args) => Some(unnest_zip_rows(args)?),
3661                None => None,
3662            };
3663        // Evaluate the array expression once. Empty schema / empty
3664        // row — uncorrelated UNNEST cannot reference outer columns.
3665        // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3666        // introspection family (enum_range / enum_first / enum_last) resolves
3667        // its labels from the argument's STATIC enum type against the
3668        // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3669        // fell through to the generic arm, got NULL, and expanded to zero rows
3670        // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3671        // carry the catalog) worked.
3672        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3673        let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3674        let dummy_row = Row::new(alloc::vec::Vec::new());
3675        // v7.11.13 — unnest dispatches per array element type so
3676        // INT[] / BIGINT[] surface their PG types in projection.
3677        // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3678        // columns (PG: lexeme | positions | weights); everything else
3679        // keeps the alias / "unnest" defaults below.
3680        let mut composite_names: Option<&[&str]> = None;
3681        let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3682            if let Some(m) = multi {
3683                m
3684            } else {
3685                // v7.39 (round 236) — flatten a multidimensional array into
3686                // its row-major elements (PG) before the 1-D-only match.
3687                let unnest_src = {
3688                    let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3689                    crate::eval::values::flatten_2d(&v).unwrap_or(v)
3690                };
3691                let mut return_multi: Option<(
3692                    alloc::vec::Vec<DataType>,
3693                    alloc::vec::Vec<Row<'static>>,
3694                )> = None;
3695                let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3696                {
3697                    Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3698                    Value::TextArray(items) => {
3699                        let rows = items
3700                            .into_iter()
3701                            .map(|item| {
3702                                Row::new(alloc::vec![match item {
3703                                    Some(s) => Value::text(s),
3704                                    None => Value::Null,
3705                                }])
3706                            })
3707                            .collect();
3708                        (DataType::Text, rows)
3709                    }
3710                    Value::IntArray(items) => {
3711                        let rows = items
3712                            .into_iter()
3713                            .map(|item| {
3714                                Row::new(alloc::vec![match item {
3715                                    Some(n) => Value::Int(n),
3716                                    None => Value::Null,
3717                                }])
3718                            })
3719                            .collect();
3720                        (DataType::Int, rows)
3721                    }
3722                    Value::BigIntArray(items) => {
3723                        let rows = items
3724                            .into_iter()
3725                            .map(|item| {
3726                                Row::new(alloc::vec![match item {
3727                                    Some(n) => Value::BigInt(n),
3728                                    None => Value::Null,
3729                                }])
3730                            })
3731                            .collect();
3732                        (DataType::BigInt, rows)
3733                    }
3734                    Value::Multirange { kind, ranges } => {
3735                        let rows = ranges
3736                            .iter()
3737                            .map(|sp| {
3738                                Row::new(alloc::vec![Value::Range {
3739                                    kind,
3740                                    lower: sp.lower.clone(),
3741                                    upper: sp.upper.clone(),
3742                                    lower_inc: sp.lower_inc,
3743                                    upper_inc: sp.upper_inc,
3744                                    empty: false,
3745                                }])
3746                            })
3747                            .collect();
3748                        (DataType::Range(kind), rows)
3749                    }
3750                    // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3751                    // one row per lexeme, PG18-measured columns
3752                    // lexeme | positions | weights (`a | {1,3} |
3753                    // {D,D}`); a position-less lexeme (a stripped
3754                    // vector) reads NULL in both array columns.
3755                    Value::TsVector(lexemes) => {
3756                        composite_names = Some(&["lexeme", "positions", "weights"]);
3757                        let rows = lexemes
3758                            .iter()
3759                            .map(|l| {
3760                                let (pos, wts) = if l.positions.is_empty() {
3761                                    (Value::Null, Value::Null)
3762                                } else {
3763                                    let letter = match l.weight {
3764                                        3 => "A",
3765                                        2 => "B",
3766                                        1 => "C",
3767                                        _ => "D",
3768                                    };
3769                                    (
3770                                        Value::SmallIntArray(
3771                                            l.positions
3772                                                .iter()
3773                                                .map(|p| {
3774                                                    Some(i16::try_from(*p).unwrap_or(i16::MAX))
3775                                                })
3776                                                .collect(),
3777                                        ),
3778                                        Value::TextArray(
3779                                            l.positions
3780                                                .iter()
3781                                                .map(|_| Some(letter.into()))
3782                                                .collect(),
3783                                        ),
3784                                    )
3785                                };
3786                                Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3787                            })
3788                            .collect();
3789                        return_multi = Some((
3790                            alloc::vec![
3791                                DataType::Text,
3792                                DataType::SmallIntArray,
3793                                DataType::TextArray
3794                            ],
3795                            rows,
3796                        ));
3797                        (DataType::Text, alloc::vec::Vec::new())
3798                    }
3799                    // v7.39.11 — every remaining array-family value,
3800                    // through the one element menu, so a type does not
3801                    // have to be written out here a second time to be
3802                    // unnestable. `unnest(ARRAY[1,2]::smallint[])`
3803                    // raised "expects an array argument, got
3804                    // smallint[]" until this arm — the arms above name
3805                    // int / bigint / text / json and stop — and so did
3806                    // every catalog vector. Found while closing
3807                    // sentori's §4 against 7.39.10.
3808                    ref v if crate::eval::values::array_len(v).is_some() => {
3809                        let elems = crate::eval::values::array_elements(v).unwrap_or_default();
3810                        let dt = elems
3811                            .iter()
3812                            .find_map(spg_storage::Value::data_type)
3813                            .unwrap_or(DataType::Text);
3814                        let rows = elems
3815                            .into_iter()
3816                            .map(|e| Row::new(alloc::vec![e]))
3817                            .collect();
3818                        (dt, rows)
3819                    }
3820                    other => {
3821                        // v7.39 (round 622, S05a) — see table_access.rs:
3822                        // the same sentence, and it is a type mismatch.
3823                        return Err(EngineError::Eval(EvalError::TypeMismatch {
3824                            detail: alloc::format!(
3825                                "unnest() expects an array argument, got {}",
3826                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3827                            ),
3828                        }));
3829                    }
3830                };
3831                if let Some(m) = return_multi {
3832                    m
3833                } else {
3834                    (alloc::vec![elem_dtype], rows)
3835                }
3836            };
3837        let alias = primary
3838            .alias
3839            .clone()
3840            .unwrap_or_else(|| "unnest".to_string());
3841        // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3842        // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3843        // entries map positionally over the value columns. Without
3844        // the column list, a single column falls back to the table
3845        // alias (pre-v7.13.2 behaviour); multi-arg columns default
3846        // to PG's `unnest`.
3847        let n_vals = dtypes.len();
3848        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3849            .iter()
3850            .enumerate()
3851            .map(|(i, dt)| {
3852                let name = primary
3853                    .unnest_column_aliases
3854                    .get(i)
3855                    .cloned()
3856                    .unwrap_or_else(|| {
3857                        if let Some(names) = composite_names {
3858                            names
3859                                .get(i)
3860                                .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3861                        } else if n_vals == 1 {
3862                            alias.clone()
3863                        } else {
3864                            "unnest".to_string()
3865                        }
3866                    });
3867                ColumnSchema::new(name, *dt, true)
3868            })
3869            .collect();
3870        // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3871        // parser desugared a base-type-returning function here (see
3872        // TableRef::scalar_fn_item); the marker rides the column so it survives
3873        // every EvalContext an inner stage rebuilds.
3874        if primary.scalar_fn_item && schema_cols.len() == 1 {
3875            schema_cols[0].scalar_row_source = true;
3876        }
3877        // WITH ORDINALITY — trailing BIGINT counting rows from 1
3878        // in element order. The alias entry after the value
3879        // columns renames it (PG default: `ordinality`).
3880        let rows = if primary.with_ordinality {
3881            let ord_name = primary
3882                .unnest_column_aliases
3883                .get(n_vals)
3884                .cloned()
3885                .unwrap_or_else(|| "ordinality".to_string());
3886            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3887            rows.into_iter()
3888                .enumerate()
3889                .map(|(i, row)| {
3890                    let mut vals = row.values.clone();
3891                    vals.push(Value::BigInt(i as i64 + 1));
3892                    Row::new(vals)
3893                })
3894                .collect()
3895        } else {
3896            rows
3897        };
3898        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3899        // `EvalContext::new` drops it and every catalog-dependent cast
3900        // (regclass / enum / composite / domain) silently degrades.
3901        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3902        // Apply WHERE.
3903        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3904            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3905            for row in rows {
3906                cancel.check()?;
3907                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3908                if matches!(v, Value::Bool(true)) {
3909                    out.push(row);
3910                }
3911            }
3912            out
3913        } else {
3914            rows
3915        };
3916        // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3917        // unnest source. Same routing the relational scan path
3918        // already takes — without it `SELECT COUNT(*) FROM
3919        // unnest(ARRAY[…])` either errored at projection time or
3920        // returned the wrong shape.
3921        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
3922            // v7.29 — a per-query memo so correlated scalar
3923            // subqueries batch-evaluate once (group map) instead of
3924            // executing per group.
3925            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3926            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3927                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3928                    .map_err(|err| match err {
3929                        EngineError::Eval(ev) => ev,
3930                        other => eval::EvalError::TypeMismatch {
3931                            detail: alloc::format!("{other}"),
3932                        },
3933                    })
3934            };
3935            // v7.39 (round 656) — hand the rows over as they are rather than
3936            // collecting a second vector of `RowRef` wrappers. Note this is
3937            // a set-returning-function path, NOT the relational scan: the
3938            // measured O(rows) cost lived in `run_single_table_aggregate`,
3939            // and converting these four first was a miss that cost a full
3940            // round — every test stayed green and the number did not move.
3941            let agg = aggregate::run(
3942                stmt,
3943                crate::join::AggRows::Owned(&filtered),
3944                &schema_cols,
3945                Some(&alias),
3946                Some(&agg_correlated),
3947                self.parallel_runner.0.as_deref(),
3948                Some(self.active_catalog()),
3949                Some(self),
3950            )?;
3951            return self.finish_agg_result(agg, stmt, cancel);
3952        }
3953        // Projection.
3954        let projection = build_projection(
3955            &stmt.items,
3956            &schema_cols,
3957            &alias,
3958            self.speaks_mysql,
3959            Some(self.active_catalog()),
3960        )?;
3961        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3962            alloc::vec::Vec::with_capacity(filtered.len());
3963        // v7.19 P5 — Set-Returning-Function in projection
3964        // position (PG `SELECT unnest(arr) FROM t` shape). When a
3965        // SELECT item evaluates to a top-level unnest(arr) call,
3966        // expand it: for each input row, evaluate the array, emit
3967        // one output row per element, broadcasting non-SRF
3968        // projections from the same input row. Multi-SRF + LCM
3969        // padding stays a documented carve-out; mailrs uses
3970        // single-SRF for redirect_uris.
3971        // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3972        // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3973        let srf_idxs = self.srf_target_idxs(&projection);
3974        // v7.39 (round 621) — which input row each output row came from. An
3975        // SRF turns one input row into many, and the ORDER BY below used to
3976        // index the EXPANDED rows by the INPUT row's position: the result was
3977        // silently truncated to the input row count and left unsorted, so
3978        // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3979        // answered three of its six rows, in no order. Without the ORDER BY
3980        // the same query was already right.
3981        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3982        if !srf_idxs.is_empty() {
3983            let (rows, src) =
3984                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3985            projected_rows = rows;
3986            src_of_row = src;
3987        } else {
3988            // v7.24 (round-16 B) — select-list subqueries resolve
3989            // per row (correlated-aware; plain exprs take the fast
3990            // path inside).
3991            let mut proj_memo = memoize::MemoizeCache::default();
3992            for row in &filtered {
3993                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3994                for p in &projection {
3995                    vals.push(self.eval_expr_with_correlated(
3996                        &p.expr,
3997                        row,
3998                        &scan_ctx,
3999                        cancel,
4000                        Some(&mut proj_memo),
4001                    )?);
4002                }
4003                projected_rows.push(Row::new(vals));
4004            }
4005        }
4006        // ORDER BY / LIMIT — apply on the projected rows (cheap;
4007        // unnest result sets are small by design).
4008        let columns: alloc::vec::Vec<ColumnSchema> = projection
4009            .iter()
4010            // v7.39 (read01 round 54) — keep the column's enum identity through
4011            // the projection (it lives outside the DataType lattice), or a
4012            // derived table / UNION / windowed result forgets it and any outer
4013            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4014            .map(|p| p.to_column_schema())
4015            .collect();
4016        // Re-evaluate ORDER BY against the source schema (pre-projection
4017        // so col refs by name still resolve through `scan_ctx`).
4018        // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
4019        // column. Evaluated as an expression it is just the constant N: the same
4020        // key for every row, so the sort ran and changed nothing.
4021        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4022        if !order_by.is_empty() {
4023            // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
4024            // A key that names a select-list item reads it out of the expanded
4025            // row (PG sorts AFTER the expansion); one that names a source
4026            // column the query does not project is evaluated on the input row
4027            // it came from, which is what `srf_order_output_cols` decides.
4028            let out_cols = if srf_idxs.is_empty() {
4029                alloc::vec![None; order_by.len()]
4030            } else {
4031                srf_order_output_cols(&order_by, &projection)
4032            };
4033            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4034                .iter()
4035                .enumerate()
4036                .map(|(k, out)| -> Result<_, EngineError> {
4037                    let src = src_of_row.get(k).copied().unwrap_or(k);
4038                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4039                        .iter()
4040                        .zip(out_cols.iter())
4041                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
4042                        .collect();
4043                    Ok((k, keys?))
4044                })
4045                .collect::<Result<_, _>>()?;
4046            indexed.sort_by(|a, b| {
4047                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4048                    let o = &order_by[idx];
4049                    let cmp = order_by_value_cmp_in(
4050                        o.desc,
4051                        o.nulls_first,
4052                        ka,
4053                        kb,
4054                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4055                    );
4056                    if cmp != core::cmp::Ordering::Equal {
4057                        return cmp;
4058                    }
4059                }
4060                core::cmp::Ordering::Equal
4061            });
4062            projected_rows = indexed
4063                .into_iter()
4064                .map(|(i, _)| projected_rows[i].clone())
4065                .collect();
4066        }
4067        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4068        if stmt.distinct {
4069            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4070            // spec folds EVERY text position, so a column declared
4071            // `COLLATE utf8mb4_bin` had its values merged here exactly the
4072            // way 3b494b6e fixed on the main scan path. The projection is
4073            // already in scope at each of these sites, so the mask needs no
4074            // new plumbing -- it was simply never asked for.
4075            projected_rows = dedup_rows(
4076                projected_rows,
4077                FoldSpec::of_masks(
4078                    scan_ctx.mysql_dialect,
4079                    &fold_mask(&projection),
4080                    &pad_mask(&projection),
4081                ),
4082            );
4083        }
4084        // LIMIT / OFFSET — apply at the tail.
4085        if let Some(offset) = stmt.offset_literal() {
4086            let off = (offset as usize).min(projected_rows.len());
4087            projected_rows.drain(..off);
4088        }
4089        if let Some(limit) = stmt.limit_literal() {
4090            projected_rows.truncate(limit as usize);
4091        }
4092        Ok(QueryResult::Rows {
4093            columns,
4094            rows: projected_rows,
4095        })
4096    }
4097
4098    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
4099    /// step])` set-returning source. Mirrors `exec_select_unnest`'s
4100    /// shape: evaluate the arg list once against an empty row,
4101    /// materialise the row stream by stepping start → stop, then
4102    /// route through the standard WHERE / projection / ORDER BY /
4103    /// LIMIT pipeline. Two arg-type combos in v7.17:
4104    ///   * integer / integer [/ integer] — SmallInt, Int, BigInt
4105    ///     (widened to BigInt internally; step defaults to 1)
4106    ///   * timestamp / timestamp / interval — date-range
4107    ///     iteration (mailrs's daily-report pattern)
4108    fn exec_select_generate_series(
4109        &self,
4110        stmt: &SelectStatement,
4111        primary: &TableRef,
4112        cancel: CancelToken<'_>,
4113    ) -> Result<QueryResult, EngineError> {
4114        let args = primary
4115            .generate_series_args
4116            .as_ref()
4117            .expect("caller guards generate_series_args.is_some()");
4118        let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
4119        let alias = primary
4120            .alias
4121            .clone()
4122            .unwrap_or_else(|| "generate_series".to_string());
4123        // `AS t(n)` — the first column-alias entry renames the
4124        // series column (PG semantics); bare alias keeps the
4125        // pre-existing behaviour of naming the column after it.
4126        let col_name = primary
4127            .unnest_column_aliases
4128            .first()
4129            .cloned()
4130            .unwrap_or_else(|| alias.clone());
4131        let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
4132        let mut schema_cols = alloc::vec![col_schema.clone()];
4133        // WITH ORDINALITY — trailing BIGINT counting rows from 1;
4134        // the second column-alias entry renames it.
4135        let rows = if primary.with_ordinality {
4136            let ord_name = primary
4137                .unnest_column_aliases
4138                .get(1)
4139                .cloned()
4140                .unwrap_or_else(|| "ordinality".to_string());
4141            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4142            rows.into_iter()
4143                .enumerate()
4144                .map(|(i, row)| {
4145                    let mut vals = row.values.clone();
4146                    vals.push(Value::BigInt(i as i64 + 1));
4147                    Row::new(vals)
4148                })
4149                .collect()
4150        } else {
4151            rows
4152        };
4153        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4154        // `EvalContext::new` drops it and every catalog-dependent cast
4155        // (regclass / enum / composite / domain) silently degrades.
4156        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4157        // WHERE.
4158        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4159            let mut out = alloc::vec::Vec::with_capacity(rows.len());
4160            for row in rows {
4161                cancel.check()?;
4162                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4163                if matches!(v, Value::Bool(true)) {
4164                    out.push(row);
4165                }
4166            }
4167            out
4168        } else {
4169            rows
4170        };
4171        // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4172        // returning sources. When the SELECT projection contains
4173        // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4174        // …) we route the filtered row stream through the same
4175        // aggregate executor the relational scan path uses, so
4176        // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4177        // a single 100 row instead of erroring at projection
4178        // time. GROUP BY / HAVING / ORDER BY over the aggregate
4179        // output all ride through `aggregate::run`.
4180        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
4181            // v7.29 — a per-query memo so correlated scalar
4182            // subqueries batch-evaluate once (group map) instead of
4183            // executing per group.
4184            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4185            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4186                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4187                    .map_err(|err| match err {
4188                        EngineError::Eval(ev) => ev,
4189                        other => eval::EvalError::TypeMismatch {
4190                            detail: alloc::format!("{other}"),
4191                        },
4192                    })
4193            };
4194            // v7.39 (round 656) — hand the rows over as they are rather than
4195            // collecting a second vector of `RowRef` wrappers. Note this is
4196            // a set-returning-function path, NOT the relational scan: the
4197            // measured O(rows) cost lived in `run_single_table_aggregate`,
4198            // and converting these four first was a miss that cost a full
4199            // round — every test stayed green and the number did not move.
4200            let agg = aggregate::run(
4201                stmt,
4202                crate::join::AggRows::Owned(&filtered),
4203                &schema_cols,
4204                Some(&alias),
4205                Some(&agg_correlated),
4206                self.parallel_runner.0.as_deref(),
4207                Some(self.active_catalog()),
4208                Some(self),
4209            )?;
4210            return self.finish_agg_result(agg, stmt, cancel);
4211        }
4212        // Projection.
4213        let projection = build_projection(
4214            &stmt.items,
4215            &schema_cols,
4216            &alias,
4217            self.speaks_mysql,
4218            Some(self.active_catalog()),
4219        )?;
4220        // v7.39 (round 621) — and here, for the same reason.
4221        let srf_idxs = self.srf_target_idxs(&projection);
4222        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4223        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4224            alloc::vec::Vec::with_capacity(filtered.len());
4225        let mut proj_memo = memoize::MemoizeCache::default();
4226        if !srf_idxs.is_empty() {
4227            let (rows, src) =
4228                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4229            projected_rows = rows;
4230            src_of_row = src;
4231        } else {
4232            for row in &filtered {
4233                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4234                for p in &projection {
4235                    // v7.24 (round-16 B) — correlated-aware.
4236                    vals.push(self.eval_expr_with_correlated(
4237                        &p.expr,
4238                        row,
4239                        &scan_ctx,
4240                        cancel,
4241                        Some(&mut proj_memo),
4242                    )?);
4243                }
4244                projected_rows.push(Row::new(vals));
4245            }
4246        }
4247        let columns: alloc::vec::Vec<ColumnSchema> = projection
4248            .iter()
4249            // v7.39 (read01 round 54) — keep the column's enum identity through
4250            // the projection (it lives outside the DataType lattice), or a
4251            // derived table / UNION / windowed result forgets it and any outer
4252            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4253            .map(|p| p.to_column_schema())
4254            .collect();
4255        // ORDER BY against the source schema.
4256        // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4257        // more of them than there were inputs), and a positional key means the
4258        // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4259        // and what the other two synthetic-source tails already did.
4260        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4261        if !order_by.is_empty() {
4262            let out_cols = if srf_idxs.is_empty() {
4263                alloc::vec![None; order_by.len()]
4264            } else {
4265                srf_order_output_cols(&order_by, &projection)
4266            };
4267            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4268                .iter()
4269                .enumerate()
4270                .map(|(k, out)| -> Result<_, EngineError> {
4271                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4272                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4273                        .iter()
4274                        .zip(out_cols.iter())
4275                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4276                        .collect();
4277                    Ok((k, keys?))
4278                })
4279                .collect::<Result<_, _>>()?;
4280            indexed.sort_by(|a, b| {
4281                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4282                    let o = &stmt.order_by[idx];
4283                    let cmp = order_by_value_cmp_in(
4284                        o.desc,
4285                        o.nulls_first,
4286                        ka,
4287                        kb,
4288                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4289                    );
4290                    if cmp != core::cmp::Ordering::Equal {
4291                        return cmp;
4292                    }
4293                }
4294                core::cmp::Ordering::Equal
4295            });
4296            projected_rows = indexed
4297                .into_iter()
4298                .map(|(i, _)| projected_rows[i].clone())
4299                .collect();
4300        }
4301        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4302        if stmt.distinct {
4303            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4304            // spec folds EVERY text position, so a column declared
4305            // `COLLATE utf8mb4_bin` had its values merged here exactly the
4306            // way 3b494b6e fixed on the main scan path. The projection is
4307            // already in scope at each of these sites, so the mask needs no
4308            // new plumbing -- it was simply never asked for.
4309            projected_rows = dedup_rows(
4310                projected_rows,
4311                FoldSpec::of_masks(
4312                    scan_ctx.mysql_dialect,
4313                    &fold_mask(&projection),
4314                    &pad_mask(&projection),
4315                ),
4316            );
4317        }
4318        if let Some(offset) = stmt.offset_literal() {
4319            let off = (offset as usize).min(projected_rows.len());
4320            projected_rows.drain(..off);
4321        }
4322        if let Some(limit) = stmt.limit_literal() {
4323            projected_rows.truncate(limit as usize);
4324        }
4325        Ok(QueryResult::Rows {
4326            columns,
4327            rows: projected_rows,
4328        })
4329    }
4330
4331    /// The FROM shapes that are not an ordinary table scan — joins, the
4332    /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4333    ///
4334    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4335    /// reason round 848 established in the parser: a debug build gives
4336    /// EVERY branch's locals a slot in the frame, whichever branch runs.
4337    /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4338    /// stacks several of them; a plain scan reaches none of these
4339    /// branches. Moving them out took the frame to 52,336.
4340    ///
4341    /// `Ok(None)` means "not one of these shapes, carry on".
4342    #[inline(never)]
4343    fn try_from_shape_paths(
4344        &self,
4345        stmt: &SelectStatement,
4346        from: &spg_sql::ast::FromClause,
4347        cancel: CancelToken<'_>,
4348    ) -> Result<Option<QueryResult>, EngineError> {
4349        if !from.joins.is_empty() {
4350            // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4351            // elimination: when a LEFT JOIN's right side is referenced
4352            // ONLY in the ON equality and the right-side join key is
4353            // UNIQUE/PK, the join preserves outer cardinality exactly
4354            // and contributes no values used downstream. Drop the
4355            // entire join. PG does this on the
4356            // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4357            // — A's row count is what survives, B never has to be
4358            // touched.
4359            if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4360                return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4361            }
4362            // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4363            // the v7.32 joinfold rewrite that turns inner JOINs into a
4364            // single-table scan when the catalogue can prove key-only
4365            // dependency. Tests use this to assert "without joinfold,
4366            // the join still executes correctly" (joinfold is a
4367            // semantically-equivalent rewrite, not a correctness fix).
4368            if !self.env_cfg().disable_joinfold {
4369                if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4370                    return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4371                }
4372            }
4373            return self.exec_joined_select(stmt, from, cancel).map(Some);
4374        }
4375        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4376        // single-column table at SELECT entry by evaluating the
4377        // expression once against the empty row (UNNEST is
4378        // uncorrelated in v7.11; correlated / LATERAL unnest is a
4379        // v7.12 carve-out). Build a virtual `Table` in a heap-only
4380        // catalog, then route to the regular scan path.
4381        if from.primary.unnest_expr.is_some() {
4382            return self
4383                .exec_select_unnest(stmt, &from.primary, cancel)
4384                .map(Some);
4385        }
4386        // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4387        // returning function. Same dispatch shape as unnest but
4388        // emits a two-column (key TEXT, value TEXT) row stream.
4389        if from.primary.jsonb_each_text_arg.is_some() {
4390            return self
4391                .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4392                .map(Some);
4393        }
4394        // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4395        // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4396        // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4397        // array form. Each function runs; the results zip in LOCKSTEP with the
4398        // shorter padded to NULL — the SAME rule the target-list SRFs follow
4399        // (round 67), which is why `srf_values` is what evaluates each entry.
4400        if from.primary.rows_from.is_some() {
4401            let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4402            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4403                if let Some(col) = schema_cols.get_mut(i) {
4404                    col.name = new_name.clone();
4405                }
4406            }
4407            let alias = from
4408                .primary
4409                .alias
4410                .clone()
4411                .unwrap_or_else(|| from.primary.name.clone());
4412            return self
4413                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4414                .map(Some);
4415        }
4416        // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4417        // COLUMNS (...))`. Materialise the row stream + schema by
4418        // walking the row path, then run the regular pipeline over it.
4419        if let Some(jt) = &from.primary.json_table {
4420            let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4421            let alias = from
4422                .primary
4423                .alias
4424                .clone()
4425                .unwrap_or_else(|| from.primary.name.clone());
4426            return self
4427                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4428                .map(Some);
4429        }
4430        if from.primary.table_fn_call.is_some() {
4431            let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4432            // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4433            // (from 1, in output order) AFTER the function's own columns. The
4434            // alias list names it like any other, which is why it is appended
4435            // BEFORE the renaming pass below.
4436            let rows = if from.primary.with_ordinality {
4437                schema_cols.push(ColumnSchema::new(
4438                    "ordinality".to_string(),
4439                    DataType::BigInt,
4440                    false,
4441                ));
4442                rows.into_iter()
4443                    .enumerate()
4444                    .map(|(i, r)| {
4445                        let mut vals = r.values;
4446                        vals.push(Value::BigInt(i as i64 + 1));
4447                        Row::new(vals)
4448                    })
4449                    .collect()
4450            } else {
4451                rows
4452            };
4453            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4454                if let Some(col) = schema_cols.get_mut(i) {
4455                    col.name = new_name.clone();
4456                }
4457            }
4458            let alias = from
4459                .primary
4460                .alias
4461                .clone()
4462                .unwrap_or_else(|| from.primary.name.clone());
4463            return self
4464                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4465                .map(Some);
4466        }
4467        // v7.37.17 (17.6 siblings) — plain derived table in primary
4468        // position: `FROM ( SELECT … ) alias` (no joins). The inner
4469        // SELECT materialises once (it is uncorrelated by
4470        // construction), then the outer projection / WHERE /
4471        // aggregate / ORDER BY pipeline runs over the synthetic
4472        // table. Joined derived tables keep riding the LATERAL
4473        // machinery in join.rs.
4474        if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4475            // v7.39 (round 727) — flatten first. A simple derived table
4476            // (bare-column projection over one stored table, nothing that
4477            // changes cardinality or order) used to force the inner
4478            // SELECT through the SERIAL row-at-a-time projection pipeline
4479            // just to materialise a synthetic table the outer query then
4480            // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4481            // measured 18.6 ms against PG's 5 — and bare count over the
4482            // same filter WITHOUT the wrapper is 2 ms here, because it
4483            // rides the fused parallel lane. Rewriting to the unwrapped
4484            // form is PG's subquery pull-up; the whole tree gets the
4485            // fast lanes back.
4486            if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4487                return self.exec_select_cancel(&flat, cancel).map(Some);
4488            }
4489            // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4490            // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4491            // ORDER BY never changes the row count, and OFFSET drops
4492            // exactly k. The materialising path sorted 500k rows to
4493            // count 10k (57 ms); PG runs its parallel sort anyway
4494            // (28 ms). The rewrite skips the sort entirely on both
4495            // counts — a plan PG itself does not have.
4496            if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4497                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4498            }
4499            // v7.39 (round 743) — `count(*) OVER a derived whose only
4500            // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4501            // a constant-length array unnests to exactly k rows per
4502            // input row, NULL elements included. PG expands the set to
4503            // count it (6.6 ms on the panel cell); the identity doesn't.
4504            if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4505                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4506            }
4507            return self
4508                .exec_select_derived(stmt, &from.primary, cancel)
4509                .map(Some);
4510        }
4511        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4512        // [, step])` set-returning source. Dispatch mirrors UNNEST:
4513        // materialise the row stream from a single eval pass, then
4514        // run the regular projection / WHERE / ORDER BY / LIMIT
4515        // pipeline over the synthetic single-column table.
4516        if from.primary.generate_series_args.is_some() {
4517            return self
4518                .exec_select_generate_series(stmt, &from.primary, cancel)
4519                .map(Some);
4520        }
4521        Ok(None)
4522    }
4523
4524    /// Pick an index seek for this WHERE, if any of the four apply:
4525    /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4526    ///
4527    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4528    /// frame reason on `try_from_shape_paths`: in a debug build a
4529    /// closure's locals belong to the enclosing frame, and this one is
4530    /// four seek attempts wide on a function that nests.
4531    #[inline(never)]
4532    fn pick_indexed_rows<'r>(
4533        &'r self,
4534        stmt: &SelectStatement,
4535        table: &'r spg_storage::Table,
4536        schema_cols: &[spg_storage::ColumnSchema],
4537        alias: &str,
4538        ctx: &crate::eval::EvalContext<'_>,
4539        seek_snapshot: &crate::Snapshot,
4540    ) -> Option<crate::index_access::Seeked<'r>> {
4541        stmt.where_.as_ref().and_then(|w| {
4542            // BTree / col=literal seek first — covers the v7.11.3 multi-
4543            // column AND case and the leading-column equality lookup.
4544            try_index_seek(
4545                w,
4546                schema_cols,
4547                self.active_catalog(),
4548                table,
4549                alias,
4550                seek_snapshot,
4551                ctx.mysql_dialect,
4552            )
4553            .or_else(|| {
4554                // v7.12.3 — GIN-accelerated `WHERE col @@
4555                // tsquery` when the column has a `USING gin`
4556                // index. Returns an over-approximate candidate
4557                // set; the WHERE re-eval loop below verifies
4558                // the full `@@` predicate per row.
4559                try_gin_seek(
4560                    w,
4561                    schema_cols,
4562                    self.active_catalog(),
4563                    table,
4564                    alias,
4565                    ctx,
4566                    seek_snapshot,
4567                )
4568                .map(crate::index_access::Seeked::over_approximate)
4569            })
4570            .or_else(|| {
4571                // v7.15.0 — trigram-GIN-accelerated
4572                // `WHERE col LIKE / ILIKE '<pat>'` when the
4573                // column has a `gin_trgm_ops` GIN index.
4574                // Over-approximate candidate set; the WHERE
4575                // re-eval verifies the LIKE per row.
4576                try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4577                    .map(crate::index_access::Seeked::over_approximate)
4578            })
4579            .or_else(|| {
4580                // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4581                // accelerated `WHERE col @> <jsonb_literal>`
4582                // when the column has a `USING gin` index. The
4583                // posting-list intersection returns an over-
4584                // approximate candidate set; the WHERE re-eval
4585                // verifies the full `@>` predicate per row.
4586                try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4587                    .map(crate::index_access::Seeked::over_approximate)
4588            })
4589        })
4590    }
4591
4592    /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4593    /// the two `count(*)` short-circuits. Out-of-line for the frame
4594    /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4595    /// of them, and in a debug build their locals sit in the frame
4596    /// regardless.
4597    #[inline(never)]
4598    fn try_seek_fast_paths(
4599        &self,
4600        stmt: &SelectStatement,
4601        table: &spg_storage::Table,
4602        schema_cols: &[spg_storage::ColumnSchema],
4603        alias: &str,
4604        seek_snapshot: &crate::Snapshot,
4605        cancel: CancelToken<'_>,
4606    ) -> Result<Option<QueryResult>, EngineError> {
4607        if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4608            // NSW kNN dispatches against the hot-tier vector index only
4609            // (vector cells aren't promoted to cold segments), so wrap
4610            // the returned row indices as `Cow::Borrowed` for the
4611            // unified `materialise_in_order` shape.
4612            let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4613                .into_iter()
4614                .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4615                .collect();
4616            return materialise_in_order(stmt, schema_cols, alias, &ordered, self.speaks_mysql)
4617                .map(Some);
4618        }
4619
4620        // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4621        // the scan via the BTree iterator in the requested direction
4622        // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4623        // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4624        // the load-bearing consumer; this skips the materialise-every-
4625        // row + partial-sort tail entirely. Walker output is already
4626        // in ORDER BY order so `materialise_in_order` (no extra sort)
4627        // is the natural sink.
4628        if let Some(walked) = try_pk_walk_top_n(
4629            stmt,
4630            self.active_catalog(),
4631            table,
4632            schema_cols,
4633            alias,
4634            self,
4635            cancel,
4636            self.speaks_mysql,
4637        ) {
4638            return materialise_in_order(stmt, schema_cols, alias, &walked, self.speaks_mysql)
4639                .map(Some);
4640        }
4641
4642        // Index seek: if WHERE is `col = literal` (or commuted) and the
4643        // referenced column has an index, dispatch each locator through
4644        // the catalog (hot tier → borrow, cold tier → page-read +
4645        // decode) and iterate just those rows. Otherwise fall back to a
4646        // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4647        // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4648        // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4649        // we don't pay the row materialisation cost twice. Returns
4650        // a bare `Rows{count}` if the shape matches.
4651        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
4652            && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4653        {
4654            return Ok(Some(out));
4655        }
4656        // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4657        // locators directly, skipping row materialisation + WHERE re-eval.
4658        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
4659            && let Some(out) = self.try_count_star_indexed_range_fast(
4660                stmt,
4661                table,
4662                schema_cols,
4663                alias,
4664                seek_snapshot,
4665            )
4666        {
4667            return Ok(Some(out));
4668        }
4669        Ok(None)
4670    }
4671
4672    /// The two rewrites that must happen before the FROM clause is even
4673    /// looked at: a meta-view reference needs the catalog views
4674    /// materialised, and a windowed projection belongs to the window
4675    /// executor. Out-of-line for the frame reason on
4676    /// `try_from_shape_paths`.
4677    #[inline(never)]
4678    fn try_pre_from_paths(
4679        &self,
4680        stmt: &SelectStatement,
4681        cancel: CancelToken<'_>,
4682    ) -> Result<Option<QueryResult>, EngineError> {
4683        if !self.meta_views_materialised && select_references_meta_view(stmt) {
4684            return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4685        }
4686        // v4.12: window-function path. When the projection contains
4687        // any `name(args) OVER (...)` we route to the dedicated
4688        // executor — partition + sort + per-row window value before
4689        // the regular projection.
4690        if select_has_window(stmt) {
4691            // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4692            // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4693            // needs the aggregation done first, then windows over the grouped
4694            // rows. Rewrite to an aggregate derived subquery + outer window query
4695            // (which the window-over-derived path, D.13, executes). Only fires on
4696            // the currently-erroring agg+window+GROUP BY shape, so it can't
4697            // regress working window-only or aggregate-only queries.
4698            if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4699                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4700            }
4701            return self.exec_select_with_window(stmt, cancel).map(Some);
4702        }
4703        Ok(None)
4704    }
4705
4706    /// A projection naming `ctid` or another system column: the schema
4707    /// has to be widened with them before the scan. Out-of-line for the
4708    /// frame reason on `try_from_shape_paths`.
4709    #[inline(never)]
4710    fn try_ctid_projection(
4711        &self,
4712        stmt: &SelectStatement,
4713        primary: &spg_sql::ast::TableRef,
4714        table: &spg_storage::Table,
4715        schema_cols: &[spg_storage::ColumnSchema],
4716        alias: &str,
4717        cancel: CancelToken<'_>,
4718    ) -> Result<Option<QueryResult>, EngineError> {
4719        if references_ctid(stmt) {
4720            let snapshot = self.current_snapshot();
4721            let mut ext_cols = schema_cols.to_vec();
4722            for name in SYSTEM_COLUMNS {
4723                ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4724            }
4725            let table_oid =
4726                crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4727                    .unwrap_or(0);
4728            let headers = table.headers();
4729            let rows: Vec<Row<'static>> = table
4730                .scan_visible(&snapshot)
4731                .map(|(i, r)| {
4732                    let mut vals = r.values.clone();
4733                    // One block, offsets from 1, as PG numbers them.
4734                    vals.push(Value::Tid(0, i as u32 + 1));
4735                    let h = headers.get(i);
4736                    vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4737                    vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4738                    // SPG keeps no per-statement command ids; PG shows 0 for
4739                    // every row a reader can see, which is every row here.
4740                    vals.push(Value::Cid(0));
4741                    vals.push(Value::Cid(0));
4742                    vals.push(Value::BigInt(table_oid));
4743                    Row::new(vals)
4744                })
4745                .collect();
4746            return self
4747                .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4748                .map(Some);
4749        }
4750        Ok(None)
4751    }
4752
4753    /// A sequence read as a one-row relation (`SELECT last_value FROM
4754    /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4755    /// the frame reason on `try_from_shape_paths`.
4756    #[inline(never)]
4757    fn try_sequence_relation(
4758        &self,
4759        stmt: &SelectStatement,
4760        primary: &spg_sql::ast::TableRef,
4761        cancel: CancelToken<'_>,
4762    ) -> Result<Option<QueryResult>, EngineError> {
4763        if self.active_catalog().get(&primary.name).is_none()
4764            && let Some(seq) = self.active_catalog().sequence(&primary.name)
4765        {
4766            let rows = alloc::vec![Row::new(alloc::vec![
4767                Value::BigInt(seq.last_value),
4768                Value::BigInt(0),
4769                Value::Bool(seq.is_called),
4770            ])];
4771            let schema_cols = alloc::vec![
4772                ColumnSchema::new("last_value", DataType::BigInt, false),
4773                ColumnSchema::new("log_cnt", DataType::BigInt, false),
4774                ColumnSchema::new("is_called", DataType::Bool, false),
4775            ];
4776            let alias = primary
4777                .alias
4778                .clone()
4779                .unwrap_or_else(|| primary.name.clone());
4780            return self
4781                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4782                .map(Some);
4783        }
4784        Ok(None)
4785    }
4786
4787    pub(crate) fn exec_bare_select_cancel(
4788        &self,
4789        stmt: &SelectStatement,
4790        cancel: CancelToken<'_>,
4791    ) -> Result<QueryResult, EngineError> {
4792        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4793        // is meaningless without an ORDER BY; PG raises a hard
4794        // error and SPG mirrors the surface so the same DDL/app
4795        // path behaves identically on cutover.
4796        check_with_ties_requires_order_by(stmt)?;
4797        // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4798        // PG rejects window calls there outright. Checked here rather than
4799        // on the window path: `HAVING row_number() OVER () = 1` has no
4800        // window in its projection at all.
4801        crate::window::reject_window_in_row_clauses(stmt)?;
4802        // v7.39 (round 232) — the ORDER BY legality rules (positional
4803        // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4804        // check: before anything scans.
4805        crate::orderby::check_order_by_legality(stmt)?;
4806        // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4807        // equivalent statement the regular executor handles (merged join
4808        // columns collapse to a single unqualified output column; NATURAL
4809        // gets its common-column ON synthesised). The rewrite clears the
4810        // flags, so this re-entrant call is a no-op on the second pass.
4811        if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4812            return self.exec_bare_select_cancel(&rewritten, cancel);
4813        }
4814        // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4815        // exactly the group keys, IS a DISTINCT and was paying for the
4816        // aggregate executor to find that out. Same placement and shape
4817        // as the desugar above; the rewrite clears `group_by`, so the
4818        // re-entry is a no-op on the second pass. See `baregroup` for
4819        // what the gate rules out.
4820        if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4821            return self.exec_bare_select_cancel(&rewritten, cancel);
4822        }
4823        // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4824        // operand in a security-barrier subquery, then re-enter (the wrapped
4825        // operands are no longer bare RLS tables, so this is a no-op on the
4826        // second pass).
4827        if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4828            return self.exec_bare_select_cancel(&rewritten, cancel);
4829        }
4830        // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4831        // AND the RLS USING predicate into a single-table SELECT's WHERE.
4832        // Superuser sessions and non-RLS tables get `None` (no clone, no
4833        // change). Applied inline (shadowing `stmt`) rather than via re-entry
4834        // so it can't re-inject on a recursive pass.
4835        let rls_stmt;
4836        let stmt = match self.rls_select_predicate(stmt)? {
4837            Some(pred) => {
4838                let mut s = stmt.clone();
4839                s.where_ = Some(match s.where_.take() {
4840                    Some(existing) => spg_sql::ast::Expr::Binary {
4841                        lhs: alloc::boxed::Box::new(existing),
4842                        op: spg_sql::ast::BinOp::And,
4843                        rhs: alloc::boxed::Box::new(pred),
4844                    },
4845                    None => pred,
4846                });
4847                rls_stmt = s;
4848                &rls_stmt
4849            }
4850            None => stmt,
4851        };
4852        // v7.16.2 — same meta-view dispatch as
4853        // `exec_select_cancel`, applied here too because
4854        // `subquery_replacement` enters this function directly
4855        // for Exists / ScalarSubquery / InSubquery resolution
4856        // (bypassing the top-level entry to avoid double
4857        // subquery walking). Without this dispatch the subquery
4858        // hits `__spg_info_columns` and reports TableNotFound.
4859        if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4860            return Ok(done);
4861        }
4862        // Constant SELECT (no FROM) — evaluate each item once against an
4863        // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4864        // `SELECT '7'::INT`. Column references will surface as
4865        // ColumnNotFound on eval since the schema is empty.
4866        let Some(from) = &stmt.from else {
4867            return self.exec_constant_select(stmt);
4868        };
4869        // Multi-table FROM (one or more joined peers) goes through the
4870        // nested-loop join executor. Single-table FROM stays on the
4871        // existing scan + index-seek path.
4872        if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4873            return Ok(done);
4874        }
4875        // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4876        // tested — eight ORDER BY shapes byte-identical spilled against
4877        // in-memory, with 103 runs opened to prove the spill ran — and it
4878        // loses on wall clock, which is a hard stop whatever the memory
4879        // buys. Measured round 865, same psql client both sides, same
4880        // machine, row counts verified, and both sides confirmed to be
4881        // doing an external merge rather than an indexed walk:
4882        //
4883        //   PG18        178.7 - 187.0 ms   Sort Method: external merge, 85 MB
4884        //   SPG spilled 269.7 - 299.6 ms   33 spill files at peak
4885        //
4886        // Non-overlapping, about 1.55x. Re-enable by restoring the call
4887        // below once that closes; nothing else has to change, which is
4888        // the point of it being a separate path.
4889        //
4890        //   if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4891        //       return Ok(done);
4892        //   }
4893        //
4894        // v7.37 (round 882) — this walk stays unhooked, but its streaming
4895        // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4896        // bail in `try_exec_joined_streaming`. Collecting the answer was
4897        // most of what this one cost: handing rows over as the merge
4898        // produces them holds peak to the budget plus one row, and the
4899        // wall clock lands inside PG18's range rather than 1.55x outside
4900        // it. Numbers in `extsort.rs`'s header.
4901        let primary = &from.primary;
4902        // v7.39 (round 244) — a sequence is selectable as a one-row relation
4903        // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4904        // read it). Synthesize PG's three columns.
4905        if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4906            return Ok(done);
4907        }
4908        let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4909            StorageError::TableNotFound {
4910                name: primary.name.clone(),
4911            }
4912        })?;
4913        let schema_cols = &table.schema().columns;
4914        // The qualifier accepted on column refs is the alias (if any) else the
4915        // bare table name.
4916        let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4917        // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4918        // system columns at all: `SELECT ctid FROM t` answered "column
4919        // \"ctid\" does not exist", which takes out the dedup idiom every
4920        // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4921        // GROUP BY key)`.
4922        //
4923        // The value comes from the row's position, which the scan already
4924        // yields; the column is appended to the schema and the rows only
4925        // when the statement asks for it, so nothing else pays for it. That
4926        // also routes the query down the general path, past the index fast
4927        // paths below — they hand back rows without positions, and a ctid
4928        // that was sometimes right would be worse than none.
4929        if let Some(done) =
4930            self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4931        {
4932            return Ok(done);
4933        }
4934        let ctx = self.ev_ctx(schema_cols, Some(alias));
4935
4936        // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4937        // WHERE and an NSW index on `col` skips the full scan. The
4938        // walk returns rows already in ascending-distance order, so
4939        // ORDER BY / LIMIT are honoured implicitly.
4940        // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4941        // and thread it into every index-seek fast path below. No-op
4942        // today (every hot header is committed-alive).
4943        let seek_snapshot = self.current_snapshot();
4944        if let Some(done) =
4945            self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4946        {
4947            return Ok(done);
4948        }
4949        // full scan over the hot tier (cold-tier rows are only reached
4950        // via index seek in v5.1 — full table scans against cold-tier
4951        // data ship in v5.2 with the freezer's per-segment scan API).
4952        let indexed_rows =
4953            self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4954
4955        // Aggregate path: filter rows first, then hand off to the
4956        // aggregate executor which does its own projection + ORDER BY.
4957        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
4958            return self.run_single_table_aggregate(
4959                stmt,
4960                table,
4961                schema_cols,
4962                alias,
4963                indexed_rows,
4964                cancel,
4965            );
4966        }
4967        self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4968    }
4969
4970    /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4971    /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4972    /// uncorrelated FROM-primary case is the simpler shape, used by
4973    /// e2e pins. Materialises the (key, value) pair stream into a
4974    /// synthetic two-column TEXT table, then routes through the
4975    /// regular projection / WHERE / ORDER BY pipeline.
4976    /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4977    /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4978    /// item into (rows, schema). `outer_doc` is `Some` only when this
4979    /// is a NESTED level being expanded against a parent row item's
4980    /// already-parsed sub-document; the top-level call parses the doc
4981    /// expr itself. Row/column paths reuse the existing jsonpath
4982    /// evaluator (`json::json_table_path`); coercion reuses
4983    /// `coerce_value` on the JSON scalar text, so a json string
4984    /// coerces to DATE by its content, matching PG.
4985    #[allow(clippy::type_complexity)]
4986    pub(crate) fn json_table_rows(
4987        &self,
4988        jt: &spg_sql::ast::JsonTable,
4989        outer_doc: Option<&crate::json::JsonValue>,
4990    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4991        // Column schema is static (independent of data): flatten the
4992        // COLUMNS tree in declaration order (NESTED contributes its
4993        // children inline, the PG output shape).
4994        let schema = json_table_schema(&jt.columns);
4995
4996        // PASSING variables → a single JsonValue object the jsonpath
4997        // engine reads `$name` from.
4998        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4999        let ctx = EvalContext::new(&empty_schema, None);
5000        let dummy = Row::new(alloc::vec::Vec::new());
5001        let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
5002            None
5003        } else {
5004            let mut entries = alloc::vec::Vec::new();
5005            for (name, e) in &jt.passing {
5006                let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
5007                entries.push((name.clone(), value_to_json_value(&v)));
5008            }
5009            Some(crate::json::JsonValue::Object(entries))
5010        };
5011
5012        // The document root: a NESTED level gets it from the parent;
5013        // the top level parses its doc expr.
5014        let root_owned;
5015        let root: &crate::json::JsonValue = match outer_doc {
5016            Some(d) => d,
5017            None => {
5018                let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
5019                let src = match &doc_val {
5020                    Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
5021                    Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
5022                    other => {
5023                        return Err(EngineError::Unsupported(alloc::format!(
5024                            "JSON_TABLE document must be json/text, got {}",
5025                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
5026                        )));
5027                    }
5028                };
5029                root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
5030                &root_owned
5031            }
5032        };
5033
5034        let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
5035            .map_err(EngineError::Eval)?;
5036        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5037        for (idx, item) in items.iter().enumerate() {
5038            self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
5039        }
5040        Ok((rows, schema))
5041    }
5042
5043    /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
5044    /// Regular columns produce one value each; a NESTED column expands
5045    /// as an outer join (each nested match → one row sharing the
5046    /// parent cells; no nested match → one row with the nested cells
5047    /// NULL). Sibling NESTED at one level cross by concatenation of
5048    /// their independent expansions (PG's UNION-of-outer shape).
5049    fn json_table_emit_item(
5050        &self,
5051        jt: &spg_sql::ast::JsonTable,
5052        item: &crate::json::JsonValue,
5053        ordinality: usize,
5054        vars: Option<&crate::json::JsonValue>,
5055        out: &mut alloc::vec::Vec<Row<'static>>,
5056    ) -> Result<(), EngineError> {
5057        use spg_sql::ast::JsonTableColumn as C;
5058        // Parent cells (regular + ordinality), left-to-right; NESTED
5059        // columns contribute a run of child cells appended after.
5060        let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5061        let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
5062            alloc::vec::Vec::new();
5063        let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5064        for col in &jt.columns {
5065            match col {
5066                C::Ordinality { .. } => {
5067                    parent_cells.push(Value::BigInt(ordinality as i64 + 1));
5068                }
5069                C::Regular { .. } => {
5070                    parent_cells.push(self.json_table_column_value(col, item, vars)?);
5071                }
5072                C::Nested { path, columns } => {
5073                    // Recurse: a nested JSON_TABLE over `item` filtered
5074                    // by `path`, with the same PASSING vars.
5075                    let sub = spg_sql::ast::JsonTable {
5076                        doc: jt.doc.clone(), // unused (outer_doc provided)
5077                        row_path: path.clone(),
5078                        columns: columns.clone(),
5079                        passing: alloc::vec::Vec::new(),
5080                    };
5081                    let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
5082                    nested_widths.push(nschema.len());
5083                    nested_runs.push(nrows);
5084                }
5085            }
5086        }
5087        if nested_runs.is_empty() {
5088            out.push(Row::new(parent_cells));
5089            return Ok(());
5090        }
5091        // PG sibling-NESTED semantics: each sibling expands
5092        // INDEPENDENTLY and the results CONCATENATE — a row from
5093        // sibling s fills only s's cells, every other sibling's cells
5094        // NULL. An empty sibling contributes ZERO rows (not a NULL
5095        // row). Only when EVERY sibling is empty does the parent still
5096        // emit one all-NULL row (the outer-join guarantee that a parent
5097        // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
5098        // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
5099        let before = out.len();
5100        for (s_idx, run) in nested_runs.iter().enumerate() {
5101            for nrow in run {
5102                let mut cells = parent_cells.clone();
5103                for (o_idx, w) in nested_widths.iter().enumerate() {
5104                    if o_idx == s_idx {
5105                        cells.extend(nrow.values.iter().cloned());
5106                    } else {
5107                        for _ in 0..*w {
5108                            cells.push(Value::Null);
5109                        }
5110                    }
5111                }
5112                out.push(Row::new(cells));
5113            }
5114        }
5115        if out.len() == before {
5116            // Every sibling empty → one all-NULL nested row.
5117            let mut cells = parent_cells.clone();
5118            for w in &nested_widths {
5119                for _ in 0..*w {
5120                    cells.push(Value::Null);
5121                }
5122            }
5123            out.push(Row::new(cells));
5124        }
5125        Ok(())
5126    }
5127
5128    /// v7.39 (round 205) — evaluate one Regular column against a row
5129    /// item: EXISTS → bool; else path → at most one value, coerced to
5130    /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
5131    fn json_table_column_value(
5132        &self,
5133        col: &spg_sql::ast::JsonTableColumn,
5134        item: &crate::json::JsonValue,
5135        vars: Option<&crate::json::JsonValue>,
5136    ) -> Result<Value<'static>, EngineError> {
5137        use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
5138        let C::Regular {
5139            name,
5140            ty,
5141            path,
5142            exists,
5143            format_json,
5144            wrapper,
5145            on_empty,
5146            on_error,
5147        } = col
5148        else {
5149            unreachable!("caller guards Regular");
5150        };
5151        let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5152        if *exists {
5153            return Ok(Value::Bool(!matches.is_empty()));
5154        }
5155        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5156        let ctx = EvalContext::new(&empty_schema, None);
5157        let dummy = Row::new(alloc::vec::Vec::new());
5158        let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5159            match b {
5160                B::Null => Ok(Some(Value::Null)),
5161                B::Error => Ok(None),
5162                B::Default(e) => Ok(Some(
5163                    eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5164                )),
5165            }
5166        };
5167        // Empty match set → ON EMPTY.
5168        if matches.is_empty() {
5169            return match default_of(on_empty)? {
5170                Some(v) => coerce_json_table_default(v, *ty, name),
5171                None => Err(EngineError::Unsupported(alloc::format!(
5172                    "no SQL/JSON item found for JSON_TABLE column {name:?}"
5173                ))),
5174            };
5175        }
5176        let first = &matches[0];
5177        // FORMAT JSON: return the PG-canonical json representation.
5178        // WITH WRAPPER wraps the whole match SET in an array (even a
5179        // single scalar → `[5]`); without it, the single match's json.
5180        if *format_json {
5181            let text = if *wrapper {
5182                crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5183            } else {
5184                first.canonical_json_text()
5185            };
5186            return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5187        }
5188        if first.is_json_null() {
5189            return Ok(Value::Null);
5190        }
5191        // Coerce the scalar text to the declared type; on failure → ON
5192        // ERROR (default NULL, DEFAULT expr, or raise).
5193        let dt = crate::conversions::column_type_to_data_type(*ty);
5194        let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5195        match crate::conversions::coerce_value(scalar, dt, name, 0) {
5196            Ok(v) => Ok(v),
5197            Err(e) => match default_of(on_error)? {
5198                Some(v) => coerce_json_table_default(v, *ty, name),
5199                None => Err(e),
5200            },
5201        }
5202    }
5203
5204    /// table function into (rows, default schema). Dispatch by name.
5205    pub(crate) fn table_fn_rows(
5206        &self,
5207        primary: &TableRef,
5208    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5209        let (fn_name, args) = primary
5210            .table_fn_call
5211            .as_deref()
5212            .expect("caller guards table_fn_call.is_some()");
5213        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5214        let ctx = EvalContext::new(&empty_schema, None);
5215        let dummy_row = Row::new(alloc::vec::Vec::new());
5216        let arg0: Option<Value<'static>> = match args.first() {
5217            Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5218            None => None,
5219        };
5220        match fn_name.as_str() {
5221            // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5222            // `…_recordset` (+ json_ variants). The row shape is the BASE
5223            // argument's declared type — a table's or a composite type's
5224            // column list — which only the catalog knows, so the parser hands
5225            // the raw arguments here rather than desugaring blind.
5226            "jsonb_populate_record"
5227            | "json_populate_record"
5228            | "jsonb_populate_recordset"
5229            | "json_populate_recordset" => {
5230                let type_name = match args.first() {
5231                    Some(Expr::Cast {
5232                        target: spg_sql::ast::CastTarget::Named(n),
5233                        ..
5234                    }) => n.clone(),
5235                    _ => {
5236                        return Err(EngineError::Unsupported(alloc::format!(
5237                            "{fn_name}(): first argument must name a row type, \
5238                             e.g. NULL::mytable"
5239                        )));
5240                    }
5241                };
5242                let cat = self.active_catalog();
5243                let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5244                    t.schema().columns.clone()
5245                } else if let Some(c) = cat.composite_types().get(&type_name) {
5246                    c.fields
5247                        .iter()
5248                        .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5249                        .collect()
5250                } else {
5251                    return Err(EngineError::Unsupported(alloc::format!(
5252                        "type \"{type_name}\" does not exist"
5253                    )));
5254                };
5255                let json_arg = match args.get(1) {
5256                    Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5257                    None => Value::Null,
5258                };
5259                // The set form iterates the JSON array; the scalar form is
5260                // the one-element case of the same walk.
5261                let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5262                    crate::json::array_element_rows(&json_arg, false, fn_name)
5263                        .map_err(EngineError::Eval)?
5264                        .into_iter()
5265                        .map(|s| s.map_or(Value::Null, Value::json))
5266                        .collect()
5267                } else if matches!(json_arg, Value::Null) {
5268                    alloc::vec::Vec::new()
5269                } else {
5270                    alloc::vec![json_arg]
5271                };
5272                let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5273                for doc in &docs {
5274                    let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5275                    for c in &cols {
5276                        // `->>` semantics: a missing key is NULL, present keys
5277                        // arrive as text and cast to the declared column type.
5278                        let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5279                            .map_err(EngineError::Eval)?;
5280                        let v = if matches!(raw, Value::Null) {
5281                            Value::Null
5282                        } else {
5283                            crate::conversions::coerce_value(raw, c.ty, "", 0)
5284                                .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5285                        };
5286                        vals.push(v);
5287                    }
5288                    rows.push(Row::new(vals));
5289                }
5290                Ok((rows, cols))
5291            }
5292            // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5293            // a text[] of 'name=value' reloptions/fdw options → one
5294            // (option_name, option_value) row per element. NULL or an
5295            // empty array yields zero rows (PG); an element without
5296            // '=' carries a NULL option_value, matching PG's split.
5297            "pg_options_to_table" => {
5298                let schema = alloc::vec![
5299                    ColumnSchema::new("option_name", DataType::Text, true),
5300                    ColumnSchema::new("option_value", DataType::Text, true),
5301                ];
5302                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5303                if let Some(Value::TextArray(items)) = arg0 {
5304                    for item in items.into_iter().flatten() {
5305                        let (name, value) = match item.split_once('=') {
5306                            Some((n, v)) => (Value::text(n), Value::text(v)),
5307                            None => (Value::text(item.as_str()), Value::Null),
5308                        };
5309                        rows.push(Row::new(alloc::vec![name, value]));
5310                    }
5311                }
5312                Ok((rows, schema))
5313            }
5314            // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5315            // PG18's per-sequence state SRF, (last_value, is_called).
5316            // pg_dump reads it joined to pg_sequence for every dumped
5317            // sequence's setval line. The oid resolves through the
5318            // same relation_oid mapping seqrelid publishes.
5319            "pg_get_sequence_data" => {
5320                let schema = alloc::vec![
5321                    ColumnSchema::new("last_value", DataType::BigInt, false),
5322                    ColumnSchema::new("is_called", DataType::Bool, false),
5323                ];
5324                let want = match arg0 {
5325                    Some(Value::Int(n)) => i64::from(n),
5326                    Some(Value::BigInt(n)) => n,
5327                    _ => {
5328                        return Err(EngineError::Unsupported(
5329                            "pg_get_sequence_data(): argument must be a sequence oid".into(),
5330                        ));
5331                    }
5332                };
5333                let cat = self.active_catalog();
5334                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5335                for (name, def) in cat.sequences_all() {
5336                    if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5337                        rows.push(Row::new(alloc::vec![
5338                            Value::BigInt(def.last_value),
5339                            Value::Bool(def.is_called),
5340                        ]));
5341                        break;
5342                    }
5343                }
5344                Ok((rows, schema))
5345            }
5346            "pg_partition_tree" => {
5347                let cols = alloc::vec![
5348                    ColumnSchema::new("relid".to_string(), DataType::Text, true),
5349                    ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5350                    ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5351                    ColumnSchema::new("level".to_string(), DataType::Int, true),
5352                ];
5353                let Some(Value::Text(name)) = &arg0 else {
5354                    // NULL (or missing) argument → zero rows (PG).
5355                    return Ok((alloc::vec::Vec::new(), cols));
5356                };
5357                let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5358                if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5359                    return Err(EngineError::Unsupported(alloc::format!(
5360                        "relation \"{name}\" does not exist"
5361                    )));
5362                }
5363                let rows = entries
5364                    .into_iter()
5365                    .map(|(relid, parent, isleaf, level)| {
5366                        Row::new(alloc::vec![
5367                            Value::text(relid),
5368                            parent.map_or(Value::Null, Value::text),
5369                            Value::Bool(isleaf),
5370                            #[allow(clippy::cast_possible_truncation)]
5371                            Value::Int(level as i32),
5372                        ])
5373                    })
5374                    .collect();
5375                Ok((rows, cols))
5376            }
5377            "pg_partition_ancestors" => {
5378                let cols =
5379                    alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5380                let Some(Value::Text(name)) = &arg0 else {
5381                    return Ok((alloc::vec::Vec::new(), cols));
5382                };
5383                let cat = self.active_catalog();
5384                if cat.get(name.as_ref()).is_none() {
5385                    return Err(EngineError::Unsupported(alloc::format!(
5386                        "relation \"{name}\" does not exist"
5387                    )));
5388                }
5389                // A relation outside any partition tree yields no rows (PG).
5390                let in_tree = cat
5391                    .get(name.as_ref())
5392                    .is_some_and(|t| t.schema().partition_role.is_some());
5393                let rows = if in_tree {
5394                    crate::partition_walks::ancestors_of(cat, name.as_ref())
5395                        .into_iter()
5396                        .map(|n| Row::new(alloc::vec![Value::text(n)]))
5397                        .collect()
5398                } else {
5399                    alloc::vec::Vec::new()
5400                };
5401                Ok((rows, cols))
5402            }
5403            // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5404            // saw, what each token was called, which dictionary took it
5405            // and what came out. It is a projection of the same tokenizer
5406            // and the same map the indexer uses, so it cannot describe a
5407            // pipeline other than the one that runs.
5408            "ts_debug" => {
5409                use crate::fts::{TokenType, TsDict};
5410                let cols = alloc::vec![
5411                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5412                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5413                    ColumnSchema::new("token".to_string(), DataType::Text, false),
5414                    ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5415                    ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5416                    ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5417                ];
5418                // PG's one-arg form uses the session configuration; the
5419                // two-arg form names one.
5420                let (cfg_name, text) = match (&arg0, args.get(1)) {
5421                    (Some(Value::Text(c)), Some(t)) => {
5422                        let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5423                        (c.to_string(), crate::eval::value_to_text(&v))
5424                    }
5425                    (Some(v), None) => (
5426                        alloc::string::String::from("english"),
5427                        crate::eval::value_to_text(v),
5428                    ),
5429                    _ => return Ok((alloc::vec::Vec::new(), cols)),
5430                };
5431                let english = match cfg_name
5432                    .trim()
5433                    .trim_start_matches("pg_catalog.")
5434                    .to_ascii_lowercase()
5435                    .as_str()
5436                {
5437                    "english" => true,
5438                    "simple" => false,
5439                    other => {
5440                        return Err(EngineError::Unsupported(alloc::format!(
5441                            "text search configuration \"{other}\" does not exist"
5442                        )));
5443                    }
5444                };
5445                let rows = crate::fts::tokenize_typed(&text)
5446                    .into_iter()
5447                    .map(|tok| {
5448                        let dict = tok.ty.dictionary(english);
5449                        let dname = dict.map(|d| match d {
5450                            TsDict::Simple => "simple",
5451                            TsDict::EnglishStem => "english_stem",
5452                        });
5453                        let folded = tok.text.to_lowercase();
5454                        let lexemes = dict.map(|d| match d {
5455                            TsDict::Simple => alloc::vec![Some(folded.clone())],
5456                            TsDict::EnglishStem => {
5457                                if crate::fts::is_english_stopword(&folded) {
5458                                    alloc::vec::Vec::new()
5459                                } else {
5460                                    alloc::vec![Some(crate::fts::porter_stem(&folded))]
5461                                }
5462                            }
5463                        });
5464                        Row::new(alloc::vec![
5465                            Value::text(tok.ty.alias()),
5466                            Value::text(tok.ty.description()),
5467                            Value::text(tok.text),
5468                            Value::TextArray(
5469                                dname
5470                                    .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5471                                    .unwrap_or_default(),
5472                            ),
5473                            dname.map_or(Value::Null, Value::text),
5474                            lexemes.map_or(Value::Null, Value::TextArray),
5475                        ])
5476                    })
5477                    .collect();
5478                let _ = TokenType::AsciiWord;
5479                Ok((rows, cols))
5480            }
5481            // v7.39 (round 651) — `ts_token_type('default')`, the list the
5482            // parser actually produces. It is a projection of the
5483            // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5484            // read, so the three cannot disagree about what a token is.
5485            "ts_token_type" => {
5486                use crate::fts::TokenType as T;
5487                let cols = alloc::vec![
5488                    ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5489                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5490                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5491                ];
5492                // PG takes the parser by name or oid; SPG has the one.
5493                if let Some(Value::Text(p)) = &arg0
5494                    && !p.eq_ignore_ascii_case("default")
5495                    && !p.eq_ignore_ascii_case("pg_catalog.default")
5496                {
5497                    return Err(EngineError::Unsupported(alloc::format!(
5498                        "text search parser \"{p}\" does not exist"
5499                    )));
5500                }
5501                const TYPES: &[T] = &[
5502                    T::AsciiWord,
5503                    T::Word,
5504                    T::NumWord,
5505                    T::Email,
5506                    T::Url,
5507                    T::Host,
5508                    T::SFloat,
5509                    T::Version,
5510                    T::HwordNumPart,
5511                    T::HwordPart,
5512                    T::HwordAsciiPart,
5513                    T::Blank,
5514                    T::Tag,
5515                    T::Protocol,
5516                    T::NumHword,
5517                    T::AsciiHword,
5518                    T::Hword,
5519                    T::UrlPath,
5520                    T::File,
5521                    T::Float,
5522                    T::Int,
5523                    T::Uint,
5524                    T::Entity,
5525                ];
5526                let rows = TYPES
5527                    .iter()
5528                    .map(|t| {
5529                        Row::new(alloc::vec![
5530                            Value::Int(*t as i32),
5531                            Value::text(t.alias()),
5532                            Value::text(t.description()),
5533                        ])
5534                    })
5535                    .collect();
5536                Ok((rows, cols))
5537            }
5538            // v7.39 (read01 round 65) — a set-returning USER function in FROM
5539            // (`FROM rows_of(2)`). Its body runs through the real executor, like
5540            // every other function body since round 63.
5541            other => {
5542                if !self.active_catalog().functions_named(other).is_empty() {
5543                    return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5544                }
5545                Err(EngineError::Unsupported(alloc::format!(
5546                    "table function {other}() is not supported in FROM"
5547                )))
5548            }
5549        }
5550    }
5551
5552    /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5553    /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5554    /// are bound into it as literals and it goes through the read path, so the
5555    /// rows it yields are exactly the rows a hand-written query would see.
5556    ///
5557    /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5558    /// text)` names them, and a `SETOF <scalar>` yields a single column named
5559    /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5560    /// shows.
5561    fn exec_setof_user_function(
5562        &self,
5563        name: &str,
5564        args: &[spg_sql::ast::Expr],
5565        // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5566        // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5567        alias: Option<&str>,
5568    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5569        // The call's arguments belong to the ENCLOSING query, so they are
5570        // evaluated here and the body sees values.
5571        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5572        let arg_ctx = self.ev_ctx(&empty, None);
5573        let dummy = Row::new(alloc::vec::Vec::new());
5574        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5575        for a in args {
5576            vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5577        }
5578        self.setof_rows_of(name, &vals, alias)
5579    }
5580
5581    /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5582    /// arguments. Shared by the FROM position and the target-list expansion, so
5583    /// a function cannot behave differently depending on where it is called.
5584    pub(crate) fn setof_rows_of(
5585        &self,
5586        name: &str,
5587        arg_values: &[Value<'static>],
5588        alias: Option<&str>,
5589    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5590        let cat = self.active_catalog();
5591        let overloads = cat.functions_named(name);
5592        let def = overloads
5593            .iter()
5594            .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5595            .ok_or_else(|| {
5596                EngineError::Unsupported(alloc::format!(
5597                    "function {name} does not exist with {} argument(s)",
5598                    arg_values.len()
5599                ))
5600            })?;
5601        let declared = def.returns.trim().to_string();
5602        let upper = declared.to_ascii_uppercase();
5603        if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5604            return Err(EngineError::Unsupported(alloc::format!(
5605                "function {name}() does not return a set — it cannot be used in FROM"
5606            )));
5607        }
5608
5609        let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5610        // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5611        // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5612        if def.language.eq_ignore_ascii_case("plpgsql") {
5613            let out_rows = self
5614                .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5615                .map_err(EngineError::Eval)?;
5616            let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5617            let rows = out_rows.into_iter().map(Row::new).collect();
5618            return Ok((rows, cols));
5619        }
5620        let body = def.body.trim().trim_end_matches(';');
5621        let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5622            EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5623        })?;
5624        let spg_sql::ast::Statement::Select(body_select) = stmt else {
5625            return Err(EngineError::Unsupported(alloc::format!(
5626                "function {name}(): a set-returning body must be a SELECT"
5627            )));
5628        };
5629        let arg_names = spg_storage::function_arg_names(&def.args_repr);
5630        let bound = crate::eval::bind_user_fn_args(
5631            self.active_catalog(),
5632            &body_select,
5633            &arg_names,
5634            arg_values,
5635        )
5636        .map_err(EngineError::Eval)?;
5637        let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5638        let QueryResult::Rows { columns, rows } = out else {
5639            return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5640        };
5641        // Name the columns from the DECLARED shape — the same rule the plpgsql
5642        // path above uses, so a body's language cannot change the row shape.
5643        let cols = setof_column_shape_from(&declared, name, alias, &columns);
5644        Ok((rows, cols))
5645    }
5646
5647    fn exec_select_jsonb_each_text(
5648        &self,
5649        stmt: &SelectStatement,
5650        primary: &TableRef,
5651        cancel: CancelToken<'_>,
5652    ) -> Result<QueryResult, EngineError> {
5653        let (each_fn, arg_expr) = primary
5654            .jsonb_each_text_arg
5655            .as_ref()
5656            .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5657            .expect("caller guards jsonb_each_text_arg.is_some()");
5658        // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5659        // forms keep JSON rendering in the value column (JSON null
5660        // stays jsonb 'null', strings keep their quotes).
5661        let as_text = each_fn.ends_with("_text");
5662        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5663        let ctx = EvalContext::new(&empty_schema, None);
5664        let dummy_row = Row::new(alloc::vec::Vec::new());
5665        let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5666        let pairs =
5667            crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5668        let rows: alloc::vec::Vec<Row<'static>> = pairs
5669            .into_iter()
5670            .map(|(k, v)| {
5671                let key_val = Value::text(k);
5672                let value_val = match v {
5673                    Some(s) if as_text => Value::text(s),
5674                    Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5675                    None => Value::Null,
5676                };
5677                Row::new(alloc::vec![key_val, value_val])
5678            })
5679            .collect();
5680        let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5681        let value_dtype = if as_text {
5682            spg_storage::DataType::Text
5683        } else {
5684            spg_storage::DataType::Json
5685        };
5686        let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5687        let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5688        let mut schema_cols = alloc::vec![key_col, value_col];
5689        // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5690        // LATERAL-position form of the same call already honours it.
5691        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5692            if let Some(col) = schema_cols.get_mut(i) {
5693                col.name = new_name.clone();
5694            }
5695        }
5696        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5697        // `EvalContext::new` drops it and every catalog-dependent cast
5698        // (regclass / enum / composite / domain) silently degrades.
5699        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5700        // WHERE.
5701        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5702            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5703            for row in rows {
5704                cancel.check()?;
5705                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5706                if matches!(v, Value::Bool(true)) {
5707                    out.push(row);
5708                }
5709            }
5710            out
5711        } else {
5712            rows
5713        };
5714        // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5715        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
5716            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5717            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5718                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5719                    .map_err(|err| match err {
5720                        EngineError::Eval(ev) => ev,
5721                        other => eval::EvalError::TypeMismatch {
5722                            detail: alloc::format!("{other}"),
5723                        },
5724                    })
5725            };
5726            // v7.39 (round 656) — hand the rows over as they are rather than
5727            // collecting a second vector of `RowRef` wrappers. Note this is
5728            // a set-returning-function path, NOT the relational scan: the
5729            // measured O(rows) cost lived in `run_single_table_aggregate`,
5730            // and converting these four first was a miss that cost a full
5731            // round — every test stayed green and the number did not move.
5732            let agg = aggregate::run(
5733                stmt,
5734                crate::join::AggRows::Owned(&filtered),
5735                &schema_cols,
5736                Some(&alias),
5737                Some(&agg_correlated),
5738                self.parallel_runner.0.as_deref(),
5739                Some(self.active_catalog()),
5740                Some(self),
5741            )?;
5742            return self.finish_agg_result(agg, stmt, cancel);
5743        }
5744        // Projection.
5745        let projection = build_projection(
5746            &stmt.items,
5747            &schema_cols,
5748            &alias,
5749            self.speaks_mysql,
5750            Some(self.active_catalog()),
5751        )?;
5752        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5753            alloc::vec::Vec::with_capacity(filtered.len());
5754        for row in &filtered {
5755            let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5756            for p in &projection {
5757                let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5758                vals.push(v);
5759            }
5760            projected_rows.push(Row::new(vals));
5761        }
5762        let columns: alloc::vec::Vec<ColumnSchema> = projection
5763            .iter()
5764            // v7.39 (read01 round 54) — keep the column's enum identity through
5765            // the projection (it lives outside the DataType lattice), or a
5766            // derived table / UNION / windowed result forgets it and any outer
5767            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5768            .map(|p| p.to_column_schema())
5769            .collect();
5770        // ORDER BY.
5771        if !stmt.order_by.is_empty() {
5772            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5773                .iter()
5774                .enumerate()
5775                .map(|(i, r)| -> Result<_, EngineError> {
5776                    let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5777                        .order_by
5778                        .iter()
5779                        .map(|ob| {
5780                            eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5781                        })
5782                        .collect();
5783                    Ok((i, keys?))
5784                })
5785                .collect::<Result<_, _>>()?;
5786            indexed.sort_by(|a, b| {
5787                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5788                    let o = &stmt.order_by[idx];
5789                    let cmp = order_by_value_cmp_in(
5790                        o.desc,
5791                        o.nulls_first,
5792                        ka,
5793                        kb,
5794                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5795                    );
5796                    if cmp != core::cmp::Ordering::Equal {
5797                        return cmp;
5798                    }
5799                }
5800                core::cmp::Ordering::Equal
5801            });
5802            projected_rows = indexed
5803                .into_iter()
5804                .map(|(i, _)| projected_rows[i].clone())
5805                .collect();
5806        }
5807        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5808        if stmt.distinct {
5809            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
5810            // spec folds EVERY text position, so a column declared
5811            // `COLLATE utf8mb4_bin` had its values merged here exactly the
5812            // way 3b494b6e fixed on the main scan path. The projection is
5813            // already in scope at each of these sites, so the mask needs no
5814            // new plumbing -- it was simply never asked for.
5815            projected_rows = dedup_rows(
5816                projected_rows,
5817                FoldSpec::of_masks(
5818                    scan_ctx.mysql_dialect,
5819                    &fold_mask(&projection),
5820                    &pad_mask(&projection),
5821                ),
5822            );
5823        }
5824        if let Some(offset) = stmt.offset_literal() {
5825            let off = (offset as usize).min(projected_rows.len());
5826            projected_rows.drain(..off);
5827        }
5828        if let Some(limit) = stmt.limit_literal() {
5829            projected_rows.truncate(limit as usize);
5830        }
5831        Ok(QueryResult::Rows {
5832            columns,
5833            rows: projected_rows,
5834        })
5835    }
5836
5837    /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5838    /// ( SELECT … ) alias` in primary position. The inner SELECT
5839    /// materialises once through the regular bare-select executor
5840    /// (UNION tails included), then the outer WHERE / aggregate /
5841    /// projection / ORDER BY / LIMIT pipeline runs over the
5842    /// synthetic table — the same post-materialisation shape as
5843    /// exec_select_jsonb_each_text, generalised to N columns.
5844    fn exec_select_derived(
5845        &self,
5846        stmt: &SelectStatement,
5847        primary: &TableRef,
5848        cancel: CancelToken<'_>,
5849    ) -> Result<QueryResult, EngineError> {
5850        let inner = primary
5851            .lateral_subquery
5852            .as_deref()
5853            .expect("caller guards lateral_subquery.is_some()");
5854        // exec_select_cancel is the union-aware wrapper — the inner
5855        // SELECT may carry UNION tails on stmt.unions.
5856        let QueryResult::Rows {
5857            columns: inner_cols,
5858            rows,
5859        } = self.exec_select_cancel(inner, cancel)?
5860        else {
5861            return Err(EngineError::Unsupported(
5862                "derived table subquery must return rows".into(),
5863            ));
5864        };
5865        let alias = primary
5866            .alias
5867            .clone()
5868            .unwrap_or_else(|| primary.name.clone());
5869        // `AS t(a, b)` renames the materialised columns positionally
5870        // (extra inner columns keep their own names, PG behaviour).
5871        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5872        // v7.39 (read01 round 78) — a column-alias list longer than the item is
5873        // the error PG reports; SPG used to let the extra names through and then
5874        // fail two layers downstream with "column not found: <the extra name>".
5875        let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5876        if primary.unnest_column_aliases.len() > n_out {
5877            return Err(EngineError::Unsupported(alloc::format!(
5878                "table \"{alias}\" has {n_out} columns available but {} columns specified",
5879                primary.unnest_column_aliases.len()
5880            )));
5881        }
5882        if primary.scalar_fn_item && schema_cols.len() == 1 {
5883            schema_cols[0].scalar_row_source = true;
5884        }
5885        // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5886        // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5887        // The column-alias list, if given, names it like any other column.
5888        let mut rows = rows;
5889        if primary.with_ordinality {
5890            schema_cols.push(ColumnSchema::new(
5891                "ordinality".to_string(),
5892                DataType::BigInt,
5893                false,
5894            ));
5895            rows = rows
5896                .into_iter()
5897                .enumerate()
5898                .map(|(i, r)| {
5899                    let mut v = r.values;
5900                    #[allow(clippy::cast_possible_wrap)]
5901                    v.push(Value::BigInt(i as i64 + 1));
5902                    Row::new(v)
5903                })
5904                .collect();
5905        }
5906        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5907            if let Some(col) = schema_cols.get_mut(i) {
5908                col.name = new_name.clone();
5909            }
5910        }
5911        self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5912    }
5913
5914    /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5915    /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5916    /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5917    /// derived-table executor and the FROM-position table functions.
5918    fn exec_select_over_rows(
5919        &self,
5920        stmt: &SelectStatement,
5921        rows: alloc::vec::Vec<Row<'static>>,
5922        schema_cols: alloc::vec::Vec<ColumnSchema>,
5923        alias: &str,
5924        cancel: CancelToken<'_>,
5925    ) -> Result<QueryResult, EngineError> {
5926        let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5927        // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5928        // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5929        // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5930        // (the same path the aggregate branch uses); the old plain eval_expr let
5931        // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5932        let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5933        // WHERE.
5934        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5935            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5936            for row in rows {
5937                cancel.check()?;
5938                let v = self.eval_expr_with_correlated(
5939                    w,
5940                    &row,
5941                    &scan_ctx,
5942                    cancel,
5943                    Some(&mut corr_memo.borrow_mut()),
5944                )?;
5945                if matches!(v, Value::Bool(true)) {
5946                    out.push(row);
5947                }
5948            }
5949            out
5950        } else {
5951            rows
5952        };
5953        // Aggregate dispatch.
5954        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
5955            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5956            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5957                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5958                    .map_err(|err| match err {
5959                        EngineError::Eval(ev) => ev,
5960                        other => eval::EvalError::TypeMismatch {
5961                            detail: alloc::format!("{other}"),
5962                        },
5963                    })
5964            };
5965            // v7.39 (round 656) — hand the rows over as they are rather than
5966            // collecting a second vector of `RowRef` wrappers. Note this is
5967            // a set-returning-function path, NOT the relational scan: the
5968            // measured O(rows) cost lived in `run_single_table_aggregate`,
5969            // and converting these four first was a miss that cost a full
5970            // round — every test stayed green and the number did not move.
5971            let agg = aggregate::run(
5972                stmt,
5973                crate::join::AggRows::Owned(&filtered),
5974                &schema_cols,
5975                Some(alias),
5976                Some(&agg_correlated),
5977                self.parallel_runner.0.as_deref(),
5978                Some(self.active_catalog()),
5979                Some(self),
5980            )?;
5981            return self.finish_agg_result(agg, stmt, cancel);
5982        }
5983        // Projection.
5984        let projection = build_projection(
5985            &stmt.items,
5986            &schema_cols,
5987            alias,
5988            self.speaks_mysql,
5989            Some(self.active_catalog()),
5990        )?;
5991        // v7.39 (round 621) — a target-list SRF expands here too. This tail
5992        // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5993        // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5994        // answered `function unnest(integer[]) does not exist` for a query PG
5995        // answers.
5996        let srf_idxs = self.srf_target_idxs(&projection);
5997        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5998        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5999            alloc::vec::Vec::with_capacity(filtered.len());
6000        if !srf_idxs.is_empty() {
6001            let (rows, src) =
6002                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
6003            projected_rows = rows;
6004            src_of_row = src;
6005        } else {
6006            for row in &filtered {
6007                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
6008                for p in &projection {
6009                    let v = self.eval_expr_with_correlated(
6010                        &p.expr,
6011                        row,
6012                        &scan_ctx,
6013                        cancel,
6014                        Some(&mut corr_memo.borrow_mut()),
6015                    )?;
6016                    vals.push(v);
6017                }
6018                projected_rows.push(Row::new(vals));
6019            }
6020        }
6021        let columns: alloc::vec::Vec<ColumnSchema> = projection
6022            .iter()
6023            // v7.39 (read01 round 54) — keep the column's enum identity through
6024            // the projection (it lives outside the DataType lattice), or a
6025            // derived table / UNION / windowed result forgets it and any outer
6026            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
6027            .map(|p| p.to_column_schema())
6028            .collect();
6029        // ORDER BY over the source rows (same shape as the other
6030        // synthetic-table executors).
6031        // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
6032        // OUTPUT column. Evaluated as an expression, as it was here, the literal
6033        // `1` is just the constant 1: the same sort key for every row, so the
6034        // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
6035        // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
6036        // landing on this executor) came back in input order.
6037        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6038        if !order_by.is_empty() {
6039            // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
6040            // SRF makes more of them than there were inputs.
6041            let out_cols = if srf_idxs.is_empty() {
6042                alloc::vec![None; order_by.len()]
6043            } else {
6044                srf_order_output_cols(&order_by, &projection)
6045            };
6046            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
6047                .iter()
6048                .enumerate()
6049                .map(|(k, out)| -> Result<_, EngineError> {
6050                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
6051                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
6052                        .iter()
6053                        .zip(out_cols.iter())
6054                        .map(|(ob, oc)| {
6055                            // v7.39 (read01 round 54) — this path builds its
6056                            // sort keys itself instead of going through
6057                            // `build_order_keys`, so it skipped the enum-ordinal
6058                            // substitution: an OUTER `ORDER BY <enum col>` over
6059                            // a DERIVED TABLE sorted by the label TEXT, not by
6060                            // member order. Silently wrong rows, not an error.
6061                            let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
6062                            Ok(
6063                                match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
6064                                    Some(ord) => Value::Float(ord),
6065                                    None => v,
6066                                },
6067                            )
6068                        })
6069                        .collect();
6070                    Ok((k, keys?))
6071                })
6072                .collect::<Result<_, _>>()?;
6073            indexed.sort_by(|a, b| {
6074                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
6075                    let o = &stmt.order_by[idx];
6076                    let cmp = order_by_value_cmp_in(
6077                        o.desc,
6078                        o.nulls_first,
6079                        ka,
6080                        kb,
6081                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
6082                    );
6083                    if cmp != core::cmp::Ordering::Equal {
6084                        return cmp;
6085                    }
6086                }
6087                core::cmp::Ordering::Equal
6088            });
6089            projected_rows = indexed
6090                .into_iter()
6091                .map(|(i, _)| projected_rows[i].clone())
6092                .collect();
6093        }
6094        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
6095        if stmt.distinct {
6096            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
6097            // spec folds EVERY text position, so a column declared
6098            // `COLLATE utf8mb4_bin` had its values merged here exactly the
6099            // way 3b494b6e fixed on the main scan path. The projection is
6100            // already in scope at each of these sites, so the mask needs no
6101            // new plumbing -- it was simply never asked for.
6102            projected_rows = dedup_rows(
6103                projected_rows,
6104                FoldSpec::of_masks(
6105                    scan_ctx.mysql_dialect,
6106                    &fold_mask(&projection),
6107                    &pad_mask(&projection),
6108                ),
6109            );
6110        }
6111        if let Some(offset) = stmt.offset_literal() {
6112            let off = (offset as usize).min(projected_rows.len());
6113            projected_rows.drain(..off);
6114        }
6115        if let Some(limit) = stmt.limit_literal() {
6116            projected_rows.truncate(limit as usize);
6117        }
6118        Ok(QueryResult::Rows {
6119            columns,
6120            rows: projected_rows,
6121        })
6122    }
6123
6124    /// Constant `SELECT` with no FROM: evaluate each projection item
6125    /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
6126    fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
6127        let empty_schema: Vec<ColumnSchema> = Vec::new();
6128        let ctx = self.ev_ctx(&empty_schema, None);
6129        // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
6130        // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
6131        // `SELECT string_agg('x',',')` → x). Before this it fell through to the
6132        // scalar projection, where the aggregate name looked like an unknown
6133        // function. The WHERE filters that one row, so `… WHERE false` leaves
6134        // the aggregate zero input rows (`count(*)` → 0).
6135        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
6136            let dummy = Row::new(Vec::new());
6137            let passes = match &stmt.where_ {
6138                Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
6139                None => true,
6140            };
6141            let rows: Vec<RowRef<'_>> = if passes {
6142                alloc::vec![RowRef::Owned(&dummy)]
6143            } else {
6144                Vec::new()
6145            };
6146            let agg = aggregate::run(
6147                stmt,
6148                crate::join::AggRows::Refs(&rows),
6149                &empty_schema,
6150                None,
6151                None,
6152                self.parallel_runner.0.as_deref(),
6153                Some(self.active_catalog()),
6154                Some(self),
6155            )?;
6156            return self.finish_agg_result(agg, stmt, CancelToken::none());
6157        }
6158        let projection = build_projection(
6159            &stmt.items,
6160            &empty_schema,
6161            "",
6162            self.speaks_mysql,
6163            Some(self.active_catalog()),
6164        )?;
6165        // `SELECT … WHERE cond` with no FROM — the one conceptual
6166        // row survives only when the condition is true (previously
6167        // the WHERE was silently ignored: `SELECT 1 WHERE false`
6168        // returned a row).
6169        let dummy_row = Row::new(Vec::new());
6170        if let Some(w) = &stmt.where_ {
6171            let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
6172            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6173                let columns: Vec<ColumnSchema> = projection
6174                    .into_iter()
6175                    .map(|p| p.to_column_schema())
6176                    .collect();
6177                return Ok(QueryResult::Rows {
6178                    columns,
6179                    rows: Vec::new(),
6180                });
6181            }
6182        }
6183        // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6184        // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6185        // desugar to unnest) expands here: one output row per SRF row, sibling
6186        // scalar columns repeated. unnest / array_elements / path_query reach a
6187        // real FROM via the parser rewrite and never land here.
6188        // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6189        let srf_idxs = self.srf_target_idxs(&projection);
6190        if !srf_idxs.is_empty() {
6191            let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6192            let columns: Vec<ColumnSchema> = projection
6193                .into_iter()
6194                .map(|p| p.to_column_schema())
6195                .collect();
6196            // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6197            // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6198            // to. This returned straight out of the expansion, so
6199            // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6200            // input order — the sort was not wrong, it never ran. (There is
6201            // exactly one conceptual input row here, which is why the ordinary
6202            // scan pipeline is not on this path at all.)
6203            if !stmt.order_by.is_empty() {
6204                let synth_ctx =
6205                    EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6206                let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6207                    .order_by
6208                    .iter()
6209                    .map(|o| {
6210                        let mut o = o.clone();
6211                        if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6212                            && *n >= 1
6213                            && let Ok(idx) = usize::try_from(*n - 1)
6214                            && idx < columns.len()
6215                        {
6216                            o.expr = Expr::Column(spg_sql::ast::ColumnName {
6217                                qualifier: None,
6218                                name: columns[idx].name.clone(),
6219                            });
6220                        }
6221                        o
6222                    })
6223                    .collect();
6224                let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6225                let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6226                for r in rows {
6227                    // v7.39.12 — a correlated subquery in ORDER BY is resolved
6228                    // for this row before the key is built; see
6229                    // `Engine::order_by_resolved_for_row`.
6230                    let per_row = self.order_by_resolved_for_row(
6231                        &resolved,
6232                        &r,
6233                        &synth_ctx,
6234                        CancelToken::none(),
6235                    )?;
6236                    let keys =
6237                        build_order_keys(per_row.as_deref().unwrap_or(&resolved), &r, &synth_ctx)?;
6238                    tagged.push((keys, r));
6239                }
6240                sort_by_keys(&mut tagged, &descs, self.session_parallel_workers());
6241                rows = tagged.into_iter().map(|(_, r)| r).collect();
6242            }
6243            apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6244            return Ok(QueryResult::Rows { columns, rows });
6245        }
6246        let mut values = Vec::with_capacity(projection.len());
6247        for p in &projection {
6248            values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6249        }
6250        let columns: Vec<ColumnSchema> = projection
6251            .into_iter()
6252            .map(|p| p.to_column_schema())
6253            .collect();
6254        // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6255        // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6256        // returns none. (The SRF and aggregate arms above already applied
6257        // them; this tail was the one that didn't.)
6258        let mut rows = alloc::vec![Row::new(values)];
6259        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6260        Ok(QueryResult::Rows { columns, rows })
6261    }
6262
6263    /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6264    /// circuit. Catches
6265    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6266    /// BEFORE `resolve_select_subqueries` materialises the inner result
6267    /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6268    /// values into a `HashSet<i64>` directly, then probes A.pk per
6269    /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6270    /// (~150 µs / query at INSUBQ benchmark scale).
6271    pub(crate) fn try_count_star_pk_in_subquery_fast(
6272        &self,
6273        stmt: &SelectStatement,
6274        cancel: CancelToken<'_>,
6275    ) -> Result<Option<QueryResult>, EngineError> {
6276        use spg_sql::ast::SelectItem;
6277        if stmt.distinct
6278            || stmt.limit_with_ties
6279            || stmt.group_by.is_some()
6280            || stmt.having.is_some()
6281            || !stmt.unions.is_empty()
6282            || !stmt.order_by.is_empty()
6283            || stmt.limit.is_some()
6284            || stmt.offset.is_some()
6285            || stmt.items.len() != 1
6286        {
6287            return Ok(None);
6288        }
6289        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6290            return Ok(None);
6291        };
6292        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6293            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6294        if !is_count_star {
6295            return Ok(None);
6296        }
6297        let Some(from) = stmt.from.as_ref() else {
6298            return Ok(None);
6299        };
6300        if !from.joins.is_empty()
6301            || from.primary.lateral_subquery.is_some()
6302            || from.primary.unnest_expr.is_some()
6303            || from.primary.generate_series_args.is_some()
6304            || from.primary.table_fn_call.is_some()
6305            || from.primary.as_of_segment.is_some()
6306        {
6307            return Ok(None);
6308        }
6309        let Some(where_expr) = stmt.where_.as_ref() else {
6310            return Ok(None);
6311        };
6312        // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6313        // negated=false; no other predicates.
6314        let Expr::InSubquery {
6315            expr: col_expr,
6316            subquery,
6317            negated: false,
6318        } = where_expr
6319        else {
6320            return Ok(None);
6321        };
6322        let Expr::Column(c) = col_expr.as_ref() else {
6323            return Ok(None);
6324        };
6325        let outer_alias = from
6326            .primary
6327            .alias
6328            .as_deref()
6329            .unwrap_or(from.primary.name.as_str());
6330        if let Some(q) = c.qualifier.as_deref()
6331            && !q.eq_ignore_ascii_case(outer_alias)
6332        {
6333            return Ok(None);
6334        }
6335        // Outer column must be a single-column PK on integer family.
6336        let catalog = self.active_catalog();
6337        let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6338            return Ok(None);
6339        };
6340        let outer_schema = outer_table.schema();
6341        let Some(outer_pos) = outer_schema
6342            .columns
6343            .iter()
6344            .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6345        else {
6346            return Ok(None);
6347        };
6348        if !matches!(
6349            outer_schema.columns[outer_pos].ty,
6350            spg_storage::DataType::BigInt
6351                | spg_storage::DataType::Int
6352                | spg_storage::DataType::SmallInt
6353        ) {
6354            return Ok(None);
6355        }
6356        if !outer_schema
6357            .uniqueness_constraints
6358            .iter()
6359            .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6360        {
6361            return Ok(None);
6362        }
6363        let Some(idx) = outer_table.index_on(outer_pos) else {
6364            return Ok(None);
6365        };
6366        // Inner must be uncorrelated. The cheap-correlation pre-check
6367        // exists upstream; here we just attempt the bare exec.
6368        if crate::subquery::select_is_correlated(subquery) {
6369            return Ok(None);
6370        }
6371        let mut inner = (**subquery).clone();
6372        self.resolve_select_subqueries(&mut inner, cancel)?;
6373        let r = match self.exec_bare_select_cancel(&inner, cancel) {
6374            Ok(r) => r,
6375            Err(_) => return Ok(None),
6376        };
6377        let QueryResult::Rows { columns, rows, .. } = r else {
6378            return Ok(None);
6379        };
6380        if columns.len() != 1 {
6381            return Ok(None);
6382        }
6383        // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6384        // subquery projects a column known to be UNIQUE/PK on its table
6385        // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6386        // in `tbl.uniqueness_constraints`), survivor values are
6387        // guaranteed distinct and the per-survivor `HashSet::insert`
6388        // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6389        //
6390        // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6391        // projection that is a bare Column ref, table-column lookup in
6392        // catalog confirms the column appears as a unique constraint's
6393        // sole member. UNIQUE NOT NULL is required — a nullable unique
6394        // column may have multiple NULLs, but NULLs are already skipped
6395        // above (`Value::Null => continue`), so a UNIQUE-only column is
6396        // still safe to dedup-skip.
6397        let inner_unique = (|| -> bool {
6398            if inner.distinct
6399                || inner.group_by.is_some()
6400                || !inner.unions.is_empty()
6401                || inner.having.is_some()
6402                || inner.items.len() != 1
6403            {
6404                return false;
6405            }
6406            let Some(inner_from) = inner.from.as_ref() else {
6407                return false;
6408            };
6409            if !inner_from.joins.is_empty()
6410                || inner_from.primary.lateral_subquery.is_some()
6411                || inner_from.primary.unnest_expr.is_some()
6412                || inner_from.primary.generate_series_args.is_some()
6413                || inner_from.primary.table_fn_call.is_some()
6414            {
6415                return false;
6416            }
6417            let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6418                return false;
6419            };
6420            let Expr::Column(pc) = proj else {
6421                return false;
6422            };
6423            let inner_alias = inner_from
6424                .primary
6425                .alias
6426                .as_deref()
6427                .unwrap_or(inner_from.primary.name.as_str());
6428            if let Some(q) = pc.qualifier.as_deref()
6429                && !q.eq_ignore_ascii_case(inner_alias)
6430            {
6431                return false;
6432            }
6433            let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6434                return false;
6435            };
6436            let isch = inner_table.schema();
6437            let Some(ipos) = isch
6438                .columns
6439                .iter()
6440                .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6441            else {
6442                return false;
6443            };
6444            isch.uniqueness_constraints
6445                .iter()
6446                .any(|u| u.columns.as_slice() == [ipos])
6447        })();
6448        // Collect inner i64 values directly into a HashSet, then probe.
6449        let mut count: i64 = 0;
6450        let mut probed = if inner_unique {
6451            hashbrown::HashSet::<i64>::new()
6452        } else {
6453            hashbrown::HashSet::<i64>::with_capacity(rows.len())
6454        };
6455        for row in &rows {
6456            let v = row.values.first().cloned().unwrap_or(Value::Null);
6457            let n = match v {
6458                Value::BigInt(n) => n,
6459                Value::Int(n) => i64::from(n),
6460                Value::SmallInt(n) => i64::from(n),
6461                Value::Null => continue,
6462                _ => return Ok(None),
6463            };
6464            // De-duplicate inner key set so a duplicate inner value
6465            // doesn't double-count the same outer row. Skipped when
6466            // the inner projection is statically unique.
6467            if !inner_unique && !probed.insert(n) {
6468                continue;
6469            }
6470            // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6471            // the `IndexKey::from_value` enum-dispatch and the per-call
6472            // `IndexKey` wrapper construction. The outer column is
6473            // already gated to integer-family above, so an i64 key
6474            // always corresponds to a valid PK lookup.
6475            if !idx.lookup_eq_i64(n).is_empty() {
6476                count += 1;
6477            }
6478        }
6479        let columns_out = alloc::vec![ColumnSchema::new(
6480            "count".to_string(),
6481            spg_storage::DataType::BigInt,
6482            false,
6483        )];
6484        let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6485        Ok(Some(QueryResult::Rows {
6486            columns: columns_out,
6487            rows: rows_out,
6488        }))
6489    }
6490
6491    /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6492    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6493    /// (the post-subquery-replacement shape of the INSUBQ probe
6494    /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6495    /// The general aggregate path materialises every seeked row into
6496    /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6497    /// For COUNT(*) we only care how many keys hit; iterate the list
6498    /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6499    /// row materialisation, the aggregate state machine, and the per-
6500    /// row WHERE re-eval (the seek already filtered by the same list).
6501    /// Returns `None` when the shape doesn't match.
6502    fn try_count_star_pk_in_list_fast(
6503        &self,
6504        stmt: &SelectStatement,
6505        table: &spg_storage::Table,
6506        schema_cols: &[ColumnSchema],
6507        alias: &str,
6508    ) -> Option<QueryResult> {
6509        use spg_sql::ast::{ColumnName, SelectItem};
6510        // Gates on the SELECT shape.
6511        if stmt.distinct
6512            || stmt.limit_with_ties
6513            || stmt.group_by.is_some()
6514            || stmt.having.is_some()
6515            || !stmt.unions.is_empty()
6516            || !stmt.order_by.is_empty()
6517            || stmt.limit.is_some()
6518            || stmt.offset.is_some()
6519            || stmt.items.len() != 1
6520        {
6521            return None;
6522        }
6523        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6524            return None;
6525        };
6526        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6527            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6528        if !is_count_star {
6529            return None;
6530        }
6531        // WHERE must be `<col> IN (literal list)` with no other
6532        // conjuncts (the seek result is a true subset of the row
6533        // population for this predicate).
6534        let where_expr = stmt.where_.as_ref()?;
6535        let Expr::InList {
6536            expr: col_expr,
6537            list,
6538            negated: false,
6539        } = where_expr
6540        else {
6541            return None;
6542        };
6543        let Expr::Column(c) = col_expr.as_ref() else {
6544            return None;
6545        };
6546        if let Some(q) = c.qualifier.as_deref()
6547            && !q.eq_ignore_ascii_case(alias)
6548        {
6549            return None;
6550        }
6551        let col_pos = schema_cols
6552            .iter()
6553            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6554        // The column must be a single-column PK on an integer family
6555        // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6556        // so the antiset stays collision-free under `HashSet<i64>`.
6557        let schema = table.schema();
6558        if !matches!(
6559            schema.columns[col_pos].ty,
6560            spg_storage::DataType::BigInt
6561                | spg_storage::DataType::Int
6562                | spg_storage::DataType::SmallInt
6563        ) {
6564            return None;
6565        }
6566        if !schema
6567            .uniqueness_constraints
6568            .iter()
6569            .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6570        {
6571            return None;
6572        }
6573        let idx = table.index_on(col_pos)?;
6574        // Tally non-empty seek results across all literal values.
6575        let mut count: i64 = 0;
6576        for lit in list {
6577            let Expr::Literal(l) = lit else {
6578                return None;
6579            };
6580            // r1039 — through the shared resolver, so a literal spelled
6581            // in another type ('5' against an integer PK) is read as the
6582            // column's before it becomes a key. This tally answers from
6583            // the index alone, so a key in the wrong space would return a
6584            // COUNT of zero rather than fall back to a scan.
6585            let col = schema.columns.get(col_pos)?;
6586            let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6587            let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6588            if !idx.lookup_eq(&key).is_empty() {
6589                count += 1;
6590            }
6591        }
6592        let columns = alloc::vec![ColumnSchema::new(
6593            "count".to_string(),
6594            spg_storage::DataType::BigInt,
6595            false,
6596        )];
6597        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6598        let _ = ColumnName {
6599            qualifier: None,
6600            name: String::new(),
6601        };
6602        Some(QueryResult::Rows { columns, rows })
6603    }
6604
6605    /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6606    /// BETWEEN a AND b` on an indexed column. The index range walk yields
6607    /// exactly the matching (visible) rows, so we count locators directly —
6608    /// skipping the row materialisation, the aggregate state machine, and the
6609    /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6610    /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6611    /// when the shape doesn't match.
6612    fn try_count_star_indexed_range_fast(
6613        &self,
6614        stmt: &SelectStatement,
6615        table: &spg_storage::Table,
6616        schema_cols: &[ColumnSchema],
6617        alias: &str,
6618        snapshot: &spg_storage::snapshot::Snapshot,
6619    ) -> Option<QueryResult> {
6620        use spg_sql::ast::SelectItem;
6621        if stmt.distinct
6622            || stmt.limit_with_ties
6623            || stmt.group_by.is_some()
6624            || stmt.having.is_some()
6625            || !stmt.unions.is_empty()
6626            || !stmt.order_by.is_empty()
6627            || stmt.limit.is_some()
6628            || stmt.offset.is_some()
6629            || stmt.items.len() != 1
6630        {
6631            return None;
6632        }
6633        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6634            return None;
6635        };
6636        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6637            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6638        if !is_count_star {
6639            return None;
6640        }
6641        let where_expr = stmt.where_.as_ref()?;
6642        let count = crate::index_access::try_range_count(
6643            where_expr,
6644            schema_cols,
6645            table,
6646            alias,
6647            snapshot,
6648            self.speaks_mysql,
6649        )?;
6650        let columns = alloc::vec![ColumnSchema::new(
6651            "count".to_string(),
6652            spg_storage::DataType::BigInt,
6653            false,
6654        )];
6655        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6656        Some(QueryResult::Rows { columns, rows })
6657    }
6658
6659    /// Single-table aggregate path: filter the (optionally index-seeked)
6660    /// rows, then hand off to the aggregate executor which does its own
6661    /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6662    fn run_single_table_aggregate<'a>(
6663        &self,
6664        stmt: &SelectStatement,
6665        table: &'a spg_storage::Table,
6666        schema_cols: &'a [ColumnSchema],
6667        alias: &str,
6668        indexed_rows: Option<crate::index_access::Seeked<'a>>,
6669        cancel: CancelToken<'_>,
6670    ) -> Result<QueryResult, EngineError> {
6671        // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6672        // REPEATABLE (see run_single_table_scan). Aggregates
6673        // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6674        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6675        let ctx = self
6676            .ev_ctx(schema_cols, Some(alias))
6677            .with_sample_rng(&sample_cell);
6678        // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6679        // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6680        // and every abandoned buffer on the way stays resident: RSS is a
6681        // high-water mark, so the intermediates are paid for even though
6682        // they are freed. Round 656 measured the scan at 17 bytes/row
6683        // where the survivor list itself only needs 8.
6684        let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6685            Vec::with_capacity(table.rows().len())
6686        } else {
6687            // With a WHERE, the row count is an UPPER bound and reserving it
6688            // is the worse trade: `… WHERE id = 5` over 50M rows would take
6689            // 400 MB of pointers to hold one survivor. Let it grow.
6690            Vec::new()
6691        };
6692        // v6.2.6 — Memoize: per-query LRU cache for correlated
6693        // scalar subqueries. Fresh per row-loop entry so each
6694        // SELECT execution gets an isolated cache.
6695        let mut memo = memoize::MemoizeCache::new();
6696        // v7.37 (perf) — single-table aggregate's WHERE filter
6697        // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6698        // correlated`) per row, even for subquery-free WHEREs that
6699        // the single-table SCAN path has compiled since v7.32
6700        // (perf knife D). The asymmetry meant a fold-to-filter
6701        // rewrite (joinfold) that swapped a JOIN for a single-table
6702        // aggregate over a compiled WHERE saw the tree-walker
6703        // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6704        // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6705        // step. Compile once if eligible; fall back to the walker
6706        // for subquery-bearing or non-compilable WHEREs.
6707        let compiled_where: Option<eval::CompiledExpr> = stmt
6708            .where_
6709            .as_ref()
6710            .filter(|w| eval::fully_compilable(w))
6711            .map(|w| {
6712                // v7.38.8 — the scan filter runs the cheap half of its
6713                // conjunction first. Called from HERE and not from
6714                // `eval::compiled`, deliberately: the row loop lives in
6715                // that file, and adding a function to it cost this
6716                // query 11 % through layout alone while doing no work
6717                // for it. See `crate::qualorder`.
6718                match crate::qualorder::reordered(w) {
6719                    Some(r) => eval::compile_expr(&r, &ctx),
6720                    None => eval::compile_expr(w, &ctx),
6721                }
6722            });
6723        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6724        let mut row_passes_where = |row: &Row<'static>,
6725                                    eval_stack: &mut Vec<Value<'static>>,
6726                                    memo: &mut memoize::MemoizeCache|
6727         -> Result<bool, EngineError> {
6728            match (&compiled_where, &stmt.where_) {
6729                (Some(cw), _) => {
6730                    // v7.39 (round 479) — the predicate wants a bool, not a
6731                    // Value. The owned entry ended in `Value::into_owned`
6732                    // and the caller then dropped it, once per row; round
6733                    // 478's profile put that pair above the comparison
6734                    // itself.
6735                    Ok(eval::compiled::eval_compiled_pred(
6736                        cw,
6737                        row,
6738                        &ctx,
6739                        eval_stack,
6740                        ctx.mysql_dialect,
6741                    )
6742                    .map_err(EngineError::Eval)?)
6743                }
6744                (None, Some(w)) => {
6745                    let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6746                    Ok(crate::eval::predicate_is_true(
6747                        &cond,
6748                        "WHERE",
6749                        ctx.mysql_dialect,
6750                    )?)
6751                }
6752                (None, None) => Ok(true),
6753            }
6754        };
6755        if let Some(seeked) = &indexed_rows {
6756            // v7.38.19 — an EXACT seek has already applied the whole
6757            // predicate, so asking again is asking the index's question
6758            // a second time, once per row.
6759            //
6760            // Profiled on `count(*) FROM events WHERE project_id = 3`
6761            // over 200,000 rows: `try_index_seek` 1,814 leaf samples and
6762            // `binop::compare` 1,633 — and `compare`'s first arm is
6763            // `(Int, Int) => a.cmp(b)`, so it was never that a
6764            // comparison is expensive. It was that 25,000 of them were
6765            // re-deciding what the walk had decided. The same query with
6766            // `GROUP BY project_id` bolted on ran in half the time,
6767            // doing strictly more work, because that path reached the
6768            // rows differently.
6769            //
6770            // `exact` is false for every arm that has not proven it —
6771            // the GIN, trigram and jsonb walks, an `AND` whose other
6772            // conjuncts went unapplied, a collated key, a type whose key
6773            // cannot name it. See `index_access::Seeked`.
6774            if seeked.exact {
6775                filtered.extend(seeked.rows.iter().map(Cow::as_ref));
6776            } else {
6777                for cow in &seeked.rows {
6778                    let row = cow.as_ref();
6779                    if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6780                        continue;
6781                    }
6782                    filtered.push(row);
6783                }
6784            }
6785        }
6786        // v7.36 (cold-tier coverage) — single-table aggregate's
6787        // non-indexed full scan was hot-only and silently lost cold
6788        // rows on COUNT/SUM/etc. Materialise cold rows once into
6789        // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6790        // shape stays unchanged; the cold rows live until the end of
6791        // the aggregate run.
6792        let cold_rows_storage = if indexed_rows.is_none() {
6793            self.iter_cold_rows_of_table(table)
6794        } else {
6795            Vec::new()
6796        };
6797        if indexed_rows.is_none() {
6798            // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6799            // single-table aggregate full-scan path. Mirrors the gate on
6800            // `run_single_table_scan`: this is a user-query result path,
6801            // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6802            // reader's snapshot cannot see (e.g. tombstoned versions),
6803            // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6804            // under the default gate-off: every hot row is frozen or
6805            // committed-and-alive, so `is_row_visible` returns true.
6806            // Cold-tier rows are frozen (visible) by definition — left
6807            // ungated, matching the plain-scan path.
6808            let scan_snapshot = self.current_snapshot();
6809            // v7.39 (pg_stat knife B) — this full-scan branch walks
6810            // headers directly (serial and sharded alike); count the
6811            // sequential scan here.
6812            table.note_seq_scan();
6813            // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6814            // filter dominate the pre-aggregate wall time on big
6815            // scans (P1's ground truth: accumulation is only ~17%).
6816            // Shard THAT work when the host injected an executor and
6817            // the WHERE is compiled (the compiled evaluator is pure
6818            // over &row; the tree-walker fallback can hit correlated
6819            // subqueries and stays serial). Shards return surviving
6820            // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6821            // 'static bound — and the main thread only dereferences.
6822            let n = table.row_count();
6823            let par = self.parallel_runner.0.as_deref().filter(|_| {
6824                n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6825            });
6826            // v7.38.11 — ask the BRIN summary first. When it prunes,
6827            // the work left is a few thousand rows and sharding it
6828            // costs more than it saves, so the serial pruned loop below
6829            // takes it; the shard machinery is left exactly as it was
6830            // rather than taught about slots.
6831            let brin_slots = stmt
6832                .where_
6833                .as_ref()
6834                .and_then(|w| crate::brin::candidate_slots(w, table));
6835            let brin_prunes = brin_slots
6836                .as_ref()
6837                .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6838            if let Some(r) = par
6839                && !brin_prunes
6840            {
6841                let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6842                let chunk = n.div_ceil(n_shards);
6843                type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6844                let cw = &compiled_where;
6845                let snap_ref = &scan_snapshot;
6846                let results = r.run_shards(n_shards, &|s| {
6847                    let lo = s * chunk;
6848                    let hi = ((s + 1) * chunk).min(n);
6849                    let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6850                    // EvalContext carries Cells (sampler / row counters)
6851                    // and is !Sync — each shard builds its own from the
6852                    // same Sync inputs. The compiled WHERE is gated to
6853                    // the pure-scalar whitelist, which reads none of the
6854                    // session state the engine-built ctx would add
6855                    // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6856                    // sampled scans never take this branch).
6857                    let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6858                    let mut stack: Vec<Value<'static>> = Vec::new();
6859                    let out: ShardOut = (|| {
6860                        for i in lo..hi {
6861                            if !table.is_row_visible(i, snap_ref) {
6862                                continue;
6863                            }
6864                            let row = &table.rows()[i];
6865                            // v7.39 (round 480) — the parallel full-scan
6866                            // shard is the path the aggregate benchmark
6867                            // actually takes, and it was still on the OWNED
6868                            // entry: round 480's profile attributed 68.7 %
6869                            // of `drop_glue<Value>` to this closure, which
6870                            // is why round 479's fix to the indexed path
6871                            // barely moved the total.
6872                            //
6873                            // The `matches!(…, Value::Bool(true))` form was
6874                            // also a narrower reading than the rest of the
6875                            // engine uses — `predicate_is_true` is what
6876                            // handles NULL and MySQL truthiness — so the
6877                            // bool entry fixes the shape as well as the cost.
6878                            let pass = match cw {
6879                                Some(c) => eval::compiled::eval_compiled_pred(
6880                                    c,
6881                                    row,
6882                                    &shard_ctx,
6883                                    &mut stack,
6884                                    shard_ctx.mysql_dialect,
6885                                )
6886                                .map_err(EngineError::Eval)?,
6887                                None => true,
6888                            };
6889                            if pass {
6890                                keep.push(i);
6891                            }
6892                        }
6893                        Ok(keep)
6894                    })();
6895                    alloc::boxed::Box::new(out)
6896                });
6897                // v7.39 (round 567) — `rows()` is a 32-way trie, so
6898                // indexing it is four dependent loads and a scan that
6899                // reads every row paid them every row. A profile of
6900                // `SELECT sum(id)` over 500k rows put 37.8% of the
6901                // connection thread's CPU on THIS ONE LINE. The cursor
6902                // holds the leaf, making that one descent per 32.
6903                let mut rows_cur = table.rows().run_cursor();
6904                for boxed in results {
6905                    let shard = boxed
6906                        .downcast::<ShardOut>()
6907                        .expect("runner echoes the closure's box");
6908                    for i in (*shard)? {
6909                        if let Some(row) = rows_cur.get(i) {
6910                            filtered.push(row);
6911                        }
6912                    }
6913                }
6914            } else {
6915                let mut rows_cur = table.rows().run_cursor();
6916                // v7.38.11 — the slots the BRIN summary could not rule
6917                // out. The predicate still runs on every row that
6918                // survives: the summary decides what to SKIP, never
6919                // what to return.
6920                let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6921                for range in ranges {
6922                    for i in range {
6923                        if !table.is_row_visible(i, &scan_snapshot) {
6924                            continue;
6925                        }
6926                        let Some(row) = rows_cur.get(i) else { continue };
6927                        if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6928                            continue;
6929                        }
6930                        filtered.push(row);
6931                    }
6932                }
6933            }
6934            for row in &cold_rows_storage {
6935                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6936                    continue;
6937                }
6938                filtered.push(row);
6939            }
6940        }
6941        // v7.29 — a per-query memo so correlated scalar
6942        // subqueries batch-evaluate once (group map) instead of
6943        // executing per group.
6944        let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6945        let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6946            self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6947                .map_err(|err| match err {
6948                    EngineError::Eval(ev) => ev,
6949                    other => eval::EvalError::TypeMismatch {
6950                        detail: alloc::format!("{other}"),
6951                    },
6952                })
6953        };
6954        // v7.39 (round 656) — the plain relational scan. This collect() was
6955        // the measured defect: one 64-byte `RowRef` per surviving row to
6956        // wrap an 8-byte pointer `filtered` already holds. Scalar
6957        // aggregates measured ~81 bytes/row of working memory because of
6958        // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6959        // one number. `AggRows::Ptrs` reads the pointers directly.
6960        let agg = aggregate::run(
6961            stmt,
6962            crate::join::AggRows::Ptrs(&filtered),
6963            schema_cols,
6964            Some(alias),
6965            Some(&agg_correlated),
6966            self.parallel_runner.0.as_deref(),
6967            Some(self.active_catalog()),
6968            Some(self),
6969        )?;
6970        self.finish_agg_result(agg, stmt, cancel)
6971    }
6972
6973    /// Single-table scan + projection path: WHERE filter (compiled when
6974    /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6975    /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6976    fn run_single_table_scan<'a>(
6977        &self,
6978        stmt: &SelectStatement,
6979        table: &'a spg_storage::Table,
6980        schema_cols: &'a [ColumnSchema],
6981        alias: &str,
6982        indexed_rows: Option<crate::index_access::Seeked<'a>>,
6983        cancel: CancelToken<'_>,
6984    ) -> Result<QueryResult, EngineError> {
6985        // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6986        // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6987        // deterministic `__tsm_fract(seed)` draws share one scan-local
6988        // state (isolated from the global random() PRNG); a fresh cell per
6989        // scan makes a repeat / rescan reproduce the same sample. Unused
6990        // and cheap when the query carries no sample.
6991        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6992        let ctx = self
6993            .ev_ctx(schema_cols, Some(alias))
6994            .with_sample_rng(&sample_cell);
6995        let projection = build_projection(
6996            &stmt.items,
6997            schema_cols,
6998            alias,
6999            self.speaks_mysql,
7000            Some(self.active_catalog()),
7001        )?;
7002        // v7.19 P5 — single-table SELECT path for SRF
7003        // `SELECT unnest(arr) FROM t` shape. Detect a top-level
7004        // unnest in the projection list. When present, the
7005        // per-row processor emits one output row per array
7006        // element (broadcasting non-SRF projections from the
7007        // same input row). Empty / NULL arrays emit zero rows
7008        // for that input — PG semantics.
7009        // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
7010        let srf_idxs = self.srf_target_idxs(&projection);
7011        let srf_position = srf_idxs.first().copied();
7012        // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
7013        let mut srf_plan = if srf_position.is_some() {
7014            Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
7015        } else {
7016            None
7017        };
7018
7019        // Materialise the filter pass into `(order_key, projected_row)`
7020        // tuples. The order key is `None` when there's no ORDER BY clause.
7021        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
7022        // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
7023        // output row to the per-query byte budget as it is built, so a
7024        // fat single-table scan / sort REJECTS with QueryBytesExceeded
7025        // at ~the ceiling instead of materialising the whole table and
7026        // only noticing at the final enforce_row_limit check. Without
7027        // this, N concurrent fat scans peak at N×table and OOM the host.
7028        // `max_query_bytes = None` (the embedded default) = no ceiling,
7029        // so existing unbudgeted behaviour is byte-identical.
7030        let mut budget = ByteBudget::new(self.max_query_bytes);
7031        // v6.2.6 — Memoize per-row WHERE eval shares one cache.
7032        let mut memo = memoize::MemoizeCache::new();
7033        // v7.32 (perf knife D) — subquery-free WHERE compiles once;
7034        // the row loop then runs a flat step program instead of a
7035        // tree interpretation per row.
7036        let compiled_where: Option<eval::CompiledExpr> = stmt
7037            .where_
7038            .as_ref()
7039            .filter(|w| eval::fully_compilable(w))
7040            .map(|w| {
7041                // v7.38.8 — the scan filter runs the cheap half of its
7042                // conjunction first. Called from HERE and not from
7043                // `eval::compiled`, deliberately: the row loop lives in
7044                // that file, and adding a function to it cost this
7045                // query 11 % through layout alone while doing no work
7046                // for it. See `crate::qualorder`.
7047                match crate::qualorder::reordered(w) {
7048                    Some(r) => eval::compile_expr(&r, &ctx),
7049                    None => eval::compile_expr(w, &ctx),
7050                }
7051            });
7052        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7053        // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
7054        // SELECT-item scalar subquery for the PK-probe fast path. The
7055        // analysis (gate checks + catalog lookups) takes ~500 ns; doing
7056        // it once per query instead of once per row × 100 rows saves
7057        // ~50 µs and lets the per-row evaluation reduce to a single
7058        // index probe + outer-column read.
7059        let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
7060            .iter()
7061            .map(|p| {
7062                if let Expr::ScalarSubquery(inner) = &p.expr {
7063                    self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
7064                } else {
7065                    None
7066                }
7067            })
7068            .collect();
7069        let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
7070        // v7.39 (round 487) — a projection item that is a bare column
7071        // reference binds its position ONCE per query.
7072        //
7073        // Per row it used to walk `eval_expr_with_correlated` (a memo
7074        // lookup for "does this have a subquery", then an un-memoised
7075        // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
7076        // then `resolve_column`, which finds the column by scanning the
7077        // schema and comparing NAMES. On `SELECT g FROM h` that chain was
7078        // 19 % of self time for what is ultimately one cell read.
7079        //
7080        // `compile_column_pos` is the Step VM's resolver, already
7081        // `pub(crate)` and already reused by the aggregate's bind-once
7082        // path: it mirrors `resolve_column`'s happy layers and returns
7083        // None for anything that would reach an error, an ambiguity, or a
7084        // miss, so those still go the interpreter's way and keep its
7085        // exact message. A composite column is excluded for the same
7086        // reason `compile_into` excludes it — it must be rehydrated from
7087        // stored JSON, which is not a cell read.
7088        let proj_direct = bind_direct_columns(&projection, &ctx);
7089        let any_proj_direct = proj_direct.iter().any(Option::is_some);
7090        // v7.39 (round 605) — a projection item that cannot depend on the row
7091        // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
7092        // allocations a row against one for a plain column, `'abc' || 'def'`
7093        // six and `upper('abc')` five, all of them producing the same value
7094        // 50,000 times. An item that fails to evaluate is left alone, so its
7095        // error still comes from the row loop in the interpreter's wording.
7096        let proj_const: Vec<Option<Value<'static>>> = projection
7097            .iter()
7098            .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
7099            .collect();
7100        let any_proj_const = proj_const.iter().any(Option::is_some);
7101        crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
7102        // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
7103        // projection. Statement prep (`resolve_order_by_position`) can only map
7104        // `ORDER BY 1` onto the first SELECT item when that item is an
7105        // expression; a `*` is not one, so the literal survived to here and was
7106        // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
7107        // at all. The parser rewrites `SELECT unnest(a) x` into
7108        // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
7109        // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
7110        // back in input order. The projection is built by now, so the Nth output
7111        // column is known — resolve against it.
7112        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
7113        // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
7114        // EXPANDED rows, so a key naming a select-list item reads that item.
7115        let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
7116            srf_order_output_cols(&order_by, &projection)
7117        } else {
7118            Vec::new()
7119        };
7120        let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
7121        // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
7122        // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
7123        // Hoisted above the closure so the projection-eval path can
7124        // gate `memo` passing on it: the SELECT-item correlated-scalar
7125        // batch path scans the FULL inner table once (~5 ms for 12.5 k
7126        // rows) and is only a win when N outer rows is large; for small
7127        // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
7128        let early_cap: Option<usize> = if order_by.is_empty()
7129            && !stmt.distinct
7130            && !stmt.limit_with_ties
7131            && srf_position.is_none()
7132            && stmt.where_.is_none()
7133        {
7134            stmt.limit_literal()
7135                .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
7136        } else {
7137            None
7138        };
7139        // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
7140        // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
7141        // full-sort by the test gate) keep only the running top-`keep`
7142        // rows in memory instead of materialising every projected row,
7143        // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
7144        // space, not O(rows). `None` = accumulate everything (the prior
7145        // behaviour). The final `partial_sort_tagged(keep)` below still
7146        // runs and produces the identical rows.
7147        // v7.39 (round 683) — the declared collation for each ORDER BY
7148        // position, resolved once and carried beside `descs` for the same
7149        // reason `descs` is carried: it is per key position, not per row.
7150        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
7151        let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
7152            && !stmt.distinct
7153            && !stmt.limit_with_ties
7154            && srf_position.is_none()
7155            && !self.env_cfg().disable_topk
7156        {
7157            stmt.limit_literal().and_then(|l| {
7158                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
7159                (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
7160            })
7161        } else {
7162            None
7163        };
7164        // v7.38.19 — when the sort column is one the projection already
7165        // carries, build no key at all and sort by reading it.
7166        //
7167        // Restricted to the FULL sort: a top-N compares against a stored
7168        // boundary key and `WITH TIES` extends past the limit through the
7169        // keys, both of which need one to exist. DISTINCT keys on them
7170        // too, and an SRF's keys come from the EXPANDED row.
7171        // A COLLATION does not rule it out, but it has to be one that
7172        // orders these values the way bytes do -- decided on the values
7173        // themselves, further down, once they exist.
7174        let sort_by_output: Option<Vec<usize>> = if stmt.distinct
7175            || stmt.limit_with_ties
7176            || srf_position.is_some()
7177            || topk_stream.is_some()
7178        {
7179            None
7180        } else {
7181            order_by_output_cols_if_identical(&order_by, &projection, schema_cols)
7182        };
7183        // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
7184        // kept rows in `tagged`. Probing on the PROJECTED row as soon as
7185        // it is built means a duplicate costs neither a build_order_keys
7186        // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
7187        // a tagged slot, and the sort below runs over u survivors, not
7188        // n input rows — PG's hash-distinct-then-sort plan shape.
7189        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
7190            hashbrown::HashMap::new();
7191        let distinct_hb = hashbrown::DefaultHashBuilder::default();
7192        // v7.38.13 — which output positions must NOT fold. Built once per
7193        // scan from the projection, which carries the source column's
7194        // byte-wise-ness; see `FoldSpec`.
7195        let distinct_mask = fold_mask(&projection);
7196        // v7.39 (round 485) — one projection buffer for the whole scan
7197        // rather than a fresh `Vec` per input row. A row that survives
7198        // the DISTINCT probe takes the buffer with it (`mem::take`) and
7199        // the next row allocates a new one; a row that duplicates an
7200        // earlier one leaves the buffer — and its capacity — in place.
7201        // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
7202        // projected rows are duplicates, so that is 49 900 allocate /
7203        // free pairs the scan no longer performs. Shapes where every row
7204        // survives (plain projection, `DISTINCT` over a unique column)
7205        // allocate exactly as often as before.
7206        let mut proj_buf: Vec<Value<'static>> = Vec::new();
7207        // v7.39 (round 571) — buffers handed back by the top-N trim.
7208        // Round 485 made the scan share ONE projection buffer, but a
7209        // surviving row takes it (`mem::take`) and without DISTINCT
7210        // almost every row survives, so the next one starts from zero
7211        // capacity and allocates. The trim drops `keep` rows at a time
7212        // and their buffers come back here instead of being freed.
7213        let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
7214        let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
7215        // v7.39 (round 581) — the worst row the accumulator is currently
7216        // keeping. Anything that loses to it cannot reach the answer, so
7217        // it is dropped before its projection is ever built.
7218        let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
7219        // v7.38.20 — the boundary's own leading eight bytes, so a losing
7220        // row can be turned away before a key is built for it. Kept
7221        // beside the boundary and refreshed with it; `None` whenever the
7222        // boundary's first key is not one this can read, which sends
7223        // every row down the ordinary path.
7224        // v7.38.21 — and whether those bytes may be trusted under the
7225        // collation in force, which is the boundary's own text to answer.
7226        let mut topk_boundary_prefix: Option<(crate::orderby::PrefixKind, u64, bool)> = None;
7227        // v7.39 (round 582) — resolve each ORDER BY column once, not
7228        // once per row. See `order_by_bound_positions`.
7229        let order_bound =
7230            crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
7231        // v7.39.12 — a correlated scalar subquery in ORDER BY is
7232        // resolved for the row before its key is built.
7233        //
7234        // Uncorrelated subqueries are replaced by a literal before
7235        // execution; a correlated one cannot be, so it reached the
7236        // per-row evaluator — the one place that cannot run a subquery
7237        // — and the statement raised "subquery reached row eval".
7238        // Reported by sentori against 7.39.11; see
7239        // `Engine::order_by_resolved_for_row`.
7240        //
7241        // The `any` runs once, here, so an ordinary ORDER BY pays one
7242        // bool per row and nothing else.
7243        let order_has_subquery = order_by
7244            .iter()
7245            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
7246        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
7247        // v7.39 (round 581) — and it stops asking when the answer is
7248        // always "keep".
7249        //
7250        // The check earns its place only on rows it rejects. Over
7251        // ascending ids, `ORDER BY id DESC` never rejects one — every
7252        // row beats the current worst — so the comparison is pure
7253        // overhead there, measured at +5.5% in three batches out of
7254        // three. After a window of rows it looks at what it has
7255        // actually rejected and switches itself off if the shape is not
7256        // paying. The answers do not depend on it either way.
7257        // v7.38.21 — resolved once per query, not per row.
7258        //
7259        // No collation at all is the case v7.38.20 shipped. A DECLARED
7260        // one may still be answered by bytes, and which collations those
7261        // are is `Collated::ascii_byte_order`'s to say — the same
7262        // allowlist `byte_order_answers_the_collation` consults, so the
7263        // two cannot come to disagree about a collation. What that
7264        // allowlist requires of the TEXT is checked per row and on the
7265        // boundary, because a streaming top-N has no batch to check.
7266        let boundary_no_collation = order_colls.iter().all(Option::is_none);
7267        let boundary_collations_permit = boundary_no_collation
7268            || order_colls
7269                .iter()
7270                .flatten()
7271                .all(crate::collate::Collated::ascii_byte_order);
7272        const BOUNDARY_WINDOW: u32 = 8192;
7273        let mut boundary_checks: u32 = 0;
7274        let mut boundary_rejects: u32 = 0;
7275        let mut boundary_check_on = true;
7276        // Inline the per-row work in a closure so the indexed and full-
7277        // scan branches share the body.
7278        // v7.38.19 — `check_where` is per CALL SITE, not per closure: the
7279        // full-scan loops below must apply the predicate, and the
7280        // indexed loop must not when the seek already did. A captured
7281        // flag would have to be right for both.
7282        let mut process_row = |row: &Row<'static>,
7283                               loop_idx: usize,
7284                               check_where: bool|
7285         -> Result<(), EngineError> {
7286            if loop_idx.is_multiple_of(256) {
7287                cancel.check()?;
7288            }
7289            if !check_where {
7290                // The seek answered the whole predicate. See
7291                // `index_access::Seeked`.
7292            } else if let Some(cw) = &compiled_where {
7293                let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7294                    .map_err(EngineError::Eval)?;
7295                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7296                    return Ok(());
7297                }
7298            } else if let Some(where_expr) = &stmt.where_ {
7299                let cond =
7300                    self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7301                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7302                    return Ok(());
7303                }
7304            }
7305            // Under DISTINCT the keys are built AFTER the dup probe
7306            // (survivors only); the non-distinct order is unchanged.
7307            // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7308            // row further down, and building them here would evaluate the
7309            // ORDER BY against the INPUT row: a key naming the SRF's own
7310            // output became a scalar call to it, which is where
7311            // "function unnest(integer[]) does not exist" came from.
7312            let order_keys = if order_by.is_empty()
7313                || stmt.distinct
7314                || srf_position.is_some()
7315                // v7.38.19 — the branch below builds whatever key it
7316                // needs from the projected values, collation included,
7317                // so nothing has to be built here for it.
7318                //
7319                // A draft that skipped them here but still let the
7320                // COLLATED case fall through to the key-based sort put a
7321                // mixed column back in INSERT order: every key empty,
7322                // every row equal, a stable sort faithfully preserving
7323                // nothing. The rule is one decision, not two.
7324                || sort_by_output.is_some()
7325            {
7326                Vec::new()
7327            } else {
7328                // v7.38.20 — turn a decisively losing row away before
7329                // its key is built. Only the FIRST key is read, and only
7330                // its leading eight bytes; a tie there decides nothing
7331                // and falls through to the full path below.
7332                //
7333                // ASC only: under DESC the boundary is the largest kept
7334                // key and the comparison flips, which this deliberately
7335                // does not try to express — a second direction in a
7336                // fast-path predicate is how one of them ends up wrong.
7337                if boundary_check_on
7338                    && let Some((_, descs)) = &topk_stream
7339                    && !descs.first().copied().unwrap_or(false)
7340                    && order_by.len() == 1
7341                    && boundary_collations_permit
7342                    && let Some((bkind, bp, boundary_is_ascii)) = topk_boundary_prefix
7343                    && let Some((rkind, rp, row_is_ascii)) =
7344                        crate::orderby::first_key_prefix(&order_bound, row)
7345                    && bkind == rkind
7346                    && (boundary_no_collation || (boundary_is_ascii && row_is_ascii))
7347                    && rp > bp
7348                {
7349                    boundary_checks += 1;
7350                    boundary_rejects += 1;
7351                    if boundary_checks == BOUNDARY_WINDOW {
7352                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7353                    }
7354                    return Ok(());
7355                }
7356                let mut buf = key_pool.pop().unwrap_or_default();
7357                if order_has_subquery {
7358                    // A substituted literal is no longer a bound column.
7359                    let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7360                    crate::orderby::build_order_keys_bound(
7361                        per_row.as_deref().unwrap_or(&order_by),
7362                        &unbound,
7363                        &order_colls,
7364                        row,
7365                        &ctx,
7366                        &mut buf,
7367                    )?;
7368                } else {
7369                    crate::orderby::build_order_keys_bound(
7370                        &order_by,
7371                        &order_bound,
7372                        &order_colls,
7373                        row,
7374                        &ctx,
7375                        &mut buf,
7376                    )?;
7377                }
7378                // v7.39 (round 581) — reject before projecting.
7379                //
7380                // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7381                // 50 distinct `g` decides nearly every row on the FIRST
7382                // key, and PG answers it FASTER than the single-key form
7383                // (7.4 ms against 10.4) because a rejected row costs it
7384                // one comparison. SPG built both keys AND the projected
7385                // row for all 500k before throwing them away. The keys
7386                // are needed to compare; the projection is not.
7387                if boundary_check_on
7388                    && let Some((_, descs)) = &topk_stream
7389                    && let Some(b) = &topk_boundary
7390                {
7391                    boundary_checks += 1;
7392                    let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7393                        == core::cmp::Ordering::Greater;
7394                    if loses {
7395                        boundary_rejects += 1;
7396                    }
7397                    if boundary_checks == BOUNDARY_WINDOW {
7398                        // Keep asking only if it has been rejecting at
7399                        // least a quarter of what it saw.
7400                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7401                    }
7402                    if loses {
7403                        buf.clear();
7404                        key_pool.push(buf);
7405                        return Ok(());
7406                    }
7407                }
7408                buf
7409            };
7410            if srf_position.is_some() {
7411                let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7412                for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7413                    if stmt.distinct {
7414                        let bucket = seen_distinct
7415                            .entry(norm_hash_row(
7416                                &out,
7417                                &distinct_hb,
7418                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7419                            ))
7420                            .or_default();
7421                        if bucket.iter().any(|i| {
7422                            row_eq_norm(
7423                                &tagged[i].1,
7424                                &out,
7425                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7426                            )
7427                        }) {
7428                            continue;
7429                        }
7430                        bucket.push(tagged.len());
7431                    }
7432                    budget.charge(approx_row_bytes(&out))?;
7433                    // The keys come from THIS expanded row: a key naming a
7434                    // select-list item reads its value, anything else is
7435                    // still evaluated against the input row.
7436                    let keys = if order_by.is_empty() {
7437                        Vec::new()
7438                    } else {
7439                        let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7440                        for (k, ob) in order_by.iter().enumerate() {
7441                            kv.push(match srf_order_cols.get(k).copied().flatten() {
7442                                Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7443                                None => eval::eval_expr(&ob.expr, row, &ctx)
7444                                    .map_err(EngineError::Eval)?,
7445                            });
7446                        }
7447                        // Packed by the same code every other ORDER BY uses,
7448                        // so DESC / NULLS FIRST / the MySQL rule are not
7449                        // restated here.
7450                        let key_row = Row::new(kv);
7451                        let mut buf = Vec::new();
7452                        crate::orderby::build_order_keys_bound(
7453                            &order_by,
7454                            &srf_key_bound,
7455                            &order_colls,
7456                            &key_row,
7457                            &ctx,
7458                            &mut buf,
7459                        )?;
7460                        buf
7461                    };
7462                    tagged.push((keys, out));
7463                }
7464            } else {
7465                let values = &mut proj_buf;
7466                values.clear();
7467                values.reserve(projection.len());
7468                for (i, p) in projection.iter().enumerate() {
7469                    // v7.37.x (docker-fair SCALARSQ attack) — pre-
7470                    // analysed PK-probe fast path. The per-row work is
7471                    // a read of outer.col from the row plus an index
7472                    // probe — no Expr clone, no walker, no
7473                    // `eval_expr_with_correlated` framework.
7474                    if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7475                        values.push(self.probe_with_pk_fast_path(fp, row));
7476                        continue;
7477                    }
7478                    // v7.39 (round 605) — the same value every row.
7479                    if any_proj_const && let Some(v) = &proj_const[i] {
7480                        values.push(v.clone());
7481                        continue;
7482                    }
7483                    // v7.39 (round 487) — bound column: read the cell.
7484                    // This is `rehydrate_cell`'s body for a non-composite
7485                    // column, which is what the whole chain below reduces
7486                    // to once the name has been resolved.
7487                    if any_proj_direct && let Some(pos) = proj_direct[i] {
7488                        crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7489                        values.push(row.values[pos].clone().into_owned());
7490                        continue;
7491                    }
7492                    // v7.24 (round-16 B) — correlated-aware.
7493                    // v7.37.x (docker-fair SCALARSQ attack) — share the
7494                    // per-row memo with projection. Required for the
7495                    // batch-evaluated correlated-scalar path to fire on
7496                    // SELECT-item scalar subqueries; otherwise each row
7497                    // re-executes the inner.
7498                    //
7499                    // Skip the memo when the outer row count is small
7500                    // (early-limited): the batch path scans the FULL
7501                    // inner table to build a GroupMap (~5 ms for a
7502                    // 12.5 k-row inner), while per-row execution with a
7503                    // PK index seek is ~5 µs per call — much cheaper for
7504                    // N ≤ ~1000 outer rows.
7505                    let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7506                    let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7507                    values.push(
7508                        self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7509                    );
7510                }
7511                crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7512                if stmt.distinct {
7513                    let bucket = seen_distinct
7514                        .entry(norm_hash_values(
7515                            &proj_buf,
7516                            &distinct_hb,
7517                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7518                        ))
7519                        .or_default();
7520                    if bucket.iter().any(|i| {
7521                        values_eq_norm(
7522                            &tagged[i].1.values,
7523                            &proj_buf,
7524                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7525                        )
7526                    }) {
7527                        crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7528                        return Ok(());
7529                    }
7530                    bucket.push(tagged.len());
7531                }
7532                let out = Row::new(core::mem::replace(
7533                    &mut proj_buf,
7534                    proj_pool.pop().unwrap_or_default(),
7535                ));
7536                let order_keys = if stmt.distinct && !order_by.is_empty() {
7537                    // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7538                    // the bound-cell path precisely so an ORDER BY key that
7539                    // names a column is READ instead of evaluated, and the
7540                    // non-DISTINCT branch above has passed it ever since;
7541                    // this branch never did, so `SELECT DISTINCT k .. ORDER
7542                    // BY k` resolved "k" by string for every surviving row.
7543                    let mut buf = key_pool.pop().unwrap_or_default();
7544                    if order_has_subquery {
7545                        // A substituted literal is no longer a bound column.
7546                        let per_row =
7547                            self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7548                        crate::orderby::build_order_keys_bound(
7549                            per_row.as_deref().unwrap_or(&order_by),
7550                            &unbound,
7551                            &order_colls,
7552                            row,
7553                            &ctx,
7554                            &mut buf,
7555                        )?;
7556                    } else {
7557                        crate::orderby::build_order_keys_bound(
7558                            &order_by,
7559                            &order_bound,
7560                            &order_colls,
7561                            row,
7562                            &ctx,
7563                            &mut buf,
7564                        )?;
7565                    }
7566                    buf
7567                } else {
7568                    order_keys
7569                };
7570                budget.charge(approx_row_bytes(&out))?;
7571                tagged.push((order_keys, out));
7572            }
7573            // Streaming top-N: bound the accumulator to O(keep) rows.
7574            if let Some((k, descs)) = &topk_stream {
7575                crate::orderby::topk_trim_recycling(
7576                    &mut tagged,
7577                    *k,
7578                    descs,
7579                    &mut proj_pool,
7580                    &mut key_pool,
7581                    &mut topk_boundary,
7582                );
7583                // The prefix follows the boundary it summarises.
7584                topk_boundary_prefix = topk_boundary
7585                    .as_ref()
7586                    .and_then(|b| b.first())
7587                    .and_then(crate::orderby::order_key_prefix);
7588            }
7589            Ok(())
7590        };
7591        // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7592        // load-bearing full-scan path. This is the primary single-table
7593        // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7594        // in-place writers retain dead/old versions, an ungated scan
7595        // here would return them, so the gate must land BEFORE the
7596        // writers flip (see the plan's activation-order rule). A no-op
7597        // today: every hot row is frozen or committed-and-alive under
7598        // the reader's snapshot, so `is_row_visible` returns true for
7599        // all of them (verified by the full e2e suite staying green).
7600        let scan_snapshot = self.current_snapshot();
7601        let mut emitted: usize = 0;
7602        if let Some(seeked) = &indexed_rows {
7603            let recheck = !seeked.exact;
7604            for (loop_idx, cow) in seeked.rows.iter().enumerate() {
7605                if let Some(cap) = early_cap
7606                    && emitted >= cap
7607                {
7608                    break;
7609                }
7610                process_row(cow.as_ref(), loop_idx, recheck)?;
7611                emitted = emitted.saturating_add(1);
7612            }
7613        } else {
7614            // v7.39 (round 570) — the row store is a 32-way trie, so
7615            // indexing it is four dependent loads. Round 567 measured
7616            // -18% on the aggregate scan from holding the leaf between
7617            // rows; this is the same loop for the projecting scan.
7618            let mut rows_cur = table.rows().run_cursor();
7619            // v7.38.11 — see the aggregate scan above: a BRIN index on a
7620            // column this WHERE bounds says which slots cannot match.
7621            let brin_slots = stmt
7622                .where_
7623                .as_ref()
7624                .and_then(|w| crate::brin::candidate_slots(w, table))
7625                .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7626            for i in brin_slots.into_iter().flatten() {
7627                if let Some(cap) = early_cap
7628                    && emitted >= cap
7629                {
7630                    break;
7631                }
7632                // Skip rows this snapshot cannot see (invisible rows do
7633                // not count toward the LIMIT).
7634                if !table.is_row_visible(i, &scan_snapshot) {
7635                    continue;
7636                }
7637                let Some(row) = rows_cur.get(i) else { continue };
7638                process_row(row, i, true)?;
7639                emitted = emitted.saturating_add(1);
7640            }
7641            // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7642            // rows into the same loop. The full-scan path here is the
7643            // load-bearing single-table SELECT executor, and pre-
7644            // 7.35.1 it only walked `table.rows()` (hot), so any
7645            // `SELECT … FROM t` against a table with cold segments
7646            // silently returned a subset.
7647            let cold_rows = self.iter_cold_rows_of_table(table);
7648            for (offset, row) in cold_rows.iter().enumerate() {
7649                if let Some(cap) = early_cap
7650                    && emitted >= cap
7651                {
7652                    break;
7653                }
7654                process_row(row, table.row_count() + offset, true)?;
7655                emitted = emitted.saturating_add(1);
7656            }
7657        }
7658
7659        // (DISTINCT already de-duped STREAMING inside process_row, so the
7660        // sort below only sees the u survivors and the partial-sort
7661        // budget applies to DISTINCT too.)
7662        if !order_by.is_empty() {
7663            // Partial-sort fast path: when LIMIT is small relative to
7664            // the row count, select_nth_unstable + sort just the
7665            // prefix is O(n + k log k) instead of O(n log n).
7666            // WITH TIES needs the full sort so the tie extension can
7667            // scan past `limit` to find rows that share the last-kept
7668            // row's key.
7669            let keep = if stmt.limit_with_ties
7670                // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7671                // forces the full-sort fallback by suppressing the
7672                // partial-sort `keep` budget. See
7673                // `xtests/sigil/test-mode-gucs.md`.
7674                || self.env_cfg().disable_topk
7675            {
7676                None
7677            } else {
7678                stmt.limit_literal()
7679                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7680            };
7681            let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7682            if let Some(cols) = &sort_by_output {
7683                // No keys were built; the sort reads the projected row.
7684                // The comparator is the value-level one the window
7685                // functions and the key path both defer to, so DESC,
7686                // NULLS placement, the MySQL fold and the collation are
7687                // not restated here.
7688                let terms: Vec<(usize, bool, Option<bool>)> = cols
7689                    .iter()
7690                    .zip(order_by.iter())
7691                    .map(|(c, o)| (*c, o.desc, o.nulls_first))
7692                    .collect();
7693                let mysql = ctx.mysql_dialect;
7694                // v7.38.19 — sort a PERMUTATION carrying the first eight
7695                // bytes, not the rows.
7696                //
7697                // The elements above are `(Vec<OrderKey>, Row)`, 48 bytes,
7698                // and driftsort moves them ~n log n times: 7.4 M moves at
7699                // 400,000 rows. Worse, every comparison chases three
7700                // dependent loads PER SIDE to reach the byte it wants --
7701                // the row's `Vec`, the `Value`, then the string's own
7702                // buffer -- and a profile of this sort put 35% of its
7703                // working samples in the sort machinery around that.
7704                //
7705                // A `(u64, u32)` is 16 bytes and the comparison reads it
7706                // straight out of the array. The u64 is the first eight
7707                // bytes big-endian, zero-padded, which ORDERS THE SAME as
7708                // the string: if two differ inside those bytes they differ
7709                // at the same index either way, and a string shorter than
7710                // eight pads with zeros exactly where `[u8]`'s own
7711                // comparison runs out. Equal prefixes fall through to the
7712                // full comparator, so nothing rests on the padding being
7713                // clever.
7714                //
7715                // The tail-break on the index is what keeps the sort
7716                // STABLE, which `sort_by` was giving for free and an
7717                // unstable sort over a permutation would not.
7718                // v7.38.19 — three ways to sort these rows, and which
7719                // one is right turns on the values, which is why it is
7720                // decided here rather than at plan time.
7721                //
7722                //   * the collation orders these values the way bytes do
7723                //     -- take the eight-byte key below
7724                //   * it does not, but there IS a collation -- build its
7725                //     sort key once per row and order the permutation on
7726                //     those, which is what the key path did, done from
7727                //     the projected value instead of during the scan
7728                //   * no collation at all -- the eight-byte key again
7729                //
7730                // The middle case is the one a draft got wrong by
7731                // leaving the rows to a key path whose keys it had just
7732                // skipped building.
7733                let mut keep_sorted = false;
7734                let bytes_answer = byte_order_answers_the_collation(&tagged, &terms, &order_colls);
7735                if !bytes_answer && let Some(coll) = order_colls.first().and_then(Option::as_ref) {
7736                    let (first_col, first_desc, _) = terms[0];
7737                    let mut order: Vec<(Vec<u8>, u32)> = Vec::with_capacity(tagged.len());
7738                    for (i, row) in tagged.iter().enumerate() {
7739                        let k = match row.1.values.get(first_col) {
7740                            Some(Value::Text(t)) => coll.sort_key_of(t).unwrap_or_else(|| {
7741                                let mut v = Vec::with_capacity(t.len() + 1);
7742                                v.push(0);
7743                                v.extend_from_slice(t.as_bytes());
7744                                v
7745                            }),
7746                            _ => Vec::new(),
7747                        };
7748                        order.push((k, u32::try_from(i).unwrap_or(u32::MAX)));
7749                    }
7750                    // v7.40.4 — collated sort keys across threads. This
7751                    // comparator ends on the row index like every other
7752                    // one here, so it is a strict total order and the
7753                    // split cannot reach a different answer; see
7754                    // `crate::parsort`. It is also the path a customer on
7755                    // a locale collation actually runs, which is why the
7756                    // module moves its elements rather than copying them:
7757                    // an ICU sort key is a `Vec<u8>`.
7758                    let order = crate::parsort::sort_total(
7759                        order,
7760                        self.session_parallel_workers(),
7761                        &|(ka, ia): &(Vec<u8>, u32), (kb, ib): &(Vec<u8>, u32)| {
7762                            let c = ka.cmp(kb);
7763                            let c = if first_desc { c.reverse() } else { c };
7764                            if c != core::cmp::Ordering::Equal {
7765                                return c;
7766                            }
7767                            row_cmp_by_index(&tagged, &terms, &order_colls, mysql, *ia, *ib)
7768                                .then_with(|| ia.cmp(ib))
7769                        },
7770                    );
7771                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7772                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7773                    tagged = order
7774                        .iter()
7775                        .map(|&(_, i)| {
7776                            slots[i as usize]
7777                                .take()
7778                                .expect("the permutation names each row once")
7779                        })
7780                        .collect();
7781                    keep_sorted = true;
7782                }
7783                // v7.38.20 — a key that does NOT discriminate is still
7784                // worth sorting on, as long as the runs it leaves are
7785                // handled once instead of n log n times.
7786                //
7787                // `text (26 values)` is two hundred identical characters
7788                // drawn from twenty-six letters, so every eight-byte
7789                // prefix inside a letter is the same and 15,384 rows tie
7790                // on it. A comparison sort then asks ~7.4 M questions of
7791                // which nearly all are a two-hundred-byte `memcmp`
7792                // answering EQUAL: profiled, 30% of the working samples
7793                // sat in `memcmp` and 37% in the sort machinery.
7794                //
7795                // Sorting the integer keys is cheap. What each run needs
7796                // afterwards is ONE pass: if every value in it is equal,
7797                // input order already IS the stable answer, and proving
7798                // that costs n-1 comparisons rather than n log n. Only a
7799                // run that is not all-equal gets sorted.
7800                //
7801                // Single-term only. With a second ORDER BY column an
7802                // all-equal first term does not settle the row order --
7803                // the later terms still speak -- and the shortcut would
7804                // drop them.
7805                let all_keys = if keep_sorted {
7806                    None
7807                } else {
7808                    sort_keys_of(&tagged, terms[0].0)
7809                };
7810                let (worth_it, key_exact) = match all_keys.as_ref() {
7811                    Some(PrefixKeys::Narrow(k, e)) => (*e || key_discriminates(k), *e),
7812                    Some(PrefixKeys::Wide(k, e)) => (*e || key_discriminates(k), *e),
7813                    None => (false, false),
7814                };
7815                let low_card = !keep_sorted && terms.len() == 1 && !key_exact && !worth_it;
7816                let keyed = all_keys.filter(|_| worth_it || low_card);
7817                if keep_sorted {
7818                    // The collated permutation above already placed every
7819                    // row. A draft let the byte-order fallback run after
7820                    // it and undo the whole thing.
7821                } else if let Some(keys) = keyed {
7822                    let exact = key_exact;
7823                    let (first_col, first_desc, _) = terms[0];
7824                    let row_cmp = |ia: u32, ib: u32| -> core::cmp::Ordering {
7825                        let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
7826                        for (col, desc, nf) in &terms {
7827                            let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col))
7828                            else {
7829                                continue;
7830                            };
7831                            let ord = match (va, vb) {
7832                                (Value::Text(x), Value::Text(y)) if !mysql => {
7833                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7834                                    if *desc { c.reverse() } else { c }
7835                                }
7836                                _ => {
7837                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7838                                }
7839                            };
7840                            if ord != core::cmp::Ordering::Equal {
7841                                return ord;
7842                            }
7843                        }
7844                        core::cmp::Ordering::Equal
7845                    };
7846                    // v7.40.2 — the uniform check reads a COMPACT column,
7847                    // not the rows.
7848                    //
7849                    // `low_card` sorts the prefix keys and then proves,
7850                    // once per run, whether every value in it is equal.
7851                    // That proof is n-1 comparisons, and each one used to
7852                    // walk `order[i] -> tagged[i] -> values -> Value ->
7853                    // &str -> bytes`: four dependent reads, the first of
7854                    // them scattered across a 400,000-element array of fat
7855                    // rows. This file's own note at `key_discriminates`
7856                    // warned about that random read; here is its price.
7857                    //
7858                    // Measured, 400,000 rows, twenty-six distinct values,
7859                    // server-reported, against the same binary with
7860                    // `low_card` forced off (md5-witnessed, order digests
7861                    // identical):
7862                    //
7863                    //   8-byte values     54.98 ms on   54.91 ms off
7864                    //   200-byte values   89.64 ms on  163.18 ms off
7865                    //
7866                    // So the path earns 1.82x and is not in question. What
7867                    // the 8-byte and 200-byte cells say together is where
7868                    // the rest goes: the extra 192 bytes a row cost
7869                    // 34.6 ms, which is 80 MB compared at 2.3 GB/s — an
7870                    // order of magnitude under this machine's memory
7871                    // bandwidth, because the cost is the misses and not
7872                    // the compare.
7873                    //
7874                    // Collecting the column first is one sequential pass
7875                    // over `tagged` and leaves the comparison two reads:
7876                    // a 16-byte slice header, then its bytes.
7877                    // Built ONLY for the branch that uses it. `same_value`
7878                    // is called from the `low_card` run walk and nowhere
7879                    // else, so an exact key -- every value inside sixteen
7880                    // bytes -- never asks the question. The first version
7881                    // built the column unconditionally and charged 2.1 ms
7882                    // to a shape that never reads it:
7883                    //
7884                    //   8-byte values     56.45 -> 58.55 ms   (a tax)
7885                    //   200-byte values   97.18 -> 84.70 ms   (the point)
7886                    let col_strs: Option<Vec<&str>> = if low_card {
7887                        tagged
7888                            .iter()
7889                            .map(|t| match t.1.values.get(first_col) {
7890                                Some(Value::Text(x)) => Some(x.as_ref()),
7891                                _ => None,
7892                            })
7893                            .collect()
7894                    } else {
7895                        None
7896                    };
7897                    let same_value = |ia: u32, ib: u32| -> bool {
7898                        col_strs.as_ref().map_or_else(
7899                            || {
7900                                tagged[ia as usize].1.values.get(first_col)
7901                                    == tagged[ib as usize].1.values.get(first_col)
7902                            },
7903                            |c| c[ia as usize] == c[ib as usize],
7904                        )
7905                    };
7906                    let how = PrefixSort {
7907                        first_desc,
7908                        low_card,
7909                        exact,
7910                        single_term: terms.len() == 1,
7911                        workers: self.session_parallel_workers(),
7912                    };
7913                    let order: Vec<u32> = match keys {
7914                        PrefixKeys::Narrow(v, _) => {
7915                            sort_prefix_permutation(v, &how, &row_cmp, &same_value)
7916                        }
7917                        PrefixKeys::Wide(v, _) => {
7918                            sort_prefix_permutation(v, &how, &row_cmp, &same_value)
7919                        }
7920                    };
7921                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7922                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7923                    tagged = order
7924                        .iter()
7925                        .map(|&i| {
7926                            slots[i as usize]
7927                                .take()
7928                                .expect("the permutation names each row once")
7929                        })
7930                        .collect();
7931                } else {
7932                    tagged.sort_by(|a, b| {
7933                        for (i, (col, desc, nf)) in terms.iter().enumerate() {
7934                            let va = a.1.values.get(*col);
7935                            let vb = b.1.values.get(*col);
7936                            let (Some(va), Some(vb)) = (va, vb) else {
7937                                continue;
7938                            };
7939                            let _ = i;
7940                            // v7.38.19 — two non-NULL strings, no MySQL fold, is
7941                            // where a text sort spends every one of its ~7 M
7942                            // comparisons, and the shared comparator cannot be
7943                            // inlined into this loop: it carries NULL placement,
7944                            // the fold, the NUMERIC bignum gate and the float
7945                            // total order. Answering that one pair here is the
7946                            // same answer by the same route — `value_cmp`'s
7947                            // leading same-variant arm is `x.cmp(y)`, and the
7948                            // raw comparator's last act is this reverse.
7949                            let ord = match (va, vb) {
7950                                (Value::Text(x), Value::Text(y)) if !mysql => {
7951                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7952                                    if *desc { c.reverse() } else { c }
7953                                }
7954                                _ => {
7955                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7956                                }
7957                            };
7958                            if ord != core::cmp::Ordering::Equal {
7959                                return ord;
7960                            }
7961                        }
7962                        core::cmp::Ordering::Equal
7963                    });
7964                }
7965            } else {
7966                crate::orderby::partial_sort_tagged_in(
7967                    &mut tagged,
7968                    keep,
7969                    &descs,
7970                    &order_colls,
7971                    self.session_parallel_workers(),
7972                );
7973            }
7974        }
7975
7976        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7977        // past the truncated tail through every row that shares the
7978        // last-kept row's ORDER BY key. The tie check uses the
7979        // already-computed `(order_keys, row)` pairs so it matches
7980        // the sort comparator exactly. DISTINCT + WITH TIES falls
7981        // through to the no-ties path (PG also disallows their
7982        // combination; SPG silently drops the tie extension here so
7983        // the customer doesn't see a hard error mid-query — the
7984        // user-visible result is still correct, just narrower).
7985        let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7986            apply_offset_and_limit_tagged(
7987                &mut tagged,
7988                stmt.offset_literal(),
7989                stmt.limit_literal(),
7990                true,
7991            );
7992            tagged.into_iter().map(|(_, r)| r).collect()
7993        } else {
7994            // DISTINCT already de-duped pre-sort above.
7995            let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7996            apply_offset_and_limit(
7997                &mut output_rows,
7998                stmt.offset_literal(),
7999                stmt.limit_literal(),
8000            );
8001            output_rows
8002        };
8003
8004        let columns: Vec<ColumnSchema> = projection
8005            .into_iter()
8006            .map(|p| p.to_column_schema())
8007            .collect();
8008
8009        Ok(QueryResult::Rows {
8010            columns,
8011            rows: output_rows,
8012        })
8013    }
8014
8015    /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
8016    /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
8017    /// select items for the surviving rows only — PG's Result-above-
8018    /// Limit shape, where SubPlan loops equal the OUTPUT row count
8019    /// (50) instead of the group count (24k).
8020    fn finish_agg_result(
8021        &self,
8022        mut agg: aggregate::AggResult,
8023        stmt: &SelectStatement,
8024        cancel: CancelToken<'_>,
8025    ) -> Result<QueryResult, EngineError> {
8026        apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
8027        if !agg.deferred.is_empty() {
8028            apply_offset_and_limit(
8029                &mut agg.synth_rows,
8030                stmt.offset_literal(),
8031                stmt.limit_literal(),
8032            );
8033            let ctx = EvalContext::new(&agg.synth_schema, None);
8034            let mut memo = memoize::MemoizeCache::default();
8035            // v7.32 (architecture v2 P3) — keyed index-probe seeding.
8036            // Deferred subqueries are referenced only by surviving
8037            // select-list rows (≤ LIMIT), so their correlation keys are
8038            // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
8039            // each batchable subquery's group map over just those keys
8040            // via per-key index seek; the per-row splice loop below then
8041            // reuses the seeded map. A join-shaped or un-indexed inner
8042            // falls through to the all-keys batch inside the call (built
8043            // eagerly here instead of lazily on row 0 — same cost), so
8044            // it still pays the full scan, never the 715 ms per-row
8045            // direct eval; its index-nested-loop probe is the next
8046            // knife. Genuinely non-batchable shapes return None and are
8047            // left unseeded for the loop's per-row resolver, as before.
8048            for (_, expr) in &agg.deferred {
8049                let mut subs: Vec<&SelectStatement> = Vec::new();
8050                collect_scalar_subqueries(expr, &mut subs);
8051                for sub in subs {
8052                    let repr = alloc::format!("{sub}");
8053                    if memo.group_maps.contains_key(&repr) {
8054                        continue;
8055                    }
8056                    if let Some(gm) = self.try_batch_correlated_scalar(
8057                        sub,
8058                        Some((&agg.synth_rows, &ctx)),
8059                        cancel,
8060                    )? {
8061                        memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
8062                    }
8063                }
8064            }
8065            for (ri, srow) in agg.synth_rows.iter().enumerate() {
8066                cancel.check()?;
8067                for (col, expr) in &agg.deferred {
8068                    let v =
8069                        self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
8070                    if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
8071                        *cell = v;
8072                    }
8073                }
8074            }
8075        }
8076        Ok(QueryResult::Rows {
8077            columns: agg.columns,
8078            rows: agg.rows,
8079        })
8080    }
8081
8082    /// v7.37 — streaming projection for the joined-non-aggregate
8083    /// shape (multi-table FROM, all projection items bound, no
8084    /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
8085    /// UNION). Walks the deferred join survivors and emits
8086    /// `&[&Value]` borrowed straight out of the source tables — no
8087    /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
8088    /// on the mailrs `PROJ` shape (about 4 ms saved).
8089    ///
8090    /// Returns `Ok(None)` when the shape doesn't qualify; the caller
8091    /// then falls back to the materialising path.
8092    /// v7.37 (round 831) — stream a joinless SELECT straight off the
8093    /// stored table, one row at a time, without ever building a row set.
8094    ///
8095    /// Returns `Ok(None)` for anything this cannot serve, and the caller
8096    /// falls through to the deferred-join path exactly as before: a
8097    /// missing table, or a cold tier whose hydration the fallback handles.
8098    /// Sort a single-table scan through the external sorter, so the
8099    /// answer's size is bounded by `work_mem` and not by the input.
8100    ///
8101    /// Sorting held every row twice — the scan's `Vec<Row>` and the
8102    /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
8103    /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
8104    /// enough ORDER BY took the server down, which is a liveness
8105    /// problem before it is a performance one.
8106    ///
8107    /// A SEPARATE walk rather than a change to `run_single_table_scan`,
8108    /// following what round 831 did for the joinless shape. That
8109    /// function is 552 lines whose projection loop is entangled with
8110    /// DISTINCT (which indexes back into the tagged vector) and with
8111    /// streaming top-N (whose boundary moves as the scan runs); both
8112    /// assume the projection has already happened when a row is
8113    /// pushed, which is exactly what spilling has to defer. Two earlier
8114    /// attempts tried to rework that loop and were reverted. Here the
8115    /// existing path is untouched and this one only claims shapes it
8116    /// can serve, so a decline costs nothing.
8117    ///
8118    /// Records are SOURCE rows, not projected ones: `finish` re-derives
8119    /// keys from what it decodes, and an ORDER BY key need not be in
8120    /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
8121    fn try_spill_sorted_scan(
8122        &self,
8123        stmt: &SelectStatement,
8124        from: &FromClause,
8125        cancel: CancelToken<'_>,
8126    ) -> Result<Option<QueryResult>, EngineError> {
8127        // Shapes this walk does not serve. Each one either needs the
8128        // whole tagged vector addressable (DISTINCT probes back into
8129        // it, WITH TIES re-reads its tail) or is already bounded
8130        // without spilling (a LIMIT makes the partial sort O(keep)).
8131        if !self.can_spill()
8132            || stmt.order_by.is_empty()
8133            || stmt.distinct
8134            || stmt.limit_with_ties
8135            || stmt.limit_literal().is_some()
8136            || !from.joins.is_empty()
8137            || from.primary.lateral_subquery.is_some()
8138            || from.primary.unnest_expr.is_some()
8139            || from.primary.generate_series_args.is_some()
8140            || select_has_window(stmt)
8141        {
8142            return Ok(None);
8143        }
8144        // A parent's rows are its children's. These walks scan the named
8145        // relation alone, so a partitioned or inherited parent comes back
8146        // short — and silently: the corpus caught `SELECT id FROM pr
8147        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8148        // parent's own rows instead of the partitions'. `ONLY` is exactly
8149        // the case that does not fan out, so it stays, which is the test
8150        // the FROM-clause fan-out itself makes.
8151        if !from.primary.only
8152            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8153        {
8154            return Ok(None);
8155        }
8156        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8157            return Ok(None);
8158        };
8159        // Cold-tier rows live outside `rows()`; this walk would drop
8160        // them silently, the same reason round 831's walk declines.
8161        if table.has_cold_rows_fast() {
8162            return Ok(None);
8163        }
8164
8165        let alias = from
8166            .primary
8167            .alias
8168            .as_deref()
8169            .unwrap_or(from.primary.name.as_str());
8170        let cols = table.schema().columns.clone();
8171        let sess = self.dml_session();
8172        let ctx = EvalContext::new(&cols, Some(alias))
8173            .with_catalog(self.active_catalog())
8174            .with_session(&sess);
8175        let projection = build_projection(
8176            &stmt.items,
8177            &cols,
8178            alias,
8179            self.speaks_mysql,
8180            Some(self.active_catalog()),
8181        )?;
8182        let order_by = stmt.order_by.clone();
8183        // The same one-shot resolution the general path does (round
8184        // 582): each ORDER BY column is bound once, not once per row.
8185        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8186        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8187        // Resolved BEFORE the scan, because it now decides what the sort
8188        // STORES and not just what it decodes (round 995).
8189        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8190
8191        // v7.38.22 — resolved HERE, because this path did not resolve
8192        // them at all.
8193        //
8194        // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8195        // "en_US.utf8"` in BYTE order on this path — and swallowed an
8196        // unknown collation name rather than raising — because the sorter
8197        // below compared with an empty collation slice. The materialising
8198        // path honoured both. Which answer a query got depended on which
8199        // path the planner took, and this is the path a plain single-table
8200        // SELECT takes.
8201        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
8202        // v7.39.12 — a correlated scalar subquery in ORDER BY is
8203        // resolved for the row before its key is built.
8204        //
8205        // Uncorrelated subqueries are replaced by a literal before
8206        // execution; a correlated one cannot be, so it reached the
8207        // per-row evaluator — the one place that cannot run a subquery
8208        // — and the statement raised "subquery reached row eval".
8209        // Reported by sentori against 7.39.11; see
8210        // `Engine::order_by_resolved_for_row`.
8211        //
8212        // The `any` runs once, here, so an ordinary ORDER BY pays one
8213        // bool per row and nothing else.
8214        let order_has_subquery = order_by
8215            .iter()
8216            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
8217        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
8218        let mut sorter = crate::extsort::ExternalSorter::new(
8219            self.temp_run_factory,
8220            self.session_work_mem_bytes(),
8221            cols.clone(),
8222            &descs,
8223            &order_colls,
8224        )
8225        .with_stats(&self.spill_stats)
8226        .with_workers(self.session_parallel_workers())
8227        .with_pruned(&needed);
8228        let snapshot = self.current_snapshot();
8229        // One key buffer for the whole scan: `push` drains it and leaves
8230        // the capacity behind.
8231        let mut keys: Vec<OrderKey> = Vec::new();
8232        // r1024 — compile the predicate once for the scan.
8233        //
8234        // These two sorted-spill scans are the paths a single-table SELECT
8235        // with an ORDER BY takes, and they were the last row-returning ones
8236        // still walking the expression tree per row. r1023 did the
8237        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8238        // exactly this shape.
8239        //
8240        // Found from the profile's CALL TREE rather than its leaves. The
8241        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8242        // 261, `mod_op` 178 — and two attempts at reasoning out which
8243        // function asked for it were both wrong. The tree names the caller
8244        // chain, and it named this one.
8245        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8246            .where_
8247            .as_ref()
8248            .filter(|w| crate::eval::fully_compilable(w))
8249            .map(|w| crate::eval::compile_expr(w, &ctx));
8250        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8251        for (i, row) in table.scan_visible_from(0, &snapshot) {
8252            if i.is_multiple_of(256) {
8253                cancel.check()?;
8254            }
8255            if let Some(c) = &compiled_where {
8256                if !crate::eval::compiled::eval_compiled_pred(
8257                    c,
8258                    row,
8259                    &ctx,
8260                    &mut eval_stack,
8261                    ctx.mysql_dialect,
8262                )? {
8263                    continue;
8264                }
8265            } else if let Some(w) = &stmt.where_ {
8266                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8267                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8268                    continue;
8269                }
8270            }
8271            keys.clear();
8272            // The same collations the sorter compares with, and the
8273            // re-derivation below is handed the same ones. `finish`'s
8274            // contract is that a key comes back the way it was pushed;
8275            // a collation is part of the way it was pushed.
8276            if order_has_subquery {
8277                // A substituted literal is no longer a bound column.
8278                let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
8279                crate::orderby::build_order_keys_bound(
8280                    per_row.as_deref().unwrap_or(&order_by),
8281                    &unbound,
8282                    &order_colls,
8283                    row,
8284                    &ctx,
8285                    &mut keys,
8286                )?;
8287            } else {
8288                crate::orderby::build_order_keys_bound(
8289                    &order_by,
8290                    &order_bound,
8291                    &order_colls,
8292                    row,
8293                    &ctx,
8294                    &mut keys,
8295                )?;
8296            }
8297            sorter.push(&mut keys, row)?;
8298        }
8299
8300        let key_ctx = &ctx;
8301        let rows = sorter.finish(
8302            |src, buf| {
8303                crate::orderby::build_order_keys_rederived(
8304                    &order_by,
8305                    &order_bound,
8306                    &order_colls,
8307                    src,
8308                    key_ctx,
8309                    buf,
8310                )
8311            },
8312            |src| {
8313                let mut values = Vec::with_capacity(projection.len());
8314                for p in &projection {
8315                    values.push(
8316                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8317                    );
8318                }
8319                Ok(Row::new(values))
8320            },
8321        )?;
8322
8323        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8324        Ok(Some(QueryResult::Rows { columns, rows }))
8325    }
8326
8327    /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
8328    /// handing each row to the consumer instead of collecting the answer.
8329    ///
8330    /// That walk bounds the SORT and then returns `QueryResult::Rows`,
8331    /// which holds every output row. Measured at `work_mem = 4 MB` over
8332    /// 200-byte rows, RSS above the server's own baseline while the
8333    /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
8334    /// at 400k — linear — while the spill underneath worked correctly
8335    /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
8336    /// removes each file, so a count taken afterwards reads 0 whatever
8337    /// happened, and an earlier reading of "no spill at all" was that
8338    /// blind witness). The growth is the collected result, not the sort.
8339    ///
8340    /// Emitting makes peak the budget, one buffer per run and a single
8341    /// row — the state a merge already holds at every step. It also
8342    /// frees each projected row as the next is built rather than
8343    /// accumulating them, which is where the time is: a profile of the
8344    /// collecting walk put the allocator at 586 samples, more than every
8345    /// sort comparison combined (420), against 19 for `push` itself.
8346    /// v7.37 (round 923) — which of a sort record's columns the output half
8347    /// reads. The record is the SOURCE row (round 836), so a narrow projection
8348    /// decoded every column: skipping one 200-byte text halves a decode
8349    /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
8350    ///
8351    /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
8352    /// column reads NULL. Answers only when every projection item is a bare
8353    /// column reference AND every ORDER BY key is a bound column; anything
8354    /// else returns empty, decoding everything as before.
8355    /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
8356    /// drops references from expression kinds it does not enumerate.
8357    ///
8358    /// ORDER BY columns are included — the merge re-derives keys from the
8359    /// decoded row on the spilled path, so pruning one would sort NULLs.
8360    pub(crate) fn sort_record_columns_needed(
8361        items: &[SelectItem],
8362        order_bound: &[Option<usize>],
8363        arity: usize,
8364        ctx: &EvalContext,
8365    ) -> Vec<bool> {
8366        let all_bare = items.iter().all(|i| {
8367            matches!(
8368                i,
8369                SelectItem::Expr {
8370                    expr: Expr::Column(_),
8371                    ..
8372                }
8373            )
8374        });
8375        if !all_bare || order_bound.iter().any(Option::is_none) {
8376            return Vec::new();
8377        }
8378        let mut mask = alloc::vec![false; arity];
8379        for item in items {
8380            if let SelectItem::Expr {
8381                expr: Expr::Column(c),
8382                ..
8383            } = item
8384            {
8385                match crate::eval::find_column_pos(c, ctx) {
8386                    Some(p) if p < arity => mask[p] = true,
8387                    _ => return Vec::new(),
8388                }
8389            }
8390        }
8391        for p in order_bound.iter().flatten() {
8392            if *p < arity {
8393                mask[*p] = true;
8394            } else {
8395                return Vec::new();
8396            }
8397        }
8398        mask
8399    }
8400
8401    /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
8402    /// of sorting.
8403    ///
8404    /// PG serves such an ordering from the index and never sorts. We sorted:
8405    /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
8406    /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
8407    /// the sorter's own round trip — `ExternalSorter::finish_each` →
8408    /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
8409    /// Every row is encoded into the sorter's arena and decoded back out,
8410    /// for an order the index already holds.
8411    ///
8412    /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
8413    /// because it was built for top-N. This is the unbounded sibling.
8414    ///
8415    /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
8416    /// from a btree, so walking one would silently drop those rows. That is
8417    /// exactly the defect r1020 fixed on the top-N path, where it had
8418    /// shipped.
8419    /// r1044 — the index this statement's ORDER BY can be WALKED on,
8420    /// instead of sorted, or `None`.
8421    ///
8422    /// Extracted so `EXPLAIN` can ask the same question the executor
8423    /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
8424    /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
8425    /// while the executor walked the primary key — 34.9 ms against
8426    /// 147.0 for the same query ordered by an unindexed column, so the
8427    /// walk was plainly running. Round 551 fixed a different case of
8428    /// this and wrote the reason down: EXPLAIN is the first thing any
8429    /// performance question opens, and an instrument that misnames the
8430    /// access path is worse than one that says nothing.
8431    ///
8432    /// The gate is here once. Two copies of it is how the plan and the
8433    /// executor come to disagree again.
8434    /// v7.39.13 — the shape refusals both ordered-walk gates make.
8435    ///
8436    /// One list, because two of them would be two answers to "can this
8437    /// statement walk an index", and a walk that runs where EXPLAIN says
8438    /// it does not is the defect r1044 exists to prevent.
8439    /// v7.39.13 — `WHERE lead = <literal> ORDER BY next [DESC] LIMIT n`
8440    /// behind an index on `(lead, next, …)`: one seek to the key prefix,
8441    /// then n steps inside it.
8442    ///
8443    /// Sentori's busiest read, and the one shape they have reported
8444    /// unchanged for three versions: `WHERE project_id = ? ORDER BY
8445    /// received_at DESC LIMIT 20`. PostgreSQL 18 answers it with
8446    /// `Limit -> Index Scan`; SPG planned `Sort -> Seq Scan` and sorted
8447    /// the table to return twenty rows, roughly 250x behind.
8448    ///
8449    /// The ordered walk that existed could only start at an index's
8450    /// LEADING column, so an index on `(project_id, received_at)` could
8451    /// serve `ORDER BY project_id` and nothing else. What was missing is
8452    /// below it: a tree walk bounded by a key prefix, which
8453    /// `Index::iter_prefix_desc` now provides.
8454    ///
8455    /// The equality conjunct only NARROWS the walk — the statement's own
8456    /// `WHERE` still runs per row — so picking the wrong conjunct can
8457    /// cost time and cannot change an answer.
8458    pub(crate) fn index_prefix_walk_target(
8459        &self,
8460        stmt: &SelectStatement,
8461        from: &FromClause,
8462    ) -> Option<(String, usize, alloc::vec::Vec<spg_storage::IndexKey>)> {
8463        if self.walk_shape_refused(stmt, from) {
8464            return None;
8465        }
8466        // One ORDER BY term for now: a second one would have to be the
8467        // next key column again, and the tree walks one direction.
8468        if stmt.order_by.len() != 1 || stmt.distinct {
8469            return None;
8470        }
8471        let table = self.active_catalog().get(&from.primary.name)?;
8472        let alias = from
8473            .primary
8474            .alias
8475            .as_deref()
8476            .unwrap_or(from.primary.name.as_str());
8477        let cols = &table.schema().columns;
8478        let order = &stmt.order_by[0];
8479        let Expr::Column(oc) = &order.expr else {
8480            return None;
8481        };
8482        if let Some(q) = &oc.qualifier
8483            && !q.eq_ignore_ascii_case(alias)
8484        {
8485            return None;
8486        }
8487        let order_pos = cols
8488            .iter()
8489            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8490        // The walk comes out in the tree's order, so it may only take an
8491        // ORDER BY whose order that IS — the same question the leading-
8492        // column gate asks, for the same reason.
8493        let order_col = cols.get(order_pos)?;
8494        if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8495            && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8496        {
8497            return None;
8498        }
8499        // A NULL key is not in the tree, and this walk has no separate
8500        // pass for those rows the way the leading-column one does.
8501        if order_col.nullable {
8502            return None;
8503        }
8504        let where_ = stmt.where_.as_ref()?;
8505        for index in table.indices() {
8506            if !matches!(index.kind, spg_storage::IndexKind::BTreeMulti(_))
8507                || index.expression.is_some()
8508                || index.partial_predicate.is_some()
8509            {
8510                continue;
8511            }
8512            // The ORDER BY column must be the key component that follows
8513            // the equality-bound prefix.
8514            if index.extra_column_positions.first() != Some(&order_pos) {
8515                continue;
8516            }
8517            let lead_pos = index.column_position;
8518            let lead_col = cols.get(lead_pos)?;
8519            // The prefix is compared with the tree's own ordering, so the
8520            // leading column has to be one the tree orders bytewise too.
8521            if crate::index_access::collated_column(lead_col, table.db_collation()).is_none()
8522                && !crate::collate::column_key_is_bytewise(lead_col, self.speaks_mysql)
8523            {
8524                continue;
8525            }
8526            let Some(key) = self.eq_literal_key_for(where_, lead_pos, cols, alias) else {
8527                continue;
8528            };
8529            return Some((index.name.clone(), order_pos, alloc::vec![key]));
8530        }
8531        None
8532    }
8533
8534    /// The index key a top-level `AND` conjunct binds `col_pos` to, when
8535    /// one of them is `col = <literal>` (either way round).
8536    ///
8537    /// Only literals: a column reference or a function would have to be
8538    /// evaluated per row, and this runs once for the whole statement.
8539    fn eq_literal_key_for(
8540        &self,
8541        where_: &Expr,
8542        col_pos: usize,
8543        cols: &[ColumnSchema],
8544        alias: &str,
8545    ) -> Option<spg_storage::IndexKey> {
8546        let col = cols.get(col_pos)?;
8547        let mut found: Option<spg_storage::IndexKey> = None;
8548        let mut stack: alloc::vec::Vec<&Expr> = alloc::vec![where_];
8549        while let Some(e) = stack.pop() {
8550            match e {
8551                Expr::Binary {
8552                    lhs,
8553                    op: spg_sql::ast::BinOp::And,
8554                    rhs,
8555                } => {
8556                    stack.push(lhs);
8557                    stack.push(rhs);
8558                }
8559                Expr::Binary {
8560                    lhs,
8561                    op: spg_sql::ast::BinOp::Eq,
8562                    rhs,
8563                } => {
8564                    let names_col = |x: &Expr| match x {
8565                        Expr::Column(c) => {
8566                            c.name.eq_ignore_ascii_case(&col.name)
8567                                && c.qualifier
8568                                    .as_ref()
8569                                    .is_none_or(|q| q.eq_ignore_ascii_case(alias))
8570                        }
8571                        _ => false,
8572                    };
8573                    let lit = if names_col(lhs) {
8574                        Some(&**rhs)
8575                    } else if names_col(rhs) {
8576                        Some(&**lhs)
8577                    } else {
8578                        None
8579                    };
8580                    // v7.39.13 — a BARE literal means whatever the
8581                    // COLUMN says it means, and
8582                    // `literal_as_column_value` is the one place that
8583                    // decision is made. Asking
8584                    // `literal_expr_to_value` instead made this the
8585                    // fifth copy of it, and it read every string
8586                    // literal as text: `WHERE k = '\x07'` on a `bytea`
8587                    // column built no key at all, so the walk declined
8588                    // and the plan went back to sorting the table —
8589                    // while the EQUALITY seek beside it, which does ask
8590                    // the one funnel, used the very same index.
8591                    //
8592                    // Anything that is not a bare literal — a cast, a
8593                    // negation — already carries its own type, and
8594                    // `from_value_for_column` decides whether that type
8595                    // keys for this column.
8596                    let v = match lit {
8597                        Some(Expr::Literal(l)) => {
8598                            crate::index_access::literal_as_column_value(l, col, col_pos)
8599                        }
8600                        Some(other) => {
8601                            crate::conversions::literal_expr_to_value(other.clone()).ok()
8602                        }
8603                        None => None,
8604                    };
8605                    if let Some(v) = v
8606                        && !v.is_null()
8607                        && let Some(k) = spg_storage::IndexKey::from_value_for_column(&v, col.ty)
8608                    {
8609                        found = Some(k);
8610                    }
8611                }
8612                _ => {}
8613            }
8614        }
8615        found
8616    }
8617
8618    fn walk_shape_refused(&self, stmt: &SelectStatement, from: &FromClause) -> bool {
8619        // A non-literal count is refused: `LIMIT $1` is rewritten to a
8620        // literal by `resolve_limit_exprs` before dispatch, so anything
8621        // still carrying a placeholder here has not been through it.
8622        let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8623            matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8624        };
8625        if stmt.order_by.is_empty()
8626            || !stmt.distinct_on.is_empty()
8627            || stmt.limit_with_ties
8628            || !literal_count(&stmt.limit)
8629            || !literal_count(&stmt.offset)
8630            || stmt.having.is_some()
8631            || stmt.group_by.is_some()
8632            || !stmt.unions.is_empty()
8633            || !from.joins.is_empty()
8634            || from.primary.lateral_subquery.is_some()
8635            || from.primary.unnest_expr.is_some()
8636            || from.primary.as_of_segment.is_some()
8637            || from.primary.generate_series_args.is_some()
8638            || select_has_window(stmt)
8639            || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
8640        {
8641            return true;
8642        }
8643        if stmt
8644            .items
8645            .iter()
8646            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8647        {
8648            return true;
8649        }
8650        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8651            return true;
8652        };
8653        if table.has_cold_rows_fast() {
8654            return true;
8655        }
8656        !from.primary.only
8657            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8658    }
8659
8660    pub(crate) fn index_order_walk_target(
8661        &self,
8662        stmt: &SelectStatement,
8663        from: &FromClause,
8664    ) -> Option<(String, usize)> {
8665        // v7.39.11 — LIMIT / OFFSET join the walk instead of refusing it.
8666        //
8667        // Reported by sentori against 7.39.10 and measured on their own
8668        // busiest read: "the most recent N events for this project",
8669        // backed by an index on exactly that ordering. PostgreSQL 18
8670        // answered it with `Limit -> Index Scan`; SPG with
8671        // `Limit -> Sort -> Seq Scan`, 4.998 ms against 0.021 — the
8672        // whole table sorted to return twenty rows.
8673        //
8674        // The walk was built for this shape — `iter_desc`'s own doc says
8675        // "the ORDER BY <indexed col> DESC + LIMIT N executor path" —
8676        // and then the gate refused every statement that had a LIMIT, so
8677        // the one query it was written for could never reach it. The
8678        // capability was here; the routing was not.
8679        //
8680        // A non-literal count is refused: `LIMIT $1` is rewritten to a
8681        // literal by `resolve_limit_exprs` before dispatch, so anything
8682        // still carrying a placeholder here has not been through it.
8683        let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8684            matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8685        };
8686        if self.walk_shape_refused(stmt, from) {
8687            return None;
8688        }
8689        let table = self.active_catalog().get(&from.primary.name)?;
8690        let alias = from
8691            .primary
8692            .alias
8693            .as_deref()
8694            .unwrap_or(from.primary.name.as_str());
8695        let cols = &table.schema().columns;
8696        let order = &stmt.order_by[0];
8697        let Expr::Column(oc) = &order.expr else {
8698            return None;
8699        };
8700        if let Some(q) = &oc.qualifier
8701            && !q.eq_ignore_ascii_case(alias)
8702        {
8703            return None;
8704        }
8705        let order_pos = cols
8706            .iter()
8707            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8708        // r1047 — DISTINCT joins the walk when the projection IS the
8709        // order column, and only then. The index's keys are canonical
8710        // (r1039: representation equality is value equality — the
8711        // property every seek already depends on), so one key is one
8712        // distinct value and the walk can emit the first passing row of
8713        // each key group instead of hashing every row. On the release
8714        // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
8715        // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
8716        // with an ablation floor of 14.8, because the hash must
8717        // normalize and probe ALL the rows; the walk visits each key
8718        // once. A wider projection makes DISTINCT about the whole tuple,
8719        // not the key, so anything else still declines.
8720        if stmt.distinct {
8721            let only_the_order_column = stmt.items.len() == 1
8722                && match &stmt.items[0] {
8723                    SelectItem::Expr {
8724                        expr: Expr::Column(c),
8725                        ..
8726                    } => {
8727                        c.name.eq_ignore_ascii_case(&oc.name)
8728                            && match &c.qualifier {
8729                                Some(q) => q.eq_ignore_ascii_case(alias),
8730                                None => true,
8731                            }
8732                    }
8733                    _ => false,
8734                };
8735            if !only_the_order_column {
8736                return None;
8737            }
8738        }
8739        // r1046 — a nullable key no longer refuses the walk; it changes
8740        // what the walk has to do. A NULL key is not in the btree, so
8741        // walking alone would silently drop those rows — the r1020
8742        // defect, which shipped once. The walk emits them separately, at
8743        // the end SQL puts them.
8744        //
8745        // Refusing was costing every nullable indexed column a 3.4x:
8746        // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
8747        // 72.0 ms with the column nullable and 20.2 with the same data
8748        // under NOT NULL. `NOT NULL` is not the default, so that was the
8749        // common case paying for the uncommon one.
8750        // v7.39.11 — the walk comes out in the tree's order, so it may
8751        // only take an ORDER BY whose order that IS.
8752        //
8753        // The B-tree walks in BYTE order unless the column's keys are
8754        // ICU sort keys. `try_pk_walk_top_n` has asked this since
8755        // v7.38.18; this gate never did, and the answer changed when an
8756        // index appeared. Measured on `alpha / Beta / GAMMA / delta`
8757        // over a MySQL-dialect session, `SELECT t FROM s ORDER BY t`:
8758        //
8759        //   no index   alpha Beta delta GAMMA   (MySQL's own order)
8760        //   indexed    Beta GAMMA alpha delta   (bytes)
8761        //
8762        // No row is wrong and nothing raises; only the order changes,
8763        // and it changes because an index exists. Ordering is the one
8764        // thing a walk contributes, so when it is the wrong ordering
8765        // there is nothing left to keep.
8766        let order_col = cols.get(order_pos)?;
8767        if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8768            && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8769        {
8770            return None;
8771        }
8772        // v7.39.11 — a composite B-tree LEADING on the ORDER BY column
8773        // walks it too, which is what `try_pk_walk_top_n` has always
8774        // done and what this gate did not know.
8775        //
8776        // Keys sort by the whole tuple, so the leading component comes
8777        // out in order — `Index::iter_asc` says so, and the materialising
8778        // top-N walk has relied on it since v7.38.1. The consequence of
8779        // the two gates disagreeing was the thing r1044 exists to
8780        // prevent: measured on a table indexed `(a, b)`, `SELECT a FROM
8781        // m ORDER BY a LIMIT 2` planned as `Limit -> Sort -> Seq Scan`
8782        // while the executor plainly walked the index — a projection
8783        // that divides by zero on the last row in key order returned two
8784        // rows instead of raising. EXPLAIN is the first thing any
8785        // performance question opens, and an instrument that misnames
8786        // the access path is worse than one that says nothing.
8787        let index = table
8788            .index_on(order_pos)
8789            .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8790            .or_else(|| {
8791                table.indices().iter().find(|i| {
8792                    matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8793                        && i.column_position == order_pos
8794                })
8795            })?;
8796        if index.expression.is_some() || index.partial_predicate.is_some() {
8797            return None;
8798        }
8799        // v7.39.11 — more than one ORDER BY term walks when the index
8800        // holds exactly that ordering.
8801        //
8802        // Keys sort by the whole tuple, so `iter_asc` over a composite
8803        // B-tree IS `ORDER BY a, b` — the walk needs no new machinery,
8804        // only permission. Reported by sentori against 7.39.10:
8805        // `ORDER BY a, b LIMIT 10` planned as `Seq Scan -> Sort` here
8806        // against an `Incremental Sort` over an index scan on
8807        // PostgreSQL 18, on a table indexed for it.
8808        //
8809        // Three things have to hold, and each of them is the tree's
8810        // limitation rather than a conservative choice:
8811        //
8812        //   * the terms are the index's key columns, in its order, from
8813        //     the leading one — a suffix or a permutation is a different
8814        //     ordering;
8815        //   * every term runs the same direction, because the tree is
8816        //     walked one way for all of them. `(a, b DESC)` is what
8817        //     PostgreSQL serves from an index whose SECOND key is
8818        //     descending, and SPG's tree does not scan per column;
8819        //   * every key column is NOT NULL. A NULL key is not in the
8820        //     tree at all, and the separate pass that emits those rows
8821        //     (r1046) knows how to place them for ONE column, not for a
8822        //     tuple.
8823        if stmt.order_by.len() > 1 {
8824            let keys: Vec<usize> = core::iter::once(index.column_position)
8825                .chain(index.extra_column_positions.iter().copied())
8826                .collect();
8827            if stmt.order_by.len() > keys.len() {
8828                return None;
8829            }
8830            let desc = stmt.order_by[0].desc;
8831            for (term, &key_pos) in stmt.order_by.iter().zip(keys.iter()) {
8832                if term.desc != desc {
8833                    return None;
8834                }
8835                let Expr::Column(c) = &term.expr else {
8836                    return None;
8837                };
8838                if let Some(q) = &c.qualifier
8839                    && !q.eq_ignore_ascii_case(alias)
8840                {
8841                    return None;
8842                }
8843                let pos = cols
8844                    .iter()
8845                    .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
8846                if pos != key_pos {
8847                    return None;
8848                }
8849                let col = cols.get(pos)?;
8850                if col.nullable {
8851                    return None;
8852                }
8853                if crate::index_access::collated_column(col, table.db_collation()).is_none()
8854                    && !crate::collate::column_key_is_bytewise(col, self.speaks_mysql)
8855                {
8856                    return None;
8857                }
8858            }
8859        }
8860        Some((index.name.clone(), order_pos))
8861    }
8862
8863    fn try_index_order_stream<F>(
8864        &self,
8865        stmt: &SelectStatement,
8866        from: &FromClause,
8867        cancel: CancelToken<'_>,
8868        emit: &mut F,
8869    ) -> Result<Option<usize>, EngineError>
8870    where
8871        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8872    {
8873        // r1044 — the shape gate lives in `index_order_walk_target`, so
8874        // `EXPLAIN` answers the same question. What stays here is the
8875        // part that RAISES (an illegal ORDER BY has to keep erroring
8876        // from where it did) and the bindings the walk needs.
8877        crate::orderby::check_order_by_legality(stmt)?;
8878        crate::orderby::check_order_by_positions(stmt)?;
8879        crate::window::reject_window_in_row_clauses(stmt)?;
8880        // v7.39.13 — the prefix walk first: it serves a shape the
8881        // leading-column walk cannot, and refuses everything that one
8882        // takes.
8883        let (order_pos, prefix) = match self.index_prefix_walk_target(stmt, from) {
8884            Some((_, pos, keys)) => (pos, Some(keys)),
8885            None => match self.index_order_walk_target(stmt, from) {
8886                Some((_, pos)) => (pos, None),
8887                None => return Ok(None),
8888            },
8889        };
8890        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8891            return Ok(None);
8892        };
8893        let alias = from
8894            .primary
8895            .alias
8896            .as_deref()
8897            .unwrap_or(from.primary.name.as_str());
8898        let cols = table.schema().columns.clone();
8899        let order = &stmt.order_by[0];
8900        // v7.39.11 — the same lookup the gate made; see
8901        // `index_order_walk_target`.
8902        let Some(index) = (if prefix.is_some() {
8903            // The prefix planner named an index whose FIRST extra key
8904            // column is the order column; the lookup below looks for one
8905            // whose LEADING column is, and would find the wrong tree.
8906            table.indices().iter().find(|i| {
8907                matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8908                    && i.extra_column_positions.first() == Some(&order_pos)
8909                    && i.expression.is_none()
8910                    && i.partial_predicate.is_none()
8911            })
8912        } else {
8913            table
8914                .index_on(order_pos)
8915                .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8916                .or_else(|| {
8917                    table.indices().iter().find(|i| {
8918                        matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8919                            && i.column_position == order_pos
8920                    })
8921                })
8922        }) else {
8923            return Ok(None);
8924        };
8925
8926        let sess = self.dml_session();
8927        let ctx = EvalContext::new(&cols, Some(alias))
8928            .with_catalog(self.active_catalog())
8929            .with_session(&sess);
8930        let projection = build_projection(
8931            &stmt.items,
8932            &cols,
8933            alias,
8934            self.speaks_mysql,
8935            Some(self.active_catalog()),
8936        )?;
8937        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8938        emit(crate::StreamItem::Header(&columns))?;
8939        let bound_pos: Vec<Option<usize>> = projection
8940            .iter()
8941            .map(|p| match &p.expr {
8942                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8943                    Ok(Some(pos)) => Some(pos),
8944                    _ => None,
8945                },
8946                _ => None,
8947            })
8948            .collect();
8949
8950        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8951            .where_
8952            .as_ref()
8953            .filter(|w| crate::eval::fully_compilable(w))
8954            .map(|w| crate::eval::compile_expr(w, &ctx));
8955        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8956        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8957        let snapshot = self.current_snapshot();
8958
8959        // A btree holds one locator per row VERSION, so a row whose key was
8960        // updated can sit under two keys and a dead one can sit beside its
8961        // replacement. The visibility gate drops the dead; `seen` drops a
8962        // live row that the walk reaches twice, which would otherwise be a
8963        // duplicated output row rather than a slow one.
8964        let mut emitted_rows = alloc::vec![false; table.rows().len()];
8965
8966        // r1046 — the rows the index cannot hold.
8967        //
8968        // A NULL key is not in the btree, so the walk below never reaches
8969        // those rows; they are emitted here, at the end SQL puts them.
8970        // PG's default is NULLS LAST ascending and NULLS FIRST
8971        // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
8972        // the same rule `order_by_value_cmp_raw` applies to the sort this
8973        // replaces, so the two orders agree.
8974        //
8975        // Finding them costs one pass over the column. That pass is why
8976        // this is still worth doing: the sort it replaces encodes and
8977        // decodes every row, and the walk plus the pass measured 72.0 ms
8978        // down to about 22 on 400,000 rows.
8979        let nulls_first = order.nulls_first.unwrap_or(order.desc);
8980        // r1047 — under DISTINCT the walk emits the FIRST passing row of
8981        // each key group and skips the rest; the gate admits DISTINCT
8982        // only when the projection is the order column itself, so one
8983        // canonical key is one output row. NULL is one distinct value,
8984        // so the NULL pass stops at its first emit too.
8985        let distinct = stmt.distinct;
8986        let mut count = 0usize;
8987        let mut visited = 0usize;
8988        // v7.39.11 — OFFSET and LIMIT, applied as the walk goes.
8989        //
8990        // Both count PASSING rows, so a skipped row still has to run the
8991        // predicate and the projection — `stream_filter_project` is
8992        // `stream_project_row` without the emit, which is exactly that.
8993        // Stopping at `remaining == 0` is the whole point: twenty rows
8994        // off the end of an index instead of a sorted table.
8995        let mut to_skip = stmt.offset_literal().unwrap_or(0) as usize;
8996        let mut remaining: Option<usize> = stmt.limit_literal().map(|l| l as usize);
8997        let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
8998                                  eval_stack: &mut Vec<Value<'static>>,
8999                                  values: &mut Vec<Value<'static>>,
9000                                  visited: &mut usize,
9001                                  to_skip: &mut usize,
9002                                  remaining: &mut Option<usize>,
9003                                  emit: &mut F|
9004         -> Result<usize, EngineError> {
9005            if !cols[order_pos].nullable {
9006                return Ok(0);
9007            }
9008            // v7.39.11 — nothing to emit once the LIMIT is met, and
9009            // finding that out must not cost a scan.
9010            //
9011            // This pass looks for NULL-keyed rows by walking the whole
9012            // heap, because they are not in the tree. That is the price
9013            // r1046 measured and accepted for an UNBOUNDED order. With
9014            // a LIMIT the walk above has usually already produced every
9015            // row the caller asked for, and scanning 400,000 rows to
9016            // add none of them is the whole cost of the query: the
9017            // release sweep's `SELECT pad FROM t ORDER BY n LIMIT 10`
9018            // over a nullable indexed NUMERIC went 0.237 ms at 50,000
9019            // rows and 2.251 at 400,000 — linear, against PostgreSQL's
9020            // 0.155 and 0.182 — the moment this gate started accepting
9021            // LIMIT. The `remaining` check below sits after the
9022            // per-row filters, so it could never be reached.
9023            if *remaining == Some(0) {
9024                return Ok(0);
9025            }
9026            let mut n = 0usize;
9027            for (ri, row) in table.rows().iter().enumerate() {
9028                if !matches!(row.values.get(order_pos), Some(Value::Null)) {
9029                    continue;
9030                }
9031                if emitted_rows.get(ri).copied().unwrap_or(true) {
9032                    continue;
9033                }
9034                if !table.is_row_visible(ri, &snapshot) {
9035                    continue;
9036                }
9037                *visited += 1;
9038                if visited.is_multiple_of(256) {
9039                    cancel.check()?;
9040                }
9041                emitted_rows[ri] = true;
9042                if *remaining == Some(0) {
9043                    break;
9044                }
9045                let passed = if *to_skip > 0 {
9046                    let p = Self::stream_filter_project(
9047                        row,
9048                        stmt.where_.as_ref(),
9049                        compiled_where.as_ref(),
9050                        eval_stack,
9051                        &projection,
9052                        &bound_pos,
9053                        &ctx,
9054                        values,
9055                    )?;
9056                    if p {
9057                        *to_skip -= 1;
9058                    }
9059                    false
9060                } else {
9061                    Self::stream_project_row(
9062                        row,
9063                        stmt.where_.as_ref(),
9064                        compiled_where.as_ref(),
9065                        eval_stack,
9066                        &projection,
9067                        &bound_pos,
9068                        &ctx,
9069                        values,
9070                        emit,
9071                    )?
9072                };
9073                if passed {
9074                    n += 1;
9075                    if let Some(r) = remaining.as_mut() {
9076                        *r -= 1;
9077                        if *r == 0 {
9078                            break;
9079                        }
9080                    }
9081                    if distinct {
9082                        break;
9083                    }
9084                }
9085            }
9086            Ok(n)
9087        };
9088
9089        if nulls_first {
9090            count += emit_null_rows(
9091                &mut emitted_rows,
9092                &mut eval_stack,
9093                &mut values,
9094                &mut visited,
9095                &mut to_skip,
9096                &mut remaining,
9097                emit,
9098            )?;
9099        }
9100
9101        // v7.39.13 — a prefix walk when the statement binds the index's
9102        // leading column, the whole tree otherwise. The key is not read
9103        // by the loop, so the two shapes meet as posting lists.
9104        let walker: alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> =
9105            match prefix.as_ref().and_then(|p| {
9106                if order.desc {
9107                    index.iter_prefix_desc(p).map(
9108                        |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9109                            alloc::boxed::Box::new(it.map(|(_, l)| l))
9110                        },
9111                    )
9112                } else {
9113                    index.iter_prefix_asc(p).map(
9114                        |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9115                            alloc::boxed::Box::new(it.map(|(_, l)| l))
9116                        },
9117                    )
9118                }
9119            }) {
9120                Some(it) => it,
9121                None if order.desc => alloc::boxed::Box::new(index.iter_desc().map(|(_, l)| l)),
9122                None => alloc::boxed::Box::new(index.iter_asc().map(|(_, l)| l)),
9123            };
9124        'walk: for locators in walker {
9125            if remaining == Some(0) {
9126                break;
9127            }
9128            for loc in locators {
9129                let spg_storage::RowLocator::Hot(ri) = *loc else {
9130                    continue;
9131                };
9132                if emitted_rows.get(ri).copied().unwrap_or(true) {
9133                    continue;
9134                }
9135                if !table.is_row_visible(ri, &snapshot) {
9136                    continue;
9137                }
9138                let Some(row) = table.rows().get(ri) else {
9139                    continue;
9140                };
9141                visited += 1;
9142                if visited.is_multiple_of(256) {
9143                    cancel.check()?;
9144                }
9145                emitted_rows[ri] = true;
9146                // v7.39.11 — a skipped row still runs the predicate and
9147                // the projection, because OFFSET counts rows that PASS;
9148                // it just does not reach the client.
9149                let passed = if to_skip > 0 {
9150                    let p = Self::stream_filter_project(
9151                        row,
9152                        stmt.where_.as_ref(),
9153                        compiled_where.as_ref(),
9154                        &mut eval_stack,
9155                        &projection,
9156                        &bound_pos,
9157                        &ctx,
9158                        &mut values,
9159                    )?;
9160                    if p {
9161                        to_skip -= 1;
9162                    }
9163                    false
9164                } else {
9165                    Self::stream_project_row(
9166                        row,
9167                        stmt.where_.as_ref(),
9168                        compiled_where.as_ref(),
9169                        &mut eval_stack,
9170                        &projection,
9171                        &bound_pos,
9172                        &ctx,
9173                        &mut values,
9174                        emit,
9175                    )?
9176                };
9177                if passed {
9178                    count += 1;
9179                    if let Some(r) = remaining.as_mut() {
9180                        *r -= 1;
9181                        if *r == 0 {
9182                            break 'walk;
9183                        }
9184                    }
9185                    // One row per key group: the rest are the same value.
9186                    if distinct {
9187                        break;
9188                    }
9189                }
9190            }
9191        }
9192
9193        if !nulls_first {
9194            count += emit_null_rows(
9195                &mut emitted_rows,
9196                &mut eval_stack,
9197                &mut values,
9198                &mut visited,
9199                &mut to_skip,
9200                &mut remaining,
9201                emit,
9202            )?;
9203        }
9204        Ok(Some(count))
9205    }
9206
9207    /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
9208    /// building an `OrderKey` vector per row.
9209    ///
9210    /// The row-returning sorted scan allocates twice per row: one
9211    /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
9212    /// projection. Counted over 400 k rows (r1030,
9213    /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
9214    /// allocations and 208 MB of traffic for an answer of four hundred
9215    /// thousand integers.
9216    ///
9217    /// The key half is pure ceremony on this shape.
9218    /// `sort_tagged_by_inline_int_key` already sorts indices rather than
9219    /// rows, so the per-row vector is built, has one integer taken out of
9220    /// it, and is then dragged through the permutation — it exists to carry
9221    /// a number the row's column already held. This lane carries the number
9222    /// instead, in a fixed-size array that lives inside the buffer element
9223    /// and allocates nothing. Same idea as the predicate VM's integer lane.
9224    ///
9225    /// Declines to `None` for anything it does not cover, and every caller
9226    /// falls through to the general path, so the gate list is the
9227    /// specification.
9228    ///
9229    /// Ties: equal keys keep scan order, as the stable sort on the general
9230    /// path does. Rows that tie on every ORDER BY term are entitled to any
9231    /// order among themselves either way — see `STABILITY.md`.
9232    fn try_int_key_sorted_stream<F>(
9233        &self,
9234        stmt: &SelectStatement,
9235        from: &FromClause,
9236        cancel: CancelToken<'_>,
9237        emit: &mut F,
9238    ) -> Result<Option<usize>, EngineError>
9239    where
9240        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9241    {
9242        /// Sort terms this lane carries inline. Four covers every ORDER BY
9243        /// in the endpoint sweep and in the dogfood corpus; wider ones fall
9244        /// through rather than growing the buffer element for everybody.
9245        const MAX_KEYS: usize = 4;
9246
9247        if stmt.order_by.is_empty()
9248            || stmt.order_by.len() > MAX_KEYS
9249            // v7.38.14 — DISTINCT is admitted when the projected set is
9250            // exactly the ORDER BY set, and only then. This lane sorts, and
9251            // when the sort key determines the projected row every duplicate
9252            // lands ADJACENT to its twin -- so the de-duplication is a
9253            // comparison with the previous row rather than a hash table, and
9254            // the reason this lane declined DISTINCT disappears with it. The
9255            // seen-set it could not offer held indices into a materialised
9256            // vector; there is no seen-set now.
9257            //
9258            // The gate is as narrow as the bare-GROUP-BY rewrite's for the
9259            // same reason: `ORDER BY a` over a projection of `a, b` does NOT
9260            // place duplicates of the PAIR adjacent, so set EQUALITY, never
9261            // overlap.
9262            || (stmt.distinct && !Self::distinct_is_adjacent_after_sort(stmt))
9263            || stmt.limit_with_ties
9264            || stmt.limit.is_some()
9265            || stmt.offset.is_some()
9266            || stmt.having.is_some()
9267            || stmt.group_by.is_some()
9268            || !stmt.unions.is_empty()
9269            || !from.joins.is_empty()
9270            || from.primary.lateral_subquery.is_some()
9271            || from.primary.unnest_expr.is_some()
9272            || from.primary.as_of_segment.is_some()
9273            || from.primary.generate_series_args.is_some()
9274            || select_has_window(stmt)
9275            || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
9276        {
9277            return Ok(None);
9278        }
9279        if stmt
9280            .items
9281            .iter()
9282            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9283        {
9284            return Ok(None);
9285        }
9286        crate::orderby::check_order_by_legality(stmt)?;
9287        crate::orderby::check_order_by_positions(stmt)?;
9288        crate::window::reject_window_in_row_clauses(stmt)?;
9289        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9290            return Ok(None);
9291        };
9292        if table.has_cold_rows_fast() {
9293            return Ok(None);
9294        }
9295        if !from.primary.only
9296            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9297        {
9298            return Ok(None);
9299        }
9300        let alias = from
9301            .primary
9302            .alias
9303            .as_deref()
9304            .unwrap_or(from.primary.name.as_str());
9305        let cols = table.schema().columns.clone();
9306
9307        // Every ORDER BY term must be a NOT NULL integer column of this
9308        // table. NOT NULL is what lets the key be a bare integer: with
9309        // NULLs the lane would have to carry their ordering too, and
9310        // getting that subtly wrong is the r1020 defect.
9311        let mut key_pos = [0usize; MAX_KEYS];
9312        let mut descs = [false; MAX_KEYS];
9313        // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
9314        // which the AST records as `None`; `unwrap_or(desc)` is how the
9315        // rest of the engine resolves it.
9316        let mut nulls_first = [false; MAX_KEYS];
9317        let n_keys = stmt.order_by.len();
9318        for (slot, order) in stmt.order_by.iter().enumerate() {
9319            let Expr::Column(oc) = &order.expr else {
9320                return Ok(None);
9321            };
9322            if let Some(q) = &oc.qualifier
9323                && !q.eq_ignore_ascii_case(alias)
9324            {
9325                return Ok(None);
9326            }
9327            let Some(pos) = cols
9328                .iter()
9329                .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
9330            else {
9331                return Ok(None);
9332            };
9333            if !matches!(
9334                cols[pos].ty,
9335                spg_storage::DataType::SmallInt
9336                    | spg_storage::DataType::Int
9337                    | spg_storage::DataType::BigInt
9338            ) {
9339                return Ok(None);
9340            }
9341            key_pos[slot] = pos;
9342            descs[slot] = order.desc;
9343            nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
9344        }
9345
9346        let sess = self.dml_session();
9347        let ctx = EvalContext::new(&cols, Some(alias))
9348            .with_catalog(self.active_catalog())
9349            .with_session(&sess);
9350        let projection = build_projection(
9351            &stmt.items,
9352            &cols,
9353            alias,
9354            self.speaks_mysql,
9355            Some(self.active_catalog()),
9356        )?;
9357        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9358        let bound_pos: Vec<Option<usize>> = projection
9359            .iter()
9360            .map(|p| match &p.expr {
9361                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9362                    Ok(Some(pos)) => Some(pos),
9363                    _ => None,
9364                },
9365                _ => None,
9366            })
9367            .collect();
9368        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9369            .where_
9370            .as_ref()
9371            .filter(|w| crate::eval::fully_compilable(w))
9372            .map(|w| crate::eval::compile_expr(w, &ctx));
9373
9374        // The same first-observable point the materialising planner fires,
9375        // placed after the gates so it fires exactly once: this lane runs
9376        // BEFORE that planner and would otherwise be a hole in the
9377        // panic-isolation and cancellation-race coverage rather than a
9378        // faster path through it.
9379        crate::injection_point!("planner_first_row_fetch", &stmt.from);
9380
9381        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9382        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9383        let mut budget = ByteBudget::new(self.max_query_bytes);
9384        let snapshot = self.current_snapshot();
9385        // Keys, a NULL bit per key slot, and the row. The bitmask keeps
9386        // the element small: a nullable key still costs one bit rather
9387        // than a second array.
9388        let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
9389
9390        for (ri, row) in table.rows().iter().enumerate() {
9391            if ri.is_multiple_of(256) {
9392                cancel.check()?;
9393            }
9394            if !table.is_row_visible(ri, &snapshot) {
9395                continue;
9396            }
9397            // The key comes from the STORED row, before projection: an
9398            // ORDER BY column need not appear in the select list.
9399            let mut keys = [0i64; MAX_KEYS];
9400            let mut nulls = 0u8;
9401            let mut keyed = true;
9402            for slot in 0..n_keys {
9403                match row.values.get(key_pos[slot]) {
9404                    Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
9405                    Some(Value::Int(v)) => keys[slot] = i64::from(*v),
9406                    Some(Value::BigInt(v)) => keys[slot] = *v,
9407                    Some(Value::Null) | None => nulls |= 1 << slot,
9408                    // An integer column holding something else is a row
9409                    // this lane cannot order; hand the whole query back
9410                    // rather than guess at it.
9411                    _ => {
9412                        keyed = false;
9413                        break;
9414                    }
9415                }
9416            }
9417            if !keyed {
9418                return Ok(None);
9419            }
9420            if !Self::stream_filter_project(
9421                row,
9422                stmt.where_.as_ref(),
9423                compiled_where.as_ref(),
9424                &mut eval_stack,
9425                &projection,
9426                &bound_pos,
9427                &ctx,
9428                &mut values,
9429            )? {
9430                continue;
9431            }
9432            budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
9433            sorted.push((keys, nulls, core::mem::take(&mut values)));
9434            values.reserve(projection.len());
9435        }
9436
9437        sorted.sort_by(|a, b| {
9438            use core::cmp::Ordering;
9439            for slot in 0..n_keys {
9440                let bit = 1u8 << slot;
9441                let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
9442                    (true, true) => Ordering::Equal,
9443                    // Where the NULLs go is already decided — `nulls_first`
9444                    // resolved DESC's default when it was read. Reversing
9445                    // this for DESC as well would apply the direction
9446                    // twice and put them at the wrong end.
9447                    (true, false) => {
9448                        if nulls_first[slot] {
9449                            Ordering::Less
9450                        } else {
9451                            Ordering::Greater
9452                        }
9453                    }
9454                    (false, true) => {
9455                        if nulls_first[slot] {
9456                            Ordering::Greater
9457                        } else {
9458                            Ordering::Less
9459                        }
9460                    }
9461                    (false, false) => {
9462                        let o = a.0[slot].cmp(&b.0[slot]);
9463                        if descs[slot] { o.reverse() } else { o }
9464                    }
9465                };
9466                if ord != Ordering::Equal {
9467                    return ord;
9468                }
9469            }
9470            Ordering::Equal
9471        });
9472
9473        emit(crate::StreamItem::Header(&columns))?;
9474        // v7.38.14 — DISTINCT, de-duplicated against the PREVIOUS row.
9475        //
9476        // The gate above only admits DISTINCT when the sort key determines
9477        // the projected row, so every duplicate is adjacent to its twin by
9478        // the time this loop runs and one comparison replaces a hash table
9479        // of every row seen. Equality is `values_eq_norm` with the same mask
9480        // the materialising path builds -- deliberately the same function,
9481        // because a de-duplication that disagreed with the one on the other
9482        // path would make the answer depend on which lane a query took.
9483        //
9484        // A query that did not ask for DISTINCT pays one already-false bool
9485        // test per row: the short-circuit means the comparison never runs
9486        // and `prev` is never written.
9487        let dedup_mask = fold_mask(&projection);
9488        let fold = FoldSpec::of(self.speaks_mysql, &dedup_mask);
9489        let mut count = 0usize;
9490        let mut prev: Option<&[Value<'static>]> = None;
9491        for (_, _, vals) in &sorted {
9492            if stmt.distinct
9493                && let Some(p) = prev
9494                && values_eq_norm(p, vals, fold)
9495            {
9496                continue;
9497            }
9498            emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
9499            count += 1;
9500            if stmt.distinct {
9501                prev = Some(vals);
9502            }
9503        }
9504        Ok(Some(count))
9505    }
9506
9507    /// v7.38.14 — would sorting place every duplicate next to its twin?
9508    ///
9509    /// True when the projected expressions and the ORDER BY expressions are the
9510    /// same SET. Then the sort key determines the projected row, so equal rows
9511    /// are adjacent afterwards and an adjacent comparison de-duplicates exactly
9512    /// as a hash would -- and, because both sort paths are stable, the survivor
9513    /// is the first-seen row, which is the one the hash keeps too.
9514    ///
9515    /// A wildcard's expansion is not known here, so it is not a set this can
9516    /// compare; an ordinal ORDER BY names a select-list position rather than a
9517    /// value and is left alone.
9518    fn distinct_is_adjacent_after_sort(stmt: &SelectStatement) -> bool {
9519        if stmt.order_by.is_empty() || !stmt.distinct_on.is_empty() {
9520            return false;
9521        }
9522        let mut projected: alloc::vec::Vec<&Expr> =
9523            alloc::vec::Vec::with_capacity(stmt.items.len());
9524        for item in &stmt.items {
9525            match item {
9526                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return false,
9527                SelectItem::Expr { expr, .. } => projected.push(expr),
9528            }
9529        }
9530        if projected.is_empty() {
9531            return false;
9532        }
9533        let keys: alloc::vec::Vec<&Expr> = stmt.order_by.iter().map(|o| &o.expr).collect();
9534        if keys
9535            .iter()
9536            .any(|k| matches!(k, Expr::Literal(spg_sql::ast::Literal::Integer(_))))
9537        {
9538            return false;
9539        }
9540        projected.iter().all(|p| keys.contains(p)) && keys.iter().all(|k| projected.contains(k))
9541    }
9542
9543    fn try_spill_sorted_stream<F>(
9544        &self,
9545        stmt: &SelectStatement,
9546        from: &FromClause,
9547        cancel: CancelToken<'_>,
9548        emit: &mut F,
9549    ) -> Result<Option<usize>, EngineError>
9550    where
9551        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9552    {
9553        // The shapes `try_spill_sorted_scan` declines, plus the ones the
9554        // streaming executor does not carry (a LIMIT is already bounded
9555        // by a partial sort; the rest need the answer addressable).
9556        if !self.can_spill()
9557            || stmt.order_by.is_empty()
9558            || stmt.distinct
9559            || stmt.limit_with_ties
9560            || stmt.limit.is_some()
9561            || stmt.offset.is_some()
9562            || stmt.having.is_some()
9563            || stmt.group_by.is_some()
9564            || !stmt.unions.is_empty()
9565            || !from.joins.is_empty()
9566            || from.primary.lateral_subquery.is_some()
9567            || from.primary.unnest_expr.is_some()
9568            || from.primary.as_of_segment.is_some()
9569            || from.primary.generate_series_args.is_some()
9570            || select_has_window(stmt)
9571            || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
9572        {
9573            return Ok(None);
9574        }
9575        if stmt
9576            .items
9577            .iter()
9578            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9579        {
9580            return Ok(None);
9581        }
9582        // Everything `exec_bare_select_cancel` does before it scans runs
9583        // BELOW this path, so a statement claimed here skips it. Three of
9584        // those were missed on the way in and each was caught by a
9585        // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
9586        // ORDER BY 2` sorted happily instead of raising 42P10), the
9587        // cancellation check by another, the partition fan-out by the
9588        // differential corpus. What is reconciled, item by item: with-ties
9589        // needs ORDER BY (gated above), USING/NATURAL and RLS join
9590        // rewrites (joins gated above), the single-table RLS predicate
9591        // (the dispatcher declines a policy-subject table before this is
9592        // reached), the meta-view dispatch (those names are not in the
9593        // catalog, so the lookup below declines). These three are calls,
9594        // so the message and SQLSTATE are the ones the fall-back gives —
9595        // `select_has_window` above reads the select list and ORDER BY but
9596        // not WHERE, which is the case the third one covers.
9597        crate::orderby::check_order_by_legality(stmt)?;
9598        crate::orderby::check_order_by_positions(stmt)?;
9599        crate::window::reject_window_in_row_clauses(stmt)?;
9600        // A parent's rows are its children's. These walks scan the named
9601        // relation alone, so a partitioned or inherited parent comes back
9602        // short — and silently: the corpus caught `SELECT id FROM pr
9603        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
9604        // parent's own rows instead of the partitions'. `ONLY` is exactly
9605        // the case that does not fan out, so it stays, which is the test
9606        // the FROM-clause fan-out itself makes.
9607        if !from.primary.only
9608            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9609        {
9610            return Ok(None);
9611        }
9612        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9613            return Ok(None);
9614        };
9615        // Cold-tier rows live outside `rows()`; this walk would drop
9616        // them silently, the same reason round 831's walk declines.
9617        if table.has_cold_rows_fast() {
9618            return Ok(None);
9619        }
9620
9621        let alias = from
9622            .primary
9623            .alias
9624            .as_deref()
9625            .unwrap_or(from.primary.name.as_str());
9626        let cols = table.schema().columns.clone();
9627        let sess = self.dml_session();
9628        let ctx = EvalContext::new(&cols, Some(alias))
9629            .with_catalog(self.active_catalog())
9630            .with_session(&sess);
9631        let projection = build_projection(
9632            &stmt.items,
9633            &cols,
9634            alias,
9635            self.speaks_mysql,
9636            Some(self.active_catalog()),
9637        )?;
9638        let order_by = stmt.order_by.clone();
9639        // The same one-shot resolution the general path does (round
9640        // 582): each ORDER BY column is bound once, not once per row.
9641        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
9642        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
9643        // Resolved BEFORE the scan, because it now decides what the sort
9644        // STORES and not just what it decodes (round 995).
9645        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
9646
9647        // v7.38.22 — resolved HERE, because this path did not resolve
9648        // them at all.
9649        //
9650        // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
9651        // "en_US.utf8"` in BYTE order on this path — and swallowed an
9652        // unknown collation name rather than raising — because the sorter
9653        // below compared with an empty collation slice. The materialising
9654        // path honoured both. Which answer a query got depended on which
9655        // path the planner took, and this is the path a plain single-table
9656        // SELECT takes.
9657        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
9658        // v7.39.12 — a correlated scalar subquery in ORDER BY is
9659        // resolved for the row before its key is built.
9660        //
9661        // Uncorrelated subqueries are replaced by a literal before
9662        // execution; a correlated one cannot be, so it reached the
9663        // per-row evaluator — the one place that cannot run a subquery
9664        // — and the statement raised "subquery reached row eval".
9665        // Reported by sentori against 7.39.11; see
9666        // `Engine::order_by_resolved_for_row`.
9667        //
9668        // The `any` runs once, here, so an ordinary ORDER BY pays one
9669        // bool per row and nothing else.
9670        let order_has_subquery = order_by
9671            .iter()
9672            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
9673        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
9674        let mut sorter = crate::extsort::ExternalSorter::new(
9675            self.temp_run_factory,
9676            self.session_work_mem_bytes(),
9677            cols.clone(),
9678            &descs,
9679            &order_colls,
9680        )
9681        .with_stats(&self.spill_stats)
9682        .with_workers(self.session_parallel_workers())
9683        .with_pruned(&needed);
9684        let snapshot = self.current_snapshot();
9685        // One key buffer for the whole scan: `push` drains it and leaves
9686        // the capacity behind.
9687        let mut keys: Vec<OrderKey> = Vec::new();
9688        // r1024 — compile the predicate once for the scan.
9689        //
9690        // These two sorted-spill scans are the paths a single-table SELECT
9691        // with an ORDER BY takes, and they were the last row-returning ones
9692        // still walking the expression tree per row. r1023 did the
9693        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
9694        // exactly this shape.
9695        //
9696        // Found from the profile's CALL TREE rather than its leaves. The
9697        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
9698        // 261, `mod_op` 178 — and two attempts at reasoning out which
9699        // function asked for it were both wrong. The tree names the caller
9700        // chain, and it named this one.
9701        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9702            .where_
9703            .as_ref()
9704            .filter(|w| crate::eval::fully_compilable(w))
9705            .map(|w| crate::eval::compile_expr(w, &ctx));
9706        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9707        for (i, row) in table.scan_visible_from(0, &snapshot) {
9708            if i.is_multiple_of(256) {
9709                cancel.check()?;
9710            }
9711            if let Some(c) = &compiled_where {
9712                if !crate::eval::compiled::eval_compiled_pred(
9713                    c,
9714                    row,
9715                    &ctx,
9716                    &mut eval_stack,
9717                    ctx.mysql_dialect,
9718                )? {
9719                    continue;
9720                }
9721            } else if let Some(w) = &stmt.where_ {
9722                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
9723                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9724                    continue;
9725                }
9726            }
9727            keys.clear();
9728            // The same collations the sorter compares with, and the
9729            // re-derivation below is handed the same ones. `finish`'s
9730            // contract is that a key comes back the way it was pushed;
9731            // a collation is part of the way it was pushed.
9732            if order_has_subquery {
9733                // A substituted literal is no longer a bound column.
9734                let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
9735                crate::orderby::build_order_keys_bound(
9736                    per_row.as_deref().unwrap_or(&order_by),
9737                    &unbound,
9738                    &order_colls,
9739                    row,
9740                    &ctx,
9741                    &mut keys,
9742                )?;
9743            } else {
9744                crate::orderby::build_order_keys_bound(
9745                    &order_by,
9746                    &order_bound,
9747                    &order_colls,
9748                    row,
9749                    &ctx,
9750                    &mut keys,
9751                )?;
9752            }
9753            sorter.push(&mut keys, row)?;
9754        }
9755
9756        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9757        emit(crate::StreamItem::Header(&columns))?;
9758
9759        let key_ctx = &ctx;
9760        let mut emitted_since_check = 0usize;
9761        let n = sorter.finish_each(
9762            |src, buf| {
9763                crate::orderby::build_order_keys_rederived(
9764                    &order_by,
9765                    &order_bound,
9766                    &order_colls,
9767                    src,
9768                    key_ctx,
9769                    buf,
9770                )
9771            },
9772            |src, values| {
9773                for p in &projection {
9774                    values.push(
9775                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
9776                    );
9777                }
9778                Ok(())
9779            },
9780            |cells| {
9781                // The merge is the long half of a big sort, and the scan's
9782                // check above stops running once it ends: a cancelled
9783                // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
9784                // anyway. Same stride as the scan.
9785                emitted_since_check += 1;
9786                if emitted_since_check >= 256 {
9787                    emitted_since_check = 0;
9788                    cancel.check()?;
9789                }
9790                emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
9791            },
9792        )?;
9793        Ok(Some(n))
9794    }
9795
9796    /// One row of the single-table streaming walk: the WHERE test, the
9797    /// projection, the emit. Returns whether a row was emitted.
9798    ///
9799    /// v7.39 (round 970) — factored out because the walk now has two ways
9800    /// to reach a row, the sequential scan and an index seek's candidate
9801    /// positions, and both must do IDENTICALLY this. A copy in each is how
9802    /// two paths for one job drift; this file already carries the cost of
9803    /// that lesson twice (rounds 823 and 961, both resolvers).
9804    ///
9805    /// `#[inline]` so the scan loop keeps the shape round 957 measured it
9806    /// in — a shared hot path pays for a new abstraction whether or not it
9807    /// uses it, and this one is on the scan.
9808    #[inline]
9809    #[allow(clippy::too_many_arguments)]
9810    fn stream_filter_project(
9811        row: &spg_storage::Row<'static>,
9812        where_: Option<&Expr>,
9813        // r1023 — the same WHERE, compiled once by the caller. `None` means
9814        // the expression did not qualify and `where_` is evaluated as before.
9815        compiled_where: Option<&crate::eval::CompiledExpr>,
9816        eval_stack: &mut Vec<Value<'static>>,
9817        projection: &[ProjectedItem],
9818        bound_pos: &[Option<usize>],
9819        ctx: &crate::eval::EvalContext<'_>,
9820        values: &mut Vec<Value<'static>>,
9821    ) -> Result<bool, EngineError> {
9822        // r1023 — this scan ran its predicate through the TREE INTERPRETER,
9823        // once per row, and it was the only row-returning path that did.
9824        // The aggregate path, `table_access`, and the PK walker all compile
9825        // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
9826        // server's live samples were `eval_expr` 99, `apply_binary` 81,
9827        // `mod_op` 29 — the interpreter, not delivery.
9828        //
9829        // The arithmetic accounted for it exactly. Over the wire, the same
9830        // filter costs 6.375 ms returning rows and 0.679 ms counting them;
9831        // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
9832        // which is what an interpreted predicate costs against the compiled
9833        // lane's 11.7. It was named "delivery after a filter" before this
9834        // profile, and it was never delivery.
9835        if let Some(c) = compiled_where {
9836            if !crate::eval::compiled::eval_compiled_pred(
9837                c,
9838                row,
9839                ctx,
9840                eval_stack,
9841                ctx.mysql_dialect,
9842            )? {
9843                return Ok(false);
9844            }
9845        } else if let Some(w) = where_ {
9846            let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
9847            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9848                return Ok(false);
9849            }
9850        }
9851        values.clear();
9852        for (p, bound) in projection.iter().zip(bound_pos) {
9853            values.push(match bound {
9854                Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
9855                None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
9856            });
9857        }
9858        Ok(true)
9859    }
9860
9861    /// The same filter and projection, then emit. Split from
9862    /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
9863    /// before it can emit them — a sort — runs the identical predicate and
9864    /// projection rather than a second copy of them.
9865    #[allow(clippy::too_many_arguments)]
9866    fn stream_project_row<F>(
9867        row: &spg_storage::Row<'static>,
9868        where_: Option<&Expr>,
9869        compiled_where: Option<&crate::eval::CompiledExpr>,
9870        eval_stack: &mut Vec<Value<'static>>,
9871        projection: &[ProjectedItem],
9872        bound_pos: &[Option<usize>],
9873        ctx: &crate::eval::EvalContext<'_>,
9874        values: &mut Vec<Value<'static>>,
9875        emit: &mut F,
9876    ) -> Result<bool, EngineError>
9877    where
9878        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9879    {
9880        if !Self::stream_filter_project(
9881            row,
9882            where_,
9883            compiled_where,
9884            eval_stack,
9885            projection,
9886            bound_pos,
9887            ctx,
9888            values,
9889        )? {
9890            return Ok(false);
9891        }
9892        emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
9893        Ok(true)
9894    }
9895
9896    fn try_stream_single_table<F>(
9897        &self,
9898        stmt: &SelectStatement,
9899        from: &FromClause,
9900        cancel: CancelToken<'_>,
9901        emit: &mut F,
9902    ) -> Result<Option<usize>, EngineError>
9903    where
9904        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9905    {
9906        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9907            return Ok(None);
9908        };
9909        // Cold-tier rows live outside `rows()`; the materialising fallback
9910        // covers both tiers and this walk would silently drop them.
9911        if table.has_cold_rows_fast() {
9912            return Ok(None);
9913        }
9914        let alias = from
9915            .primary
9916            .alias
9917            .as_deref()
9918            .unwrap_or(from.primary.name.as_str());
9919        let cols = table.schema().columns.clone();
9920        let sess = self.dml_session();
9921        let ctx = EvalContext::new(&cols, Some(alias))
9922            .with_catalog(self.active_catalog())
9923            .with_session(&sess);
9924        let projection = build_projection(
9925            &stmt.items,
9926            &cols,
9927            alias,
9928            self.speaks_mysql,
9929            Some(self.active_catalog()),
9930        )?;
9931
9932        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9933        emit(crate::StreamItem::Header(&columns))?;
9934
9935        // v7.37 (round 957) — resolve each bare-column projection ONCE
9936        // instead of once per row. `find_column_pos`-style resolution is a
9937        // linear walk of the schema comparing column-name strings, and the
9938        // row loop below ran it for every cell of every row: measured at
9939        // 400k rows, binding it out of the loop took `SELECT pad` from
9940        // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
9941        //
9942        // ORDER BY has bound its keys this way since round 582
9943        // (`order_by_bound_positions`); the projection never did.
9944        //
9945        // `locate_column` is the same resolution `resolve_column` performs,
9946        // returning the site instead of the value, so the two cannot drift
9947        // apart the way a second hand-written resolver would. Anything it
9948        // declines — an expression, a whole-row reference, a name that does
9949        // not resolve — binds to `None` and takes the general path below,
9950        // errors included, so an empty table still reports nothing rather
9951        // than raising at bind time.
9952        let bound_pos: Vec<Option<usize>> = projection
9953            .iter()
9954            .map(|p| match &p.expr {
9955                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9956                    Ok(Some(pos)) => Some(pos),
9957                    _ => None,
9958                },
9959                _ => None,
9960            })
9961            .collect();
9962
9963        // One snapshot for the whole scan, as the materialising path takes.
9964        let snapshot = self.current_snapshot();
9965
9966        // v7.39 (round 970) — ask the indices BEFORE walking the table.
9967        //
9968        // This walk had no index step at all, and it is preferred over the
9969        // materialising path, which does have one (`pick_indexed_rows` ->
9970        // `try_index_seek`). So a primary-key point lookup — the commonest
9971        // statement there is — read every row: measured on 500k rows,
9972        // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
9973        // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
9974        // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
9975        //
9976        // The control that named it: `... OFFSET 0` — semantically the same
9977        // query — answered in 0.159 ms, because OFFSET is one of the shape
9978        // gates that declines this walk and sends the statement to the path
9979        // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
9980        // no semantics in common; what they share is making this function
9981        // stand down.
9982        //
9983        // The seek only NARROWS: every candidate still goes through the
9984        // full WHERE below, exactly as the mutation paths use it, so a
9985        // partial index match cannot change an answer. Positions come back
9986        // already visibility-filtered and already capped at a quarter of the
9987        // table (round 490), so a seek can never cost more than the scan it
9988        // replaces, and `None` means "walk the table" as before.
9989        //
9990        // Sorted because the scan would have produced table order and the
9991        // index produces key order. Without an ORDER BY neither is promised,
9992        // but a walk that silently reorders its answer when an index happens
9993        // to exist is a difference nobody asked for.
9994        let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
9995            crate::index_access::try_index_seek_positions(
9996                w,
9997                &cols,
9998                table,
9999                alias,
10000                &snapshot,
10001                self.speaks_mysql,
10002            )
10003        });
10004
10005        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
10006        // r1023 — compile the predicate once for the whole scan. Same gate
10007        // every other path uses: `fully_compilable` or keep the interpreter,
10008        // so a shape the VM cannot take answers exactly as it did before.
10009        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
10010            .where_
10011            .as_ref()
10012            .filter(|w| crate::eval::fully_compilable(w))
10013            .map(|w| crate::eval::compile_expr(w, &ctx));
10014        let mut eval_stack: Vec<Value<'static>> = Vec::new();
10015        let mut count: usize = 0;
10016        match seek_positions {
10017            Some(mut positions) => {
10018                positions.sort_unstable();
10019                for (n, pos) in positions.into_iter().enumerate() {
10020                    if n.is_multiple_of(256) {
10021                        cancel.check()?;
10022                    }
10023                    let Some(row) = table.rows().get(pos) else {
10024                        continue;
10025                    };
10026                    if Self::stream_project_row(
10027                        row,
10028                        stmt.where_.as_ref(),
10029                        compiled_where.as_ref(),
10030                        &mut eval_stack,
10031                        &projection,
10032                        &bound_pos,
10033                        &ctx,
10034                        &mut values,
10035                        emit,
10036                    )? {
10037                        count += 1;
10038                    }
10039                }
10040            }
10041            None => {
10042                // v7.38.11 — the streaming scan is the path a client
10043                // reaches over the wire, so it is the one that has to
10044                // ask the BRIN summary which slots can be skipped. The
10045                // predicate still runs on every row that survives.
10046                let slots = stmt
10047                    .where_
10048                    .as_ref()
10049                    .and_then(|w| crate::brin::candidate_slots(w, table))
10050                    .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
10051                for (i, row) in table.scan_visible_slots(slots, &snapshot) {
10052                    if i.is_multiple_of(256) {
10053                        cancel.check()?;
10054                    }
10055                    if Self::stream_project_row(
10056                        row,
10057                        stmt.where_.as_ref(),
10058                        compiled_where.as_ref(),
10059                        &mut eval_stack,
10060                        &projection,
10061                        &bound_pos,
10062                        &ctx,
10063                        &mut values,
10064                        emit,
10065                    )? {
10066                        count += 1;
10067                    }
10068                }
10069            }
10070        }
10071        Ok(Some(count))
10072    }
10073
10074    pub(crate) fn try_exec_joined_streaming<F>(
10075        &self,
10076        stmt: &SelectStatement,
10077        cancel: CancelToken<'_>,
10078        emit: &mut F,
10079    ) -> Result<Option<usize>, EngineError>
10080    where
10081        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
10082    {
10083        // Shape gates — keep the streamable surface narrow on
10084        // purpose. The fall-back path still handles everything else.
10085        let Some(from) = &stmt.from else {
10086            return Ok(None);
10087        };
10088        // v7.40.10 — the FROM kinds this path can actually produce rows
10089        // for, as a match with NO wildcard arm: a kind added to
10090        // `FromItemKind` is a compile error here rather than a statement
10091        // that falls through to the table lookup and answers
10092        // `relation "<the function's own name>" does not exist`.
10093        //
10094        // Which is what `SELECT * FROM jsonb_each_text('{"a":1}'::jsonb)`
10095        // did over the extended protocol, while `count(*)` over the same
10096        // item answered — the aggregate gate below sends that one to the
10097        // materialising executor, which handles every kind. The simple
10098        // query protocol answered too, and so did all three of the
10099        // embedded engine's own entry points. Present on the published
10100        // 7.40.9; the guard that let it through named four of the seven
10101        // slots a FROM item can carry.
10102        const fn streaming_can_produce(k: spg_sql::ast::FromItemKind) -> bool {
10103            use spg_sql::ast::FromItemKind as K;
10104            match k {
10105                K::Relation | K::Unnest | K::GenerateSeries | K::Subquery => true,
10106                K::JsonbEach | K::TableFn | K::RowsFrom | K::JsonTable | K::ScalarFn => false,
10107            }
10108        }
10109        if !streaming_can_produce(from.primary.kind())
10110            || from
10111                .joins
10112                .iter()
10113                .any(|j| !streaming_can_produce(j.table.kind()))
10114        {
10115            return Ok(None);
10116        }
10117        // v7.37 (round 830) — decline anything a row-security policy binds
10118        // for this session. Policies are injected in
10119        // `exec_bare_select_cancel`, below this path, so a statement claimed
10120        // here would read the table unfiltered: measured, `SELECT val FROM
10121        // sec` returned all three rows to a session whose policy allows two,
10122        // while `SELECT upper(val) FROM sec` — declined by the shape gates
10123        // and so materialised — returned the correct two.
10124        //
10125        // Declining sends it to the path that enforces. Teaching this one to
10126        // inject the predicate itself would keep the streaming benefit for
10127        // RLS tables and is the better end state; it is not what a
10128        // correctness fix should carry, and the fall-back is exactly as
10129        // correct, only slower.
10130        if self.select_reads_policy_subject_table(stmt) {
10131            return Ok(None);
10132        }
10133        // r1058 — a WITH list this path never materialises: the CTE
10134        // name would be resolved as a physical relation and error
10135        // ("relation \"big\" does not exist" over the extended
10136        // protocol, caught by the perm-runner's wire legs). The
10137        // materialising fallback owns CTE execution.
10138        if !stmt.ctes.is_empty() {
10139            return Ok(None);
10140        }
10141        // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
10142        // tables` and kin) exist only as synth arms on the
10143        // materialising path; claiming one here errored "relation
10144        // does not exist" over the extended protocol for a query the
10145        // simple protocol answered. Prefix test only — a genuinely
10146        // missing relation must keep erroring in-path.
10147        if from.primary.name.starts_with("__spg_")
10148            || from
10149                .joins
10150                .iter()
10151                .any(|j| j.table.name.starts_with("__spg_"))
10152        {
10153            return Ok(None);
10154        }
10155        // r1058 — decline partitioned / inheritance parents, same
10156        // shape of bug as the RLS decline above: this path scans the
10157        // named table's own (empty) heap, so `SELECT id, region FROM
10158        // cust` on a partition parent streamed ZERO rows over the wire
10159        // while COUNT(*) — an aggregate, materialised below — said 3.
10160        // Caught by the perm-runner's server permutations; the
10161        // materialising fallback expands children correctly.
10162        if crate::partition::has_children(self.active_catalog(), &from.primary.name)
10163            || from
10164                .joins
10165                .iter()
10166                .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
10167        {
10168            return Ok(None);
10169        }
10170        // v7.39 (round 790) — single-table SELECTs stream too. This
10171        // gate said "joins only" because the path was written for
10172        // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
10173        // fell to the materialising fallback, which builds the whole
10174        // `Vec<Row<'static>>` and only then iterates it. Measured on
10175        // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
10176        // reached through a one-row JOIN — 2.6x, purely for lacking a
10177        // join. The deferred-join structure handles one source as the
10178        // degenerate stride-1 case, so the walk below is unchanged.
10179        let _single_table = from.joins.is_empty();
10180        // An ORDER BY that the bounded sort can serve streams; everything
10181        // else still falls to the materialising fallback below.
10182        // r1025 — an ordering the index already holds needs no sort at all.
10183        // Tried before the spill sort, which is the path it replaces.
10184        if !stmt.order_by.is_empty()
10185            && from.joins.is_empty()
10186            && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
10187        {
10188            return Ok(Some(n));
10189        }
10190        if !stmt.order_by.is_empty()
10191            && from.joins.is_empty()
10192            && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
10193        {
10194            return Ok(Some(n));
10195        }
10196        // r1031 — integer keys carried inline instead of an `OrderKey`
10197        // vector per row. Tried AFTER the spill sort on purpose: this lane
10198        // buffers the whole answer, so anything the spill path would take
10199        // must keep taking it rather than be turned back into an in-memory
10200        // sort that answers with a budget error.
10201        if !stmt.order_by.is_empty()
10202            && from.joins.is_empty()
10203            && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
10204        {
10205            return Ok(Some(n));
10206        }
10207        if !stmt.order_by.is_empty()
10208            || stmt.limit.is_some()
10209            || stmt.offset.is_some()
10210            || stmt.having.is_some()
10211            || stmt.group_by.is_some()
10212            || stmt.distinct
10213            || !stmt.unions.is_empty()
10214            || stmt.limit_with_ties
10215        {
10216            return Ok(None);
10217        }
10218        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
10219            return Ok(None);
10220        }
10221        // No window / SRF on the streaming path.
10222        if select_has_window(stmt) {
10223            return Ok(None);
10224        }
10225        if stmt
10226            .items
10227            .iter()
10228            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
10229        {
10230            return Ok(None);
10231        }
10232        // v7.37 (round 831) — a joinless FROM over a plain stored table
10233        // never needs the deferred structure, and building one costs the
10234        // whole table. `materialise_table_ref_filtered` clones every row
10235        // into a `Vec<Row<'static>>` before anything is filtered or
10236        // projected, so peak cost tracks the TABLE, not the result:
10237        // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
10238        // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
10239        // projection saving nothing, while an arithmetic projection — which
10240        // the shape gates decline, so it materialises through the ordinary
10241        // executor — cost +21 MB.
10242        //
10243        // Scanning in batches and releasing each one is what `cursor_fill`
10244        // already does for a lazy cursor, and it is the same walk: resume
10245        // from a slot, take visible rows, evaluate, hand them over, drop
10246        // them. Round 800's finding stands and is why this reads rows OUT
10247        // rather than seeding the join by index — touching the stored
10248        // `PersistentVec` in place makes the whole table resident, which is
10249        // worse than the copy. Each batch is copied, then freed.
10250        // v7.40.10 — the complete question, asked once.
10251        //
10252        // This named four of the seven slots a FROM item can carry, so
10253        // `jsonb_each_text`, `ROWS FROM` and `JSON_TABLE` fell into a
10254        // path that looks the item up as a table: `SELECT * FROM
10255        // jsonb_each_text('{"a":1}'::jsonb)` answered `relation
10256        // "jsonb_each_text" does not exist` over the extended protocol,
10257        // while `count(*)` over the same item answered and the simple
10258        // query protocol answered. See `TableRef::names_a_relation`.
10259        if from.joins.is_empty()
10260            && from.primary.names_a_relation()
10261            && from.primary.as_of_segment.is_none()
10262            && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
10263        {
10264            return Ok(Some(n));
10265        }
10266        // Build the deferred join under the regular byte budget.
10267        let mut budget = ByteBudget::new(self.max_query_bytes);
10268        let deferred = {
10269            let mut needed = alloc::collections::BTreeSet::new();
10270            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10271            self.build_joined_filtered_rows(
10272                from,
10273                stmt.where_.as_ref(),
10274                cancel,
10275                if prunable { Some(&needed) } else { None },
10276                &mut budget,
10277            )?
10278        };
10279        let combined_schema = &deferred.combined_schema;
10280        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10281        // `::regclass` / enum cast in a joined projection or HAVING needs it.
10282        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10283        // the same predicate the unjoined shape carries.
10284        let joined_sess = self.dml_session();
10285        // v7.38.18 — and the DIALECT. This context carried the catalog and
10286        // the session and not the one field that decides how text
10287        // compares, so a joined row was evaluated in PostgreSQL
10288        // semantics inside a MySQL session.
10289        //
10290        // It showed up only where the two sides had DIFFERENT text types:
10291        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10292        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10293        // were fine and the same comparison inside one table was fine.
10294        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10295        // so the wrong semantics were invisible until a CHAR's padding
10296        // had to be stripped and PostgreSQL's arm does not strip it.
10297        //
10298        // `with_engine` is what sets it; the next line already reaches
10299        // for `self.backslash_escapes`, so the dialect was in hand.
10300        let ctx = EvalContext::new(combined_schema, None)
10301            .with_catalog(self.active_catalog())
10302            .with_engine(self)
10303            .with_session(&joined_sess);
10304        let projection = build_projection(
10305            &stmt.items,
10306            combined_schema,
10307            "",
10308            self.speaks_mysql,
10309            Some(self.active_catalog()),
10310        )?;
10311        // Every projection item must be a bound qualified column —
10312        // anything that needs `eval_expr_with_correlated` keeps the
10313        // materialising path.
10314        let bound_pos = |e: &Expr| -> Option<usize> {
10315            match e {
10316                // v7.39 (round 822) — an UNQUALIFIED column resolves here
10317                // too. The `qualifier.is_some()` guard this replaces meant
10318                // `SELECT pad FROM big` — the commonest projection there is
10319                // — never reached the streaming walk: it fell out at this
10320                // gate and re-ran on the materialising path, after the
10321                // deferred join structure had already been built and paid
10322                // for. Measured (round 821, statement_timeout=120 over 400k
10323                // rows): `big.pad` and `b.pad` streamed and cancelled at
10324                // ~65k rows in 0.14 s, while bare `pad` ran to completion in
10325                // 0.80 s with the timeout never consulted. `find_column_pos`
10326                // has always handled the unqualified case (it falls through
10327                // to a by-name match), so the guard narrowed the gate for no
10328                // reason it recorded.
10329                Expr::Column(c) => eval::find_column_pos(c, &ctx),
10330                _ => None,
10331            }
10332        };
10333        let proj_decomposed: Vec<(usize, usize)> = {
10334            let mut out = Vec::with_capacity(projection.len());
10335            for p in &projection {
10336                let Some(abs) = bound_pos(&p.expr) else {
10337                    return Ok(None);
10338                };
10339                let Some(k) = deferred
10340                    .offsets
10341                    .partition_point(|&o| o <= abs)
10342                    .checked_sub(1)
10343                else {
10344                    return Ok(None);
10345                };
10346                out.push((k, abs - deferred.offsets[k]));
10347            }
10348            out
10349        };
10350        // Emit columns once.
10351        let columns: Vec<ColumnSchema> = projection
10352            .iter()
10353            // v7.39 (read01 round 54) — keep the column's enum identity through
10354            // the projection (it lives outside the DataType lattice), or a
10355            // derived table / UNION / windowed result forgets it and any outer
10356            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
10357            .map(|p| p.to_column_schema())
10358            .collect();
10359        emit(crate::StreamItem::Header(&columns))?;
10360        let sources_ref = &deferred.sources;
10361        let stride = deferred.stride;
10362        let survivors_ref = &deferred.survivors;
10363        let n_surv = if stride == 0 {
10364            0
10365        } else {
10366            survivors_ref.len() / stride
10367        };
10368        // Reused per-row cell-ref scratch — pushes are zero-alloc
10369        // after the first row.
10370        let null_value = Value::Null;
10371        let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
10372        let mut count: usize = 0;
10373        for surv_i in 0..n_surv {
10374            if surv_i.is_multiple_of(256) {
10375                cancel.check()?;
10376            }
10377            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10378            cell_refs.clear();
10379            for &(k, col_in_src) in &proj_decomposed {
10380                let ri = tuple[k];
10381                let v: &Value = if ri == usize::MAX {
10382                    &null_value
10383                } else {
10384                    sources_ref[k]
10385                        .get(ri)
10386                        .and_then(|r| r.values.get(col_in_src))
10387                        .unwrap_or(&null_value)
10388                };
10389                cell_refs.push(v);
10390            }
10391            emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
10392            count += 1;
10393        }
10394        Ok(Some(count))
10395    }
10396
10397    fn exec_joined_select(
10398        &self,
10399        stmt: &SelectStatement,
10400        from: &FromClause,
10401        cancel: CancelToken<'_>,
10402    ) -> Result<QueryResult, EngineError> {
10403        // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
10404        // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
10405        // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
10406        // FROM B WHERE B.k = A.k)` into
10407        //   SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
10408        //   WHERE B.k IS NULL
10409        // The general join executor builds a hash, probes every outer
10410        // tuple, materialises (left_padded_with_null) for every miss,
10411        // then runs the aggregate over the result set. For COUNT(*) we
10412        // only need the count — skip the tuple materialisation. Build
10413        // a HashSet of B's unique join values, scan A's PK index, and
10414        // increment the counter on each miss. PG's Merge Anti-Join
10415        // does roughly this; ours becomes a simple HashSet probe.
10416        if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
10417            return Ok(out);
10418        }
10419        // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
10420        // When ORDER BY is on an indexed primary column, walking the
10421        // btree in the requested direction lets the streamer break
10422        // after `LIMIT + OFFSET` survivors without ever materialising
10423        // the rest of the join — the 80 ms `mailrs_prod_not_exists`
10424        // plateau is exactly this shape.
10425        if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
10426            return Ok(out);
10427        }
10428        // v7.30.3 (mailrs round-26) — the bounded single-join path
10429        // first; peak memory scales with LIMIT instead of the table.
10430        if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
10431            return Ok(out);
10432        }
10433        // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
10434        // WHERE materialisation to the shared helper so the LATERAL
10435        // / UNNEST / regular-catalog paths route through one place.
10436        // (`build_joined_filtered_rows` carries LATERAL support as
10437        // of Phase 3.P0-41.) Downstream we still handle aggregate /
10438        // projection / ORDER BY / DISTINCT / LIMIT inline because
10439        // those depend on the SelectStatement's items list.
10440        let mut budget = ByteBudget::new(self.max_query_bytes);
10441        let deferred = {
10442            let mut needed = alloc::collections::BTreeSet::new();
10443            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10444            self.build_joined_filtered_rows(
10445                from,
10446                stmt.where_.as_ref(),
10447                cancel,
10448                if prunable { Some(&needed) } else { None },
10449                &mut budget,
10450            )?
10451        };
10452        let combined_schema = &deferred.combined_schema;
10453        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10454        // `::regclass` / enum cast in a joined projection or HAVING needs it.
10455        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10456        // the same predicate the unjoined shape carries.
10457        let joined_sess = self.dml_session();
10458        // v7.38.18 — and the DIALECT. This context carried the catalog and
10459        // the session and not the one field that decides how text
10460        // compares, so a joined row was evaluated in PostgreSQL
10461        // semantics inside a MySQL session.
10462        //
10463        // It showed up only where the two sides had DIFFERENT text types:
10464        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10465        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10466        // were fine and the same comparison inside one table was fine.
10467        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10468        // so the wrong semantics were invisible until a CHAR's padding
10469        // had to be stripped and PostgreSQL's arm does not strip it.
10470        //
10471        // `with_engine` is what sets it; the next line already reaches
10472        // for `self.backslash_escapes`, so the dialect was in hand.
10473        let ctx = EvalContext::new(combined_schema, None)
10474            .with_catalog(self.active_catalog())
10475            .with_engine(self)
10476            .with_session(&joined_sess);
10477        // Aggregate path: handle GROUP BY / aggregate calls over the
10478        // joined+filtered rows.
10479        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
10480            // v7.32 (P4 borrow channel, increment 2) — borrow each
10481            // surviving join tuple as a RowRef::Tuple; the aggregate
10482            // engine reads source cells by reference (bound fast path =
10483            // zero clone) instead of consuming materialised combined
10484            // Rows. This is where the +211k materialise_tuple_vals
10485            // clones disappear for the join+aggregate shape.
10486            let refs = deferred.row_refs();
10487            // v7.29 — a per-query memo so correlated scalar
10488            // subqueries batch-evaluate once (group map) instead of
10489            // executing per group.
10490            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
10491            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
10492                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
10493                    .map_err(|err| match err {
10494                        EngineError::Eval(ev) => ev,
10495                        other => eval::EvalError::TypeMismatch {
10496                            detail: alloc::format!("{other}"),
10497                        },
10498                    })
10499            };
10500            let agg = aggregate::run(
10501                stmt,
10502                crate::join::AggRows::Refs(&refs),
10503                combined_schema,
10504                None,
10505                Some(&agg_correlated),
10506                self.parallel_runner.0.as_deref(),
10507                Some(self.active_catalog()),
10508                Some(self),
10509            )?;
10510            return self.finish_agg_result(agg, stmt, cancel);
10511        }
10512
10513        let projection = build_projection(
10514            &stmt.items,
10515            combined_schema,
10516            "",
10517            self.speaks_mysql,
10518            Some(self.active_catalog()),
10519        )?;
10520        // v7.39 (round 734) — a set-returning projection over a JOIN.
10521        // This executor's projection loop treats every item as a scalar,
10522        // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
10523        // "function unnest(integer[]) does not exist" where PG expands
10524        // it. The row-set executor already carries the full SRF pipeline
10525        // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
10526        // sharding): materialise the joined survivors and hand over. The
10527        // WHERE is cleared — the join already applied it, and combined
10528        // columns resolve identically in both executors.
10529        if !self.srf_target_idxs(&projection).is_empty() {
10530            let refs = deferred.row_refs();
10531            let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
10532            let mut s2 = stmt.clone();
10533            s2.where_ = None;
10534            let schema = combined_schema.clone();
10535            return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
10536        }
10537        // v7.33 (P4 borrow channel, increment 3) — project directly off
10538        // the deferred row-index tuples instead of materialising an
10539        // intermediate combined Row per survivor. A bound qualified
10540        // column is read by reference (`RowRef::get` → `tuple_value`) and
10541        // cloned ONCE into the output row; the old `materialise()` (a full
10542        // combined Row plus a source→intermediate clone per referenced
10543        // cell, for every survivor) is gone. A row materialises on demand
10544        // only when a projection or ORDER BY expression needs the eval
10545        // path (subquery / function / arithmetic / unqualified column).
10546        // Same bind-once classification the aggregate input fast path uses
10547        // (`accumulate_groups`), reading the same `tuple_value` mapping the
10548        // differential gate already covers.
10549        let refs = deferred.row_refs();
10550        let bound_pos = |e: &Expr| -> Option<usize> {
10551            match e {
10552                Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
10553                _ => None,
10554            }
10555        };
10556        let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
10557        let all_proj_bound = proj_pos.iter().all(Option::is_some);
10558        // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
10559        // pre-decompose each bound projection position into
10560        // `(source_k, col_in_source)` so the per-row column read
10561        // skips the per-cell `tuple_value` partition_point + slice
10562        // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
10563        // calls) that walk dominated; this version reaches into
10564        // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
10565        let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
10566            .iter()
10567            .map(|p| {
10568                p.and_then(|abs| {
10569                    let k = deferred
10570                        .offsets
10571                        .partition_point(|&o| o <= abs)
10572                        .checked_sub(1)?;
10573                    Some((k, abs - deferred.offsets[k]))
10574                })
10575            })
10576            .collect();
10577        // v7.39 (round 962) — which projection items are whole-row
10578        // references, and to which join source. The test is
10579        // `locate_column` declining the name, which is the SAME resolver
10580        // the evaluation path uses, so this cannot drift from it: a real
10581        // column carrying an alias's name resolves to a position and is
10582        // not reported here. The source index comes from the alias
10583        // prefix, the way the combined schema names its columns.
10584        let whole_row_src: Vec<Option<usize>> = projection
10585            .iter()
10586            .map(|p| {
10587                let Expr::Column(c) = &p.expr else {
10588                    return None;
10589                };
10590                if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
10591                    return None;
10592                }
10593                let prefix = alloc::format!("{name}.", name = c.name);
10594                let abs = deferred
10595                    .combined_schema
10596                    .iter()
10597                    .position(|s| s.name.starts_with(&prefix))?;
10598                deferred
10599                    .offsets
10600                    .partition_point(|&o| o <= abs)
10601                    .checked_sub(1)
10602            })
10603            .collect();
10604        // ORDER BY (when present) still evaluates against a materialised
10605        // Row — keep the order-key encoder correct rather than fork it.
10606        let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
10607        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
10608        let mut proj_memo = memoize::MemoizeCache::default();
10609        let sources_ref = &deferred.sources;
10610        let stride = deferred.stride;
10611        let survivors_ref = &deferred.survivors;
10612        let n_surv = survivors_ref.len() / stride.max(1);
10613        // v7.38 (read01 B8) — streaming top-N budget (see the sibling
10614        // single-table path). Bounds this JOIN projection's accumulator
10615        // to O(keep) for `ORDER BY … LIMIT k`.
10616        let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
10617            && !stmt.distinct
10618            && !stmt.limit_with_ties
10619            && !self.env_cfg().disable_topk
10620        {
10621            stmt.limit_literal().and_then(|l| {
10622                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
10623                (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
10624            })
10625        } else {
10626            None
10627        };
10628        // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
10629        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
10630            hashbrown::HashMap::new();
10631        let distinct_hb = hashbrown::DefaultHashBuilder::default();
10632        // v7.38.13 — which output positions must NOT fold. Built once per
10633        // scan from the projection, which carries the source column's
10634        // byte-wise-ness; see `FoldSpec`.
10635        let distinct_mask = fold_mask(&projection);
10636        for surv_i in 0..n_surv {
10637            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10638            let row = &refs[surv_i];
10639            let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
10640                Some(row.as_row())
10641            } else {
10642                None
10643            };
10644            let mut values = Vec::with_capacity(projection.len());
10645            for (i, p) in projection.iter().enumerate() {
10646                if let Some((k, col_in_src)) = proj_decomposed[i] {
10647                    // v7.36 — direct (source_k, col) lookup, no
10648                    // partition_point. tuple[k] is the row index in
10649                    // sources[k]; LEFT-NULL slots are `usize::MAX`.
10650                    let ri = tuple[k];
10651                    let v: Value<'static> = if ri == usize::MAX {
10652                        Value::Null
10653                    } else {
10654                        sources_ref[k]
10655                            .get(ri)
10656                            .and_then(|r| r.values.get(col_in_src))
10657                            .cloned()
10658                            .map(Value::into_owned)
10659                            .unwrap_or(Value::Null)
10660                    };
10661                    values.push(v);
10662                } else if let Some(pos) = proj_pos[i] {
10663                    // Bound but couldn't decompose (shouldn't normally
10664                    // happen — keep as a safe path).
10665                    values.push(
10666                        row.get(pos)
10667                            .cloned()
10668                            .map(Value::into_owned)
10669                            .unwrap_or(Value::Null),
10670                    );
10671                } else if let Some(k) = whole_row_src[i]
10672                    && tuple[k] == usize::MAX
10673                {
10674                    // v7.39 (round 962) — a whole-row reference to a side
10675                    // an OUTER join null-extended is NULL, not a
10676                    // composite whose fields are all NULL. PG18.4 answers
10677                    // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
10678                    // an empty cell; round 961 answered `(,)`.
10679                    //
10680                    // The evaluator below cannot tell the two apart: it
10681                    // reads the MATERIALISED combined row, where a
10682                    // null-extended side is indistinguishable from a real
10683                    // row whose every column is NULL — and that row is
10684                    // `(,)` in PG too, so guessing by "all fields NULL"
10685                    // would trade one wrong answer for another. The
10686                    // tuple, which is still in hand here, does know:
10687                    // `usize::MAX` is the sentinel the join writes for
10688                    // exactly this.
10689                    values.push(Value::Null);
10690                } else {
10691                    // Eval path — `materialised` is Some whenever any
10692                    // projection item is non-bound (need_eval_row true).
10693                    // v7.24 (round-16 B) — select-list subqueries under a
10694                    // JOIN go through the correlated-aware evaluator too.
10695                    let mrow = materialised.as_deref().expect("materialised for eval");
10696                    values.push(self.eval_expr_with_correlated(
10697                        &p.expr,
10698                        mrow,
10699                        &ctx,
10700                        cancel,
10701                        Some(&mut proj_memo),
10702                    )?);
10703                }
10704            }
10705            let out_row = Row::new(values);
10706            // v7.37.16 — streaming DISTINCT (see the scan-path twin):
10707            // probe on the projected row; duplicates skip the
10708            // build_order_keys eval and never enter `tagged`.
10709            if stmt.distinct {
10710                let bucket = seen_distinct
10711                    .entry(norm_hash_row(
10712                        &out_row,
10713                        &distinct_hb,
10714                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10715                    ))
10716                    .or_default();
10717                if bucket.iter().any(|i| {
10718                    row_eq_norm(
10719                        &tagged[i].1,
10720                        &out_row,
10721                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10722                    )
10723                }) {
10724                    continue;
10725                }
10726                bucket.push(tagged.len());
10727            }
10728            let order_keys = if stmt.order_by.is_empty() {
10729                Vec::new()
10730            } else {
10731                let mrow = materialised.as_deref().expect("materialised for order by");
10732                build_order_keys(&stmt.order_by, mrow, &ctx)?
10733            };
10734            budget.charge(approx_row_bytes(&out_row))?;
10735            tagged.push((order_keys, out_row));
10736            if let Some((k, descs)) = &topk_stream {
10737                topk_trim(&mut tagged, *k, descs);
10738            }
10739        }
10740        if !stmt.order_by.is_empty() {
10741            // v7.38 元机制 D acceptor — see other call site above.
10742            let keep = if self.env_cfg().disable_topk {
10743                None
10744            } else {
10745                stmt.limit_literal()
10746                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
10747            };
10748            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
10749            // v7.39 (round 688) — the join's ORDER BY resolves its keys
10750            // against `ctx`, which is built from `build_combined_schema`, so
10751            // this is where a declared collation reaches the sort. There was
10752            // exactly ONE resolver call in the engine before this — the
10753            // single-table scan's — which is why every other shape sorted by
10754            // bytes no matter what the schemas carried.
10755            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
10756            crate::orderby::partial_sort_tagged_in(
10757                &mut tagged,
10758                keep,
10759                &descs,
10760                &colls,
10761                self.session_parallel_workers(),
10762            );
10763        }
10764        let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
10765        apply_offset_and_limit(
10766            &mut output_rows,
10767            stmt.offset_literal(),
10768            stmt.limit_literal(),
10769        );
10770        let columns: Vec<ColumnSchema> = projection
10771            .into_iter()
10772            .map(|p| p.to_column_schema())
10773            .collect();
10774        Ok(QueryResult::Rows {
10775            columns,
10776            rows: output_rows,
10777        })
10778    }
10779}
10780
10781impl Engine {
10782    /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
10783    /// by id, decodes each row body against the table's current
10784    /// schema, applies the SELECT's projection + optional WHERE +
10785    /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
10786    /// / ORDER BY are unsupported on this path (STABILITY carve-
10787    /// out); operators wanting them should restore the segment
10788    /// into a regular table first.
10789    fn exec_select_as_of_segment(
10790        &self,
10791        stmt: &SelectStatement,
10792        from: &spg_sql::ast::FromClause,
10793        segment_id: u32,
10794    ) -> Result<QueryResult, EngineError> {
10795        // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
10796        // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
10797        if !from.joins.is_empty()
10798            || stmt.group_by.is_some()
10799            || stmt.having.is_some()
10800            || !stmt.unions.is_empty()
10801            || !stmt.order_by.is_empty()
10802            || stmt.offset.is_some()
10803            || stmt.distinct
10804            || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
10805        {
10806            return Err(EngineError::Unsupported(
10807                "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
10808                 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
10809                    .into(),
10810            ));
10811        }
10812        let table = self
10813            .active_catalog()
10814            .get(&from.primary.name)
10815            .ok_or_else(|| StorageError::TableNotFound {
10816                name: from.primary.name.clone(),
10817            })?;
10818        let schema = table.schema().clone();
10819        let schema_cols = &schema.columns;
10820        let alias = from
10821            .primary
10822            .alias
10823            .as_deref()
10824            .unwrap_or(from.primary.name.as_str());
10825        let ctx = self.ev_ctx(schema_cols, Some(alias));
10826        let seg = self
10827            .active_catalog()
10828            .cold_segment(segment_id)
10829            .ok_or_else(|| {
10830                EngineError::Unsupported(alloc::format!(
10831                    "AS OF SEGMENT: cold segment {segment_id} not registered"
10832                ))
10833            })?;
10834        let mut out_rows: Vec<Row<'static>> = Vec::new();
10835        let mut limit_remaining: Option<usize> =
10836            stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
10837        for (_key, body) in seg.scan() {
10838            let (row, _consumed) =
10839                spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
10840                    .map_err(EngineError::Storage)?;
10841            if let Some(where_expr) = &stmt.where_ {
10842                let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
10843                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
10844                    continue;
10845                }
10846            }
10847            // Projection.
10848            let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
10849            out_rows.push(projected);
10850            if let Some(rem) = limit_remaining.as_mut() {
10851                if *rem == 0 {
10852                    out_rows.pop();
10853                    break;
10854                }
10855                *rem -= 1;
10856            }
10857        }
10858        // Output column schema: derive from SELECT items.
10859        let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
10860        Ok(QueryResult::Rows {
10861            columns,
10862            rows: out_rows,
10863        })
10864    }
10865
10866    /// v6.10.2 — simple-path WHERE eval that doesn't go through
10867    /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
10868    /// scan paths predicate against a snapshot frozen segment, no
10869    /// cross-row state.
10870    fn eval_expr_simple(
10871        &self,
10872        expr: &Expr,
10873        row: &Row<'static>,
10874        ctx: &EvalContext,
10875    ) -> Result<Value<'static>, EngineError> {
10876        let cancel = CancelToken::none();
10877        self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
10878    }
10879}
10880
10881// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
10882
10883/// One row-producing projection: an expression to evaluate, the resulting
10884/// column's user-visible name, its inferred type, and nullability.
10885#[derive(Debug, Clone)]
10886pub(crate) struct ProjectedItem {
10887    pub(crate) expr: Expr,
10888    pub(crate) output_name: String,
10889    pub(crate) ty: DataType,
10890    pub(crate) nullable: bool,
10891    /// v7.39 (read01 round 54) — a projected enum column keeps its enum
10892    /// identity. Enum-ness lives outside the DataType lattice (the value is a
10893    /// Text), so a projection that dropped this made the RESULT schema forget
10894    /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
10895    /// that schema, silently fell back to TEXT order instead of member order.
10896    pub(crate) user_enum_type: Option<String>,
10897    /// v7.39 (round 425) — a projected MySQL temporal column keeps its
10898    /// declared fractional-seconds precision, so the renderer can pad to
10899    /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
10900    /// a whole second). Like `user_enum_type` this lives outside the
10901    /// DataType lattice, so a projection that dropped it made the RESULT
10902    /// schema forget how wide the fraction should print.
10903    pub(crate) mysql_fsp: Option<u8>,
10904    /// v7.39 (round 688) — and its declared collation, the third thing to
10905    /// live outside the DataType lattice and the third to be lost the same
10906    /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
10907    /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
10908    /// projection rebuilt the output column and the ORDER BY resolves
10909    /// against THAT schema.
10910    pub(crate) collation_name: Option<String>,
10911    /// v7.38.13 — and whether this position must NOT fold when DISTINCT
10912    /// de-dups it. The fourth thing to live outside the DataType lattice
10913    /// and the fourth to be lost the same way: a column declared
10914    /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
10915    /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
10916    /// returns two.
10917    ///
10918    /// A BOOL rather than the `Collation` enum on purpose. The enum's
10919    /// storage default is `Binary`, but the FOLD default under MySQL is
10920    /// case-insensitive — carrying the enum would silently mean
10921    /// "exempt" for every projected expression that is not a column.
10922    /// This field states the question it answers.
10923    pub(crate) fold_exempt: bool,
10924    /// v7.38.18 — does this column's collation make trailing spaces
10925    /// insignificant? A separate question from `fold_exempt`:
10926    /// `utf8mb4_bin` is fold-exempt AND pads, `utf8mb4_0900_ai_ci`
10927    /// folds and does not. Read off the same column, at the same
10928    /// place, so the two masks cannot drift apart.
10929    pub(crate) pads: bool,
10930}
10931
10932impl ProjectedItem {
10933    /// v7.38.14 — the output column this projected item describes.
10934    ///
10935    /// There were TWENTY-ONE places converting a `ProjectedItem` into a
10936    /// `ColumnSchema`, each written as `ColumnSchema::new(..)` followed by a
10937    /// hand-picked list of attributes to copy after it, and the lists did not
10938    /// agree: six carried enum identity, the collation NAME and MySQL fsp; ten
10939    /// carried the first and last but not the name; five carried nothing at
10940    /// all. Not one carried `collation`, the enum every MySQL text comparison
10941    /// actually reads.
10942    ///
10943    /// That is how a declared collation vanished between a subquery and the
10944    /// query that selects from it: the inner SELECT's output schema claimed
10945    /// `ColumnSchema::new`'s default, which is `Binary` — a value downstream
10946    /// reads as "byte-wise ON PURPOSE" rather than as "unknown", so the loss
10947    /// presents as a deliberate declaration.
10948    ///
10949    /// One conversion, so a field added to either type has one place to be
10950    /// remembered instead of twenty-one.
10951    pub(crate) fn to_column_schema(&self) -> ColumnSchema {
10952        let mut c = ColumnSchema::new(self.output_name.clone(), self.ty, self.nullable);
10953        c.user_enum_type.clone_from(&self.user_enum_type);
10954        c.collation_name.clone_from(&self.collation_name);
10955        c.mysql_fsp = self.mysql_fsp;
10956        // `fold_exempt` is the projection's answer to the same question
10957        // `ColumnSchema::collation` answers downstream, and it was computed
10958        // from the source column. Keeping the two in step here is what stops
10959        // a de-duplication site further on from asking the schema and being
10960        // told the opposite of what the projection knew.
10961        c.collation = if self.fold_exempt {
10962            spg_storage::Collation::Binary
10963        } else {
10964            spg_storage::Collation::CaseInsensitive
10965        };
10966        c
10967    }
10968}
10969
10970/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
10971/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
10972/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
10973/// the spec's "two NULLs are not distinct"; the second is a tolerated
10974/// quirk for v1 (no NaN literals are reachable from the SQL surface).
10975/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
10976fn expr_is_aggregate_call(e: &Expr) -> bool {
10977    match e {
10978        Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
10979        Expr::AggregateOrdered { .. } => true,
10980        _ => false,
10981    }
10982}
10983
10984/// Collect distinct top-level aggregate call expressions (dedup by value). Does
10985/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
10986/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
10987/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
10988/// than today — never a regression on a working query).
10989fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
10990    if expr_is_aggregate_call(e) {
10991        if !out.iter().any(|x| x == e) {
10992            out.push(e.clone());
10993        }
10994        return;
10995    }
10996    match e {
10997        Expr::Binary { lhs, rhs, .. } => {
10998            collect_agg_exprs(lhs, out);
10999            collect_agg_exprs(rhs, out);
11000        }
11001        Expr::Unary { expr, .. }
11002        | Expr::Cast { expr, .. }
11003        | Expr::IsNull { expr, .. }
11004        | Expr::BoolTest { expr, .. }
11005        | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
11006        Expr::FunctionCall { args, .. } => {
11007            for a in args {
11008                collect_agg_exprs(a, out);
11009            }
11010        }
11011        Expr::Like { expr, pattern, .. } => {
11012            collect_agg_exprs(expr, out);
11013            collect_agg_exprs(pattern, out);
11014        }
11015        Expr::Extract { source, .. } => collect_agg_exprs(source, out),
11016        Expr::WindowFunction {
11017            args,
11018            partition_by,
11019            order_by,
11020            ..
11021        } => {
11022            for a in args {
11023                collect_agg_exprs(a, out);
11024            }
11025            for p in partition_by {
11026                collect_agg_exprs(p, out);
11027            }
11028            for (o, _, _) in order_by {
11029                collect_agg_exprs(o, out);
11030            }
11031        }
11032        _ => {}
11033    }
11034}
11035
11036/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
11037fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
11038    if expr_is_aggregate_call(e) {
11039        if let Some(idx) = aggs.iter().position(|x| x == e) {
11040            *e = Expr::Column(ColumnName {
11041                qualifier: None,
11042                name: alloc::format!("__agg{idx}"),
11043            });
11044        }
11045        return;
11046    }
11047    match e {
11048        Expr::Binary { lhs, rhs, .. } => {
11049            replace_agg_exprs(lhs, aggs);
11050            replace_agg_exprs(rhs, aggs);
11051        }
11052        Expr::Unary { expr, .. }
11053        | Expr::Cast { expr, .. }
11054        | Expr::IsNull { expr, .. }
11055        | Expr::BoolTest { expr, .. }
11056        | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
11057        Expr::FunctionCall { args, .. } => {
11058            for a in args {
11059                replace_agg_exprs(a, aggs);
11060            }
11061        }
11062        Expr::Like { expr, pattern, .. } => {
11063            replace_agg_exprs(expr, aggs);
11064            replace_agg_exprs(pattern, aggs);
11065        }
11066        Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
11067        Expr::WindowFunction {
11068            args,
11069            partition_by,
11070            order_by,
11071            ..
11072        } => {
11073            for a in args {
11074                replace_agg_exprs(a, aggs);
11075            }
11076            for p in partition_by {
11077                replace_agg_exprs(p, aggs);
11078            }
11079            for (o, _, _) in order_by {
11080                replace_agg_exprs(o, aggs);
11081            }
11082        }
11083        _ => {}
11084    }
11085}
11086
11087/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
11088/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
11089/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
11090/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
11091/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
11092/// Returns None outside the bounded subset (leaves current behaviour). Only fires
11093/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
11094/// window-only / aggregate-only queries.
11095fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
11096    if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
11097        return None;
11098    }
11099    // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
11100    if !stmt.unions.is_empty() {
11101        return None;
11102    }
11103    let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
11104    if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
11105        return None;
11106    }
11107    stmt.from.as_ref()?;
11108    // Collect the aggregate calls to hoist from projection + outer ORDER BY.
11109    let mut aggs: Vec<Expr> = Vec::new();
11110    for item in &stmt.items {
11111        if let SelectItem::Expr { expr, .. } = item {
11112            collect_agg_exprs(expr, &mut aggs);
11113        }
11114    }
11115    for ob in &stmt.order_by {
11116        collect_agg_exprs(&ob.expr, &mut aggs);
11117    }
11118    // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
11119    let mut inner_items: Vec<SelectItem> = Vec::new();
11120    for g in &group_cols {
11121        inner_items.push(SelectItem::Expr {
11122            expr: g.clone(),
11123            alias: None,
11124        });
11125    }
11126    for (i, a) in aggs.iter().enumerate() {
11127        inner_items.push(SelectItem::Expr {
11128            expr: a.clone(),
11129            alias: Some(alloc::format!("__agg{i}")),
11130        });
11131    }
11132    let inner = SelectStatement {
11133        items: inner_items,
11134        distinct: false,
11135        distinct_on: Vec::new(),
11136        unions: Vec::new(),
11137        order_by: Vec::new(),
11138        limit: None,
11139        offset: None,
11140        limit_with_ties: false,
11141        window_check_exprs: Vec::new(),
11142        ..stmt.clone()
11143    };
11144    let derived = TableRef {
11145        name: "__aggwin".into(),
11146        alias: Some("__aggwin".into()),
11147        only: false,
11148        as_of_segment: None,
11149        unnest_expr: None,
11150        unnest_column_aliases: Vec::new(),
11151        with_ordinality: false,
11152        generate_series_args: None,
11153        lateral_subquery: Some(alloc::boxed::Box::new(inner)),
11154        jsonb_each_text_arg: None,
11155        table_fn_call: None,
11156        rows_from: None,
11157        json_table: None,
11158        scalar_fn_item: false,
11159    };
11160    // Outer window query over the derived rows: aggregates → __aggN column refs.
11161    let mut outer_items = stmt.items.clone();
11162    for item in &mut outer_items {
11163        if let SelectItem::Expr { expr, alias } = item {
11164            // Preserve PG's column label for a bare aggregate projection.
11165            if alias.is_none()
11166                && let Expr::FunctionCall { name, .. } = expr
11167                && crate::aggregate::is_aggregate_name(name)
11168            {
11169                *alias = Some(name.to_ascii_lowercase());
11170            }
11171            replace_agg_exprs(expr, &aggs);
11172        }
11173    }
11174    let mut outer_order = stmt.order_by.clone();
11175    for ob in &mut outer_order {
11176        replace_agg_exprs(&mut ob.expr, &aggs);
11177    }
11178    let mut outer_distinct_on = stmt.distinct_on.clone();
11179    for e in &mut outer_distinct_on {
11180        replace_agg_exprs(e, &aggs);
11181    }
11182    Some(SelectStatement {
11183        locking: None,
11184        ctes: Vec::new(),
11185        distinct: stmt.distinct,
11186        distinct_on: outer_distinct_on,
11187        items: outer_items,
11188        from: Some(FromClause {
11189            primary: derived,
11190            joins: Vec::new(),
11191        }),
11192        where_: None,
11193        group_by: None,
11194        group_by_all: false,
11195        having: None,
11196        unions: Vec::new(),
11197        order_by: outer_order,
11198        limit: stmt.limit.clone(),
11199        offset: stmt.offset.clone(),
11200        limit_with_ties: stmt.limit_with_ties,
11201        window_check_exprs: Vec::new(),
11202    })
11203}
11204
11205/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
11206/// membership.
11207///
11208/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
11209/// there?", and all four answered by scanning the whole right side once per
11210/// left row. The cost was (left rows x right rows), which is why
11211/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
11212/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
11213/// row that does not pays for all of it. Over 100k left rows, raising the
11214/// right side from 100 to 10,000 took 35 ms to 2848.
11215///
11216/// This is the shape round 485 already solved for DISTINCT, and it reuses
11217/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
11218/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
11219/// every bucket with the exact comparator, so a collision costs time and
11220/// never an answer.
11221struct PeerIndex<'r> {
11222    bh: hashbrown::DefaultHashBuilder,
11223    buckets: hashbrown::HashMap<u64, Vec<usize>>,
11224    rows: &'r [Row<'static>],
11225    fold: FoldSpec<'r>,
11226}
11227
11228impl<'r> PeerIndex<'r> {
11229    fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
11230        // ONE hasher for the whole pass: the default builder is seeded per
11231        // instance, so a fresh one per row would put equal rows in different
11232        // buckets.
11233        let bh = hashbrown::DefaultHashBuilder::default();
11234        let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
11235            hashbrown::HashMap::with_capacity(rows.len());
11236        for (i, r) in rows.iter().enumerate() {
11237            buckets
11238                .entry(norm_hash_row(r, &bh, fold))
11239                .or_default()
11240                .push(i);
11241        }
11242        Self {
11243            bh,
11244            buckets,
11245            rows,
11246            fold,
11247        }
11248    }
11249
11250    fn contains(&self, r: &Row<'static>) -> bool {
11251        let h = norm_hash_row(r, &self.bh, self.fold);
11252        self.buckets
11253            .get(&h)
11254            .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
11255    }
11256
11257    /// Remove ONE occurrence, so the multiset forms cancel row for row the
11258    /// way the pool they replaced did.
11259    fn take_one(&mut self, r: &Row<'static>) -> bool {
11260        let h = norm_hash_row(r, &self.bh, self.fold);
11261        let Some(b) = self.buckets.get_mut(&h) else {
11262            return false;
11263        };
11264        let Some(pos) = b
11265            .iter()
11266            .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
11267        else {
11268            return false;
11269        };
11270        b.swap_remove(pos);
11271        true
11272    }
11273}
11274
11275pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
11276    dedup_by_row(rows, |r| r, fold)
11277}
11278
11279/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
11280/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
11281/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
11282/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
11283/// order is preserved, and correctness needs only the one-way guarantee
11284/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
11285/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
11286fn dedup_by_row<T>(
11287    items: Vec<T>,
11288    row_of: impl Fn(&T) -> &Row<'static>,
11289    fold: FoldSpec<'_>,
11290) -> Vec<T> {
11291    if items.len() <= 32 {
11292        let mut out: Vec<T> = Vec::with_capacity(items.len());
11293        for it in items {
11294            if !out
11295                .iter()
11296                .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
11297            {
11298                out.push(it);
11299            }
11300        }
11301        return out;
11302    }
11303    // ONE BuildHasher instance for the whole pass — the default builder
11304    // is randomly seeded PER INSTANCE, so a fresh one per row would give
11305    // equal rows different hashes and never dedup.
11306    let bh = hashbrown::DefaultHashBuilder::default();
11307    let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
11308    let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
11309        hashbrown::HashMap::with_capacity(items.len());
11310    for it in items {
11311        let h = norm_hash_row(row_of(&it), &bh, fold);
11312        let bucket = buckets.entry(h).or_default();
11313        if !bucket
11314            .iter()
11315            .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
11316        {
11317            bucket.push(out.len());
11318            out.push(it);
11319        }
11320    }
11321    out
11322}
11323
11324/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
11325/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
11326/// rows may collide (buckets are re-checked with the exact comparator).
11327///
11328/// Domain design mirrors `value_cmp`'s equivalence classes:
11329/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
11330///   shares one domain: a value that is an integer fitting i64 hashes the
11331///   i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
11332///   anything else hashes the f64 approximation computed by THE SAME
11333///   formula the value_cmp float arms use (`numeric_to_f64`), so
11334///   `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
11335///   hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
11336///   Known un-closable corner: an integer in [2^53, 2^63) can compare
11337///   Equal to a float via value_cmp's lossy f64 arm while hashing in the
11338///   exact-i64 domain — mixed int/float rows at that magnitude may miss a
11339///   dedup (PG itself compares int8↔float8 in the lossy float8 domain).
11340/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
11341///   compares them blank-insensitively; plain Text pairs that differ only
11342///   in trailing blanks merely collide and are separated exactly).
11343/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
11344///   hash their fields under a distinct tag.
11345/// - Everything value_cmp falls back to debug-format ordering for
11346///   (Json, arrays, vectors, geometry, ranges, …) shares one constant
11347///   bucket — degrades to the exact linear scan, never wrong.
11348fn norm_hash_row(
11349    row: &Row<'static>,
11350    bh: &hashbrown::DefaultHashBuilder,
11351    fold: FoldSpec<'_>,
11352) -> u64 {
11353    norm_hash_values(&row.values, bh, fold)
11354}
11355
11356/// v7.39 (round 485) — the same hash over a bare value slice, so the
11357/// DISTINCT probe can run against a reused buffer instead of demanding a
11358/// `Row` that has to be allocated first (see `values_eq_norm`).
11359fn norm_hash_values(
11360    values: &[Value<'static>],
11361    bh: &hashbrown::DefaultHashBuilder,
11362    fold: FoldSpec<'_>,
11363) -> u64 {
11364    use core::hash::{BuildHasher, Hash, Hasher};
11365    let mut h = bh.build_hasher();
11366    for (i, v) in values.iter().enumerate() {
11367        // v7.39 (round 410) — hash the folded key when the MySQL collation
11368        // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
11369        // `'A'` vs `'a '`) share a hash bucket.
11370        //
11371        // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
11372        // byte-wise column that folded here while the comparator did not
11373        // would scatter equal rows across buckets and stop de-duplicating
11374        // at all; the hash and the comparator have to read the same mask.
11375        if fold.folds(i)
11376            && let Some(folded) = mysql_dedup_fold(v, fold.pads_at(i))
11377        {
11378            folded.hash(&mut h);
11379            continue;
11380        }
11381        norm_hash_value(v, &mut h);
11382    }
11383    h.finish()
11384}
11385
11386/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
11387///
11388/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
11389const fn pow10_i128(p: u16) -> Option<i128> {
11390    const P: [i128; 39] = {
11391        let mut t = [1i128; 39];
11392        let mut i = 1;
11393        while i < 39 {
11394            t[i] = t[i - 1] * 10;
11395            i += 1;
11396        }
11397        t
11398    };
11399    if (p as usize) < P.len() {
11400        Some(P[p as usize])
11401    } else {
11402        None
11403    }
11404}
11405
11406fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
11407    const TAG_NULL: u8 = 0;
11408    const TAG_BOOL: u8 = 1;
11409    const TAG_NUM_I64: u8 = 2;
11410    const TAG_NUM_F64: u8 = 3;
11411    const TAG_TEXT: u8 = 4;
11412    const TAG_DATE: u8 = 6;
11413    const TAG_TIME: u8 = 7;
11414    const TAG_TIMESTAMP: u8 = 8;
11415    const TAG_TIMETZ: u8 = 10;
11416    const TAG_UUID: u8 = 11;
11417    const TAG_MONEY: u8 = 12;
11418    const TAG_BYTES: u8 = 13;
11419    const TAG_INTERVAL: u8 = 14;
11420    const TAG_CHAR1: u8 = 15;
11421    const TAG_OPAQUE: u8 = 255;
11422    // One shared writer for the numeric family: an integer value
11423    // representable as i64 goes exact (round-trip probe — no_std, so no
11424    // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
11425    // through 0i64, folding it into 0.0 as value_cmp requires.
11426    let num_f64 = |h: &mut H, x: f64| {
11427        if x.is_nan() {
11428            h.write_u8(TAG_NUM_F64);
11429            h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
11430            return;
11431        }
11432        const TWO63: f64 = 9_223_372_036_854_775_808.0;
11433        if (-TWO63..TWO63).contains(&x) {
11434            #[allow(clippy::cast_possible_truncation)]
11435            let n = x as i64;
11436            #[allow(clippy::cast_precision_loss)]
11437            if (n as f64) == x {
11438                h.write_u8(TAG_NUM_I64);
11439                h.write_i64(n);
11440                return;
11441            }
11442        }
11443        h.write_u8(TAG_NUM_F64);
11444        h.write_u64(x.to_bits());
11445    };
11446    match v {
11447        Value::Null => h.write_u8(TAG_NULL),
11448        Value::Bool(b) => {
11449            h.write_u8(TAG_BOOL);
11450            h.write_u8(u8::from(*b));
11451        }
11452        Value::SmallInt(n) => {
11453            h.write_u8(TAG_NUM_I64);
11454            h.write_i64(i64::from(*n));
11455        }
11456        Value::Int(n) => {
11457            h.write_u8(TAG_NUM_I64);
11458            h.write_i64(i64::from(*n));
11459        }
11460        Value::BigInt(n) => {
11461            h.write_u8(TAG_NUM_I64);
11462            h.write_i64(*n);
11463        }
11464        Value::Float(x) => num_f64(h, *x),
11465        Value::Numeric {
11466            scaled,
11467            scale,
11468            kind,
11469        } => match kind {
11470            spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
11471            spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
11472            spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
11473            spg_storage::NumericKind::Finite => {
11474                // Reduce trailing fractional zeros so 1.50 and 1.5 share a
11475                // representation, then: exact integers fitting i64 go to the
11476                // i64 domain; everything else uses numeric_to_f64 — the SAME
11477                // formula value_cmp's Numeric↔Float arm compares with.
11478                // r1044 — the reduction is required (`1.5` and `1.50` are
11479                // one value and must land in one bucket) and it used to
11480                // walk one digit at a time. That is O(scale), and scale
11481                // is not small in practice: `n / 100` on a NUMERIC
11482                // column stores `9.1900000000000000`, scale 16, so the
11483                // loop ran fourteen times PER ROW.
11484                //
11485                // Priced by ablation rather than guessed at — removing
11486                // the loop entirely took `SELECT DISTINCT n FROM t ORDER
11487                // BY n` over 400,000 rows from 52 ms to 14.8, against
11488                // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
11489                // tried first moved it not at all, which is why this one
11490                // was measured before it was written.
11491                //
11492                // Binary search over the same powers finds the whole
11493                // run of trailing zeros in at most six tests and one
11494                // division, instead of one test and one division per
11495                // digit.
11496                let (mut s, mut sc) = (*scaled, *scale);
11497                if sc > 0 && s != 0 {
11498                    let mut lo: u16 = 0;
11499                    let mut hi: u16 = sc;
11500                    while lo < hi {
11501                        let mid = (lo + hi).div_ceil(2);
11502                        match pow10_i128(mid) {
11503                            Some(p) if s % p == 0 => lo = mid,
11504                            _ => hi = mid - 1,
11505                        }
11506                    }
11507                    if lo > 0 {
11508                        if let Some(p) = pow10_i128(lo) {
11509                            s /= p;
11510                            sc -= lo;
11511                        }
11512                    }
11513                }
11514                if sc == 0 {
11515                    if let Ok(n) = i64::try_from(s) {
11516                        h.write_u8(TAG_NUM_I64);
11517                        h.write_i64(n);
11518                    } else {
11519                        num_f64(h, crate::orderby::numeric_to_f64(s, 0));
11520                    }
11521                } else {
11522                    num_f64(h, crate::orderby::numeric_to_f64(s, sc));
11523                }
11524            }
11525        },
11526        // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
11527        // value that also fits i128 reuses the Numeric path above so
11528        // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
11529        // any i128-representable value — constant bucket is safe.
11530        Value::NumericBig(b) => match b.to_i128() {
11531            Some(s) => norm_hash_value(
11532                &Value::Numeric {
11533                    scaled: s,
11534                    scale: b.scale(),
11535                    kind: spg_storage::NumericKind::Finite,
11536                },
11537                h,
11538            ),
11539            None => h.write_u8(TAG_OPAQUE),
11540        },
11541        // value_cmp compares Text↔BpChar blank-insensitively (both sides
11542        // trimmed), so both hash the trimmed bytes. Text pairs differing
11543        // only in trailing blanks collide and are split exactly in-bucket.
11544        Value::Text(s) | Value::BpChar(s) => {
11545            h.write_u8(TAG_TEXT);
11546            h.write(s.trim_end_matches(' ').as_bytes());
11547        }
11548        Value::Char1(c) => {
11549            h.write_u8(TAG_CHAR1);
11550            h.write_u8(*c);
11551        }
11552        Value::Date(d) => {
11553            h.write_u8(TAG_DATE);
11554            h.write_i32(*d);
11555        }
11556        Value::Time(t) => {
11557            h.write_u8(TAG_TIME);
11558            h.write_i64(*t);
11559        }
11560        Value::Timestamp(t) => {
11561            h.write_u8(TAG_TIMESTAMP);
11562            h.write_i64(*t);
11563        }
11564        Value::TimeTz { us, offset_secs } => {
11565            h.write_u8(TAG_TIMETZ);
11566            h.write_i64(*us);
11567            h.write_i32(*offset_secs);
11568        }
11569        Value::Uuid(u) => {
11570            h.write_u8(TAG_UUID);
11571            h.write(u);
11572        }
11573        Value::Money(c) => {
11574            h.write_u8(TAG_MONEY);
11575            h.write_i64(*c);
11576        }
11577        Value::Bytes(b) => {
11578            h.write_u8(TAG_BYTES);
11579            h.write(b.as_ref());
11580        }
11581        Value::Interval {
11582            months,
11583            days,
11584            micros,
11585            kind,
11586        } => {
11587            h.write_u8(TAG_INTERVAL);
11588            h.write_i32(*months);
11589            h.write_i32(*days);
11590            h.write_i64(*micros);
11591        }
11592        // v7.37.16 — REAL joined the numeric value_cmp family (widened
11593        // to f64, same formulas as the arms), so it hashes in the shared
11594        // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
11595        // f32→f64 is exact, so equal-under-cmp implies equal bits here.
11596        Value::Real(x) => num_f64(h, f64::from(*x)),
11597        // Json (structural equality), vector families (float rendering),
11598        // arrays / geometry / net / ranges / composites (debug-format
11599        // fallback): one constant bucket — exact linear within.
11600        _ => h.write_u8(TAG_OPAQUE),
11601    }
11602}
11603
11604/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
11605/// treats numerically-equal exact values as one regardless of type or scale
11606/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
11607/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
11608/// `Row` `==` would keep them distinct.
11609/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
11610/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
11611/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
11612/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
11613/// the folded comparison key for a text value, None for anything else (which
11614/// keeps the byte-exact `value_cmp` path).
11615fn mysql_dedup_fold(v: &Value, pads: bool) -> Option<String> {
11616    match v {
11617        // v7.38.17 — CHAR's trailing spaces are padding; TEXT's are
11618        // data. The comment above named `utf8mb4_uca1400_ai_ci`, which
11619        // is MariaDB's default and PAD SPACE. SPG advertises MySQL 8.0,
11620        // whose default is NO PAD, so `'alpha'` and `'alpha  '` are two
11621        // rows to a `SELECT DISTINCT` and one to `count(DISTINCT)` was
11622        // the same question answered twice.
11623        // v7.38.18 — a CHAR's padding is the TYPE's and never counts; a
11624        // TEXT's is the collation's, which `pads` carries per position.
11625        Value::BpChar(s) => Some(spg_storage::mysql_compare_fold_char(s)),
11626        Value::Text(s) if pads => Some(spg_storage::mysql_compare_fold_char(s)),
11627        Value::Text(s) => Some(spg_storage::mysql_compare_fold(s)),
11628        _ => None,
11629    }
11630}
11631
11632/// v7.39 (round 485) — how many projected rows the single-table scan
11633/// builds, and how many of those the DISTINCT probe throws away again.
11634///
11635/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
11636/// 21 % of all samples in malloc/free called straight from the scan
11637/// closure. The closure's one per-row allocation is the projected
11638/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
11639/// instructions later — but "most" is a guess until it is a number, so
11640/// these count it. (Round 480 was spent acting on an inference about a
11641/// branch that turned out never to run.)
11642/// v7.39 (round 488) — reachability counters for round 487's projection
11643/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
11644/// and a never-called-function probe rules out code layout — so the
11645/// question is whether that shape reaches this code at all, which is a
11646/// number, not an inference.
11647pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11648pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11649
11650pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11651pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
11652    core::sync::atomic::AtomicU64::new(0);
11653
11654/// v7.38.13 — how DISTINCT must compare one row of output.
11655///
11656/// The MySQL default collation folds case and trailing spaces when it
11657/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
11658/// must not fold — `e2e_mysql_collate_binary_round370` calls the
11659/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
11660/// one when the schema asked to keep them apart", and names DISTINCT as
11661/// one of the sites that has to honour it.
11662///
11663/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
11664/// value in a MySQL session, because a bool cannot see a column. The
11665/// GROUP BY path consults the schema and was right all along; the test
11666/// only ever exercised that spelling, so the DISTINCT hole was never
11667/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
11668///
11669/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
11670/// which is what a caller with no schema to offer gets.
11671#[derive(Clone, Copy)]
11672pub(crate) struct FoldSpec<'c> {
11673    mysql: bool,
11674    binary: &'c [bool],
11675    /// v7.38.18 — the padding mask, in lockstep with `binary`. Read the
11676    /// note on `folds`: a hash and its comparator must consult the same
11677    /// masks or equal rows scatter across buckets.
11678    pads: &'c [bool],
11679}
11680
11681impl<'c> FoldSpec<'c> {
11682    /// No column information — every Text position folds under MySQL.
11683    pub(crate) const fn dialect(mysql: bool) -> Self {
11684        Self {
11685            mysql,
11686            binary: &[],
11687            pads: &[],
11688        }
11689    }
11690
11691    /// The mask read off the output columns.
11692    pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
11693        Self {
11694            mysql,
11695            binary,
11696            pads: &[],
11697        }
11698    }
11699
11700    /// The masks read off the output columns — fold-exemption AND
11701    /// padding, which are different questions about the same collation.
11702    pub(crate) fn of_masks(mysql: bool, binary: &'c [bool], pads: &'c [bool]) -> Self {
11703        Self {
11704            mysql,
11705            binary,
11706            pads,
11707        }
11708    }
11709
11710    /// Does position `i` treat trailing spaces as insignificant?
11711    #[inline]
11712    fn pads_at(&self, i: usize) -> bool {
11713        self.pads.get(i).copied().unwrap_or(false)
11714    }
11715
11716    /// Does position `i` fold?
11717    #[inline]
11718    fn folds(&self, i: usize) -> bool {
11719        self.mysql && !self.binary.get(i).copied().unwrap_or(false)
11720    }
11721}
11722
11723/// The fold-exempt mask for a projection.
11724///
11725/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
11726/// projection rebuilds that schema through `ColumnSchema::new`, whose
11727/// collation default is `Binary` — a mask built from it would mark
11728/// EVERY column byte-wise and stop DISTINCT folding at all.
11729/// The padding mask for a projection, read off the same items as
11730/// [`fold_mask`] so the two cannot come from different places.
11731pub(crate) fn pad_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11732    projection.iter().map(|p| p.pads).collect()
11733}
11734
11735pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11736    projection.iter().map(|p| p.fold_exempt).collect()
11737}
11738
11739/// v7.38.14 — the same mask, from an OUTPUT SCHEMA instead of a
11740/// projection.
11741///
11742/// Some de-duplication sites hold `Vec<ColumnSchema>` and never see the
11743/// `ProjectedItem`s it came from. `ProjectedItem::fold_exempt` is built
11744/// from exactly this test (`select.rs`, `build_projection`), so the two
11745/// must keep answering identically -- a site that decided "byte-wise" one
11746/// way while its neighbour decided the other is how the answer came to
11747/// depend on which executor ran the query.
11748///
11749/// The direction matters: `Collation::Binary` is `ColumnSchema::new`'s
11750/// DEFAULT, so a schema rebuilt without carrying the field reads as
11751/// "byte-wise on purpose" here. That is a real trap and it has caught
11752/// five fields so far; it is why S4 of this release exists.
11753/// v7.38.18 — the padding mask from output columns, the sibling of
11754/// [`fold_mask_of_columns`]. Whether a column folds and whether it
11755/// pads are different questions about the same collation.
11756pub(crate) fn pad_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11757    columns
11758        .iter()
11759        .map(|c| crate::collate::pads_space(c.collation_name.as_deref()))
11760        .collect()
11761}
11762
11763pub(crate) fn fold_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11764    columns
11765        .iter()
11766        .map(|c| matches!(c.collation, spg_storage::Collation::Binary))
11767        .collect()
11768}
11769
11770pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
11771    values_eq_norm(&a.values, &b.values, fold)
11772}
11773
11774/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
11775/// DISTINCT probe can compare a reused projection buffer against a kept
11776/// row without building a `Row` for it.
11777pub(crate) fn values_eq_norm(
11778    a: &[Value<'static>],
11779    b: &[Value<'static>],
11780    fold: FoldSpec<'_>,
11781) -> bool {
11782    a.len() == b.len()
11783        && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
11784            if fold.folds(i)
11785                && let (Some(fx), Some(fy)) = (
11786                    mysql_dedup_fold(x, fold.pads_at(i)),
11787                    mysql_dedup_fold(y, fold.pads_at(i)),
11788                )
11789            {
11790                return fx == fy;
11791            }
11792            crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
11793        })
11794}
11795
11796/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
11797/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
11798/// order via the byte values; vectors are not sortable.
11799pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
11800    // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
11801    // so values sharing a ≥6-byte common prefix (`product_001` vs
11802    // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
11803    // order by their exact bytes instead of the old lossy f64 coarse key.
11804    // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
11805    // matches PG's default C / binary text collation. Every other type
11806    // keeps the lossless-enough `f64` fast path below.
11807    if let Value::Text(s) = v {
11808        return Ok(OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())));
11809    }
11810    // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
11811    // byte order (PG bpcharcmp under C collation), so mixed-pad values of
11812    // the same logical string order equal.
11813    if let Value::BpChar(s) = v {
11814        return Ok(OrderKey::Text(crate::orderby::CompactText::new(
11815            s.trim_end_matches(' '),
11816        )));
11817    }
11818    // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
11819    // carry the parsed value and compare it structurally (see
11820    // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
11821    if let Value::Json(s) = v {
11822        return Ok(match crate::json::parse(s) {
11823            Ok(jv) => OrderKey::Json(jv),
11824            Err(_) => OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())),
11825        });
11826    }
11827    // v7.37 — byte-orderable types PG sorts byte-wise but that have no
11828    // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
11829    // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
11830    // matching PG's network ordering.
11831    match v {
11832        Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
11833        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
11834        Value::NumericBig(b) => {
11835            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11836                spg_storage::NumericKey::from_big(b),
11837            )));
11838        }
11839        Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
11840        Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11841        Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11842        Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
11843        Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
11844            let mut key = alloc::vec::Vec::with_capacity(18);
11845            key.push(*family);
11846            key.extend_from_slice(addr);
11847            key.push(*bits);
11848            return Ok(OrderKey::Bytes(key));
11849        }
11850        _ => {}
11851    }
11852    // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
11853    // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
11854    // own OrderKey so integer arrays sort numerically; a NULL element rides to
11855    // the end via the +INF sentinel.
11856    let inf = || OrderKey::NullBig;
11857    let arr = match v {
11858        Value::IntArray(a) => Some(
11859            a.iter()
11860                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11861                .collect(),
11862        ),
11863        Value::SmallIntArray(a) => Some(
11864            a.iter()
11865                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11866                .collect(),
11867        ),
11868        Value::BigIntArray(a) => Some(
11869            a.iter()
11870                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11871                .collect(),
11872        ),
11873        Value::BoolArray(a) => Some(
11874            a.iter()
11875                .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
11876                .collect(),
11877        ),
11878        Value::TextArray(a) => Some(
11879            a.iter()
11880                .map(|o| {
11881                    o.as_ref()
11882                        .map_or_else(inf, |s| OrderKey::Text(crate::orderby::CompactText::new(s)))
11883                })
11884                .collect(),
11885        ),
11886        #[allow(clippy::cast_precision_loss)]
11887        Value::FloatArray(a) => Some(
11888            a.iter()
11889                .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
11890                .collect(),
11891        ),
11892        // r1040 — array elements take the same exact key their scalar
11893        // form does; an f64 projection here would order `{0.1}` against
11894        // `{0.1000000000000000001}` by luck.
11895        Value::NumericArray(a) => Some(
11896            a.iter()
11897                .map(|o| {
11898                    o.map_or_else(inf, |(m, s)| {
11899                        OrderKey::Numeric(alloc::boxed::Box::new(
11900                            spg_storage::NumericKey::from_numeric(
11901                                m,
11902                                s,
11903                                spg_storage::NumericKind::Finite,
11904                            ),
11905                        ))
11906                    })
11907                })
11908                .collect(),
11909        ),
11910        Value::DateArray(a) => Some(
11911            a.iter()
11912                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11913                .collect(),
11914        ),
11915        _ => None,
11916    };
11917    if let Some(elements) = arr {
11918        return Ok(OrderKey::Array(elements));
11919    }
11920    // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
11921    // right, which is exactly the lexicographic element order an Array key
11922    // already gives: `(2,'b') < (9,'a')` because the leading field decides.
11923    if let Value::Composite(fields) = v {
11924        let elements = fields
11925            .iter()
11926            .map(|(_, fv)| value_to_order_key(fv))
11927            .collect::<Result<alloc::vec::Vec<_>, _>>()?;
11928        return Ok(OrderKey::Array(elements));
11929    }
11930    // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
11931    // Projecting these to f64 (the historic path) silently collapses BigInt /
11932    // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
11933    // the wrong order for large ids and microsecond timestamps.
11934    match v {
11935        Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11936        Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
11937        Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11938        // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
11939        // integer (days / micros / cents / calendar year); TIMETZ by the
11940        // UTC-equivalent micros (local wall - offset) so the same physical
11941        // instant in different zones sorts equal.
11942        Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
11943        Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
11944        Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
11945        Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
11946        // v7.39.13 — the UTC instant is only HALF the key.
11947        //
11948        // This ordered by the instant alone, so the values that share
11949        // one were called equal and a stable sort then returned them in
11950        // insertion order — an answer, not a tie-break. Measured on
11951        // PostgreSQL 18.6 against this engine, six rows, one column:
11952        //
11953        // ```text
11954        //   PG 18.6        SPG 7.39.12
11955        //   07:00:00+01    07:00:00+01
11956        //   06:59:59+00    06:59:59+00
11957        //   09:00:00+02    07:00:00+00   <- the four that share
11958        //   07:00:00+00    02:00:00-05      07:00 UTC, in the
11959        //   02:00:00-05    09:00:00+02      order they were written
11960        //   01:00:00-06    01:00:00-06
11961        // ```
11962        //
11963        // PostgreSQL breaks the tie by OFFSET DESCENDING, and
11964        // `'07:00:00+00' = '02:00:00-05'` is FALSE there. Shifting the
11965        // instant left by 32 bits leaves room for the offset underneath
11966        // it — `i128` holds both exactly, where `i64` could not — and
11967        // `compare` in `eval::binop` orders the same pair the same way,
11968        // from the same measurement.
11969        Value::TimeTz { us, offset_secs } => {
11970            return Ok(OrderKey::Int(i128::from(spg_storage::timetz_sort_key(
11971                *us,
11972                *offset_secs,
11973            ))));
11974        }
11975        Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
11976        _ => {}
11977    }
11978    let num = match v {
11979        // Callers without NULLS FIRST/LAST context (array elements,
11980        // histogram sampling) put NULL last, as before.
11981        Value::Null => return Ok(OrderKey::NullBig),
11982        // v7.17.0 Phase 3.P0-38 — range ordering is not supported
11983        // in v7.17.0 (needs lex-then-inclusivity tiebreak).
11984        Value::Range { .. } => {
11985            return Err(EngineError::Unsupported(
11986                "ORDER BY of a range value is not supported in v7.17.0".into(),
11987            ));
11988        }
11989        // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
11990        Value::Hstore(_) => {
11991            return Err(EngineError::Unsupported(
11992                "ORDER BY of a hstore value is not supported".into(),
11993            ));
11994        }
11995        // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
11996        Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
11997            return Err(EngineError::Unsupported(
11998                "ORDER BY of a 2D array is not supported in v7.17.0".into(),
11999            ));
12000        }
12001        // r1039/r1040 — the exact canonical key, not an f64 projection.
12002        //
12003        // r1039 fixed the three specials, which carry a canonical zero in
12004        // `scaled` and so all sorted as the number 0. The projection
12005        // itself was the rest of the defect: "precision losses here only
12006        // matter for tie-breaks well past 15 significant digits" was the
12007        // comment, and the measurement disagreed — f64 called
12008        // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
12009        // returned them in insertion order. Three of ten values came back
12010        // in the wrong place against PG18.4.
12011        Value::Numeric {
12012            scaled,
12013            scale,
12014            kind,
12015        } => {
12016            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
12017                spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
12018            )));
12019        }
12020        Value::Float(x) => *x,
12021        // v7.37.16 — REAL sorts by its exact f64 widening (it had no
12022        // arm and fell through to the unsupported error).
12023        Value::Real(x) => f64::from(*x),
12024        Value::Bool(b) => {
12025            if *b {
12026                1.0
12027            } else {
12028                0.0
12029            }
12030        }
12031        Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
12032            return Err(EngineError::Unsupported(
12033                "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
12034            ));
12035        }
12036        // v7.37 — PG orders INTERVAL by its total time, treating a month as
12037        // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
12038        // f64 is exact for any interval under ~285 years, and only ORDER BY
12039        // tie-breaks past that magnitude lose precision. Matches the
12040        // min/max(interval) comparator in aggregate.rs.
12041        #[allow(clippy::cast_precision_loss)]
12042        Value::Interval {
12043            months,
12044            days,
12045            micros,
12046            kind,
12047        } => {
12048            let total = i128::from(*months) * 30 * 86_400_000_000
12049                + i128::from(*days) * 86_400_000_000
12050                + i128::from(*micros);
12051            total as f64
12052        }
12053        Value::Json(_) => {
12054            return Err(EngineError::Unsupported(
12055                "ORDER BY of a JSON value is not supported — cast the document to text first"
12056                    .into(),
12057            ));
12058        }
12059        // v7.5.0 — Value is #[non_exhaustive]; future variants need
12060        // an explicit ORDER BY mapping. Surface as Unsupported until
12061        // engine support is added.
12062        _ => {
12063            return Err(EngineError::Unsupported(
12064                "ORDER BY of this value type is not supported".into(),
12065            ));
12066        }
12067    };
12068    Ok(OrderKey::Num(num))
12069}
12070
12071/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
12072/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
12073/// `EngineError` so the projection-build path keeps `UnknownQualifier`
12074/// vs `ColumnNotFound` distinct.
12075/// PG's name for the physical row identity. It is reserved there — no table
12076/// can have a column called this — which is what lets `*` skip it by name.
12077pub(crate) const CTID_COLUMN: &str = "ctid";
12078
12079/// v7.39 (round 512) — PG's system columns, in the order they are appended.
12080/// All six are reserved names there, which is what lets `*` skip them and
12081/// lets a scan tell them from a user column without a flag.
12082pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
12083
12084/// Is this name one of them?
12085pub(crate) fn is_system_column(name: &str) -> bool {
12086    SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
12087}
12088
12089/// Where the scan's appended system columns begin, if this schema carries
12090/// them: the trailing six, named in order. A catalog view with a column of
12091/// its own called `xmin` does not match, which is the point.
12092fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
12093    let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
12094    cols[start..]
12095        .iter()
12096        .zip(SYSTEM_COLUMNS)
12097        .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
12098        .then_some(start)
12099}
12100
12101/// v7.39 (round 540) — which positions `*` must skip.
12102///
12103/// The rule stays round 512's — the synthetic columns are the trailing
12104/// six of a relation's block, matched by POSITION so a genuine `xmin`
12105/// column is not lost — but a JOINED schema names its columns
12106/// `alias.column` and lays the peers out end to end, so a peer's six sit
12107/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
12108/// "trailing six" test back on the block it was written for.
12109fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
12110    let mut skip = alloc::vec![false; cols.len()];
12111    fn qualifier(n: &str) -> Option<&str> {
12112        n.rsplit_once('.').map(|(q, _)| q)
12113    }
12114    fn bare(n: &str) -> &str {
12115        n.rsplit('.').next().unwrap_or(n)
12116    }
12117    let mut i = 0;
12118    while i < cols.len() {
12119        let q = qualifier(&cols[i].name);
12120        let mut end = i;
12121        while end < cols.len() && qualifier(&cols[end].name) == q {
12122            end += 1;
12123        }
12124        if let Some(start) = (end - i)
12125            .checked_sub(SYSTEM_COLUMNS.len())
12126            .map(|off| i + off)
12127            && cols[start..end]
12128                .iter()
12129                .zip(SYSTEM_COLUMNS)
12130                .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
12131        {
12132            for s in skip.iter_mut().take(end).skip(start) {
12133                *s = true;
12134            }
12135        }
12136        i = end;
12137    }
12138    skip
12139}
12140
12141/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
12142/// read? Only then is the column materialised.
12143pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
12144    let mut found = false;
12145    crate::expr_analysis::visit_expr_columns_and_subqueries(
12146        e,
12147        &mut |c| {
12148            if is_system_column(&c.name) {
12149                found = true;
12150            }
12151        },
12152        &mut |_| {},
12153    );
12154    found
12155}
12156
12157fn references_ctid(stmt: &SelectStatement) -> bool {
12158    let in_expr = expr_references_ctid;
12159    stmt.items.iter().any(|i| match i {
12160        SelectItem::Expr { expr, .. } => in_expr(expr),
12161        _ => false,
12162    }) || stmt.where_.as_ref().is_some_and(in_expr)
12163        || stmt.order_by.iter().any(|o| in_expr(&o.expr))
12164        || stmt
12165            .group_by
12166            .as_ref()
12167            .is_some_and(|g| g.iter().any(in_expr))
12168        || stmt.having.as_ref().is_some_and(in_expr)
12169}
12170
12171/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
12172/// is a name the projection has to TYPE before any row exists.
12173///
12174/// Evaluation has answered this since round T9 (`resolve_column` builds a
12175/// `Value::Composite` of every column), but the typing side below had no
12176/// such branch and raised `column "t" does not exist` first — so the
12177/// feature was unreachable through a projection. Measured against PG18.4:
12178/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
12179///
12180/// The type is `Jsonb` + a composite marker, which is exactly how a
12181/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
12182/// the value travels as a `Value::Composite` and renders in the canonical
12183/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
12184/// so the marker names the alias and no rehydration keys off it — the
12185/// value arrives already built.
12186fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
12187    let mut s = ColumnSchema::new(
12188        alloc::string::String::from(alias),
12189        spg_storage::DataType::Jsonb,
12190        true,
12191    );
12192    s.user_composite_type = Some(alloc::string::String::from(alias));
12193    s
12194}
12195
12196/// v7.39.3 — `mysql` makes the name comparison case-INSENSITIVE, which
12197/// is MySQL's rule for column names (measured on 9.7.2: `mycol`,
12198/// `MYCOL` and a backquoted `MyCol` all resolve the same column).
12199///
12200/// SPG compared byte for byte and its lexer folds an UNQUOTED
12201/// identifier, so a table restored from a `mysqldump` — where every
12202/// identifier is backquoted and keeps its case — had every mixed-case
12203/// column unreachable from ordinary unquoted SQL. Same "two spellings,
12204/// two things" defect v7.39.1 closed for relation names.
12205pub(crate) fn resolve_projection_column<'a>(
12206    c: &ColumnName,
12207    schema_cols: &'a [ColumnSchema],
12208    table_alias: &str,
12209    mysql: bool,
12210) -> Result<Cow<'a, ColumnSchema>, EngineError> {
12211    let same = |a: &str, b: &str| {
12212        if mysql {
12213            a.eq_ignore_ascii_case(b)
12214        } else {
12215            a == b
12216        }
12217    };
12218    if let Some(q) = &c.qualifier {
12219        let composite = alloc::format!("{q}.{name}", name = c.name);
12220        if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &composite)) {
12221            return Ok(Cow::Borrowed(s));
12222        }
12223        // Single-table case: the qualifier may equal the active alias —
12224        // then look for the bare column name.
12225        if same(q, table_alias)
12226            && let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name))
12227        {
12228            return Ok(Cow::Borrowed(s));
12229        }
12230        // For multi-table schemas the qualifier is unknown only if no
12231        // column bears the "<q>." prefix. For single-table, the alias
12232        // mismatch alone is enough.
12233        let prefix = alloc::format!("{q}.");
12234        let qualifier_known =
12235            same(q, table_alias) || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
12236        if !qualifier_known {
12237            return Err(EngineError::Eval(EvalError::UnknownQualifier {
12238                qualifier: q.clone(),
12239                column: c.name.clone(),
12240            }));
12241        }
12242        return Err(EngineError::Eval(EvalError::ColumnNotFound {
12243            name: c.name.clone(),
12244        }));
12245    }
12246    if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name)) {
12247        return Ok(Cow::Borrowed(s));
12248    }
12249    let suffix = alloc::format!(".{name}", name = c.name);
12250    let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
12251    let first = matches.next();
12252    let extra = matches.next();
12253    match (first, extra) {
12254        (Some(s), None) => Ok(Cow::Borrowed(s)),
12255        (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
12256            detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
12257        })),
12258        // The whole-row reference, checked LAST so a real column carrying
12259        // the alias's name still wins — the same precedence
12260        // `resolve_column` applies on the evaluation side.
12261        //
12262        // Two schema shapes reach here. A single-table (or subquery, or
12263        // CTE) scan carries its alias and bare column names, so the name
12264        // has to equal the alias. A JOIN's combined schema carries no
12265        // alias at all and qualifies every column `alias.col`, so the
12266        // alias is identified by the prefix instead — which is exactly
12267        // how `whole_row_composite` picks the fields out on the
12268        // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
12269        // answers `(7,z)` on PG18.4 and errored here until this arm
12270        // covered the joined shape too.
12271        _ if !table_alias.is_empty() && c.name == table_alias => {
12272            Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
12273        }
12274        _ if table_alias.is_empty() && {
12275            let prefix = alloc::format!("{name}.", name = c.name);
12276            schema_cols.iter().any(|s| s.name.starts_with(&prefix))
12277        } =>
12278        {
12279            Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
12280        }
12281        _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
12282            name: c.name.clone(),
12283        })),
12284    }
12285}
12286
12287/// v7.40.0 — a column the grouping-set rewrite injected purely to sort
12288/// on, and which must not reach the client. Two families: `__grp_ord_*`
12289/// carries a branch's `grouping()` mask (round 135), `__grp_key_*`
12290/// carries a key the rollup orders by that the query did not project —
12291/// without it `SELECT SUM(qty) … GROUP BY qty WITH ROLLUP` answered
12292/// `column "qty" does not exist`, because a UNION's ORDER BY can only
12293/// name output columns.
12294fn is_synthetic_group_col(name: &str) -> bool {
12295    name.starts_with("__grp_ord_") || name.starts_with("__grp_key_")
12296}
12297
12298/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
12299/// parser to carry per-branch GROUPING() masks into a grouping-set query's
12300/// ORDER BY. They must never reach the output. No-op unless such a column is
12301/// present, so the common path is untouched.
12302/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
12303///
12304/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
12305/// a `LIMIT 2` that should have answered two groups answered one.
12306fn apply_deferred_limit(
12307    rows: alloc::vec::Vec<Row<'static>>,
12308    deferred: &(
12309        Option<spg_sql::ast::LimitExpr>,
12310        Option<spg_sql::ast::LimitExpr>,
12311    ),
12312) -> alloc::vec::Vec<Row<'static>> {
12313    let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
12314        Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
12315        _ => None,
12316    };
12317    let mut rows = rows;
12318    if let Some(off) = count(&deferred.1) {
12319        rows = rows.split_off(off.min(rows.len()));
12320    }
12321    if let Some(lim) = count(&deferred.0) {
12322        rows.truncate(lim);
12323    }
12324    rows
12325}
12326
12327fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
12328    let QueryResult::Rows { columns, rows } = result else {
12329        return result;
12330    };
12331    if !columns.iter().any(|c| is_synthetic_group_col(&c.name)) {
12332        return QueryResult::Rows { columns, rows };
12333    }
12334    let keep: Vec<usize> = columns
12335        .iter()
12336        .enumerate()
12337        .filter(|(_, c)| !is_synthetic_group_col(&c.name))
12338        .map(|(i, _)| i)
12339        .collect();
12340    let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
12341    let new_rows: Vec<Row<'static>> = rows
12342        .into_iter()
12343        .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
12344        .collect();
12345    QueryResult::Rows {
12346        columns: new_cols,
12347        rows: new_rows,
12348    }
12349}
12350
12351/// v7.39 (round 487) — bind every projection item that is a bare column
12352/// reference to its position, once per query.
12353///
12354/// `#[inline(never)]` and out of line on purpose. Round 486 established
12355/// that adding code inside these scan bodies moves neighbouring hot
12356/// functions around under fat LTO: the first version of this had the loop
12357/// inline in `run_single_table_scan` and four aggregate shapes that never
12358/// touch that function — `full_agg`, `join_agg`, `group_500k`,
12359/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
12360/// the same machine. Keeping it out of line kept them still.
12361#[inline(never)]
12362fn bind_direct_columns(
12363    projection: &[ProjectedItem],
12364    ctx: &eval::EvalContext<'_>,
12365) -> Vec<Option<usize>> {
12366    projection
12367        .iter()
12368        .map(|p| match &p.expr {
12369            Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
12370                // Same exclusion `compile_into` makes: a composite column
12371                // has to be rehydrated from stored JSON, which is not a
12372                // cell read.
12373                ctx.columns
12374                    .get(*pos)
12375                    .is_none_or(|sc| sc.user_composite_type.is_none())
12376            }),
12377            _ => None,
12378        })
12379        .collect()
12380}
12381
12382/// v7.39 (round 505) — the name an un-aliased projected expression reports.
12383///
12384/// PG18 names a call for its function and everything else `?column?`;
12385/// measured with `\gdesc`. SPG used to print the parsed expression back
12386/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
12387/// name-keyed row access found nothing under `upper`.
12388///
12389/// The MySQL half is NOT this rule and is deliberately left alone here:
12390/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
12391/// which needs the parser to hand over spans the AST does not carry yet.
12392/// Until it does, a MySQL session keeps the printed form — closer to what
12393/// MariaDB answers than `?column?` would be.
12394pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
12395    if mysql {
12396        return expr.to_string();
12397    }
12398    spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
12399}
12400
12401pub(crate) fn build_projection(
12402    items: &[SelectItem],
12403    schema_cols: &[ColumnSchema],
12404    table_alias: &str,
12405    mysql: bool,
12406    cat: Option<&Catalog>,
12407) -> Result<Vec<ProjectedItem>, EngineError> {
12408    build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0, cat)
12409}
12410
12411/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
12412/// invisible to `*`.
12413///
12414/// The windowed-SELECT path appends a synthetic `__win_N` column per window
12415/// function so the rewritten projection can reference the computed values as
12416/// ordinary columns. `*` then expanded them too, and
12417/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
12418/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
12419/// silent one: the row simply had one more field than the client asked for.
12420///
12421/// Hidden by POSITION rather than by name, for the reason round 512 recorded
12422/// about the system columns: a name test looks safe until a real column
12423/// happens to carry the name. These are appended last, so the count is what
12424/// identifies them.
12425pub(crate) fn build_projection_hiding_tail(
12426    items: &[SelectItem],
12427    schema_cols: &[ColumnSchema],
12428    table_alias: &str,
12429    mysql: bool,
12430    hidden_tail: usize,
12431    // v7.38.19 — the catalog, so a user-defined function's DECLARED
12432    // return type reaches the projection. Without it `describe_expr`
12433    // cannot type `f_sql()` and the column falls back to text, which is
12434    // what psql reads to decide alignment: `SELECT 7::bigint, f_sql()`
12435    // right-aligned one cell and left-aligned the other while both held
12436    // a bigint. Reported by sentori against 7.38.18 (their §2.2), who
12437    // also established that the EXECUTOR was never confused -- CTAS off
12438    // the same expression gives a bigint column, and arithmetic on it
12439    // works. Only the type travelling in the RowDescription was wrong.
12440    cat: Option<&Catalog>,
12441) -> Result<Vec<ProjectedItem>, EngineError> {
12442    let visible = schema_cols.len().saturating_sub(hidden_tail);
12443    // v7.39 (round 462) — a join's combined schema qualifies every column
12444    // `alias.col` so the deferred-join cell lookups resolve by composite
12445    // name. That is an internal convention, and `*` was handing it to the
12446    // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
12447    // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
12448    // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
12449    // already learned this for `q.*`; plain `*` never got the same rule.
12450    //
12451    // The signal is the schema itself, not the call site: only a combined
12452    // join schema arrives with no table alias AND every column qualified.
12453    // A single-table schema carries its alias, an empty schema has nothing
12454    // to strip, and a synthetic schema's names carry no dot.
12455    let joined_schema = table_alias.is_empty()
12456        && !schema_cols.is_empty()
12457        && schema_cols.iter().all(|c| c.name.contains('.'));
12458    let bare_name = |name: &str| -> String {
12459        if !joined_schema {
12460            return name.to_string();
12461        }
12462        match name.split_once('.') {
12463            Some((_, rest)) if !rest.is_empty() => rest.to_string(),
12464            _ => name.to_string(),
12465        }
12466    };
12467    let mut out = Vec::new();
12468    for item in items {
12469        match item {
12470            SelectItem::Wildcard => {
12471                // v7.39 (round 511) — `*` never expands a system column, as
12472                // PG's does not. They join the schema only when the statement
12473                // asked for them, so this matters for the mixed shape
12474                // `SELECT *, ctid FROM t`.
12475                //
12476                // v7.39 (round 512) — by POSITION, not by name. Matching on
12477                // the name alone looked safe because PG reserves them, and it
12478                // is not: `pg_replication_slots` genuinely has a column called
12479                // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
12480                // Only the trailing six, in the order the scan appends them,
12481                // are the synthetic ones.
12482                let sys_skip = synthetic_system_positions(schema_cols);
12483                for (idx, col) in schema_cols.iter().enumerate() {
12484                    if sys_skip[idx] || idx >= visible {
12485                        continue;
12486                    }
12487                    out.push(ProjectedItem {
12488                        expr: Expr::Column(ColumnName {
12489                            qualifier: None,
12490                            name: col.name.clone(),
12491                        }),
12492                        output_name: bare_name(&col.name),
12493                        ty: col.ty,
12494                        nullable: col.nullable,
12495                        user_enum_type: col.user_enum_type.clone(),
12496                        mysql_fsp: col.mysql_fsp,
12497                        collation_name: col.collation_name.clone(),
12498                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12499                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
12500                    });
12501                }
12502            }
12503            // v7.39 (round 128) — `q.*` expands to every column belonging to
12504            // the qualifier `q`. Single-table schemas carry bare column names
12505            // reachable via `table_alias`; a join's combined schema carries
12506            // `alias.col` names, so a column belongs to `q` when its name has
12507            // the `q.` prefix. PG labels the expanded columns by their bare
12508            // name, so the `alias.` prefix is stripped from the output name.
12509            SelectItem::QualifiedWildcard(q) => {
12510                let prefix = alloc::format!("{q}.");
12511                let single_table = !table_alias.is_empty() && q == table_alias;
12512                let mut matched = 0usize;
12513                for col in &schema_cols[..visible] {
12514                    let belongs =
12515                        col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
12516                    if !belongs {
12517                        continue;
12518                    }
12519                    matched += 1;
12520                    let output_name = col
12521                        .name
12522                        .strip_prefix(&prefix)
12523                        .unwrap_or(&col.name)
12524                        .to_string();
12525                    out.push(ProjectedItem {
12526                        expr: Expr::Column(ColumnName {
12527                            qualifier: None,
12528                            name: col.name.clone(),
12529                        }),
12530                        output_name,
12531                        ty: col.ty,
12532                        nullable: col.nullable,
12533                        user_enum_type: col.user_enum_type.clone(),
12534                        mysql_fsp: col.mysql_fsp,
12535                        collation_name: col.collation_name.clone(),
12536                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12537                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
12538                    });
12539                }
12540                if matched == 0 {
12541                    // `q.*` names no column, so the reference IS the star.
12542                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
12543                        qualifier: q.clone(),
12544                        column: alloc::string::String::from("*"),
12545                    }));
12546                }
12547            }
12548            SelectItem::Expr { expr, alias } => {
12549                // Plain column ref keeps full schema info (real type +
12550                // nullability). For compound expressions try the
12551                // describe-side function-return-type table first
12552                // (e.g. `SELECT now()` → Timestamptz, `SELECT
12553                // concat(…)` → Text). Falls back to nullable Text
12554                // for shapes the describe path can't resolve.
12555                if let Expr::Column(c) = expr {
12556                    let sch = resolve_projection_column(c, schema_cols, table_alias, mysql)?;
12557                    let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
12558                    out.push(ProjectedItem {
12559                        expr: expr.clone(),
12560                        output_name,
12561                        ty: sch.ty,
12562                        nullable: sch.nullable,
12563                        // v7.39 (read01 round 54) — a bare enum column keeps
12564                        // its enum identity through the projection.
12565                        user_enum_type: sch.user_enum_type.clone(),
12566                        mysql_fsp: sch.mysql_fsp,
12567                        collation_name: sch.collation_name.clone(),
12568                        // v7.38.13 — and its byte-wise-ness. This is the
12569                        // site `SELECT DISTINCT t FROM t` arrives at.
12570                        fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
12571                        pads: crate::collate::pads_space(sch.collation_name.as_deref()),
12572                    });
12573                } else if let Some(shape) = describe::describe_expr_in(expr, schema_cols, cat) {
12574                    let output_name = alias
12575                        .clone()
12576                        .unwrap_or_else(|| default_output_name(expr, mysql));
12577                    out.push(ProjectedItem {
12578                        expr: expr.clone(),
12579                        // v7.38.18 — a projected EXPRESSION has no column collation
12580                        // to read, so it takes the session default, which is MySQL
12581                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12582                        pads: false,
12583                        output_name,
12584                        ty: shape.ty,
12585                        // v7.39 (round 258) — a projected EXPRESSION keeps its
12586                        // enum identity too, not just a bare column. `FROM
12587                        // (VALUES ('happy'::mood), …) t(m)` lowers to constant
12588                        // SELECTs, so the derived column arrived here as a cast
12589                        // and lost the enum — making the outer ORDER BY / min /
12590                        // max / array_agg sort by the label's TEXT.
12591                        nullable: shape.nullable,
12592                        user_enum_type: None,
12593                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12594                        // A bare column reference keeps its collation; any
12595                        // other expression produces a new value and has none.
12596                        collation_name: match expr {
12597                            Expr::Column(c) => schema_cols
12598                                .iter()
12599                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12600                                .and_then(|sc| sc.collation_name.clone()),
12601                            _ => None,
12602                        },
12603                        fold_exempt: match expr {
12604                            Expr::Column(c) => schema_cols
12605                                .iter()
12606                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12607                                .is_some_and(|sc| {
12608                                    matches!(sc.collation, spg_storage::Collation::Binary)
12609                                }),
12610                            // Not a column: no declared collation to honour,
12611                            // so the session default applies and it folds.
12612                            _ => false,
12613                        },
12614                    });
12615                } else {
12616                    let output_name = alias
12617                        .clone()
12618                        .unwrap_or_else(|| default_output_name(expr, mysql));
12619                    out.push(ProjectedItem {
12620                        expr: expr.clone(),
12621                        // v7.38.18 — a projected EXPRESSION has no column collation
12622                        // to read, so it takes the session default, which is MySQL
12623                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12624                        pads: false,
12625                        output_name,
12626                        // A user ENUM has no DataType of its own, so
12627                        // `describe_expr` cannot type `'ok'::mood` and the
12628                        // item lands HERE, defaulting to text — which is why
12629                        // pg_typeof answered `text` and a derived table sorted
12630                        // enum values by their label.
12631                        ty: DataType::Text,
12632                        nullable: true,
12633                        user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
12634                            .map(alloc::string::String::from),
12635                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12636                        collation_name: match expr {
12637                            Expr::Column(c) => schema_cols
12638                                .iter()
12639                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12640                                .and_then(|sc| sc.collation_name.clone()),
12641                            _ => None,
12642                        },
12643                        fold_exempt: match expr {
12644                            Expr::Column(c) => schema_cols
12645                                .iter()
12646                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12647                                .is_some_and(|sc| {
12648                                    matches!(sc.collation, spg_storage::Collation::Binary)
12649                                }),
12650                            // Not a column: no declared collation to honour,
12651                            // so the session default applies and it folds.
12652                            _ => false,
12653                        },
12654                    });
12655                }
12656            }
12657        }
12658    }
12659    Ok(out)
12660}
12661
12662// ---- v4.12 window-function helpers ----
12663// The (partition-key, order-key, original-index) tuple shape used
12664// across these helpers is intrinsic to the planner. Factoring it
12665// into a typedef adds indirection without making the code clearer,
12666// so several lints are allowed inline on the affected functions
12667// rather than module-wide.
12668
12669/// v4.22: pick more specific column types from observed rows when
12670/// the projection builder defaulted to Text (the v1.x behavior for
12671/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
12672/// land an Int column in the CTE storage table rather than failing
12673/// the insert with "expected TEXT, got INT".
12674pub(crate) fn infer_column_types(
12675    columns: &[ColumnSchema],
12676    rows: &[Row<'static>],
12677) -> Vec<ColumnSchema> {
12678    let mut out = columns.to_vec();
12679    for (col_idx, col) in out.iter_mut().enumerate() {
12680        if col.ty != DataType::Text {
12681            continue;
12682        }
12683        let mut inferred: Option<DataType> = None;
12684        let mut all_null = true;
12685        for row in rows {
12686            let Some(v) = row.values.get(col_idx) else {
12687                continue;
12688            };
12689            let ty = match v {
12690                Value::Null => continue,
12691                Value::SmallInt(_) => DataType::SmallInt,
12692                Value::Int(_) => DataType::Int,
12693                Value::BigInt(_) => DataType::BigInt,
12694                Value::Float(_) => DataType::Float,
12695                Value::Bool(_) => DataType::Bool,
12696                Value::Vector(_) => DataType::Vector {
12697                    dim: 0,
12698                    encoding: VecEncoding::F32,
12699                },
12700                // v7.38 (read01 U16) — carry array values through with an
12701                // array type so a recursive CTE that projects an array
12702                // (e.g. a SEARCH/CYCLE ord / path column) types the working
12703                // column as an array, not Text.
12704                Value::TextArray(_) => DataType::TextArray,
12705                Value::IntArray(_) => DataType::IntArray,
12706                Value::BigIntArray(_) => DataType::BigIntArray,
12707                Value::SmallIntArray(_) => DataType::SmallIntArray,
12708                Value::FloatArray(_) => DataType::FloatArray,
12709                Value::BoolArray(_) => DataType::BoolArray,
12710                // v7.39 (GUC knife 2) — an interval projection describes
12711                // as INTERVAL (typed drivers read the RowDescription OID).
12712                Value::Interval { .. } => DataType::Interval,
12713                _ => DataType::Text,
12714            };
12715            all_null = false;
12716            inferred = Some(match inferred {
12717                None => ty,
12718                Some(prev) if prev == ty => prev,
12719                Some(_) => DataType::Text,
12720            });
12721        }
12722        if let Some(t) = inferred {
12723            col.ty = t;
12724            col.nullable = true;
12725        } else if all_null {
12726            col.nullable = true;
12727        }
12728    }
12729    out
12730}
12731
12732/// Numeric widening rank for UNION type resolution (higher = wider).
12733fn numeric_rank(t: DataType) -> Option<u8> {
12734    match t {
12735        DataType::SmallInt => Some(1),
12736        DataType::Int => Some(2),
12737        DataType::BigInt => Some(3),
12738        DataType::Numeric { .. } => Some(4),
12739        DataType::Float => Some(5),
12740        _ => None,
12741    }
12742}
12743
12744/// Resolve the common result type for a UNION / VALUES column from the
12745/// set of concrete (non-NULL) branch types, following the safe subset
12746/// of PG's type resolution:
12747///   * all-numeric  → the widest numeric (int ∪ bigint → bigint, … ∪
12748///     numeric → numeric, … ∪ float → float);
12749///   * DATE ∪ TIMESTAMP → TIMESTAMP;
12750///   * exactly one concrete non-TEXT type mixed with TEXT literals →
12751///     that concrete type (the TEXT cells get parsed into it).
12752/// Returns `None` for anything ambiguous, so the caller leaves the
12753/// column untouched rather than risk a wrong or failing coercion.
12754fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
12755    // NB: types are collected from RUNTIME values, which are coarser
12756    // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
12757    // a single-concrete-type fast path must NOT overwrite the column
12758    // type — it would downgrade tstz to ts. NULL-only unification (PG:
12759    // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
12760    // row's pg_typeof) needs schema-level resolution — recorded, not
12761    // attempted here.
12762    if types.len() < 2 {
12763        return None;
12764    }
12765    if types.iter().all(|t| numeric_rank(*t).is_some()) {
12766        return types
12767            .iter()
12768            .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
12769            .copied();
12770    }
12771    let non_text: Vec<&DataType> = types
12772        .iter()
12773        .filter(|t| !matches!(t, DataType::Text))
12774        .collect();
12775    // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
12776    // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
12777    // if any is timestamp the result is timestamp (ts ∪ date). All values are
12778    // the same UTC-micros instant, so widening date/ts to tstz is lossless.
12779    if non_text.iter().all(|t| {
12780        matches!(
12781            t,
12782            DataType::Date | DataType::Timestamp | DataType::Timestamptz
12783        )
12784    }) && non_text
12785        .iter()
12786        .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
12787    {
12788        if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
12789            return Some(DataType::Timestamptz);
12790        }
12791        return Some(DataType::Timestamp);
12792    }
12793    // A single concrete non-TEXT type mixed with TEXT literals.
12794    if non_text.len() == 1 {
12795        return Some(*non_text[0]);
12796    }
12797    // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
12798    // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
12799    // text): resolve the concrete set first (PG treats the unknown-
12800    // typed string literals as castable to whatever the knowns
12801    // resolve to), then the TEXT cells parse into that target — the
12802    // caller's coercion dry-run still abandons the column if any
12803    // literal doesn't parse.
12804    if !non_text.is_empty() && non_text.len() < types.len() {
12805        let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
12806        return resolve_union_common_type(&concrete);
12807    }
12808    None
12809}
12810
12811/// Coerce every cell of a UNION / VALUES result column to one common
12812/// type (see [`resolve_union_common_type`]). Conservative: a column
12813/// whose branches already agree, or whose types don't resolve, or where
12814/// any cell fails to coerce, is left exactly as it was — this never
12815/// turns a previously-working query into an error.
12816fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
12817    for col_idx in 0..columns.len() {
12818        let mut seen: Vec<DataType> = Vec::new();
12819        for row in rows.iter() {
12820            if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
12821                if !seen.contains(&dt) {
12822                    seen.push(dt);
12823                }
12824            }
12825        }
12826        // v7.37.16 — a single concrete runtime type under a TEXT-typed
12827        // column means the column type came off a NULL (or unknown-text)
12828        // branch: NULL literals describe as TEXT (`L::Null → Text`), so
12829        // `VALUES (NULL),(1.5)` left the column "text" while every
12830        // non-NULL cell is numeric. Adopt the concrete type — schema
12831        // only, no cell changes. tstz-safe by construction: a real
12832        // timestamptz column's schema type is Timestamptz, not Text, so
12833        // the coarser runtime type (Value::Timestamp) can't downgrade it
12834        // through this arm; and a real text column's non-NULL cells are
12835        // Text, which keeps seen == [Text] and skips it.
12836        if seen.len() == 1
12837            && matches!(columns[col_idx].ty, DataType::Text)
12838            && !matches!(seen[0], DataType::Text)
12839        {
12840            columns[col_idx].ty = seen[0];
12841            continue;
12842        }
12843        let Some(target) = resolve_union_common_type(&seen) else {
12844            continue;
12845        };
12846        // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
12847        // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
12848        // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
12849        // existing numeric cell untouched and only promote integers (to scale 0)
12850        // rather than rescaling everything to the widest scale.
12851        let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
12852        // Dry-run the coercion; abandon the whole column if any fails.
12853        let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
12854        let mut ok = true;
12855        for row in rows.iter() {
12856            match row.values.get(col_idx) {
12857                Some(Value::Numeric { .. }) if scale_preserving_numeric => {
12858                    coerced.push(Some(row.values[col_idx].clone()));
12859                }
12860                Some(v) => {
12861                    let cell_target = if scale_preserving_numeric {
12862                        DataType::Numeric {
12863                            precision: 0,
12864                            scale: 0,
12865                        }
12866                    } else {
12867                        target
12868                    };
12869                    match crate::conversions::coerce_value(
12870                        v.clone(),
12871                        cell_target,
12872                        &columns[col_idx].name,
12873                        col_idx,
12874                    ) {
12875                        Ok(cv) => coerced.push(Some(cv)),
12876                        Err(_) => {
12877                            ok = false;
12878                            break;
12879                        }
12880                    }
12881                }
12882                None => coerced.push(None),
12883            }
12884        }
12885        if !ok {
12886            continue;
12887        }
12888        for (row, cv) in rows.iter_mut().zip(coerced) {
12889            if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
12890                *slot = nv;
12891            }
12892        }
12893        columns[col_idx].ty = target;
12894    }
12895}
12896
12897/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
12898/// dedup inside the recursive iteration. Crude but deterministic
12899/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
12900fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
12901    let mut out = Vec::new();
12902    for v in &row.values {
12903        // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
12904        // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
12905        // like PG (and like GROUP BY, which already normalizes). The old
12906        // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
12907        // the exact-decimal family through one scale-stripped canonical form.
12908        match v {
12909            Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12910            Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12911            Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12912            Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
12913            other => {
12914                let s = alloc::format!("{other:?}|");
12915                out.extend_from_slice(s.as_bytes());
12916            }
12917        }
12918    }
12919    out
12920}
12921
12922/// Append a scale-independent canonical key for an exact-decimal value: strip
12923/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
12924/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
12925fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
12926    while scale > 0 && scaled % 10 == 0 {
12927        scaled /= 10;
12928        scale -= 1;
12929    }
12930    let s = alloc::format!("\u{1}{scaled}e-{scale}|");
12931    out.extend_from_slice(s.as_bytes());
12932}
12933
12934/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
12935/// (uncorrelated; outer refs were substituted upstream), then zip
12936/// them in parallel, NULL-padding shorter arrays to the longest
12937/// (PG's ROWS FROM shorthand). Shared by the primary-position
12938/// executor and the join-position materialiser, which both detect
12939/// the parser's `__unnest_zip` marker call.
12940pub(crate) fn unnest_zip_rows(
12941    args: &[Expr],
12942) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
12943    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12944    let ctx = EvalContext::new(&empty_schema, None);
12945    let dummy_row = Row::new(alloc::vec::Vec::new());
12946    let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
12947    let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
12948        alloc::vec::Vec::with_capacity(args.len());
12949    for a in args {
12950        let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
12951        // v7.39.13 — the element menu the rest of the workspace already
12952        // has, not a third copy of a shortened one.
12953        //
12954        // This arm listed Text, Int and BigInt and refused everything
12955        // else, so `unnest(uuid[], text[])` raised while
12956        // `unnest(uuid[])` — a different path — did not. A shipped
12957        // endpoint of a customer's returned 500 on every call because
12958        // of it. `array_elements` and `array_element_type` are the two
12959        // halves of the menu that `array_element_at`'s own comment
12960        // describes: "previously only matched Text/Int/BigInt arrays
12961        // and errored on every other element type". Same sentence,
12962        // third arm.
12963        let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = if matches!(v, Value::Null) {
12964            (DataType::Text, alloc::vec::Vec::new())
12965        } else if let Some(items) = crate::eval::values::array_elements(&v) {
12966            let dt = v
12967                .data_type()
12968                .and_then(crate::describe::array_element_type)
12969                .unwrap_or(DataType::Text);
12970            (dt, items)
12971        } else {
12972            return Err(EngineError::Unsupported(alloc::format!(
12973                "unnest() expects array arguments, got {}",
12974                crate::conversions::pg_type_name_for_error_opt(v.data_type())
12975            )));
12976        };
12977        dtypes.push(dt);
12978        columns.push(items);
12979    }
12980    let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
12981    let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
12982    for i in 0..max_len {
12983        let vals: alloc::vec::Vec<Value<'static>> = columns
12984            .iter()
12985            .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
12986            .collect();
12987        rows.push(Row::new(vals));
12988    }
12989    Ok((dtypes, rows))
12990}
12991
12992/// Detect the parser's multi-arg unnest marker on an unnest_expr.
12993pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
12994    match expr {
12995        Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
12996        _ => None,
12997    }
12998}
12999
13000/// Evaluate generate_series arguments (uncorrelated — outer refs
13001/// were substituted upstream where applicable) and build the row
13002/// stream. Dispatches on the start value's shape and rejects
13003/// mixed-shape calls early (e.g. start = timestamp, stop =
13004/// integer) so the caller gets a clean error rather than a panic.
13005/// Shared by the primary-position executor and the join-position
13006/// materialiser.
13007pub(crate) fn generate_series_rows(
13008    args: &[Expr],
13009    cancel: &CancelToken<'_>,
13010) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
13011    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13012    let ctx = EvalContext::new(&empty_schema, None);
13013    let dummy_row = Row::new(alloc::vec::Vec::new());
13014    let mut arg_values: alloc::vec::Vec<Value<'static>> =
13015        alloc::vec::Vec::with_capacity(args.len());
13016    for a in args {
13017        arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
13018    }
13019    generate_series_from_values(arg_values, args, cancel)
13020}
13021
13022/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
13023/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
13024/// full integer / numeric / timestamp overload set with the FROM-clause path.
13025/// Before this split the target-list arm reimplemented only the integer case,
13026/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
13027/// NULL for the timestamp column instead of the series. `arg_values` are the
13028/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
13029/// timestamp type resolution (it inspects the argument expressions' types).
13030pub(crate) fn generate_series_from_values(
13031    mut arg_values: alloc::vec::Vec<Value<'static>>,
13032    args: &[Expr],
13033    cancel: &CancelToken<'_>,
13034) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
13035    // PG: a NULL bound or step yields zero rows (also keeps the
13036    // NULL-padded lateral probe alive — schema without data).
13037    if arg_values.iter().any(|v| matches!(v, Value::Null)) {
13038        return Ok((DataType::BigInt, alloc::vec::Vec::new()));
13039    }
13040    // PG resolves `generate_series(date, date, interval)` to the
13041    // timestamp/timestamptz overload by implicitly casting each date
13042    // bound up to a timestamp at midnight (verified vs live PG18.4:
13043    // date args yield rows anchored at 00:00:00). SPG's TZ-naive
13044    // timestamp model renders the same instants, so fold any Date
13045    // bound to its midnight Timestamp (canonical `days *
13046    // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
13047    // the shape match so the existing timestamp arm drives the walk.
13048    // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
13049    // `generate_series(date, date, interval)` has no date overload, and among
13050    // the two candidates PG prefers the timestamptz one (timestamptz is the
13051    // preferred type of the datetime category), so the column comes back
13052    // `timestamp with time zone` — the rows render with a `+00` offset. A
13053    // timestamptz bound obviously lands there too. Only genuinely
13054    // timestamp-typed bounds keep the TZ-naive result type.
13055    let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13056    let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
13057        || args.iter().any(|a| {
13058            crate::describe::describe_expr(a, &empty_cols)
13059                .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
13060        });
13061    for v in &mut arg_values {
13062        if let Value::Date(d) = *v {
13063            *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
13064        }
13065    }
13066    match arg_values.as_slice() {
13067        [Value::Timestamp(start), Value::Timestamp(stop), step] => {
13068            let interval_step = match step {
13069                Value::Interval { .. } => step.clone(),
13070                // v7.38 (read01) — PG resolves an unknown-type string step
13071                // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
13072                // a bare text step by parsing it the same way `::interval` does.
13073                Value::Text(s) => crate::conversions::coerce_value(
13074                    Value::text(s.as_ref()),
13075                    DataType::Interval,
13076                    "",
13077                    0,
13078                )
13079                .map_err(|_| {
13080                    EngineError::Unsupported(alloc::format!(
13081                        "generate_series(timestamp, timestamp, …): \
13082                         could not parse step {s:?} as INTERVAL"
13083                    ))
13084                })?,
13085                other => {
13086                    return Err(EngineError::Unsupported(alloc::format!(
13087                        "generate_series(timestamp, timestamp, …): \
13088                         step must be INTERVAL, got {}",
13089                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
13090                    )));
13091                }
13092            };
13093            let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
13094            Ok((
13095                if tz {
13096                    DataType::Timestamptz
13097                } else {
13098                    DataType::Timestamp
13099                },
13100                rows,
13101            ))
13102        }
13103        [start, stop, step]
13104            if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
13105        {
13106            let s = value_to_i64(start);
13107            let e = value_to_i64(stop);
13108            let st = value_to_i64(step);
13109            // PG types the series by the argument type: int4 args → int4
13110            // elements, int8 (bigint) args → int8. Any BigInt operand widens.
13111            let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
13112            let rows = generate_series_integers(s, e, st, wide, cancel)?;
13113            Ok((
13114                if wide {
13115                    DataType::BigInt
13116                } else {
13117                    DataType::Int
13118                },
13119                rows,
13120            ))
13121        }
13122        [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
13123            let s = value_to_i64(start);
13124            let e = value_to_i64(stop);
13125            let wide = value_is_bigint(start) || value_is_bigint(stop);
13126            let rows = generate_series_integers(s, e, 1, wide, cancel)?;
13127            Ok((
13128                if wide {
13129                    DataType::BigInt
13130                } else {
13131                    DataType::Int
13132                },
13133                rows,
13134            ))
13135        }
13136        // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
13137        // series in exact numeric arithmetic; NaN / infinity bounds and a
13138        // zero step get dedicated wordings, and a mixed int/numeric call
13139        // resolves here via the implicit int→numeric cast.
13140        [_, _] | [_, _, _]
13141            if arg_values
13142                .iter()
13143                .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
13144                && arg_values.iter().all(|v| {
13145                    matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
13146                }) =>
13147        {
13148            use spg_storage::NumericKind as K;
13149            let words: [(&str, &str); 3] = [
13150                (
13151                    "start value cannot be NaN",
13152                    "start value cannot be infinity",
13153                ),
13154                ("stop value cannot be NaN", "stop value cannot be infinity"),
13155                ("step size cannot be NaN", "step size cannot be infinity"),
13156            ];
13157            for (i, v) in arg_values.iter().enumerate() {
13158                if let Value::Numeric { kind, .. } = v {
13159                    if *kind != K::Finite {
13160                        let (nan_w, inf_w) = words[i];
13161                        return Err(EngineError::Unsupported(
13162                            if *kind == K::NaN { nan_w } else { inf_w }.into(),
13163                        ));
13164                    }
13165                }
13166            }
13167            let big =
13168                |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
13169            let start = big(&arg_values[0]);
13170            let stop = big(&arg_values[1]);
13171            let step = if arg_values.len() == 3 {
13172                big(&arg_values[2])
13173            } else {
13174                spg_storage::bignum::BigNumeric::from_i128(1, 0)
13175            };
13176            if step.is_zero() {
13177                return Err(EngineError::Unsupported(
13178                    "step size cannot equal zero".into(),
13179                ));
13180            }
13181            let descending = step.parts().0;
13182            let mut rows = alloc::vec::Vec::new();
13183            let mut cur = start;
13184            const MAX_ROWS: usize = 10_000_000;
13185            loop {
13186                cancel.check()?;
13187                let c = cur.cmp(&stop);
13188                if descending {
13189                    if c == core::cmp::Ordering::Less {
13190                        break;
13191                    }
13192                } else if c == core::cmp::Ordering::Greater {
13193                    break;
13194                }
13195                if rows.len() >= MAX_ROWS {
13196                    return Err(EngineError::Unsupported(alloc::format!(
13197                        "generate_series() result exceeds {MAX_ROWS} rows"
13198                    )));
13199                }
13200                rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
13201                    cur.clone()
13202                )]));
13203                cur = cur.add(&step);
13204            }
13205            Ok((
13206                DataType::Numeric {
13207                    precision: 0,
13208                    scale: 0,
13209                },
13210                rows,
13211            ))
13212        }
13213        _ => Err(EngineError::Unsupported(alloc::format!(
13214            "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
13215             argument shapes; got {}",
13216            arg_values
13217                .iter()
13218                .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
13219                .collect::<alloc::vec::Vec<_>>()
13220                .join(", ")
13221        ))),
13222    }
13223}
13224
13225/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
13226/// Step direction follows the sign: positive step iterates upward
13227/// (stops when current > stop); negative iterates downward; zero
13228/// errors. Caller-facing row stream is `BigInt`-typed so a single
13229/// projection schema covers SmallInt / Int / BigInt callers.
13230fn generate_series_integers(
13231    start: i64,
13232    stop: i64,
13233    step: i64,
13234    wide: bool,
13235    cancel: &CancelToken<'_>,
13236) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13237    if step == 0 {
13238        return Err(EngineError::Unsupported(
13239            "step size cannot equal zero".into(),
13240        ));
13241    }
13242    let mut out = alloc::vec::Vec::new();
13243    let mut cur = start;
13244    // Hard cap to keep a runaway call from eating all memory. PG
13245    // has no such cap but does honour query timeout; SPG's cancel
13246    // token will fire too — this is a defense-in-depth backstop.
13247    const MAX_ROWS: usize = 10_000_000;
13248    loop {
13249        cancel.check()?;
13250        if step > 0 && cur > stop {
13251            break;
13252        }
13253        if step < 0 && cur < stop {
13254            break;
13255        }
13256        out.push(Row::new(alloc::vec![if wide {
13257            Value::BigInt(cur)
13258        } else {
13259            Value::Int(cur as i32)
13260        }]));
13261        if out.len() > MAX_ROWS {
13262            return Err(EngineError::Unsupported(alloc::format!(
13263                "generate_series(): exceeded {MAX_ROWS} rows; \
13264                 narrow start/stop or use a larger step"
13265            )));
13266        }
13267        cur = match cur.checked_add(step) {
13268            Some(n) => n,
13269            None => break,
13270        };
13271    }
13272    Ok(out)
13273}
13274
13275/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
13276/// `Value::Interval { months, micros }` per the caller's guard;
13277/// each iteration adds the interval via `apply_binary_interval`
13278/// so month-shifting handles short-month rollover (PG semantics).
13279fn generate_series_timestamps(
13280    start: i64,
13281    stop: i64,
13282    step: Value,
13283    cancel: &CancelToken<'_>,
13284) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13285    let (months, days, micros) = match &step {
13286        Value::Interval {
13287            months,
13288            days,
13289            micros,
13290            kind,
13291        } => (*months, *days, *micros),
13292        _ => unreachable!("caller guards step.is_interval"),
13293    };
13294    if months == 0 && days == 0 && micros == 0 {
13295        return Err(EngineError::Unsupported(
13296            "generate_series(): INTERVAL step cannot be zero".into(),
13297        ));
13298    }
13299    let ascending = months > 0 || days > 0 || micros > 0;
13300    let mut out = alloc::vec::Vec::new();
13301    let mut cur = Value::Timestamp(start);
13302    const MAX_ROWS: usize = 10_000_000;
13303    loop {
13304        cancel.check()?;
13305        let cur_t = match cur {
13306            Value::Timestamp(t) => t,
13307            _ => unreachable!("loop invariant: cur is Timestamp"),
13308        };
13309        if ascending && cur_t > stop {
13310            break;
13311        }
13312        if !ascending && cur_t < stop {
13313            break;
13314        }
13315        out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
13316        if out.len() > MAX_ROWS {
13317            return Err(EngineError::Unsupported(alloc::format!(
13318                "generate_series(): exceeded {MAX_ROWS} rows; \
13319                 narrow start/stop or use a larger step"
13320            )));
13321        }
13322        let next = eval::apply_binary_interval(
13323            spg_sql::ast::BinOp::Add,
13324            &cur,
13325            &Value::Interval {
13326                months,
13327                days,
13328                micros,
13329                kind: spg_storage::IntervalKind::Finite,
13330            },
13331        )
13332        .map_err(EngineError::Eval)?;
13333        cur = match next {
13334            Some(v) => v,
13335            None => break,
13336        };
13337    }
13338    Ok(out)
13339}
13340
13341/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
13342/// WITH TIES` requires an `ORDER BY`. Without one, there's no
13343/// way to identify "ties" deterministically, so PG errors at
13344/// plan time. SPG mirrors that surface so the same DDL / app
13345/// behaviour holds on cutover.
13346fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
13347    if stmt.limit_with_ties && stmt.order_by.is_empty() {
13348        return Err(EngineError::Unsupported(alloc::string::String::from(
13349            "WITH TIES cannot be specified without ORDER BY clause",
13350        )));
13351    }
13352    Ok(())
13353}
13354
13355/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
13356/// (case-insensitive). Used by `exec_select_cancel`'s
13357/// projection loop to detect Set-Returning-Function rows that
13358/// need per-row expansion. Only the top-level call counts —
13359/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
13360/// projection's perspective; it would surface as an "unknown
13361/// function" mismatch downstream, which is what we want
13362/// (multi-SRF / nested SRF is documented carve-out for v7.19).
13363fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
13364    top_level_srf_kind(expr).is_some()
13365}
13366
13367/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
13368/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
13369/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
13370/// source row.
13371#[derive(Clone, Copy, PartialEq, Eq)]
13372pub(crate) enum SrfKind {
13373    Unnest,
13374    /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
13375    /// list. It used to be handled ONLY by the parser's lift into FROM, so a
13376    /// second one in the same list came back as "unknown function".
13377    GenerateSeries,
13378    GenerateSubscripts,
13379    /// `_text` variants unwrap scalars to their lexeme; the plain forms render
13380    /// every value as compact JSON text.
13381    ArrayElements {
13382        as_text: bool,
13383    },
13384    PathQuery,
13385    RegexpMatches,
13386    Each {
13387        as_text: bool,
13388    },
13389    ObjectKeys,
13390}
13391
13392/// Case-insensitive match against any of `names`.
13393fn name_is(name: &str, names: &[&str]) -> bool {
13394    names.iter().any(|n| name.eq_ignore_ascii_case(n))
13395}
13396
13397pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
13398    let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13399        return None;
13400    };
13401    let n = args.len();
13402    // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
13403    // SELECT list (it returned an array there before) and shares the unnest
13404    // expansion machinery.
13405    if n == 1 && name.eq_ignore_ascii_case("unnest") {
13406        return Some(SrfKind::Unnest);
13407    }
13408    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
13409        return Some(SrfKind::GenerateSeries);
13410    }
13411    if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
13412        return Some(SrfKind::GenerateSubscripts);
13413    }
13414    // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
13415    // per element / match in the SELECT list; they collapsed to a single row
13416    // (a TextArray, or an "unknown function" error for `each`) before.
13417    if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
13418        return Some(SrfKind::ArrayElements { as_text: false });
13419    }
13420    if n == 1
13421        && name_is(
13422            name,
13423            &["jsonb_array_elements_text", "json_array_elements_text"],
13424        )
13425    {
13426        return Some(SrfKind::ArrayElements { as_text: true });
13427    }
13428    // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
13429    if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
13430        return Some(SrfKind::PathQuery);
13431    }
13432    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
13433        return Some(SrfKind::RegexpMatches);
13434    }
13435    if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
13436        return Some(SrfKind::Each { as_text: false });
13437    }
13438    if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
13439        return Some(SrfKind::Each { as_text: true });
13440    }
13441    if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
13442        return Some(SrfKind::ObjectKeys);
13443    }
13444    None
13445}
13446
13447/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
13448/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
13449/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
13450/// rows, as in PG).
13451pub(crate) fn top_level_srf_output(
13452    expr: &spg_sql::ast::Expr,
13453    row: &Row<'static>,
13454    ctx: &EvalContext<'_>,
13455) -> Result<Vec<Value<'static>>, EngineError> {
13456    let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
13457        (top_level_srf_kind(expr), expr)
13458    else {
13459        return Err(EngineError::Unsupported(
13460            "expected a SELECT-list SRF call".into(),
13461        ));
13462    };
13463    match kind {
13464        SrfKind::Unnest => {
13465            // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
13466            // the elements DIRECTLY: the old path built the whole
13467            // Value::Array (one eval + a clone per element) only for
13468            // array_value_to_elements to clone every element back out.
13469            // Any other argument shape (a column, a function result)
13470            // keeps the build-then-split path.
13471            if let spg_sql::ast::Expr::Array(items) = &args[0] {
13472                return items
13473                    .iter()
13474                    .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
13475                    .collect();
13476            }
13477            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13478            array_value_to_elements(&arr)
13479        }
13480        SrfKind::GenerateSeries => {
13481            // v7.39 (read01 round 96) — evaluate the args against the actual
13482            // row, then hand off to the shared core so the numeric and
13483            // timestamp/timestamptz overloads work here too (this arm used to
13484            // handle only integers, silently NULLing a temporal/numeric series
13485            // when it shared a target list with another SRF).
13486            let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
13487            for a in args {
13488                arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13489            }
13490            let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
13491            Ok(rows
13492                .into_iter()
13493                .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
13494                .collect())
13495        }
13496        SrfKind::GenerateSubscripts => {
13497            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13498            let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13499            if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
13500                return Ok(Vec::new());
13501            }
13502            let len = array_value_to_elements(&arr)?.len();
13503            Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
13504        }
13505        // One Value per array element (`_text` → text / SQL NULL, plain → the
13506        // element's compact JSON text) — the element list the FROM-clause form
13507        // materialises.
13508        SrfKind::ArrayElements { as_text } => {
13509            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13510            if matches!(arg, Value::Null) {
13511                return Ok(Vec::new());
13512            }
13513            let items =
13514                crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13515            Ok(items
13516                .into_iter()
13517                .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13518                .collect())
13519        }
13520        // The scalar form already yields a TextArray of the keys (or errors on
13521        // a non-object, like PG); expand it into rows.
13522        SrfKind::ObjectKeys => {
13523            let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
13524            array_value_to_elements(&v)
13525        }
13526        // One row per match, each a text[] of the pattern's capture groups.
13527        SrfKind::RegexpMatches => {
13528            let vals: Vec<Value<'static>> = args
13529                .iter()
13530                .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
13531                .collect::<Result<_, _>>()?;
13532            crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
13533        }
13534        // One composite `(key, value)` row per object member (plain → jsonb
13535        // value, `_text` → text / SQL NULL).
13536        SrfKind::Each { as_text } => {
13537            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13538            if matches!(arg, Value::Null) {
13539                return Ok(Vec::new());
13540            }
13541            let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13542            Ok(pairs
13543                .into_iter()
13544                .map(|(k, v)| {
13545                    let val = if as_text {
13546                        v.map(Value::text).unwrap_or(Value::Null)
13547                    } else {
13548                        v.map(Value::json).unwrap_or(Value::Null)
13549                    };
13550                    Value::Composite(alloc::vec![
13551                        ("key".to_string(), Value::text(k)),
13552                        ("value".to_string(), val),
13553                    ])
13554                })
13555                .collect())
13556        }
13557        // One Value per matched JSON value.
13558        SrfKind::PathQuery => {
13559            let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13560            let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13561            // v7.39 — optional vars document (3rd arg).
13562            let vars = match args.get(2) {
13563                Some(a) => {
13564                    let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
13565                    crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
13566                }
13567                None => None,
13568            };
13569            match crate::json::path_query_vars(&doc, &path, vars.as_ref())
13570                .map_err(EngineError::Eval)?
13571            {
13572                Value::Null => Ok(Vec::new()),
13573                Value::TextArray(items) => Ok(items
13574                    .into_iter()
13575                    .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13576                    .collect()),
13577                other => Ok(alloc::vec![other]),
13578            }
13579        }
13580    }
13581}
13582
13583/// v7.19 P5 — turn an array-typed `Value` into the element list
13584/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
13585/// = (no rows)`). Non-array values fall through to a type-mismatch
13586/// error.
13587pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
13588    // v7.39 (round 236) — PG unnests a multidimensional array into its
13589    // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
13590    // rows). SPG stores 2-D arrays as their own variants, which fell
13591    // through to the type-mismatch arm below.
13592    if let Some(flat) = crate::eval::values::flatten_2d(v) {
13593        return array_value_to_elements(&flat);
13594    }
13595    // v7.39.11 — every array-family value, through the one element
13596    // menu. The arms below name int / bigint / text / json and stop, so
13597    // `SELECT unnest(ARRAY[1,2]::smallint[])` raised "expects an array
13598    // argument, got smallint[]" — the type it had just been given —
13599    // and so did every catalog vector. Found while closing sentori's
13600    // §4 against 7.39.10; the FROM-clause unnest has the same arm.
13601    if crate::eval::values::array_len(v).is_some() {
13602        if let Some(elems) = crate::eval::values::array_elements(v) {
13603            return Ok(elems);
13604        }
13605    }
13606    match v {
13607        Value::Null => Ok(Vec::new()),
13608        Value::TextArray(items) => Ok(items
13609            .iter()
13610            .map(|opt| {
13611                opt.as_ref()
13612                    .map(|s| Value::text(s.clone()))
13613                    .unwrap_or(Value::Null)
13614            })
13615            .collect()),
13616        Value::IntArray(items) => Ok(items
13617            .iter()
13618            .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
13619            .collect()),
13620        Value::BigIntArray(items) => Ok(items
13621            .iter()
13622            .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
13623            .collect()),
13624        // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
13625        // range per canonical span.
13626        Value::Multirange { kind, ranges } => Ok(ranges
13627            .iter()
13628            .map(|s| Value::Range {
13629                kind: *kind,
13630                lower: s.lower.clone(),
13631                upper: s.upper.clone(),
13632                lower_inc: s.lower_inc,
13633                upper_inc: s.upper_inc,
13634                empty: false,
13635            })
13636            .collect()),
13637        other => Err(EngineError::Eval(EvalError::TypeMismatch {
13638            detail: alloc::format!(
13639                "unnest() expects an array argument, got {}",
13640                crate::conversions::pg_type_name_for_error_opt(other.data_type())
13641            ),
13642        })),
13643    }
13644}
13645
13646impl Engine {
13647    /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
13648    /// the SELECT's FROM / JOIN graph, re-parse each view's body
13649    /// source, and prepend it as a synthetic CTE on the
13650    /// returned SelectStatement. Returns `None` when no view
13651    /// references are found (caller proceeds with the original
13652    /// statement); returns `Some(rewritten)` otherwise (caller
13653    /// re-runs exec_select_cancel on the rewritten form so the
13654    /// regular CTE materialiser handles it).
13655    fn expand_views_in_select(
13656        &self,
13657        stmt: &SelectStatement,
13658    ) -> Result<Option<SelectStatement>, EngineError> {
13659        let cat = self.active_catalog();
13660        let mut referenced: Vec<String> = Vec::new();
13661        if let Some(from) = &stmt.from {
13662            collect_view_refs(&from.primary, cat, &mut referenced);
13663            for j in &from.joins {
13664                collect_view_refs(&j.table, cat, &mut referenced);
13665            }
13666        }
13667        // Don't expand a view name that's already shadowed by a
13668        // CTE on the same SELECT — the CTE wins per PG.
13669        referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
13670        if referenced.is_empty() {
13671            return Ok(None);
13672        }
13673        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
13674        for name in &referenced {
13675            let view = cat.view(name).ok_or_else(|| {
13676                EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13677                    "view {name:?} disappeared mid-expansion"
13678                )))
13679            })?;
13680            let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
13681                EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
13682            })?;
13683            let Statement::Select(body) = parsed else {
13684                return Err(EngineError::Unsupported(alloc::format!(
13685                    "view {name:?} body is not a SELECT (catalog corruption)"
13686                )));
13687            };
13688            new_ctes.push(spg_sql::ast::Cte {
13689                name: name.clone(),
13690                body: spg_sql::ast::CteBody::Select(body),
13691                recursive: false,
13692                column_overrides: view.columns.clone(),
13693                search: None,
13694                cycle: None,
13695            });
13696        }
13697        let mut out = stmt.clone();
13698        // Prepend so view CTEs are visible to caller-supplied CTEs.
13699        new_ctes.extend(out.ctes);
13700        out.ctes = new_ctes;
13701        Ok(Some(out))
13702    }
13703
13704    /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
13705    /// any partition-parent table, rewrite the SELECT so each parent
13706    /// reference resolves to a CTE whose body is a `UNION ALL` over the
13707    /// children that pass the WHERE-derived partition-key range. Returns
13708    /// `None`(no rewrite needed)when no parent is referenced or all
13709    /// references are shadowed by a same-name CTE.
13710    ///
13711    /// Pruning vocabulary at v7.37.6-B:
13712    ///   * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
13713    ///     and `<key> BETWEEN literal AND literal`.
13714    ///   * Anything outside that(OR / nested IN / function call on the
13715    ///     key)defaults to "no pruning" — every child + DEFAULT lands
13716    ///     in the UNION. Correctness is preserved; only the plan size
13717    ///     widens.
13718    fn expand_partition_parents_in_select(
13719        &self,
13720        stmt: &SelectStatement,
13721    ) -> Result<Option<SelectStatement>, EngineError> {
13722        let cat = self.active_catalog();
13723        let Some(from) = &stmt.from else {
13724            return Ok(None);
13725        };
13726        let mut parent_refs: Vec<String> = Vec::new();
13727        collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
13728        for j in &from.joins {
13729            collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
13730        }
13731        // Drop names shadowed by a CTE on the same SELECT(PG semantics
13732        // — same as view expansion above).
13733        parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
13734        if parent_refs.is_empty() {
13735            return Ok(None);
13736        }
13737        // Synthesise a CTE name per parent so the existing
13738        // "CTE shadows a real table" guard doesn't fire (the parent
13739        // IS a real table in the catalog, unlike VIEW expansion's
13740        // case). The FROM-clause TableRef walker below rewrites
13741        // every parent reference to point at the synthetic CTE.
13742        let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
13743        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
13744        let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
13745        for parent_name in &parent_refs {
13746            // No children = no rewrite. The parent itself is a real
13747            // (empty-rows) table — the regular FROM-resolution path
13748            // will scan it and return 0 rows, matching the
13749            // "partition parent with no children" plan. Skipping the
13750            // CTE here also avoids `SELECT * FROM parent` re-entering
13751            // this rewrite on the synthetic body (infinite recursion).
13752            let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
13753                continue;
13754            };
13755            new_ctes.push(spg_sql::ast::Cte {
13756                name: synth_name(parent_name),
13757                body: spg_sql::ast::CteBody::Select(body),
13758                recursive: false,
13759                column_overrides: Vec::new(),
13760                search: None,
13761                cycle: None,
13762            });
13763            expanded_parents.push(parent_name.clone());
13764        }
13765        if expanded_parents.is_empty() {
13766            return Ok(None);
13767        }
13768        let mut out = stmt.clone();
13769        if let Some(from) = out.from.as_mut() {
13770            rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
13771            for j in &mut from.joins {
13772                rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
13773            }
13774        }
13775        new_ctes.extend(out.ctes);
13776        out.ctes = new_ctes;
13777        Ok(Some(out))
13778    }
13779
13780    /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
13781    /// Children include every overlap-hit `Range` plus(always)the
13782    /// `Default` child(if any). Returns `Ok(None)` when no children
13783    /// would survive — caller skips the CTE injection and lets the
13784    /// parent fall through to the regular(empty-rows)scan path,
13785    /// avoiding the infinite recursion that an empty-body CTE
13786    /// referencing the parent name would trigger.
13787    /// v7.37.16 (16.10) — public helper invoked from explain.rs to
13788    /// surface "which children survive the WHERE-clause prune" in
13789    /// EXPLAIN output. Returns `None` when `parent_name` isn't
13790    /// actually a partition parent; otherwise returns the list of
13791    /// children the planner would scan (same algorithm as
13792    /// [`Self::build_partition_parent_union_body`] but without the
13793    /// SQL re-parse).
13794    /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
13795    /// expression (the PG-shaped EXPLAIN's scan builder has no full
13796    /// SelectStatement in hand). Wraps the original by synthesising a
13797    /// minimal statement carrying just the predicate.
13798    pub(crate) fn explain_partition_kept_children_by_where(
13799        &self,
13800        parent_name: &str,
13801        where_: Option<&spg_sql::ast::Expr>,
13802    ) -> Option<Vec<alloc::string::String>> {
13803        let mut synth = SelectStatement::default();
13804        synth.where_ = where_.cloned();
13805        self.explain_partition_kept_children(parent_name, &synth)
13806    }
13807
13808    pub(crate) fn explain_partition_kept_children(
13809        &self,
13810        parent_name: &str,
13811        outer: &SelectStatement,
13812    ) -> Option<Vec<alloc::string::String>> {
13813        use spg_storage::PartitionRole;
13814        let cat = self.active_catalog();
13815        let parent = cat.get(parent_name)?;
13816        let (key_position, parent_kind) = match &parent.schema().partition_role {
13817            Some(PartitionRole::Parent {
13818                key_column_positions,
13819                kind,
13820                ..
13821            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13822            _ => return None,
13823        };
13824        let key_col_name = parent.schema().columns[key_position].name.clone();
13825        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13826            Some(expr) => extract_key_range(expr, &key_col_name),
13827            None => (None, None),
13828        };
13829        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13830            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13831            None => None,
13832        };
13833        let children = crate::partition::children_of_parent(cat, parent_name);
13834        let mut kept: Vec<alloc::string::String> = Vec::new();
13835        let mut default_child: Option<alloc::string::String> = None;
13836        for child_name in &children {
13837            let Some(child) = cat.get(child_name) else {
13838                continue;
13839            };
13840            match &child.schema().partition_role {
13841                Some(PartitionRole::Range { lower, upper, .. }) => {
13842                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13843                        kept.push(child_name.clone());
13844                    }
13845                }
13846                Some(PartitionRole::List { values, .. }) => match &eq_value {
13847                    Some(v) => {
13848                        if values.iter().any(|b| b.equals_value(v)) {
13849                            kept.push(child_name.clone());
13850                        }
13851                    }
13852                    None => kept.push(child_name.clone()),
13853                },
13854                Some(PartitionRole::Hash {
13855                    modulus, remainder, ..
13856                }) => match &eq_value {
13857                    Some(v) => {
13858                        let h = crate::partition::pg_compatible_hash(v);
13859                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13860                            kept.push(child_name.clone());
13861                        }
13862                    }
13863                    None => kept.push(child_name.clone()),
13864                },
13865                Some(PartitionRole::Default { .. }) => {
13866                    default_child = Some(child_name.clone());
13867                }
13868                _ => {}
13869            }
13870        }
13871        let _ = parent_kind;
13872        if let Some(d) = default_child {
13873            if kept.is_empty() || eq_value.is_none() {
13874                kept.push(d);
13875            }
13876        }
13877        Some(kept)
13878    }
13879
13880    fn build_partition_parent_union_body(
13881        &self,
13882        parent_name: &str,
13883        outer: &SelectStatement,
13884    ) -> Result<Option<SelectStatement>, EngineError> {
13885        use spg_storage::PartitionRole;
13886        let cat = self.active_catalog();
13887        let parent = cat.get(parent_name).ok_or_else(|| {
13888            EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13889                "partition parent {parent_name:?} disappeared mid-expansion"
13890            )))
13891        })?;
13892        let (key_position, parent_kind) = match &parent.schema().partition_role {
13893            Some(PartitionRole::Parent {
13894                key_column_positions,
13895                kind,
13896                ..
13897            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13898            // v7.39 (round 645) — an INHERITANCE parent, which has no
13899            // role of its own: the relationship is recorded only in the
13900            // children. Three things differ from a partition parent and
13901            // all three are in this body.
13902            //
13903            //   * The parent HOLDS ROWS, so it is a term of the union —
13904            //     `FROM ONLY`, or expanding it would recurse.
13905            //   * There is no partition key, so there is nothing to
13906            //     prune: every child is a term.
13907            //   * A child may declare columns of its own, so the terms
13908            //     name the PARENT's columns rather than `*`. PG's
13909            //     `SELECT * FROM parent` returns the parent's shape.
13910            //
13911            // Answered from this match rather than a branch before it —
13912            // round 644 measured what an extra early return beside an
13913            // existing test costs in this file.
13914            _ if crate::partition::has_inheritance_children(cat, parent_name) => {
13915                let cols = parent
13916                    .schema()
13917                    .columns
13918                    .iter()
13919                    .map(|c| quote_ident_for_sql(&c.name))
13920                    .collect::<Vec<_>>()
13921                    .join(", ");
13922                let carry_sys = references_ctid(outer);
13923                let sys = if carry_sys {
13924                    let mut t = alloc::string::String::new();
13925                    for s in SYSTEM_COLUMNS {
13926                        t.push_str(", ");
13927                        t.push_str(s);
13928                    }
13929                    t
13930                } else {
13931                    alloc::string::String::new()
13932                };
13933                let mut body = alloc::format!(
13934                    "SELECT {cols}{sys} FROM ONLY {}",
13935                    quote_ident_for_sql(parent_name)
13936                );
13937                for child in crate::partition::children_of_parent(cat, parent_name) {
13938                    body.push_str(&alloc::format!(
13939                        " UNION ALL SELECT {cols}{sys} FROM {}",
13940                        quote_ident_for_sql(&child)
13941                    ));
13942                }
13943                return parse_select_or_corrupt(&body).map(Some);
13944            }
13945            _ => {
13946                return Err(EngineError::Unsupported(alloc::format!(
13947                    "partition expansion: {parent_name:?} is not a parent"
13948                )));
13949            }
13950        };
13951        let key_col_name = parent.schema().columns[key_position].name.clone();
13952        // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
13953        // off the WHERE; for LIST / HASH we extract a single `=`
13954        // literal (and the rest of the planner falls back to "keep
13955        // every child" — same conservative path as 16.1/16.2).
13956        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13957            Some(expr) => extract_key_range(expr, &key_col_name),
13958            None => (None, None),
13959        };
13960        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13961            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13962            None => None,
13963        };
13964        let children = crate::partition::children_of_parent(cat, parent_name);
13965        let mut kept: Vec<String> = Vec::new();
13966        let mut default_child: Option<String> = None;
13967        // First pass — apply per-strategy gates, defer DEFAULT until
13968        // we know whether some non-DEFAULT child matched.
13969        for child_name in &children {
13970            let Some(child) = cat.get(child_name) else {
13971                continue;
13972            };
13973            match &child.schema().partition_role {
13974                Some(PartitionRole::Range { lower, upper, .. }) => {
13975                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13976                        kept.push(child_name.clone());
13977                    }
13978                }
13979                // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
13980                // = <lit>`, only the child whose values contain that
13981                // literal survives. Otherwise (no equality predicate
13982                // or planner couldn't extract one) keep the child
13983                // conservatively.
13984                Some(PartitionRole::List { values, .. }) => match &eq_value {
13985                    Some(v) => {
13986                        if values.iter().any(|b| b.equals_value(v)) {
13987                            kept.push(child_name.clone());
13988                        }
13989                    }
13990                    None => kept.push(child_name.clone()),
13991                },
13992                // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
13993                // we know the residue class deterministically, so
13994                // only the matching REMAINDER child survives.
13995                Some(PartitionRole::Hash {
13996                    modulus, remainder, ..
13997                }) => match &eq_value {
13998                    Some(v) => {
13999                        let h = crate::partition::pg_compatible_hash(v);
14000                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
14001                            kept.push(child_name.clone());
14002                        }
14003                    }
14004                    None => kept.push(child_name.clone()),
14005                },
14006                Some(PartitionRole::Default { .. }) => {
14007                    default_child = Some(child_name.clone());
14008                }
14009                _ => {}
14010            }
14011        }
14012        // PG-style DEFAULT semantics: the DEFAULT child must be
14013        // scanned iff some row could fall outside every concrete
14014        // child's bound predicate. We approximate that as "no
14015        // concrete child matched" (== full prune) — strictly
14016        // conservative for LIST / HASH (DEFAULT also catches rows
14017        // outside the union of value-sets / residues), and matches
14018        // PG for the equality case where we *do* know the routing
14019        // outcome.
14020        let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
14021        if let Some(d) = default_child {
14022            if kept.is_empty() {
14023                kept.push(d);
14024            } else if eq_value.is_none() {
14025                // Without an equality literal, the DEFAULT child may
14026                // still hold matching rows (e.g. LIKE on TEXT keys
14027                // for which a LIST partition exists). Keep it.
14028                kept.push(d);
14029            }
14030        }
14031        // Build the UNION ALL body text and re-parse — keeps the
14032        // rewrite expressible in surface SQL so the engine's existing
14033        // parser path handles the AST shape uniformly.
14034        if kept.is_empty() {
14035            // No children survive — caller falls back to scanning the
14036            // (empty) parent table. Returning None here is what
14037            // prevents the synthetic CTE from referring back to the
14038            // parent name and re-entering this rewrite pass.
14039            let _ = parent_name;
14040            return Ok(None);
14041        }
14042        // v7.39 (round 622, S05a) — the system columns of the CHILD the row
14043        // actually lives in.
14044        //
14045        // The parent is read through a synthetic CTE, so a `tableoid` on it
14046        // resolved against that CTE: every row of every child reported
14047        // `__spg_partition_pm`, an internal name no user ever typed, where
14048        // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
14049        // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
14050        // one asks "which partition is this row in", answering 0 rows where
14051        // PG answers 1. `ctid` had the same shape: it numbered the CTE's
14052        // output, so rows in different children got distinct ctids instead
14053        // of each child's own physical position.
14054        //
14055        // Naming them in the term is what carries them: the child scan
14056        // materialises its own six because the statement now references
14057        // them, and they land in SYSTEM_COLUMNS order right after the user
14058        // columns — the exact layout the positional `*` skip already
14059        // expects. Only done when the outer statement asks for one, so a
14060        // plain `SELECT * FROM parent` scans exactly what it scanned.
14061        let carry_sys = references_ctid(outer);
14062        let mut body = alloc::string::String::new();
14063        for (i, child_name) in kept.iter().enumerate() {
14064            if i > 0 {
14065                body.push_str(" UNION ALL ");
14066            }
14067            body.push_str("SELECT *");
14068            if carry_sys {
14069                for sys in SYSTEM_COLUMNS {
14070                    body.push_str(", ");
14071                    body.push_str(sys);
14072                }
14073            }
14074            body.push_str(" FROM ");
14075            body.push_str(&quote_ident_for_sql(child_name));
14076        }
14077        parse_select_or_corrupt(&body).map(Some)
14078    }
14079}
14080
14081/// Rewrite a `TableRef` pointing at a partition parent so it
14082/// references the synthetic CTE created by the expansion. If the
14083/// original ref had no alias, preserve the parent name as an alias
14084/// so column references like `events_partitioned.received_at`
14085/// keep resolving.
14086fn rewrite_partition_parent_table_ref(
14087    t: &mut spg_sql::ast::TableRef,
14088    parents: &[alloc::string::String],
14089    synth_name: &impl Fn(&str) -> alloc::string::String,
14090) {
14091    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
14092        return;
14093    }
14094    // v7.39 (round 644) — an ONLY reference stays pointed at the parent
14095    // itself. The rewrite is keyed on the NAME, so in
14096    // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
14097    // parent list and this then rewrote BOTH — including the one that
14098    // asked not to descend. PG answers 0 for that join; SPG answered 2.
14099    // Folded into the existing test — see the note in
14100    // `collect_partition_parent_refs` for what a separate one cost.
14101    if t.only || !parents.iter().any(|p| p == &t.name) {
14102        return;
14103    }
14104    if t.alias.is_none() {
14105        t.alias = Some(t.name.clone());
14106    }
14107    t.name = synth_name(&t.name);
14108}
14109
14110/// Walk a `TableRef` and push its `name` if it resolves to a partition
14111/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
14112/// `generate_series_args` references — those aren't catalog tables.
14113fn collect_partition_parent_refs(
14114    t: &spg_sql::ast::TableRef,
14115    cat: &spg_storage::Catalog,
14116    out: &mut Vec<alloc::string::String>,
14117) {
14118    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
14119        return;
14120    }
14121    // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
14122    // The keyword used to be absorbed at parse time, so this fanned out
14123    // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
14124    // answered 2 where PG answers 0.
14125    //
14126    // Folded into the existing test rather than given an early return of
14127    // its own: as two extra lines in this function's body it cost
14128    // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
14129    // outside the panel. Rounds 641 and 643 met the same wall from the
14130    // other two directions — adding to a hot function and taking away
14131    // from a cold one. What goes in a body near the row loop is a
14132    // codegen decision whatever its shape.
14133    if !t.only && crate::partition::has_children(cat, &t.name) {
14134        out.push(t.name.clone());
14135    }
14136}
14137
14138/// v7.37.6-B partition-key range derived from a WHERE expression.
14139/// `i64` microseconds since epoch with the same sign convention as
14140/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
14141/// / `=`),`false` ⇒ exclusive(`>` / `<`).
14142#[derive(Debug, Clone, Copy)]
14143pub(crate) struct PartitionFilterBound {
14144    pub micros: i64,
14145    pub inclusive: bool,
14146}
14147
14148/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
14149/// shapes; tighten the running lo / hi as we go. Anything outside that
14150/// (OR / nested calls / non-key columns)is ignored — caller treats
14151/// `None` as "no constraint on that side."
14152fn extract_key_range(
14153    expr: &spg_sql::ast::Expr,
14154    key_col: &str,
14155) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
14156    let mut lo: Option<PartitionFilterBound> = None;
14157    let mut hi: Option<PartitionFilterBound> = None;
14158    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14159    while let Some(e) = stack.pop() {
14160        match e {
14161            spg_sql::ast::Expr::Binary {
14162                lhs,
14163                op: spg_sql::ast::BinOp::And,
14164                rhs,
14165            } => {
14166                stack.push(lhs);
14167                stack.push(rhs);
14168            }
14169            // BETWEEN is desugared at parse time into `lhs >= low AND
14170            // lhs <= high`, so it lands here as two regular Binary
14171            // arms via the AND walker above.
14172            spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
14173                let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
14174                    (Some(lhs.as_ref()), rhs.as_ref(), false)
14175                } else if is_column_ref(rhs, key_col) {
14176                    (Some(rhs.as_ref()), lhs.as_ref(), true)
14177                } else {
14178                    (None, lhs.as_ref(), false)
14179                };
14180                if col_ref.is_none() {
14181                    continue;
14182                }
14183                let Some(lit) = literal_to_micros(lit_side) else {
14184                    continue;
14185                };
14186                use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
14187                let effective_op = if swapped {
14188                    match op {
14189                        Lt => Gt,
14190                        LtEq => GtEq,
14191                        Gt => Lt,
14192                        GtEq => LtEq,
14193                        other => *other,
14194                    }
14195                } else {
14196                    *op
14197                };
14198                match effective_op {
14199                    Eq => {
14200                        tighten_lo(
14201                            &mut lo,
14202                            PartitionFilterBound {
14203                                micros: lit,
14204                                inclusive: true,
14205                            },
14206                        );
14207                        tighten_hi(
14208                            &mut hi,
14209                            PartitionFilterBound {
14210                                micros: lit,
14211                                inclusive: true,
14212                            },
14213                        );
14214                    }
14215                    GtEq => {
14216                        tighten_lo(
14217                            &mut lo,
14218                            PartitionFilterBound {
14219                                micros: lit,
14220                                inclusive: true,
14221                            },
14222                        );
14223                    }
14224                    Gt => {
14225                        tighten_lo(
14226                            &mut lo,
14227                            PartitionFilterBound {
14228                                micros: lit,
14229                                inclusive: false,
14230                            },
14231                        );
14232                    }
14233                    LtEq => {
14234                        tighten_hi(
14235                            &mut hi,
14236                            PartitionFilterBound {
14237                                micros: lit,
14238                                inclusive: true,
14239                            },
14240                        );
14241                    }
14242                    Lt => {
14243                        tighten_hi(
14244                            &mut hi,
14245                            PartitionFilterBound {
14246                                micros: lit,
14247                                inclusive: false,
14248                            },
14249                        );
14250                    }
14251                    _ => {}
14252                }
14253            }
14254            _ => {}
14255        }
14256    }
14257    (lo, hi)
14258}
14259
14260fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14261    match slot {
14262        None => *slot = Some(new),
14263        Some(cur) => {
14264            if new.micros > cur.micros
14265                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14266            {
14267                *slot = Some(new);
14268            }
14269        }
14270    }
14271}
14272
14273fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14274    match slot {
14275        None => *slot = Some(new),
14276        Some(cur) => {
14277            if new.micros < cur.micros
14278                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14279            {
14280                *slot = Some(new);
14281            }
14282        }
14283    }
14284}
14285
14286fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
14287    if let spg_sql::ast::Expr::Column(c) = e {
14288        c.name.eq_ignore_ascii_case(key_col)
14289    } else {
14290        false
14291    }
14292}
14293
14294/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
14295/// `key_col = <literal>` predicate out for LIST/HASH partition
14296/// pruning. Returns `None` when no equality literal can be lifted
14297/// (planner then keeps every child — correctness preserved). The
14298/// returned `Value<'static>` is an owned coercion so the caller can
14299/// outlive any AST node it was extracted from.
14300pub(crate) fn extract_key_eq_value(
14301    expr: &spg_sql::ast::Expr,
14302    key_col: &str,
14303) -> Option<spg_storage::Value<'static>> {
14304    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14305    while let Some(e) = stack.pop() {
14306        match e {
14307            spg_sql::ast::Expr::Binary {
14308                lhs,
14309                op: spg_sql::ast::BinOp::And,
14310                rhs,
14311            } => {
14312                stack.push(lhs);
14313                stack.push(rhs);
14314            }
14315            spg_sql::ast::Expr::Binary {
14316                lhs,
14317                op: spg_sql::ast::BinOp::Eq,
14318                rhs,
14319            } => {
14320                let lit_side = if is_column_ref(lhs, key_col) {
14321                    rhs.as_ref()
14322                } else if is_column_ref(rhs, key_col) {
14323                    lhs.as_ref()
14324                } else {
14325                    continue;
14326                };
14327                let cloned = lit_side.clone();
14328                let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
14329                    continue;
14330                };
14331                // Coerce to an owned Value<'static> so the caller
14332                // can hold it past the WHERE expression's lifetime.
14333                let owned: spg_storage::Value<'static> = match v {
14334                    spg_storage::Value::Text(s) => {
14335                        spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
14336                    }
14337                    spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
14338                    spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
14339                    spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
14340                    spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
14341                    spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
14342                    spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
14343                    spg_storage::Value::Null => spg_storage::Value::Null,
14344                    // Anything else (Vector / Json / Bytes / Numeric /
14345                    // arrays / interval / …) isn't a current partition
14346                    // key type; skip without pruning.
14347                    _ => continue,
14348                };
14349                return Some(owned);
14350            }
14351            _ => {}
14352        }
14353    }
14354    None
14355}
14356
14357/// Coerce a literal Expr(after the parser folded sequence calls etc.)
14358/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
14359/// pruning and routing agree on the literal vocabulary. Returns
14360/// `None` when the literal isn't recognised(planner then skips
14361/// pruning on that branch — correctness preserved).
14362fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
14363    let cloned = e.clone();
14364    let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
14365    match value {
14366        spg_storage::Value::Timestamp(m) => Some(m),
14367        spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
14368        spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
14369        _ => None,
14370    }
14371}
14372
14373/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
14374/// satisfying the WHERE-derived filter range. PG-style half-open:
14375/// child upper exclusive. Filter inclusivity is honoured per-bound.
14376fn range_satisfies_filter(
14377    range_lo: &spg_storage::PartitionBound,
14378    range_hi: &spg_storage::PartitionBound,
14379    filter_lo: Option<&PartitionFilterBound>,
14380    filter_hi: Option<&PartitionFilterBound>,
14381) -> bool {
14382    use spg_storage::PartitionBound;
14383    // For each filter side, reject children that can't host any row
14384    // matching the predicate.
14385    if let Some(lo) = filter_lo {
14386        // child upper bound vs filter lower:
14387        //   if filter is x >= L, child rejects iff child.hi <= L
14388        //   if filter is x  > L, child rejects iff child.hi <= L
14389        //   (child.hi exclusive, so equality with L still rejects)
14390        match range_hi {
14391            PartitionBound::MinValue => return false,
14392            PartitionBound::MaxValue => {}
14393            PartitionBound::TimestampTz(hi) => {
14394                if *hi <= lo.micros {
14395                    return false;
14396                }
14397            }
14398            // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
14399            // matched against TIMESTAMPTZ filters here; keep child
14400            // (conservative: don't prune).
14401            PartitionBound::BigInt(_)
14402            | PartitionBound::Int(_)
14403            | PartitionBound::SmallInt(_)
14404            | PartitionBound::Date(_)
14405            | PartitionBound::Text(_) => {}
14406        }
14407    }
14408    if let Some(hi) = filter_hi {
14409        // child lower bound vs filter upper:
14410        //   if filter is x <= U, child rejects iff child.lo > U
14411        //   if filter is x  < U, child rejects iff child.lo >= U
14412        match range_lo {
14413            PartitionBound::MaxValue => return false,
14414            PartitionBound::MinValue => {}
14415            PartitionBound::TimestampTz(lo) => {
14416                let rejects = if hi.inclusive {
14417                    *lo > hi.micros
14418                } else {
14419                    *lo >= hi.micros
14420                };
14421                if rejects {
14422                    return false;
14423                }
14424            }
14425            PartitionBound::BigInt(_)
14426            | PartitionBound::Int(_)
14427            | PartitionBound::SmallInt(_)
14428            | PartitionBound::Date(_)
14429            | PartitionBound::Text(_) => {}
14430        }
14431    }
14432    true
14433}
14434
14435fn quote_ident_for_sql(name: &str) -> alloc::string::String {
14436    // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
14437    // identifier, otherwise quoted). Conservative: always quote so
14438    // children with reserved names round-trip safely through the
14439    // CTE-body parse.
14440    let mut out = alloc::string::String::with_capacity(name.len() + 2);
14441    out.push('"');
14442    for c in name.chars() {
14443        if c == '"' {
14444            out.push('"');
14445        }
14446        out.push(c);
14447    }
14448    out.push('"');
14449    out
14450}
14451
14452fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
14453    let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
14454        EngineError::Unsupported(alloc::format!(
14455            "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
14456        ))
14457    })?;
14458    let Statement::Select(body) = parsed else {
14459        return Err(EngineError::Unsupported(alloc::format!(
14460            "partition expansion: generated SQL {sql:?} is not a SELECT"
14461        )));
14462    };
14463    Ok(body)
14464}
14465
14466/// v7.39 (read01 round 65/66) — the column shape a set-returning function
14467/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
14468/// yields ONE column named after the call's alias when there is one (`FROM
14469/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
14470/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
14471fn setof_column_shape_from(
14472    declared: &str,
14473    name: &str,
14474    alias: Option<&str>,
14475    got: &[ColumnSchema],
14476) -> alloc::vec::Vec<ColumnSchema> {
14477    let upper = declared.to_ascii_uppercase();
14478    if upper.starts_with("TABLE(") {
14479        let raw = &declared["TABLE(".len()..declared.len() - 1];
14480        return raw
14481            .split(',')
14482            .zip(got.iter())
14483            .map(|(decl, g)| {
14484                let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
14485                ColumnSchema::new(cname.to_string(), g.ty, true)
14486            })
14487            .collect();
14488    }
14489    let cname = alias.unwrap_or(name);
14490    got.first()
14491        .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
14492        .unwrap_or_default()
14493}
14494
14495/// The plpgsql twin: the interpreter hands back raw value rows, so the types
14496/// come off the first row.
14497fn setof_column_shape(
14498    declared: &str,
14499    name: &str,
14500    alias: Option<&str>,
14501    first_row: Option<&alloc::vec::Vec<Value<'static>>>,
14502) -> alloc::vec::Vec<ColumnSchema> {
14503    let got: alloc::vec::Vec<ColumnSchema> = first_row
14504        .map(|r| {
14505            r.iter()
14506                .enumerate()
14507                .map(|(i, v)| {
14508                    ColumnSchema::new(
14509                        alloc::format!("col{i}"),
14510                        v.data_type().unwrap_or(DataType::Text),
14511                        true,
14512                    )
14513                })
14514                .collect()
14515        })
14516        .unwrap_or_default();
14517    setof_column_shape_from(declared, name, alias, &got)
14518}
14519
14520/// v7.39 (read01 round 67) — expand every set-returning call in a target list
14521/// for ONE input row, PG's ProjectSet semantics.
14522///
14523/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
14524/// output has as many rows as the LONGEST of them, and a shorter one is padded
14525/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
14526/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
14527/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
14528/// is zero rows, not one NULL row.
14529///
14530/// Non-SRF items repeat, evaluated once per output row from the same input row.
14531/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
14532/// used to reach the scalar function dispatcher, which reported the aggregate as
14533/// an *unknown function* — the same "symptom two layers above the cause" shape
14534/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
14535/// sees a call, not the clause it came from. The statement knows.
14536/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
14537/// clause may appear.
14538///
14539/// PG rejects `FOR UPDATE` on exactly the shapes that have no
14540/// identifiable base row to lock, each with its own wording. SPG
14541/// accepted all of them and locked nothing, so a query that PG refuses
14542/// outright came back looking like it had taken locks.
14543///
14544/// Every wording read off live PG 18.4.
14545impl crate::Engine {
14546    /// v7.39.2 — a column name in WHERE / ORDER BY / GROUP BY / HAVING
14547    /// that names nothing is refused before the scan, not when a row
14548    /// reaches it.
14549    ///
14550    /// The projection resolves its names eagerly; a predicate only meets
14551    /// them per row. So on an EMPTY table `SELECT a FROM t WHERE nosuch
14552    /// = 1` answered zero rows and no error, and the same statement over
14553    /// a table with one row raised. Measured on PostgreSQL 18.6 and
14554    /// MySQL 9.7.2: both refuse it whatever the row count. A typo in a
14555    /// predicate therefore passed a test written against an empty
14556    /// fixture and failed in production — or, worse, ran nightly over an
14557    /// empty window and reported nothing.
14558    ///
14559    /// Deliberately narrow: ONE plain base table, nothing else. A join,
14560    /// a CTE, a set operation, a lateral or function source, or a
14561    /// subquery in the clause all bring a second scope into which a name
14562    /// may legitimately resolve, and refusing one of those would be a
14563    /// worse defect than the one this closes. Those shapes keep the
14564    /// old behaviour; the walk below does not descend into a subquery
14565    /// for the same reason.
14566    /// v7.39.2 — refuse a call whose argument count no overload accepts,
14567    /// BEFORE the scan rather than per row.
14568    ///
14569    /// `SELECT lower(t, n) FROM t` answered zero rows and no error over
14570    /// an EMPTY table and raised the moment the table had one row in it,
14571    /// because the arity check lives inside the row-time dispatch. It is
14572    /// the same shape as the unknown-column-in-a-predicate defect closed
14573    /// earlier in this release, and it hides in the same place: a query
14574    /// written against an empty fixture passes its test.
14575    ///
14576    /// The accepted counts come from `eval::arity::REFUSED_ARITIES`,
14577    /// which is derived by asking the dispatch itself offline and can
14578    /// only ever UNDER-refuse — see that file for why the two other
14579    /// candidate oracles were refuted.
14580    /// v7.39.3 — MySQL's column names are case-insensitive; PostgreSQL's
14581    /// quoted ones are not. See `EvalContext::col_eq`.
14582    fn col_name_eq(&self, a: &str, b: &str) -> bool {
14583        if self.speaks_mysql {
14584            a.eq_ignore_ascii_case(b)
14585        } else {
14586            a == b
14587        }
14588    }
14589
14590    pub(crate) fn validate_function_arity(
14591        &self,
14592        stmt: &SelectStatement,
14593    ) -> Result<(), EngineError> {
14594        let mut calls: Vec<(alloc::string::String, Vec<Expr>)> = Vec::new();
14595        for it in &stmt.items {
14596            if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14597                collect_function_calls(expr, &mut calls);
14598            }
14599        }
14600        if let Some(w) = &stmt.where_ {
14601            collect_function_calls(w, &mut calls);
14602        }
14603        for o in &stmt.order_by {
14604            collect_function_calls(&o.expr, &mut calls);
14605        }
14606        // The columns a name in this statement could resolve to. Only
14607        // plain base tables; anything else and the types are not
14608        // statically knowable, so nothing is refused early.
14609        let cat = self.active_catalog();
14610        let mut cols: Vec<ColumnSchema> = Vec::new();
14611        if let Some(from) = &stmt.from {
14612            for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14613                if let Some(table) = cat.get(&t.name) {
14614                    cols.extend(table.schema().columns.iter().cloned());
14615                }
14616            }
14617        }
14618        for (name, args) in calls {
14619            let Ok(i) = crate::eval::arity::REFUSED_ARITIES
14620                .binary_search_by(|(n, _)| (*n).cmp(name.as_str()))
14621            else {
14622                continue;
14623            };
14624            if !crate::eval::arity::REFUSED_ARITIES[i]
14625                .1
14626                .contains(&args.len())
14627            {
14628                continue;
14629            }
14630            // v7.39.2 — PostgreSQL names the SIGNATURE it could not
14631            // match, and before the scan there are no values to read a
14632            // type from. Where every argument's type is knowable
14633            // statically — a column of a source table, or a literal —
14634            // the sentence is PostgreSQL's exactly; where one is not,
14635            // this leaves the call to the row-time raise, which has the
14636            // values. Refusing early with a WORSE message would trade
14637            // one defect for another.
14638            let mut types: Vec<alloc::string::String> = Vec::new();
14639            for a in &args {
14640                let Some(t) = static_arg_type(a, &cols) else {
14641                    types.clear();
14642                    break;
14643                };
14644                types.push(t);
14645            }
14646            if types.len() != args.len() {
14647                continue;
14648            }
14649            return Err(EngineError::Eval(EvalError::WrongArity {
14650                name,
14651                types: types.join(", "),
14652            }));
14653        }
14654        Ok(())
14655    }
14656
14657    pub(crate) fn validate_clause_columns(
14658        &self,
14659        stmt: &SelectStatement,
14660    ) -> Result<(), EngineError> {
14661        let Some(from) = &stmt.from else {
14662            return Ok(());
14663        };
14664        if !stmt.ctes.is_empty() {
14665            return Ok(());
14666        }
14667        // v7.39.2 — every source, not just the first. A join is checkable
14668        // for the same reason one table is: with no CTE and no
14669        // subquery-shaped source, a bare name has to come from one of
14670        // them. Refusing the check for joins left `SELECT … FROM a JOIN b
14671        // … WHERE nosuch = 1` labelled `'field list'` where MySQL 9.7.2
14672        // says `'where clause'`.
14673        // v7.40.10 — this closure WAS the complete list, and the only
14674        // complete one in the engine. It moved to the type so the other
14675        // fifty-five sites can ask the same question.
14676        let plain = |t: &spg_sql::ast::TableRef| -> bool { t.names_a_relation() };
14677        let cat = self.active_catalog();
14678        let mut sources: Vec<(String, &spg_storage::Table)> = Vec::new();
14679        for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14680            if !plain(t) {
14681                return Ok(());
14682            }
14683            let Some(table) = cat.get(&t.name) else {
14684                return Ok(());
14685            };
14686            sources.push((t.alias.clone().unwrap_or_else(|| t.name.clone()), table));
14687        }
14688        let known = |c: &spg_sql::ast::ColumnName| -> bool {
14689            // A system column is not in a table's list and is a perfectly
14690            // good predicate: `WHERE ctid = '(0,4)'::tid` and `WHERE
14691            // tableoid::regclass::text = 'pm_a'` are both real, and the
14692            // first draft of this check refused them. The e2e suite said
14693            // so immediately, which is what it is for.
14694            if is_system_column(&c.name) {
14695                return true;
14696            }
14697            if let Some(q) = &c.qualifier {
14698                // A qualifier must name one of this statement's sources,
14699                // and that source must carry the column. An alias
14700                // REPLACES the written name, which is PostgreSQL's rule
14701                // and MySQL's: `FROM pg_cast c WHERE pg_cast.oid <> 0`
14702                // is an error on both.
14703                return match sources.iter().find(|(a, _)| a == q) {
14704                    Some((_, t)) => t
14705                        .schema()
14706                        .columns
14707                        .iter()
14708                        .any(|sc| self.col_name_eq(&sc.name, &c.name)),
14709                    None => false,
14710                };
14711            }
14712            sources
14713                .iter()
14714                .any(|(_, t)| {
14715                    t.schema()
14716                        .columns
14717                        .iter()
14718                        .any(|sc| self.col_name_eq(&sc.name, &c.name))
14719                })
14720                // An output name the statement itself defines: ORDER BY,
14721                // GROUP BY and HAVING may all name one.
14722                || stmt.items.iter().any(|it| match it {
14723                    SelectItem::Expr { expr, alias } => {
14724                        alias.as_deref() == Some(c.name.as_str())
14725                            || matches!(expr, Expr::Column(pc) if pc.name == c.name)
14726                    }
14727                    _ => false,
14728                })
14729        };
14730        // v7.39.2 — the CLAUSE travels with the reference, because MySQL
14731        // names it: `Unknown column 'x' in 'where clause'`, `'order
14732        // clause'`, `'group statement'`, `'having clause'`. Measured on
14733        // 9.7.2, and a driver's error handling reads the sentence as well
14734        // as the number. PostgreSQL says only `column "x" does not
14735        // exist`, with no clause, so its wording is unchanged.
14736        //
14737        // This walk is the only place the clause is still known: by the
14738        // time a row-time resolver meets the name, the expression has
14739        // been detached from the statement that held it.
14740        let mut refs: Vec<(spg_sql::ast::ColumnName, &'static str)> = Vec::new();
14741        let mut push = |e: &Expr, ctx: &'static str, out: &mut Vec<_>| {
14742            let mut here: Vec<spg_sql::ast::ColumnName> = Vec::new();
14743            collect_plain_column_refs(e, &mut here);
14744            out.extend(here.into_iter().map(|c| (c, ctx)));
14745        };
14746        if let Some(w) = &stmt.where_ {
14747            push(w, "where clause", &mut refs);
14748        }
14749        if let Some(g) = &stmt.group_by {
14750            for e in g {
14751                push(e, "group statement", &mut refs);
14752            }
14753        }
14754        if let Some(h) = &stmt.having {
14755            push(h, "having clause", &mut refs);
14756        }
14757        for o in &stmt.order_by {
14758            push(&o.expr, "order clause", &mut refs);
14759        }
14760        // v7.39.2 — and the join predicates, which MySQL calls the `on
14761        // clause`. Measured on 9.7.2: `Unknown column 'j1.nosuch' in 'on
14762        // clause'`, qualifier and all.
14763        for j in &from.joins {
14764            if let Some(on) = &j.on {
14765                push(on, "on clause", &mut refs);
14766            }
14767        }
14768        for (c, ctx) in &refs {
14769            if !known(c) {
14770                if self.speaks_mysql {
14771                    // The QUALIFIER travels with it: MySQL 9.7.2 answers
14772                    // `Unknown column 'j1.nosuch' in 'on clause'`, not the
14773                    // bare name. Measured.
14774                    let shown = match &c.qualifier {
14775                        Some(q) => alloc::format!("{q}.{}", c.name),
14776                        None => c.name.clone(),
14777                    };
14778                    return Err(EngineError::Eval(EvalError::TypeMismatch {
14779                        detail: alloc::format!("Unknown column '{shown}' in '{ctx}'"),
14780                    }));
14781                }
14782                // PostgreSQL 18.6 names the missing TABLE when the
14783                // qualifier is the part that resolves to nothing
14784                // (`missing FROM-clause entry for table "pg_cast"`) and
14785                // the COLUMN otherwise. Raising the column error for both
14786                // dropped the table name a caller matches on.
14787                if let Some(q) = &c.qualifier
14788                    && !sources.iter().any(|(a, _)| a == q)
14789                {
14790                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
14791                        qualifier: q.clone(),
14792                        column: c.name.clone(),
14793                    }));
14794                }
14795                // v7.39.2 — and a qualified reference whose qualifier
14796                // DOES resolve prints the whole thing, unquoted:
14797                // `column ea.no_such does not exist` (measured on PG
14798                // 18.6). The bare `column "no_such" does not exist` drops
14799                // the alias a caller matches on, which is what the
14800                // sqlx round-20 pin says.
14801                if let Some(q) = &c.qualifier {
14802                    return Err(EngineError::Eval(EvalError::QualifiedColumnNotFound {
14803                        qualifier: q.clone(),
14804                        column: c.name.clone(),
14805                    }));
14806                }
14807                return Err(EngineError::Eval(EvalError::ColumnNotFound {
14808                    name: c.name.clone(),
14809                }));
14810            }
14811        }
14812        Ok(())
14813    }
14814}
14815
14816/// v7.39.2 — the column references of an expression, NOT descending into
14817/// a subquery.
14818///
14819/// A correlated subquery resolves its names against an outer scope this
14820/// walk cannot see, so descending would refuse valid queries. Missing a
14821/// typo inside one is the safe direction; refusing a good query is not.
14822/// v7.39.2 — the type PostgreSQL would name for an argument, when it
14823/// can be known without a row: a column of a source table, or a
14824/// literal. `None` for anything else, which is what keeps the pre-scan
14825/// refusal from printing a worse sentence than the row-time one.
14826pub(crate) fn static_arg_type(e: &Expr, cols: &[ColumnSchema]) -> Option<alloc::string::String> {
14827    use spg_sql::ast::Literal as L;
14828    match e {
14829        Expr::Column(c) => cols
14830            .iter()
14831            .find(|s| s.name.eq_ignore_ascii_case(&c.name))
14832            .map(|s| crate::conversions::pg_type_name_for_error(s.ty)),
14833        // A bare literal has no type yet on PostgreSQL — it names it
14834        // `unknown` in this very sentence — except where the lexeme
14835        // fixes one.
14836        Expr::Literal(L::String(_)) | Expr::Literal(L::Null) => {
14837            Some(alloc::string::String::from("unknown"))
14838        }
14839        Expr::Literal(L::Integer(_)) => Some(alloc::string::String::from("integer")),
14840        Expr::Literal(L::Bool(_)) => Some(alloc::string::String::from("boolean")),
14841        _ => None,
14842    }
14843}
14844
14845/// v7.39.2 — the function calls of an expression, name and argument
14846/// count, NOT descending into a subquery (its scope is its own).
14847fn collect_function_calls(e: &Expr, out: &mut Vec<(alloc::string::String, Vec<Expr>)>) {
14848    match e {
14849        Expr::FunctionCall { name, args } => {
14850            out.push((name.to_ascii_lowercase(), args.clone()));
14851            for a in args {
14852                collect_function_calls(a, out);
14853            }
14854        }
14855        Expr::Binary { lhs, rhs, .. } => {
14856            collect_function_calls(lhs, out);
14857            collect_function_calls(rhs, out);
14858        }
14859        Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14860            collect_function_calls(expr, out);
14861        }
14862        _ => {}
14863    }
14864}
14865
14866fn collect_plain_column_refs(e: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
14867    match e {
14868        Expr::Column(c) => out.push(c.clone()),
14869        Expr::Binary { lhs, rhs, .. } => {
14870            collect_plain_column_refs(lhs, out);
14871            collect_plain_column_refs(rhs, out);
14872        }
14873        Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14874            collect_plain_column_refs(expr, out);
14875        }
14876        Expr::FunctionCall { args, .. } => {
14877            for a in args {
14878                collect_plain_column_refs(a, out);
14879            }
14880        }
14881        _ => {}
14882    }
14883}
14884
14885fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
14886    let Some(lock) = &stmt.locking else {
14887        return Ok(());
14888    };
14889    let verb = lock_clause_verb(lock.strength);
14890    let refuse = |what: &str| {
14891        Err(EngineError::Unsupported(alloc::format!(
14892            "{verb} is not allowed with {what}"
14893        )))
14894    };
14895    if !stmt.unions.is_empty() {
14896        return refuse("UNION/INTERSECT/EXCEPT");
14897    }
14898    if stmt.distinct || !stmt.distinct_on.is_empty() {
14899        return refuse("DISTINCT clause");
14900    }
14901    if stmt.group_by.is_some() || stmt.group_by_all {
14902        return refuse("GROUP BY clause");
14903    }
14904    let has_agg = stmt.items.iter().any(|it| match it {
14905        spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
14906        _ => false,
14907    });
14908    if has_agg {
14909        return refuse("aggregate functions");
14910    }
14911    // `FOR UPDATE OF t` must name a relation that is actually in FROM.
14912    for want in &lock.of_tables {
14913        if !locking_from_names(stmt)
14914            .iter()
14915            .any(|n| n.eq_ignore_ascii_case(want))
14916        {
14917            return Err(EngineError::Unsupported(alloc::format!(
14918                "relation \"{want}\" in {verb} clause not found in FROM clause"
14919            )));
14920        }
14921    }
14922    Ok(())
14923}
14924
14925/// How PG names the clause in its diagnostics.
14926const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
14927    use spg_sql::ast::LockStrength as LS;
14928    match s {
14929        LS::Update => "FOR UPDATE",
14930        LS::NoKeyUpdate => "FOR NO KEY UPDATE",
14931        LS::Share => "FOR SHARE",
14932        LS::KeyShare => "FOR KEY SHARE",
14933    }
14934}
14935
14936/// Every relation name (or alias) the FROM clause exposes.
14937fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
14938    let mut out = alloc::vec::Vec::new();
14939    if let Some(f) = &stmt.from {
14940        let mut push = |t: &spg_sql::ast::TableRef| {
14941            if let Some(a) = &t.alias {
14942                out.push(a.clone());
14943            }
14944            out.push(t.name.clone());
14945        };
14946        push(&f.primary);
14947        for j in &f.joins {
14948            push(&j.table);
14949        }
14950    }
14951    out
14952}
14953
14954fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
14955    use spg_sql::ast::Expr;
14956    if let Some(w) = &stmt.where_
14957        && aggregate::contains_aggregate(w)
14958    {
14959        return Err(EngineError::Unsupported(
14960            "aggregate functions are not allowed in WHERE".into(),
14961        ));
14962    }
14963    let mut nested = false;
14964    let mut check = |e: &Expr| {
14965        let mut probe = e.clone();
14966        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14967            let args = match n {
14968                Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
14969                _ => return false,
14970            };
14971            if args.iter().any(aggregate::contains_aggregate) {
14972                nested = true;
14973            }
14974            false
14975        });
14976    };
14977    for it in &stmt.items {
14978        if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14979            check(expr);
14980        }
14981    }
14982    if let Some(h) = &stmt.having {
14983        check(h);
14984    }
14985    for o in &stmt.order_by {
14986        check(&o.expr);
14987    }
14988    if nested {
14989        return Err(EngineError::Unsupported(
14990            "aggregate function calls cannot be nested".into(),
14991        ));
14992    }
14993    Ok(())
14994}
14995
14996/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
14997/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
14998/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
14999/// to a set and then applies the enclosing expression once per element. SPG only
15000/// ever recognised an SRF that WAS the item, so everything above died on
15001/// "unknown function unnest" — the set-returning call, wrapped in anything at
15002/// all, fell through to the scalar function dispatcher which has no such name.
15003///
15004/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
15005/// rewritten to read that column, and the rewritten expression is evaluated once
15006/// per output row against the input row extended with the lifted values. The
15007/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
15008/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
15009/// executors (the single-table scan, the synthetic-table pipeline, and the
15010/// unnest FROM path) each evaluated the key as an ordinary expression, where the
15011/// literal `n` is just the constant n — the same sort key for every row. The
15012/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
15013/// back in input order, not in a wrong order. Statement prep resolves the common
15014/// case, but only when the SELECT item is an expression — a `*` is not one, and
15015/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
15016/// spelling landed on exactly the shape prep could not resolve.
15017///
15018/// A set-returning item is left alone: copying it into ORDER BY would make the
15019/// key "the whole set", evaluated once per INPUT row.
15020fn resolve_positional_order_by(
15021    order_by: &[spg_sql::ast::OrderBy],
15022    projection: &[ProjectedItem],
15023) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
15024    order_by
15025        .iter()
15026        .filter_map(|o| {
15027            let mut o = o.clone();
15028            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
15029                && *n >= 1
15030                && let Ok(idx) = usize::try_from(*n - 1)
15031                && let Some(item) = projection.get(idx)
15032                && !expr_contains_builtin_srf(&item.expr)
15033            {
15034                // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
15035                // item is itself an integer LITERAL must not be
15036                // substituted textually: the literal would read as an
15037                // ordinal again downstream, and `SELECT 10 … ORDER BY
15038                // 1` died with "position 10 is not in select list"
15039                // where PG happily returns the rows. Ordering by a
15040                // constant orders nothing, so the key drops.
15041                if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
15042                    return None;
15043                }
15044                o.expr = item.expr.clone();
15045            }
15046            Some(o)
15047        })
15048        .collect()
15049}
15050
15051/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
15052/// this expression? Statement preparation (`resolve_order_by_position`) runs
15053/// before any catalog is in hand, and it only needs to know "is this item's value
15054/// a set", which the builtin SRFs answer syntactically.
15055pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
15056    let mut found = false;
15057    let mut probe = e.clone();
15058    crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
15059        if is_top_level_unnest(n) {
15060            found = true;
15061            return true;
15062        }
15063        false
15064    });
15065    found
15066}
15067
15068/// v7.39 (round 599) — everything about a target-list SRF that does not
15069/// depend on the row.
15070///
15071/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
15072/// each SRF-bearing projection expression, walked and rewrote the tree,
15073/// formatted a `__srf_N` name per node, and copied the whole column schema.
15074/// A counting allocator put the path at 24 allocations per input row for a
15075/// single-element `unnest`, against 0 for the same scan without one — 211 MB
15076/// where the plain scan took 4.3 — and the shape held whatever the array
15077/// contained, which is what invariant work looks like.
15078struct SrfPlan {
15079    /// The lifted SRF calls, in slot order.
15080    nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
15081    /// Per projection position, the expression with its SRF calls replaced
15082    /// by `__srf_N` column references. `None` means the item has none.
15083    rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
15084    /// The input schema followed by one column per slot. Only the slots'
15085    /// TYPES vary per row, and they are patched in place.
15086    ext_cols: alloc::vec::Vec<ColumnSchema>,
15087    /// v7.39 (round 743) — the rewritten projection COMPILED against the
15088    /// extended schema, once per plan. The per-output-row evaluation ran
15089    /// the interpreter (~560 ns/row on the unnest panel cell); the Step
15090    /// VM reads the `__srf_N` slots as plain columns. `None` = that item
15091    /// is not fully compilable and keeps the interpreter.
15092    compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
15093    base_cols: usize,
15094}
15095
15096fn build_srf_plan(
15097    engine: &Engine,
15098    projection: &[ProjectedItem],
15099    srf_idxs: &[usize],
15100    ctx: &EvalContext<'_>,
15101) -> Result<SrfPlan, EngineError> {
15102    // Lift every SRF node out of every item that contains one.
15103    let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
15104    let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
15105    let mut reject: Option<EngineError> = None;
15106    for &i in srf_idxs {
15107        let mut e = projection[i].expr.clone();
15108        crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
15109            if reject.is_some() {
15110                return true;
15111            }
15112            // PG refuses a set-returning function inside a conditional: the set
15113            // would have to be produced before anyone knows whether the branch
15114            // is even taken.
15115            let conditional = match n {
15116                spg_sql::ast::Expr::Case { .. } => Some("CASE"),
15117                spg_sql::ast::Expr::FunctionCall { name, .. }
15118                    if name.eq_ignore_ascii_case("coalesce") =>
15119                {
15120                    Some("COALESCE")
15121                }
15122                _ => None,
15123            };
15124            if let Some(kind) = conditional
15125                && engine.expr_contains_srf(n)
15126            {
15127                reject = Some(EngineError::Unsupported(alloc::format!(
15128                    "set-returning functions are not allowed in {kind}"
15129                )));
15130                return true;
15131            }
15132            if !engine.is_srf_node(n) {
15133                return false;
15134            }
15135            let slot = nodes.len();
15136            nodes.push(n.clone());
15137            *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
15138                qualifier: None,
15139                name: alloc::format!("__srf_{slot}"),
15140            });
15141            true
15142        });
15143        rewritten[i] = Some(e);
15144    }
15145    if let Some(err) = reject {
15146        return Err(err);
15147    }
15148    let base_cols = ctx.columns.len();
15149    let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
15150    for slot in 0..nodes.len() {
15151        ext_cols.push(ColumnSchema::new(
15152            alloc::format!("__srf_{slot}"),
15153            DataType::Text,
15154            true,
15155        ));
15156    }
15157    // v7.39 (round 743) — compile the rewritten items against the
15158    // EXTENDED schema. The slot columns' declared type is a per-row
15159    // patched detail the compiled column read does not consult.
15160    let compiled: Vec<Option<eval::CompiledExpr>> = {
15161        let mut ext_ctx = ctx.clone();
15162        ext_ctx.columns = &ext_cols;
15163        projection
15164            .iter()
15165            .enumerate()
15166            .map(|(i, p)| {
15167                let e = rewritten[i].as_ref().unwrap_or(&p.expr);
15168                if eval::fully_compilable(e) {
15169                    Some(eval::compile_expr(e, &ext_ctx))
15170                } else {
15171                    None
15172                }
15173            })
15174            .collect()
15175    };
15176    Ok(SrfPlan {
15177        nodes,
15178        rewritten,
15179        ext_cols,
15180        compiled,
15181        base_cols,
15182    })
15183}
15184
15185/// One input row expanded through a plan built once for the whole scan.
15186/// v7.39 (round 621) — expand a projection whose target list contains
15187/// set-returning items, remembering which INPUT row each output row came from.
15188///
15189/// The three materialised-source tails — `FROM unnest(…)`, `FROM
15190/// generate_series(…)`, and the one that serves VALUES / a derived table /
15191/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
15192/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
15193/// v(x)` answered `function unnest(integer[]) does not exist` on all the
15194/// others, for a query PG answers. Sharing the expansion is the point: a
15195/// fourth copy would have been the fourth place to forget.
15196fn expand_projection_srfs(
15197    engine: &Engine,
15198    projection: &[ProjectedItem],
15199    srf_idxs: &[usize],
15200    filtered: &[Row<'static>],
15201    ctx: &EvalContext<'_>,
15202) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
15203    let mut out = alloc::vec::Vec::with_capacity(filtered.len());
15204    let mut src = alloc::vec::Vec::with_capacity(filtered.len());
15205    // v7.39 (round 726) — ONE plan for the whole scan. The per-row
15206    // spelling rebuilt it for every input row: a full clone of the
15207    // rewritten projection trees and the extended schema, 50k times on
15208    // the panel's unnest cell.
15209    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15210    // v7.39 (round 733) — shard the expansion. Each shard clones the
15211    // plan (its ext_cols slot types are per-row mutable) and builds a
15212    // MINIMAL context — EvalContext is not Sync — which is sound only
15213    // when every expression involved is pure: the whole projection and
15214    // every SRF argument must be fully_compilable, or the row loop
15215    // stays serial with the full session context.
15216    // The projection is judged in its REWRITTEN form — the SRF call
15217    // itself is never compilable, but after the lift it is a plain
15218    // `__srf_N` column reference.
15219    let all_pure = projection
15220        .iter()
15221        .enumerate()
15222        .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
15223        && plan.nodes.iter().all(|n| match n {
15224            Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
15225            other => eval::fully_compilable(other),
15226        });
15227    if all_pure
15228        && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
15229        && let Some(r) = engine.parallel_runner.0.as_deref()
15230    {
15231        let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
15232        let chunk = filtered.len().div_ceil(n_shards);
15233        type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
15234        let schema_cols = ctx.columns;
15235        let alias = ctx.table_alias;
15236        let mysql = ctx.mysql_dialect;
15237        let style = ctx.render_style;
15238        let plan_ref = &plan;
15239        let results = r.run_shards(n_shards, &|si| {
15240            let lo = si * chunk;
15241            let hi = ((si + 1) * chunk).min(filtered.len());
15242            let mut sctx = eval::EvalContext::new(schema_cols, alias);
15243            sctx.mysql_dialect = mysql;
15244            sctx.render_style = style;
15245            // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
15246            // compiled programs); each shard rebuilds it, which also
15247            // recompiles against the shard's own context. Build errors
15248            // were already surfaced by the outer build above.
15249            let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
15250                Ok(p) => p,
15251                Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
15252            };
15253            let mut run = || -> ShardOut {
15254                let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
15255                let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
15256                for (i, row) in filtered[lo..hi].iter().enumerate() {
15257                    let expanded =
15258                        expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
15259                    sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
15260                    o.extend(expanded);
15261                }
15262                Ok((o, sidx))
15263            };
15264            alloc::boxed::Box::new(run())
15265        });
15266        for boxed in results {
15267            let shard = boxed
15268                .downcast::<ShardOut>()
15269                .expect("runner echoes the closure's box");
15270            let (o, sidx) = (*shard)?;
15271            out.extend(o);
15272            src.extend(sidx);
15273        }
15274        return Ok((out, src));
15275    }
15276    for (i, row) in filtered.iter().enumerate() {
15277        let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
15278        src.extend(core::iter::repeat_n(i, expanded.len()));
15279        out.extend(expanded);
15280    }
15281    Ok((out, src))
15282}
15283
15284/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
15285///
15286/// A key that names a select-list item reads it out of the EXPANDED row,
15287/// because PG sorts after the expansion. A key that names a source column the
15288/// query does not project is evaluated against the input row that output row
15289/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
15290fn srf_order_key(
15291    ob: &spg_sql::ast::OrderBy,
15292    out_col: Option<usize>,
15293    out: &Row<'static>,
15294    src: &Row<'static>,
15295    ctx: &EvalContext<'_>,
15296) -> Result<Value<'static>, EngineError> {
15297    match out_col {
15298        Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
15299        None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
15300    }
15301}
15302
15303fn expand_srf_row_with(
15304    engine: &Engine,
15305    plan: &mut SrfPlan,
15306    projection: &[ProjectedItem],
15307    row: &Row<'static>,
15308    ctx: &EvalContext<'_>,
15309) -> Result<Vec<Row<'static>>, EngineError> {
15310    let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
15311    for n in &plan.nodes {
15312        lists.push(engine.srf_values(n, row, ctx)?);
15313    }
15314    let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
15315    // Only the slots' element types depend on the row; the names and the
15316    // input schema around them do not.
15317    for (slot, list) in lists.iter().enumerate() {
15318        plan.ext_cols[plan.base_cols + slot].ty = list
15319            .iter()
15320            .find_map(|v| v.data_type())
15321            .unwrap_or(DataType::Text);
15322    }
15323    let mut ext_ctx = ctx.clone();
15324    ext_ctx.columns = &plan.ext_cols;
15325    let mut out = Vec::with_capacity(n_rows);
15326    // v7.39 (round 726) — the base columns are the SAME for every
15327    // expanded row; clone them once and rewrite only the SRF slots per
15328    // k. The old form cloned the whole input row per OUTPUT row — for
15329    // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
15330    // TEXT column the projection never reads.
15331    let base_len = row.values.len();
15332    let mut ext_vals = row.values.clone();
15333    ext_vals.resize(base_len + lists.len(), Value::Null);
15334    let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15335    for k in 0..n_rows {
15336        for (slot, list) in lists.iter().enumerate() {
15337            // Past the end of THIS srf's rows → NULL (PG pads).
15338            ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
15339        }
15340        let ext_row = Row::new(core::mem::take(&mut ext_vals));
15341        let mut vals = Vec::with_capacity(projection.len());
15342        for (i, p) in projection.iter().enumerate() {
15343            // v7.39 (round 743) — compiled when possible; the
15344            // interpreter for the rest, with its exact wording.
15345            vals.push(match &plan.compiled[i] {
15346                Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
15347                    .map_err(EngineError::Eval)?,
15348                None => {
15349                    let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
15350                    eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
15351                }
15352            });
15353        }
15354        ext_vals = ext_row.values;
15355        out.push(Row::new(vals));
15356    }
15357    Ok(out)
15358}
15359
15360/// The one-shot spelling, for the callers that expand a single row.
15361/// v7.39 (round 600) — which output column each ORDER BY key names, for a
15362/// query whose target list contains a set-returning function.
15363///
15364/// The keys used to be built from the INPUT row, before the SRF expanded, so
15365/// anything that named the SRF's own output was evaluated as a scalar call:
15366/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
15367/// "function unnest(integer[]) does not exist", and so did the spellings that
15368/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
15369/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
15370/// back in input order. PG sorts AFTER the expansion, so a key that names a
15371/// select-list item reads that item's value out of the expanded row.
15372///
15373/// `None` keeps the key on the input row, which is where an ORDER BY naming
15374/// a column the query does not project has to be evaluated.
15375/// v7.38.19 — the output column an ORDER BY term reads, when reading it
15376/// is provably the same as building a key from the input row.
15377///
15378/// A sort key is a COPY of the sort column, made because the source row
15379/// is gone by the time the sort runs — only the projection survives. On
15380/// `SELECT s_long FROM t ORDER BY s_long` that copy is of data the
15381/// projected row already holds, and on 400,000 rows of 192-character
15382/// text it is 400,000 allocations, 400,000 frees and 77 MB of copying.
15383/// A profile of that cell put the allocator at 2,025 leaf samples of the
15384/// working set, second only to the comparison chain.
15385///
15386/// The condition is narrow on purpose. `srf_order_output_cols` resolves
15387/// an ORDER BY term the way SQL does — a positional ordinal, or a name
15388/// matching the select list — and SQL resolves against the select list
15389/// BEFORE the input columns. The key path resolves against the INPUT
15390/// columns. For `SELECT g AS id … ORDER BY id` on a table that also has
15391/// an `id`, those are different columns, and swapping one for the other
15392/// would change answers rather than timings.
15393///
15394/// So this takes only the case where the two cannot disagree: a bare
15395/// unqualified column name, matching exactly one output item, whose own
15396/// expression is that same column. The projected cell then IS the input
15397/// cell, and the key would have been its copy.
15398/// True when comparing two of this column's VALUES gives the same order
15399/// as comparing the sort KEYS built from them.
15400///
15401/// It does not hold widely. A user ENUM stores its label as text but
15402/// orders by DECLARATION position; an array orders element-wise; a
15403/// domain or composite carries its own rules. For those the two paths
15404/// answer differently, and a sort that skipped the key would silently
15405/// reorder the result. This is the short list where they agree.
15406fn value_order_is_key_order(col: &ColumnSchema) -> bool {
15407    use spg_storage::DataType as T;
15408    col.user_enum_type.is_none()
15409        && col.user_domain_type.is_none()
15410        && col.user_composite_type.is_none()
15411        && col.collation_name.is_none()
15412        && col.collation == spg_storage::Collation::Binary
15413        && matches!(
15414            col.ty,
15415            T::SmallInt | T::Int | T::BigInt | T::Text | T::Varchar(_) | T::Bool | T::Uuid
15416        )
15417}
15418
15419/// The full ORDER BY comparison between two rows, named by index.
15420///
15421/// v7.38.19 — what a permutation sort falls back to when its key ties.
15422fn row_cmp_by_index(
15423    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15424    terms: &[(usize, bool, Option<bool>)],
15425    colls: &[Option<crate::collate::Collated>],
15426    mysql: bool,
15427    ia: u32,
15428    ib: u32,
15429) -> core::cmp::Ordering {
15430    let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
15431    for (i, (col, desc, nf)) in terms.iter().enumerate() {
15432        let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col)) else {
15433            continue;
15434        };
15435        let ord = match (va, vb) {
15436            (Value::Text(x), Value::Text(y)) => match colls.get(i).and_then(Option::as_ref) {
15437                Some(c) => {
15438                    let o = c.compare(x, y);
15439                    if *desc { o.reverse() } else { o }
15440                }
15441                None if !mysql => {
15442                    let o = crate::orderby::str_cmp_prefix_first(x, y);
15443                    if *desc { o.reverse() } else { o }
15444                }
15445                None => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15446            },
15447            _ => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15448        };
15449        if ord != core::cmp::Ordering::Equal {
15450            return ord;
15451        }
15452    }
15453    core::cmp::Ordering::Equal
15454}
15455
15456/// Whether ordering these rows by BYTES is what the collation in force
15457/// would have answered anyway.
15458///
15459/// v7.38.19 — a collated sort used to be shut out of the keyed path
15460/// entirely, and the cost of that showed up the moment the byte path
15461/// got fast: on the same fixture, the same binary took 92 ms under `C`
15462/// and 371 ms under `en_US`, so declaring a collation had become a
15463/// four-fold tax on a query that sorts md5 hex.
15464///
15465/// It need not be. For several locales `[0-9a-z]` orders exactly as
15466/// bytes do -- `collate::ascii_byte_order` carries that fact, and the
15467/// test beside it re-derives the whole allowlist by sorting a corpus
15468/// twice rather than asserting it. So when the collation is one of
15469/// those AND every value in every sort column is drawn from that
15470/// alphabet, the byte answer IS the collated answer.
15471///
15472/// Both halves are required. A collation outside the list can put `z`
15473/// between `s` and `t`; a value outside the alphabet can be `Ápple`,
15474/// which no locale in the list orders by its bytes. Either one and this
15475/// returns false, and the sort takes the collator's own path.
15476fn byte_order_answers_the_collation(
15477    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15478    terms: &[(usize, bool, Option<bool>)],
15479    colls: &[Option<crate::collate::Collated>],
15480) -> bool {
15481    if colls.iter().all(Option::is_none) {
15482        return true;
15483    }
15484    if !colls
15485        .iter()
15486        .flatten()
15487        .all(crate::collate::Collated::ascii_byte_order)
15488    {
15489        return false;
15490    }
15491    tagged.iter().all(|(_, row)| {
15492        terms.iter().all(|(col, _, _)| match row.values.get(*col) {
15493            // Only TEXT is collation-sensitive; a number or a NULL
15494            // orders the same under every collation there is.
15495            Some(Value::Text(t)) => crate::collate::is_ascii_alnum_lower(t),
15496            _ => true,
15497        })
15498    })
15499}
15500
15501/// An eight-byte key for each row's sort column, paired with the row's
15502/// index — or `None` when the column cannot give one on every row.
15503///
15504/// v7.38.19 — the pair is what the sort array holds instead of the row.
15505/// Two kinds of column can supply it:
15506///
15507///   * an INTEGER, whose whole value fits. Flipping the sign bit maps
15508///     the signed order onto the unsigned one, so the key is EXACT and
15509///     a comparison never has to look at the row at all.
15510///   * TEXT, as the first eight bytes big-endian, zero-padded. That
15511///     orders the same as the string — two that differ inside those
15512///     bytes differ at the same index either way, and one shorter than
15513///     eight pads with zeros exactly where `[u8]`'s own comparison runs
15514///     out — but it is a PREFIX, so equal keys must still ask the full
15515///     comparator.
15516///
15517/// The `None` is the safety of it: a NULL or any other type has no
15518/// faithful eight-byte key, so such a column takes the ordinary path
15519/// rather than being given a made-up one.
15520/// The prefix keys for a sort, at the width the DATA asks for.
15521///
15522/// v7.40.1 — the width used to be eight bytes for every text column, and
15523/// the panel's two text cells priced both halves of that choice against
15524/// PostgreSQL 18.6, in memory on both legs, 400,000 rows:
15525///
15526/// ```text
15527///   short text (9 bytes, shared prefix)    SPG 96.6   PG 71.0   1.36x behind
15528///   long text (192 bytes, byte 0 decides)  SPG 70.9   PG 72.4   parity
15529/// ```
15530///
15531/// `'k' || lpad(n, 8, '0')` is nine bytes, so an eight-byte prefix drops
15532/// the last digit: ten rows share every key, forty thousand tie-runs
15533/// each fall back to the full comparator, and each of those reads at
15534/// random into a 400,000-element array. The md5 column decides on byte
15535/// zero and never ties, which is why only one of the two cells lost.
15536///
15537/// Widened to sixteen bytes and measured -- same window, two binaries
15538/// named by md5, order digests identical:
15539///
15540/// ```text
15541///   short text   104.9 -> 56.9 ms   1.84x faster (and 0.80x of PG)
15542///   long text     74.9 -> 90.7 ms   1.21x SLOWER
15543/// ```
15544///
15545/// So a fixed width is the wrong shape either way: `(u128, u32)` is 32
15546/// bytes against `(u64, u32)`'s 16, and a column that already decided on
15547/// byte zero pays double the sort's memory traffic for eight bytes it
15548/// never reads. That is the tax a shared hot path levies on the workload
15549/// it does not help.
15550///
15551/// The width comes from the longest value instead, which is exact and
15552/// free -- it is one pass the loop below already makes. Every value at
15553/// sixteen bytes or under makes the wide key the WHOLE key, so `exact`
15554/// is true and the tie fallback with its random reads disappears
15555/// altogether; anything longer keeps the narrow key and pays nothing.
15556enum PrefixKeys {
15557    Narrow(Vec<(u64, u32)>, bool),
15558    Wide(Vec<(u128, u32)>, bool),
15559}
15560
15561fn sort_keys_of(
15562    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15563    col: usize,
15564) -> Option<PrefixKeys> {
15565    let n = u32::try_from(tagged.len()).ok()?;
15566    let is_text = match tagged.first()?.1.values.get(col)? {
15567        Value::Text(_) => true,
15568        Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => false,
15569        _ => return None,
15570    };
15571    if !is_text {
15572        let mut out: Vec<(u64, u32)> = Vec::with_capacity(tagged.len());
15573        for (i, row) in (0..n).zip(tagged.iter()) {
15574            let key = match row.1.values.get(col) {
15575                Some(Value::SmallInt(v)) => (i64::from(*v) as u64) ^ (1 << 63),
15576                Some(Value::Int(v)) => (i64::from(*v) as u64) ^ (1 << 63),
15577                Some(Value::BigInt(v)) => (*v as u64) ^ (1 << 63),
15578                _ => return None,
15579            };
15580            out.push((key, i));
15581        }
15582        return Some(PrefixKeys::Narrow(out, true));
15583    }
15584    // One pass, and it answers both questions: the bytes of every key,
15585    // and whether the longest of them fits the wide one.
15586    let mut wide: Vec<(u128, u32)> = Vec::with_capacity(tagged.len());
15587    let mut longest = 0usize;
15588    for (i, row) in (0..n).zip(tagged.iter()) {
15589        let Some(Value::Text(t)) = row.1.values.get(col) else {
15590            return None;
15591        };
15592        let bytes = t.as_bytes();
15593        longest = longest.max(bytes.len());
15594        let mut k = [0u8; 16];
15595        let take = bytes.len().min(16);
15596        k[..take].copy_from_slice(&bytes[..take]);
15597        wide.push((u128::from_be_bytes(k), i));
15598    }
15599    if longest <= 16 {
15600        return Some(PrefixKeys::Wide(wide, true));
15601    }
15602    // Longer than the wide key: the narrow one costs half the memory
15603    // traffic and decides exactly as much, since neither is the whole
15604    // value. Built from the wide keys rather than reading the rows again.
15605    let narrow = wide
15606        .into_iter()
15607        .map(|(k, i)| ((k >> 64) as u64, i))
15608        .collect();
15609    Some(PrefixKeys::Narrow(narrow, false))
15610}
15611
15612/// Sort a prefix-key permutation, whatever the key's width.
15613///
15614/// v7.40.1 -- extracted so the two widths share one body. `low_card`
15615/// keeps the run-at-a-time shortcut and `exact` keeps the "a tie means
15616/// the values are equal" one; both are the caller's to decide.
15617struct PrefixSort {
15618    /// The first ORDER BY term is descending.
15619    first_desc: bool,
15620    /// The key does not discriminate, so sort it and settle each run of
15621    /// equal keys in one pass instead of n log n comparisons.
15622    low_card: bool,
15623    /// The key IS the value, so a tie means the values are equal.
15624    exact: bool,
15625    /// One ORDER BY term, so nothing else can speak after a tie.
15626    single_term: bool,
15627    /// v7.40.4 — what the two parallelism GUCs say. See `crate::parsort`.
15628    workers: crate::parsort::Workers,
15629}
15630
15631fn sort_prefix_permutation<K: Copy + Ord + Send + Sync>(
15632    mut order: Vec<(K, u32)>,
15633    how: &PrefixSort,
15634    row_cmp: &(dyn Fn(u32, u32) -> core::cmp::Ordering + Sync),
15635    same_value: &dyn Fn(u32, u32) -> bool,
15636) -> Vec<u32> {
15637    let PrefixSort {
15638        first_desc,
15639        low_card,
15640        exact,
15641        single_term,
15642        workers,
15643    } = *how;
15644    if low_card {
15645        // Integer sort first, then one pass per run.
15646        order = crate::parsort::sort_total(
15647            order,
15648            workers,
15649            &|&(pa, ia): &(K, u32), &(pb, ib): &(K, u32)| {
15650                let c = pa.cmp(&pb);
15651                let c = if first_desc { c.reverse() } else { c };
15652                c.then_with(|| ia.cmp(&ib))
15653            },
15654        );
15655        let mut lo = 0;
15656        while lo < order.len() {
15657            let mut hi = lo + 1;
15658            while hi < order.len() && order[hi].0 == order[lo].0 {
15659                hi += 1;
15660            }
15661            if hi - lo > 1 {
15662                let head = order[lo].1;
15663                let uniform = order[lo + 1..hi].iter().all(|&(_, i)| same_value(head, i));
15664                if !uniform {
15665                    order[lo..hi]
15666                        .sort_by(|&(_, ia), &(_, ib)| row_cmp(ia, ib).then_with(|| ia.cmp(&ib)));
15667                }
15668                // A uniform run is already in index order, which IS the
15669                // stable answer.
15670            }
15671            lo = hi;
15672        }
15673    } else {
15674        order = crate::parsort::sort_total(
15675            order,
15676            workers,
15677            &|&(pa, ia): &(K, u32), &(pb, ib): &(K, u32)| {
15678                let c = pa.cmp(&pb);
15679                let c = if first_desc { c.reverse() } else { c };
15680                if c != core::cmp::Ordering::Equal {
15681                    return c;
15682                }
15683                // An EXACT key that ties means the values are equal, so only
15684                // the remaining terms can speak. A prefix that ties has
15685                // decided nothing yet and the first term must be asked again,
15686                // which `row_cmp` does by walking every term from the start.
15687                if exact && single_term {
15688                    return ia.cmp(&ib);
15689                }
15690                row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
15691            },
15692        );
15693    }
15694    order.into_iter().map(|(_, i)| i).collect()
15695}
15696
15697/// Whether a PREFIX key is worth sorting a permutation on.
15698///
15699/// v7.38.19 — it is not always, and the panel says so in one cell. The
15700/// `text (26 values)` fixture is two hundred identical characters drawn
15701/// from twenty-six letters, so every eight-byte prefix inside a letter
15702/// is the same and 15,000 rows tie on it. Each tie then pays the prefix
15703/// compare, a two-hundred-byte comparison, AND a random read into a
15704/// 400,000-element array — while sorting the rows in place keeps the
15705/// partition contiguous. Measured: 160 ms sorting rows, 247 ms sorting
15706/// the permutation, on the very fixture built to be degenerate.
15707///
15708/// So the permutation is taken when the key DECIDES, and a sample says
15709/// whether it does. An exact key always decides; a prefix has to earn
15710/// it.
15711fn key_discriminates<K: Copy + Ord>(keys: &[(K, u32)]) -> bool {
15712    const SAMPLE: usize = 1024;
15713    let step = (keys.len() / SAMPLE).max(1);
15714    let mut seen: Vec<K> = keys
15715        .iter()
15716        .step_by(step)
15717        .take(SAMPLE)
15718        .map(|&(k, _)| k)
15719        .collect();
15720    let taken = seen.len();
15721    if taken < 8 {
15722        return true;
15723    }
15724    seen.sort_unstable();
15725    seen.dedup();
15726    seen.len() * 2 >= taken
15727}
15728
15729fn order_by_output_cols_if_identical(
15730    order_by: &[spg_sql::ast::OrderBy],
15731    projection: &[ProjectedItem],
15732    schema_cols: &[ColumnSchema],
15733) -> Option<Vec<usize>> {
15734    if order_by.is_empty() {
15735        return None;
15736    }
15737    let mut out = Vec::with_capacity(order_by.len());
15738    for ob in order_by {
15739        let Expr::Column(c) = &ob.expr else {
15740            return None;
15741        };
15742        if c.qualifier.is_some() {
15743            return None;
15744        }
15745        let mut hit = None;
15746        for (i, p) in projection.iter().enumerate() {
15747            if !p.output_name.eq_ignore_ascii_case(&c.name) {
15748                continue;
15749            }
15750            if hit.is_some() {
15751                return None; // ambiguous — SQL would reject it too
15752            }
15753            // The item must BE that column, not merely be named for it.
15754            let Expr::Column(pc) = &p.expr else {
15755                return None;
15756            };
15757            if !pc.name.eq_ignore_ascii_case(&c.name) {
15758                return None;
15759            }
15760            let sc = schema_cols
15761                .iter()
15762                .find(|s| s.name.eq_ignore_ascii_case(&c.name))?;
15763            if !value_order_is_key_order(sc) {
15764                return None;
15765            }
15766            hit = Some(i);
15767        }
15768        out.push(hit?);
15769    }
15770    Some(out)
15771}
15772
15773fn srf_order_output_cols(
15774    order_by: &[spg_sql::ast::OrderBy],
15775    projection: &[ProjectedItem],
15776) -> Vec<Option<usize>> {
15777    order_by
15778        .iter()
15779        .map(|ob| {
15780            // A positive ordinal is the Nth output column, directly.
15781            // `resolve_positional_order_by` deliberately leaves an ordinal
15782            // pointing at a set-returning item alone — copying the call into
15783            // ORDER BY would have made the key "the whole set" back when keys
15784            // came from the input row. Reading the expanded row's column is
15785            // what it should have meant, and is what this does.
15786            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
15787                && *n >= 1
15788                && let Ok(idx) = usize::try_from(*n - 1)
15789                && idx < projection.len()
15790            {
15791                return Some(idx);
15792            }
15793            // An unqualified name matching exactly one output name. SQL
15794            // resolves ORDER BY against the select list first, so this wins
15795            // over an input column of the same name — which is the whole
15796            // point of `SELECT g AS id … ORDER BY id`.
15797            if let Expr::Column(c) = &ob.expr
15798                && c.qualifier.is_none()
15799            {
15800                let mut hit = None;
15801                for (i, p) in projection.iter().enumerate() {
15802                    if p.output_name.eq_ignore_ascii_case(&c.name) {
15803                        if hit.is_some() {
15804                            hit = None;
15805                            break;
15806                        }
15807                        hit = Some(i);
15808                    }
15809                }
15810                if hit.is_some() {
15811                    return hit;
15812                }
15813            }
15814            // Or the same expression as a select-list item — which is what
15815            // `ORDER BY 1` becomes once `resolve_positional_order_by` has
15816            // run, and what a repeated `ORDER BY unnest(…)` is.
15817            projection.iter().position(|p| p.expr == ob.expr)
15818        })
15819        .collect()
15820}
15821
15822fn expand_srf_row(
15823    engine: &Engine,
15824    projection: &[ProjectedItem],
15825    srf_idxs: &[usize],
15826    row: &Row<'static>,
15827    ctx: &EvalContext<'_>,
15828) -> Result<Vec<Row<'static>>, EngineError> {
15829    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15830    expand_srf_row_with(engine, &mut plan, projection, row, ctx)
15831}
15832
15833impl Engine {
15834    /// The rows one target-list SRF yields for an input row. `None` from
15835    /// `srf_target_idxs` means the expression is not set-returning at all.
15836    fn srf_values(
15837        &self,
15838        expr: &spg_sql::ast::Expr,
15839        row: &Row<'static>,
15840        ctx: &EvalContext<'_>,
15841    ) -> Result<Vec<Value<'static>>, EngineError> {
15842        if top_level_srf_kind(expr).is_some() {
15843            return top_level_srf_output(expr, row, ctx);
15844        }
15845        // A user set-returning function. Its body runs through the real
15846        // executor, like every function body since round 63.
15847        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
15848            return Err(EngineError::Unsupported(
15849                "expected a SELECT-list SRF call".into(),
15850            ));
15851        };
15852        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15853        for a in args {
15854            vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
15855        }
15856        let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
15857        // v7.39 (read01 round 68) — in a target list a multi-column function is
15858        // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
15859        // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
15860        // what it is for. A single-column function contributes its bare value.
15861        Ok(rows
15862            .into_iter()
15863            .map(|r| {
15864                if r.values.len() == 1 {
15865                    r.values.into_iter().next().unwrap_or(Value::Null)
15866                } else {
15867                    Value::Composite(
15868                        cols.iter()
15869                            .map(|c| c.name.clone())
15870                            .zip(r.values)
15871                            .collect::<alloc::vec::Vec<_>>(),
15872                    )
15873                }
15874            })
15875            .collect())
15876    }
15877
15878    /// Is THIS node a set-returning call: one of the builtin kinds, or a user
15879    /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
15880    fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
15881        if is_top_level_unnest(e) {
15882            return true;
15883        }
15884        let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
15885            return false;
15886        };
15887        self.active_catalog().functions_named(name).iter().any(|f| {
15888            let r = f.returns.trim().to_ascii_uppercase();
15889            r.starts_with("SETOF") || r.starts_with("TABLE(")
15890        })
15891    }
15892
15893    /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
15894    fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
15895        let mut found = false;
15896        let mut probe = e.clone();
15897        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
15898            if self.is_srf_node(n) {
15899                found = true;
15900                return true;
15901            }
15902            false
15903        });
15904        found
15905    }
15906
15907    /// Which projection items CONTAIN a set-returning call. Before round 78 this
15908    /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
15909    /// ordinary scalar call all the way down to the function dispatcher, which
15910    /// then reported `unnest` as an unknown function.
15911    fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
15912        projection
15913            .iter()
15914            .enumerate()
15915            .filter(|(_, p)| self.expr_contains_srf(&p.expr))
15916            .map(|(i, _)| i)
15917            .collect()
15918    }
15919}
15920
15921impl Engine {
15922    /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
15923    /// no `(f(args)).*` item.
15924    fn lower_record_expansion(
15925        &self,
15926        stmt: &SelectStatement,
15927    ) -> Result<Option<SelectStatement>, EngineError> {
15928        use spg_sql::ast::{Expr, SelectItem};
15929        let is_marker = |it: &SelectItem| {
15930            matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
15931                if name == "__record_expand")
15932        };
15933        if !stmt.items.iter().any(is_marker) {
15934            return Ok(None);
15935        }
15936        let mut out = stmt.clone();
15937        let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
15938        let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
15939        for (n, item) in stmt.items.iter().enumerate() {
15940            if !is_marker(item) {
15941                items.push(item.clone());
15942                continue;
15943            }
15944            let SelectItem::Expr {
15945                expr: Expr::FunctionCall { args, .. },
15946                ..
15947            } = item
15948            else {
15949                unreachable!("checked by is_marker");
15950            };
15951            let Some(Expr::FunctionCall {
15952                name: fname,
15953                args: fargs,
15954            }) = args.first()
15955            else {
15956                return Err(EngineError::Unsupported(
15957                    "(<expr>).* expands a function's record — it needs a function call".into(),
15958                ));
15959            };
15960            let cols = self.setof_declared_columns(fname)?;
15961            let alias = alloc::format!("__rec{n}");
15962            let mut tref = bare_table_ref_named(&alias);
15963            tref.table_fn_call = Some(alloc::boxed::Box::new((
15964                fname.to_ascii_lowercase(),
15965                fargs.clone(),
15966            )));
15967            tref.alias = Some(alias.clone());
15968            lateral_refs.push(tref);
15969            for c in cols {
15970                items.push(SelectItem::Expr {
15971                    expr: Expr::Column(spg_sql::ast::ColumnName {
15972                        qualifier: Some(alias.clone()),
15973                        name: c,
15974                    }),
15975                    alias: None,
15976                });
15977            }
15978        }
15979        out.items = items;
15980        // The function joins the FROM. With no FROM it BECOMES the FROM; with one
15981        // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
15982        // (the arguments may reference the outer row — the round-69 correlation).
15983        for tref in lateral_refs {
15984            match &mut out.from {
15985                None => {
15986                    out.from = Some(spg_sql::ast::FromClause {
15987                        primary: tref,
15988                        joins: alloc::vec::Vec::new(),
15989                    });
15990                }
15991                Some(from) => from.joins.push(spg_sql::ast::FromJoin {
15992                    kind: spg_sql::ast::JoinKind::Cross,
15993                    table: tref,
15994                    on: None,
15995                    using_cols: None,
15996                    natural: false,
15997                }),
15998            }
15999        }
16000        Ok(Some(out))
16001    }
16002
16003    /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
16004    /// v text)` names them; a `SETOF <scalar>` is one column named after the
16005    /// function.
16006    fn setof_declared_columns(
16007        &self,
16008        name: &str,
16009    ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
16010        let cat = self.active_catalog();
16011        let overloads = cat.functions_named(name);
16012        let def = overloads.first().ok_or_else(|| {
16013            EngineError::Unsupported(alloc::format!("function {name} does not exist"))
16014        })?;
16015        let declared = def.returns.trim();
16016        let upper = declared.to_ascii_uppercase();
16017        if upper.starts_with("TABLE(") {
16018            let raw = &declared["TABLE(".len()..declared.len() - 1];
16019            return Ok(raw
16020                .split(',')
16021                .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
16022                .collect());
16023        }
16024        Ok(alloc::vec![name.to_string()])
16025    }
16026}
16027
16028/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
16029/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
16030/// COLUMNS list (data-independent), NESTED children inlined in
16031/// declaration order (PG's flattened output shape).
16032/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
16033/// correlated JSON_TABLE's static schema without evaluating its doc.
16034pub(crate) fn json_table_schema_pub(
16035    cols: &[spg_sql::ast::JsonTableColumn],
16036) -> alloc::vec::Vec<ColumnSchema> {
16037    json_table_schema(cols)
16038}
16039
16040fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
16041    use spg_sql::ast::JsonTableColumn as C;
16042    let mut out = alloc::vec::Vec::new();
16043    for c in cols {
16044        match c {
16045            C::Ordinality { name } => {
16046                out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
16047            }
16048            C::Regular {
16049                name, ty, exists, ..
16050            } => {
16051                let dt = if *exists {
16052                    DataType::Bool
16053                } else {
16054                    crate::conversions::column_type_to_data_type(*ty)
16055                };
16056                out.push(ColumnSchema::new(name.clone(), dt, true));
16057            }
16058            C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
16059        }
16060    }
16061    out
16062}
16063
16064/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
16065/// JSON_TABLE column's declared type (the DEFAULT expr may be a
16066/// string literal like `'none'` that must land as the column type).
16067fn coerce_json_table_default(
16068    v: Value<'static>,
16069    ty: spg_sql::ast::ColumnTypeName,
16070    name: &str,
16071) -> Result<Value<'static>, EngineError> {
16072    if v.is_null() {
16073        return Ok(Value::Null);
16074    }
16075    let dt = crate::conversions::column_type_to_data_type(ty);
16076    crate::conversions::coerce_value(v, dt, name, 0)
16077}
16078
16079/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
16080fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
16081    use crate::json::JsonValue as J;
16082    match v {
16083        Value::Null => J::Null,
16084        Value::Bool(b) => J::Bool(*b),
16085        Value::SmallInt(n) => J::Number(f64::from(*n)),
16086        Value::Int(n) => J::Number(f64::from(*n)),
16087        Value::BigInt(n) => J::Number(*n as f64),
16088        Value::Float(x) => J::Number(*x),
16089        Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
16090        other => J::String(crate::eval::value_to_text(other)),
16091    }
16092}
16093
16094fn bare_table_ref_named(name: &str) -> TableRef {
16095    TableRef {
16096        name: name.to_string(),
16097        alias: None,
16098        only: false,
16099        as_of_segment: None,
16100        unnest_expr: None,
16101        unnest_column_aliases: alloc::vec::Vec::new(),
16102        with_ordinality: false,
16103        generate_series_args: None,
16104        lateral_subquery: None,
16105        jsonb_each_text_arg: None,
16106        table_fn_call: None,
16107        rows_from: None,
16108        json_table: None,
16109        scalar_fn_item: false,
16110    }
16111}
16112
16113impl Engine {
16114    /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
16115    /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
16116    /// entries are the array-able SRFs, already lowered by the parser into their
16117    /// scalar array form.
16118    fn rows_from_rows(
16119        &self,
16120        primary: &TableRef,
16121    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
16122        let entries = primary
16123            .rows_from
16124            .as_ref()
16125            .expect("caller guards rows_from.is_some()");
16126        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
16127        let ctx = self.ev_ctx(&empty, None);
16128        let dummy = Row::new(alloc::vec::Vec::new());
16129        let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
16130        let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
16131        for (name, args) in entries {
16132            let (vals, colname) = if name == "__array" {
16133                // The parser lowered this one to `<array expr>`; its rows are the
16134                // array's elements.
16135                let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
16136                (
16137                    array_value_to_elements(&arr)?,
16138                    alloc::string::String::from("unnest"),
16139                )
16140            } else {
16141                let call = spg_sql::ast::Expr::FunctionCall {
16142                    name: name.clone(),
16143                    args: args.clone(),
16144                };
16145                (self.srf_values(&call, &dummy, &ctx)?, name.clone())
16146            };
16147            let ty = vals
16148                .first()
16149                .and_then(spg_storage::Value::data_type)
16150                .unwrap_or(DataType::Text);
16151            cols.push(ColumnSchema::new(colname, ty, true));
16152            lists.push(vals);
16153        }
16154        let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
16155        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
16156        for k in 0..n {
16157            let mut vals: alloc::vec::Vec<Value<'static>> =
16158                alloc::vec::Vec::with_capacity(lists.len() + 1);
16159            for l in &lists {
16160                vals.push(l.get(k).cloned().unwrap_or(Value::Null));
16161            }
16162            rows.push(Row::new(vals));
16163        }
16164        if primary.with_ordinality {
16165            cols.push(ColumnSchema::new(
16166                "ordinality".to_string(),
16167                DataType::BigInt,
16168                false,
16169            ));
16170            rows = rows
16171                .into_iter()
16172                .enumerate()
16173                .map(|(i, r)| {
16174                    let mut v = r.values;
16175                    v.push(Value::BigInt(i as i64 + 1));
16176                    Row::new(v)
16177                })
16178                .collect();
16179        }
16180        Ok((rows, cols))
16181    }
16182}
16183
16184/// v7.39 (round 232) — PG names the offending set operation in its
16185/// arity / type-mismatch messages ("each UNION query must have the same
16186/// number of columns"). `UNION ALL` is still spelled UNION there.
16187fn set_op_name(kind: UnionKind) -> &'static str {
16188    match kind {
16189        UnionKind::All | UnionKind::Distinct => "UNION",
16190        UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
16191        UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
16192    }
16193}
16194
16195/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
16196/// type: a bare string or NULL literal that no context has typed yet. SPG
16197/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
16198/// be the syntax. A wildcard or a non-literal expression is never unknown.
16199/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
16200/// LABELS as text (the wire render) but the value is an oid-carrying
16201/// dual, so a UNION with a numeric column must not be refused on the
16202/// label (pg_dump: `SELECT classid … UNION ALL SELECT
16203/// 'pg_opfamily'::regclass …`).
16204fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
16205    fn is_regcast(e: &Expr) -> bool {
16206        matches!(
16207            e,
16208            Expr::Cast {
16209                target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
16210                ..
16211            }
16212        )
16213    }
16214    stmt.items
16215        .iter()
16216        .map(|item| match item {
16217            SelectItem::Expr { expr, .. } => is_regcast(expr),
16218            _ => false,
16219        })
16220        .collect()
16221}
16222
16223fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
16224    stmt.items
16225        .iter()
16226        .map(|item| match item {
16227            SelectItem::Expr { expr, .. } => matches!(
16228                expr,
16229                Expr::Literal(spg_sql::ast::Literal::String(_))
16230                    | Expr::Literal(spg_sql::ast::Literal::Null)
16231            ),
16232            _ => false,
16233        })
16234        .collect()
16235}
16236
16237/// v7.39 (round 233) — retype one branch column's cells, reporting the
16238/// conversion failure the way PG does rather than leaving the column
16239/// half-converted. Used when the other branch typed an untyped literal.
16240fn coerce_branch_column(
16241    rows: &mut [Row<'static>],
16242    col_idx: usize,
16243    target: DataType,
16244    col_name: &str,
16245) -> Result<(), EngineError> {
16246    for row in rows.iter_mut() {
16247        let Some(slot) = row.values.get_mut(col_idx) else {
16248            continue;
16249        };
16250        if matches!(slot, Value::Null) {
16251            continue;
16252        }
16253        *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
16254    }
16255    Ok(())
16256}
16257
16258/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
16259/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
16260/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
16261/// reference to q's output columns substituted by the underlying column.
16262///
16263/// Admission is deliberately narrow — anything that changes cardinality,
16264/// order, or scope stays on the materialising path:
16265/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
16266///   FROM with no ordinality or positional column aliases, and no
16267///   subquery anywhere its expressions (an inner scope could reference
16268///   q too — descending is a later knife);
16269/// * inner: one stored table, bare-column projection only, no
16270///   CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
16271/// * every outer column reference must resolve inside q's output list —
16272///   a name that does not is an ERROR today, and flattening would
16273///   silently legalise it against the base table.
16274fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16275    use spg_sql::ast::SelectItem;
16276    let inner = primary.lateral_subquery.as_deref()?;
16277    // Outer shape.
16278    if !stmt.ctes.is_empty()
16279        || !stmt.unions.is_empty()
16280        || stmt.distinct
16281        || !stmt.distinct_on.is_empty()
16282        || !stmt.window_check_exprs.is_empty()
16283        || stmt.locking.is_some()
16284        || primary.with_ordinality
16285        || !primary.unnest_column_aliases.is_empty()
16286    {
16287        return None;
16288    }
16289    // Inner shape.
16290    if !inner.ctes.is_empty()
16291        || !inner.unions.is_empty()
16292        || inner.distinct
16293        || !inner.distinct_on.is_empty()
16294        || inner.group_by.is_some()
16295        || inner.group_by_all
16296        || inner.having.is_some()
16297        || !inner.order_by.is_empty()
16298        || inner.limit.is_some()
16299        || inner.offset.is_some()
16300        || !inner.window_check_exprs.is_empty()
16301        || inner.locking.is_some()
16302    {
16303        return None;
16304    }
16305    let ifrom = inner.from.as_ref()?;
16306    let it = &ifrom.primary;
16307    if !ifrom.joins.is_empty()
16308        || it.name.is_empty()
16309        || it.lateral_subquery.is_some()
16310        || it.unnest_expr.is_some()
16311        || it.generate_series_args.is_some()
16312        || it.as_of_segment.is_some()
16313        || it.jsonb_each_text_arg.is_some()
16314        || it.table_fn_call.is_some()
16315        || it.rows_from.is_some()
16316        || it.json_table.is_some()
16317        || it.with_ordinality
16318        || !it.unnest_column_aliases.is_empty()
16319    {
16320        return None;
16321    }
16322    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16323        return None;
16324    }
16325    // The output map: q's visible name -> the underlying column.
16326    let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
16327    let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
16328        alloc::collections::BTreeMap::new();
16329    for item in &inner.items {
16330        let SelectItem::Expr { expr, alias } = item else {
16331            return None;
16332        };
16333        let Expr::Column(c) = expr else {
16334            return None;
16335        };
16336        if let Some(q) = c.qualifier.as_deref()
16337            && !q.eq_ignore_ascii_case(&inner_alias)
16338        {
16339            return None;
16340        }
16341        let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
16342        // A duplicated output name would make substitution ambiguous.
16343        if map
16344            .insert(out_name.to_ascii_lowercase(), c.clone())
16345            .is_some()
16346        {
16347            return None;
16348        }
16349    }
16350    if map.is_empty() {
16351        return None;
16352    }
16353    let derived_alias = primary
16354        .alias
16355        .clone()
16356        .unwrap_or_else(|| primary.name.clone())
16357        .to_ascii_lowercase();
16358    // Substitute in a clone; bail (None) on the first reference the map
16359    // cannot answer.
16360    let mut out = stmt.clone();
16361    let ok = core::cell::Cell::new(true);
16362    let mut subst = |e: &mut Expr| -> bool {
16363        match e {
16364            Expr::Column(c) => {
16365                match c.qualifier.as_deref() {
16366                    Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
16367                    None => {}
16368                    Some(_) => {
16369                        ok.set(false);
16370                        return true;
16371                    }
16372                }
16373                match map.get(&c.name.to_ascii_lowercase()) {
16374                    Some(target) => *c = target.clone(),
16375                    None => ok.set(false),
16376                }
16377                true
16378            }
16379            // Any subquery could reference q from its own scope;
16380            // descending is a later knife — bail for now.
16381            Expr::ScalarSubquery(_)
16382            | Expr::Exists { .. }
16383            | Expr::InSubquery { .. }
16384            | Expr::RowInSubquery { .. }
16385            | Expr::RowCmpSubquery { .. } => {
16386                ok.set(false);
16387                true
16388            }
16389            _ => false,
16390        }
16391    };
16392    for item in &mut out.items {
16393        match item {
16394            SelectItem::Expr { expr, .. } => {
16395                crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
16396            }
16397            // `SELECT * FROM (…) q` means q's columns, in q's order.
16398            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
16399        }
16400    }
16401    if let Some(w) = &mut out.where_ {
16402        crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
16403    }
16404    if let Some(gs) = &mut out.group_by {
16405        for g in gs {
16406            crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
16407        }
16408    }
16409    if let Some(h) = &mut out.having {
16410        crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
16411    }
16412    for o in &mut out.order_by {
16413        crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
16414    }
16415    for d in &mut out.distinct_on {
16416        crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
16417    }
16418    if !ok.get() {
16419        return None;
16420    }
16421    // FROM becomes the stored table; the filters conjoin.
16422    out.from = Some(spg_sql::ast::FromClause {
16423        primary: it.clone(),
16424        joins: Vec::new(),
16425    });
16426    out.where_ = match (inner.where_.clone(), out.where_.take()) {
16427        (Some(a), Some(b)) => Some(Expr::Binary {
16428            lhs: alloc::boxed::Box::new(a),
16429            op: spg_sql::ast::BinOp::And,
16430            rhs: alloc::boxed::Box::new(b),
16431        }),
16432        (Some(a), None) => Some(a),
16433        (None, b) => b,
16434    };
16435    Some(out)
16436}
16437
16438/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
16439/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
16440/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
16441/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
16442/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
16443/// DISTINCT, an SRF, or an unprovable inner shape stays put.
16444fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16445    use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
16446    let inner = primary.lateral_subquery.as_deref()?;
16447    // Outer: exactly `SELECT count(*)`, nothing else.
16448    if !stmt.ctes.is_empty()
16449        || !stmt.unions.is_empty()
16450        || stmt.distinct
16451        || !stmt.distinct_on.is_empty()
16452        || stmt.where_.is_some()
16453        || stmt.group_by.is_some()
16454        || stmt.having.is_some()
16455        || !stmt.order_by.is_empty()
16456        || stmt.limit.is_some()
16457        || stmt.offset.is_some()
16458        || stmt.items.len() != 1
16459    {
16460        return None;
16461    }
16462    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16463        return None;
16464    };
16465    let E::FunctionCall { name, args } = expr else {
16466        return None;
16467    };
16468    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16469        return None;
16470    }
16471    // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
16472    let Some(LimitExpr::Literal(k)) = &inner.offset else {
16473        return None;
16474    };
16475    let k = i64::from(*k);
16476    if inner.limit.is_some() || inner.order_by.is_empty() {
16477        return None;
16478    }
16479    let mut counted = inner.clone();
16480    counted.order_by = Vec::new();
16481    counted.offset = None;
16482    // The stripped inner must now be a provable simple shape (its
16483    // items become irrelevant — count(*) reads none of them — but an
16484    // SRF item would change the row count, so the flatten predicate's
16485    // scrutiny still applies).
16486    let base = matview_flatten_probe(&counted)?;
16487    let mut out = stmt.clone();
16488    out.items = alloc::vec![SelectItem::Expr {
16489        expr: E::FunctionCall {
16490            name: String::from("greatest"),
16491            args: alloc::vec![
16492                E::Binary {
16493                    lhs: alloc::boxed::Box::new(E::FunctionCall {
16494                        name: String::from("count_star"),
16495                        args: alloc::vec![],
16496                    }),
16497                    op: spg_sql::ast::BinOp::Sub,
16498                    rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16499                },
16500                E::Literal(spg_sql::ast::Literal::Integer(0)),
16501            ],
16502        },
16503        alias: Some(String::from("count")),
16504    }];
16505    out.from = Some(spg_sql::ast::FromClause {
16506        primary: base,
16507        joins: Vec::new(),
16508    });
16509    out.where_ = counted.where_.clone();
16510    Some(out)
16511}
16512
16513/// The inner-shape probe `try_count_over_offset` shares with the
16514/// flatten: single stored table, no modifiers, no subqueries, no SRF
16515/// items. Returns the base TableRef.
16516fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
16517    use spg_sql::ast::SelectItem;
16518    if !inner.ctes.is_empty()
16519        || !inner.unions.is_empty()
16520        || inner.distinct
16521        || !inner.distinct_on.is_empty()
16522        || inner.group_by.is_some()
16523        || inner.group_by_all
16524        || inner.having.is_some()
16525        || !inner.order_by.is_empty()
16526        || inner.limit.is_some()
16527        || inner.offset.is_some()
16528        || !inner.window_check_exprs.is_empty()
16529        || inner.locking.is_some()
16530    {
16531        return None;
16532    }
16533    let ifrom = inner.from.as_ref()?;
16534    let it = &ifrom.primary;
16535    if !ifrom.joins.is_empty()
16536        || it.name.is_empty()
16537        || it.lateral_subquery.is_some()
16538        || it.unnest_expr.is_some()
16539        || it.generate_series_args.is_some()
16540        || it.as_of_segment.is_some()
16541        || it.jsonb_each_text_arg.is_some()
16542        || it.table_fn_call.is_some()
16543        || it.rows_from.is_some()
16544        || it.json_table.is_some()
16545        || it.with_ordinality
16546    {
16547        return None;
16548    }
16549    for item in &inner.items {
16550        match item {
16551            SelectItem::Expr { expr, .. } => {
16552                if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
16553                    return None;
16554                }
16555            }
16556            SelectItem::Wildcard => {}
16557            SelectItem::QualifiedWildcard(_) => return None,
16558        }
16559    }
16560    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16561        return None;
16562    }
16563    Some(it.clone())
16564}
16565
16566/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
16567/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
16568/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
16569/// constant-LENGTH array literal unnests to exactly k rows per input
16570/// row (NULL elements are rows too). One SRF item only, elements
16571/// subquery-free, and the stripped inner must pass the same probe the
16572/// count-over-offset rewrite uses.
16573fn try_count_over_const_unnest(
16574    stmt: &SelectStatement,
16575    primary: &TableRef,
16576) -> Option<SelectStatement> {
16577    use spg_sql::ast::{Expr as E, SelectItem};
16578    let inner = primary.lateral_subquery.as_deref()?;
16579    if !stmt.ctes.is_empty()
16580        || !stmt.unions.is_empty()
16581        || stmt.distinct
16582        || !stmt.distinct_on.is_empty()
16583        || stmt.where_.is_some()
16584        || stmt.group_by.is_some()
16585        || stmt.having.is_some()
16586        || !stmt.order_by.is_empty()
16587        || stmt.limit.is_some()
16588        || stmt.offset.is_some()
16589        || stmt.items.len() != 1
16590    {
16591        return None;
16592    }
16593    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16594        return None;
16595    };
16596    let E::FunctionCall { name, args } = expr else {
16597        return None;
16598    };
16599    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16600        return None;
16601    }
16602    // Inner: exactly one item, and it is unnest(ARRAY[...]).
16603    if inner.items.len() != 1
16604        || !inner.order_by.is_empty()
16605        || inner.limit.is_some()
16606        || inner.offset.is_some()
16607    {
16608        return None;
16609    }
16610    let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
16611        return None;
16612    };
16613    let E::FunctionCall {
16614        name: fname,
16615        args: fargs,
16616    } = item
16617    else {
16618        return None;
16619    };
16620    if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
16621        return None;
16622    }
16623    let E::Array(elems) = &fargs[0] else {
16624        return None;
16625    };
16626    if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
16627        return None;
16628    }
16629    let k = elems.len() as i64;
16630    // The stripped inner (the SRF item replaced by a plain constant)
16631    // must be the provable simple shape.
16632    let mut counted = inner.clone();
16633    counted.items = alloc::vec![SelectItem::Expr {
16634        expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
16635        alias: None,
16636    }];
16637    let base = matview_flatten_probe(&counted)?;
16638    let mut out = stmt.clone();
16639    out.items = alloc::vec![SelectItem::Expr {
16640        expr: E::Binary {
16641            lhs: alloc::boxed::Box::new(E::FunctionCall {
16642                name: String::from("count_star"),
16643                args: alloc::vec![],
16644            }),
16645            op: spg_sql::ast::BinOp::Mul,
16646            rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16647        },
16648        alias: Some(String::from("count")),
16649    }];
16650    out.from = Some(spg_sql::ast::FromClause {
16651        primary: base,
16652        joins: Vec::new(),
16653    });
16654    out.where_ = counted.where_.clone();
16655    Some(out)
16656}