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(&mut tagged, &descs, &colls);
824        }
825        let mut out_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
826        // v7.37 D.41 — `SELECT DISTINCT` over a window projection: the window
827        // pipeline builds one output row per input row, so DISTINCT must dedup the
828        // projected rows (PG evaluates window functions before DISTINCT). Applied
829        // after ORDER BY (duplicate rows share sort keys, so order is preserved)
830        // and before LIMIT.
831        if stmt.distinct {
832            // v7.38.14 — see the synthetic-source sites below: the mask was
833            // always available here, from the same projection this function
834            // already built.
835            out_rows = dedup_rows(
836                out_rows,
837                FoldSpec::of_masks(
838                    self.speaks_mysql,
839                    &fold_mask(&projection),
840                    &pad_mask(&projection),
841                ),
842            );
843        }
844        apply_offset_and_limit(&mut out_rows, stmt.offset_literal(), stmt.limit_literal());
845        let final_cols: Vec<ColumnSchema> = projection
846            .into_iter()
847            .map(|p| p.to_column_schema())
848            .collect();
849        Ok(QueryResult::Rows {
850            columns: final_cols,
851            rows: out_rows,
852        })
853    }
854
855    /// v4.11: materialise each CTE into a temp table inside a
856    /// cloned catalog, then run the body SELECT against a fresh
857    /// engine instance that owns the enriched catalog. The clone
858    /// is moderately expensive — only paid by CTE-bearing queries.
859    /// Subqueries inside CTE bodies / the main body resolve as
860    /// usual; `clock_fn` is propagated so `NOW()` lines up.
861    /// v7.16.2 — mailrs round-10 A.3. Materialise the
862    /// `information_schema.*` / `pg_catalog.*` virtual views
863    /// the SELECT references, then re-execute the SELECT
864    /// against an enriched catalog where those views are real
865    /// tables. Same pattern as `exec_with_ctes`. The temp
866    /// engine carries `meta_views_materialised = true` so its
867    /// own meta-dispatch short-circuits — without that we'd
868    /// infinite-recurse since the temp catalog's view name
869    /// still starts with `__spg_info_` and re-triggers the
870    /// check.
871    pub(crate) fn exec_select_with_meta_views(
872        &self,
873        stmt: &SelectStatement,
874        cancel: CancelToken<'_>,
875    ) -> Result<QueryResult, EngineError> {
876        let catalog = self.meta_view_catalog(stmt)?;
877        let mut temp = Engine::restore(catalog);
878        if let Some(c) = self.clock {
879            temp = temp.with_clock(c);
880        }
881        if let Some(f) = self.salt_fn {
882            temp = temp.with_salt_fn(f);
883        }
884        // v7.39 (round 522) — the temp engine holds the materialised
885        // catalog and, until now, nothing of the SESSION. So every
886        // session-scoped answer changed the moment a system view
887        // appeared in the FROM clause: `SELECT current_user` said
888        // `unmei` and `SELECT current_user FROM pg_class` said `admin`;
889        // `current_setting('work_mem')` fell back to the boot default
890        // after a SET; `application_name` read empty. A privilege check
891        // written against a catalog join was reading a different
892        // identity than the same check written without one.
893        //
894        // Carry what a session can be observed through — its parameters
895        // (which is also where the session user lives), the role store
896        // the privilege builtins read, the dialect, and the rendering
897        // settings a timestamp is spelled with.
898        temp.session_params.clone_from(&self.session_params);
899        temp.users.clone_from(&self.users);
900        temp.backslash_escapes = self.backslash_escapes;
901        temp.speaks_mysql = self.speaks_mysql;
902        temp.mysql_strict = self.mysql_strict;
903        temp.render_style = self.render_style;
904        temp.tz_offset_fn = self.tz_offset_fn;
905        temp.tz_localize_fn = self.tz_localize_fn;
906        temp.tz_abbrev_fn = self.tz_abbrev_fn;
907        temp.meta_views_materialised = true;
908        temp.exec_select_cancel(stmt, cancel)
909    }
910
911    /// v7.39 (round 462) — the catalog a meta-view SELECT resolves
912    /// against: this engine's catalog with every `__spg_*` view the
913    /// statement references materialised into it.
914    ///
915    /// Split out of `exec_select_with_meta_views` so Describe can reach
916    /// the same shapes execution reaches. Describe used to look the FROM
917    /// relation up in the plain catalog, where a system view does not
918    /// exist, and reported "no columns" for every one of them — so an
919    /// extended-protocol client reading `pg_stat_user_tables` got rows
920    /// with no column metadata. Sharing the materialisation means a
921    /// view added here is described correctly the day it is added.
922    pub(crate) fn meta_view_catalog(&self, stmt: &SelectStatement) -> Result<Catalog, EngineError> {
923        let mut needed: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
924        collect_meta_view_names(stmt, &mut needed);
925        let mut catalog = self.active_catalog().clone();
926        for view in &needed {
927            if catalog.get(view).is_some() {
928                continue;
929            }
930            match view.as_str() {
931                "__spg_info_columns" => {
932                    let (schema, rows) = synth_information_schema_columns(
933                        self.active_catalog(),
934                        self.speaks_mysql,
935                        &self.mysql_schema_name(),
936                    );
937                    materialise_meta_view(&mut catalog, view, schema, rows)?;
938                }
939                "__spg_info_tables" => {
940                    let (schema, rows) = synth_information_schema_tables(
941                        self.active_catalog(),
942                        self.speaks_mysql,
943                        &self.mysql_schema_name(),
944                    );
945                    materialise_meta_view(&mut catalog, view, schema, rows)?;
946                }
947                "__spg_pg_class" => {
948                    let (schema, rows) = synth_pg_class(
949                        self.active_catalog(),
950                        i64::try_from(self.vacuum_oldest_active()).unwrap_or(i64::MAX),
951                    );
952                    materialise_meta_view(&mut catalog, view, schema, rows)?;
953                }
954                "__spg_pg_attribute" => {
955                    let (schema, rows) = synth_pg_attribute(self.active_catalog());
956                    materialise_meta_view(&mut catalog, view, schema, rows)?;
957                }
958                // v7.17.0 Phase 3.P0-50 — pg_catalog.pg_type for
959                // sqlx / SQLAlchemy / Diesel / pgAdmin lookups.
960                "__spg_pg_type" => {
961                    let (schema, rows) = synth_pg_type(self.active_catalog());
962                    materialise_meta_view(&mut catalog, view, schema, rows)?;
963                }
964                // v7.39 (round 621) — pg_catalog.pg_operator, which did not
965                // exist at all.
966                "__spg_pg_operator" => {
967                    let (schema, rows) = synth_pg_operator(self.active_catalog());
968                    materialise_meta_view(&mut catalog, view, schema, rows)?;
969                }
970                // v7.17.0 Phase 3.P0-51 — pg_catalog.pg_proc for
971                // function-name introspection (ORM / pgAdmin).
972                "__spg_pg_proc" => {
973                    let (schema, rows) = synth_pg_proc(self.active_catalog());
974                    materialise_meta_view(&mut catalog, view, schema, rows)?;
975                }
976                // v7.24 (round-16 D) — pg_catalog.pg_trigger. The
977                // round-16 "why doesn't prod fire the trigger"
978                // question was unanswerable because triggers had NO
979                // introspection surface; tgname/tgenabled plus the
980                // pragmatic relname/timing/events/function columns
981                // make "is it registered and enabled" a one-liner.
982                "__spg_pg_trigger" => {
983                    let (schema, rows) = synth_pg_trigger(self.active_catalog());
984                    materialise_meta_view(&mut catalog, view, schema, rows)?;
985                }
986                // v7.17.0 Phase 3.P0-52 — pg_catalog.pg_namespace
987                // (schema list for admin tools' tree views).
988                "__spg_pg_namespace" => {
989                    let (schema, rows) = synth_pg_namespace(self.active_catalog());
990                    materialise_meta_view(&mut catalog, view, schema, rows)?;
991                }
992                // v7.39 — pg_tables convenience view (was a pgwire
993                // canned response that ignored projections).
994                "__spg_pg_tables" => {
995                    let (schema, rows) =
996                        crate::system_catalog::synth_pg_tables(self.active_catalog());
997                    materialise_meta_view(&mut catalog, view, schema, rows)?;
998                }
999                // v7.37.24 (24.1) — pg_catalog.pg_enum (label list
1000                // for ENUM types; sqlx / ORM enum codecs read this).
1001                "__spg_pg_enum" => {
1002                    let (schema, rows) =
1003                        crate::system_catalog::synth_pg_enum(self.active_catalog());
1004                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1005                }
1006                // v7.37.21 (21.13) — pg_catalog.pg_replication_slots
1007                // (shape-stable empty until 21.12 persists slot state).
1008                // v7.39 (round 277) — session-scoped prepared statements.
1009                "__spg_pg_prepared_statements" => {
1010                    let (schema, rows) = crate::system_catalog::synth_pg_prepared_statements(
1011                        &self.prepared_statements,
1012                    );
1013                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1014                }
1015                "__spg_pg_replication_slots" => {
1016                    let (schema, rows) =
1017                        crate::system_catalog::synth_pg_replication_slots(self.active_catalog());
1018                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1019                }
1020                // v7.37.21 (21.13-b) — pg_catalog.pg_publication
1021                // (one row per CREATE PUBLICATION).
1022                "__spg_pg_publication" => {
1023                    let (schema, rows) = crate::system_catalog::synth_pg_publication(self);
1024                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1025                }
1026                // v7.37.21 (21.13-c) — pg_catalog.pg_subscription
1027                // (one row per CREATE SUBSCRIPTION; subconninfo
1028                // redacted so dashboards can't leak credentials).
1029                "__spg_pg_subscription" => {
1030                    let (schema, rows) = crate::system_catalog::synth_pg_subscription(self);
1031                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1032                }
1033                // v7.37.22 (22.x-stat-db) — pg_catalog.pg_stat_database
1034                // (one row for SPG's single database; counters are
1035                // shape-stable 0 until wiring lands).
1036                "__spg_pg_stat_database" => {
1037                    let (schema, rows) = crate::system_catalog::synth_pg_stat_database(
1038                        self,
1039                        self.stat_tup_inserted,
1040                        self.stat_tup_updated,
1041                        self.stat_tup_deleted,
1042                    );
1043                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1044                }
1045                // v7.37.22 (22.14) — pg_catalog.pg_stat_user_tables
1046                // (per-table churn counters; live_tup = row count).
1047                "__spg_pg_stat_user_tables" => {
1048                    // r192 — DML counters come from the engine-side
1049                    // non-transactional map, not the (tx-shadowed)
1050                    // catalog tables.
1051                    let (schema, rows) = crate::system_catalog::synth_pg_stat_user_tables(
1052                        self.active_catalog(),
1053                        &self.table_write_stats,
1054                    );
1055                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1056                }
1057                // v7.37.22 (22.15) — pg_catalog.pg_stat_user_indexes
1058                // (per-index usage counters; flag unused indexes).
1059                "__spg_pg_stat_user_indexes" => {
1060                    let (schema, rows) =
1061                        crate::system_catalog::synth_pg_stat_user_indexes(self.active_catalog());
1062                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1063                }
1064                // v7.37.22 (22.16) — pg_catalog.pg_stat_bgwriter.
1065                "__spg_pg_stat_bgwriter" => {
1066                    let (schema, rows) =
1067                        crate::system_catalog::synth_pg_stat_bgwriter(self.active_catalog());
1068                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1069                }
1070                // v7.38 (read01 P3.14) — pg_catalog.pg_stat_checkpointer /
1071                // pg_stat_wal shell views (shape-stable, counters pending).
1072                "__spg_pg_stat_checkpointer" => {
1073                    let (schema, rows) =
1074                        crate::system_catalog::synth_pg_stat_checkpointer(self.active_catalog());
1075                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1076                }
1077                "__spg_pg_stat_wal" => {
1078                    let (schema, rows) =
1079                        crate::system_catalog::synth_pg_stat_wal(self.active_catalog());
1080                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1081                }
1082                // v7.38 (read01 P3.15) — pg_catalog.pg_stat_slru /
1083                // pg_stat_subscription_stats shell views.
1084                "__spg_pg_stat_slru" => {
1085                    let (schema, rows) =
1086                        crate::system_catalog::synth_pg_stat_slru(self.active_catalog());
1087                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1088                }
1089                "__spg_pg_stat_subscription_stats" => {
1090                    let (schema, rows) = crate::system_catalog::synth_pg_stat_subscription_stats(
1091                        self.active_catalog(),
1092                    );
1093                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1094                }
1095                // v7.37.22 (22.17) — pg_catalog.pg_stat_archiver.
1096                "__spg_pg_stat_archiver" => {
1097                    let (schema, rows) =
1098                        crate::system_catalog::synth_pg_stat_archiver(self.active_catalog());
1099                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1100                }
1101                // v7.37.21 (21.13-d) — pg_catalog.pg_stat_replication.
1102                "__spg_pg_stat_replication" => {
1103                    let (schema, rows) =
1104                        crate::system_catalog::synth_pg_stat_replication(self.active_catalog());
1105                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1106                }
1107                // v7.37.24 (24.13) — pg_catalog.pg_am.
1108                "__spg_pg_am" => {
1109                    let (schema, rows) = crate::system_catalog::synth_pg_am(self.active_catalog());
1110                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1111                }
1112                // v7.37.22 (22.18) — pg_catalog.pg_stat_io (PG 16+).
1113                "__spg_pg_stat_io" => {
1114                    let (schema, rows) =
1115                        crate::system_catalog::synth_pg_stat_io(self.active_catalog());
1116                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1117                }
1118                // v7.37.22 (22.19) — pg_catalog.pg_stat_user_functions.
1119                "__spg_pg_stat_user_functions" => {
1120                    let (schema, rows) =
1121                        crate::system_catalog::synth_pg_stat_user_functions(self.active_catalog());
1122                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1123                }
1124                // v7.39 (round 287) — pg_catalog.pg_largeobject{,_metadata}.
1125                "__spg_pg_largeobject" => {
1126                    let (schema, rows) =
1127                        crate::system_catalog::synth_pg_largeobject(self.active_catalog());
1128                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1129                }
1130                "__spg_pg_largeobject_metadata" => {
1131                    let (schema, rows) =
1132                        crate::system_catalog::synth_pg_largeobject_metadata(self.active_catalog());
1133                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1134                }
1135                // v7.37.23 (23.7-a) — pg_catalog.pg_statistic_ext.
1136                "__spg_pg_statistic_ext" => {
1137                    let (schema, rows) =
1138                        crate::system_catalog::synth_pg_statistic_ext(self.active_catalog());
1139                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1140                }
1141                // v7.38.18 — pg_catalog.pg_stats, the readable view.
1142                "__spg_pg_stats" => {
1143                    let (schema, rows) = crate::system_catalog::synth_pg_stats(
1144                        self.active_catalog(),
1145                        &self.statistics,
1146                    );
1147                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1148                }
1149                // v7.37.24 (24.15) — pg_catalog.pg_statistic.
1150                "__spg_pg_statistic" => {
1151                    let (schema, rows) = crate::system_catalog::synth_pg_statistic(
1152                        self.active_catalog(),
1153                        &self.statistics,
1154                    );
1155                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1156                }
1157                // v7.37.22 (22.20) — pg_catalog.pg_stat_progress_vacuum.
1158                "__spg_pg_stat_progress_vacuum" => {
1159                    let (schema, rows) =
1160                        crate::system_catalog::synth_pg_stat_progress_vacuum(self.active_catalog());
1161                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1162                }
1163                // v7.37.22 (22.21) — pg_catalog.pg_stat_progress_create_index.
1164                "__spg_pg_stat_progress_create_index" => {
1165                    let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_create_index(
1166                        self.active_catalog(),
1167                    );
1168                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1169                }
1170                // v7.37.22 (22.22) — pg_catalog.pg_stat_progress_analyze.
1171                "__spg_pg_stat_progress_analyze" => {
1172                    let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_analyze(
1173                        self.active_catalog(),
1174                    );
1175                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1176                }
1177                // v7.37.24 (24.16) — pg_catalog.pg_inherits
1178                // (partition parent → child OID mapping).
1179                "__spg_pg_inherits" => {
1180                    let (schema, rows) =
1181                        crate::system_catalog::synth_pg_inherits(self.active_catalog());
1182                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1183                }
1184                // v7.39 (round 650) — the text-search catalogs, filled
1185                // with what SPG actually has rather than PG's thirty.
1186                "__spg_pg_ts_config_map" => {
1187                    let (schema, rows) =
1188                        crate::system_catalog::synth_pg_ts_config_map(self.active_catalog());
1189                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1190                }
1191                "__spg_pg_ts_config" => {
1192                    let (schema, rows) =
1193                        crate::system_catalog::synth_pg_ts_config(self.active_catalog());
1194                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1195                }
1196                "__spg_pg_ts_dict" => {
1197                    let (schema, rows) =
1198                        crate::system_catalog::synth_pg_ts_dict(self.active_catalog());
1199                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1200                }
1201                "__spg_pg_ts_parser" => {
1202                    let (schema, rows) =
1203                        crate::system_catalog::synth_pg_ts_parser(self.active_catalog());
1204                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1205                }
1206                "__spg_pg_ts_template" => {
1207                    let (schema, rows) =
1208                        crate::system_catalog::synth_pg_ts_template(self.active_catalog());
1209                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1210                }
1211                // v7.37.24 (24.17) — pg_catalog.pg_depend
1212                // (dependency graph; shape-stable empty since
1213                // SPG's drop enforcement is per-kind, not per-object).
1214                "__spg_pg_depend" => {
1215                    let (schema, rows) =
1216                        crate::system_catalog::synth_pg_depend(self.active_catalog());
1217                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1218                }
1219                // 7.38.1 S5.1 — pg_catalog.pg_opclass (pg_dump wall #1).
1220                "__spg_pg_opclass" => {
1221                    let (schema, rows) =
1222                        crate::system_catalog::synth_pg_opclass(self.active_catalog());
1223                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1224                }
1225                "__spg_pg_opfamily" => {
1226                    let (schema, rows) =
1227                        crate::system_catalog::synth_pg_opfamily(self.active_catalog());
1228                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1229                }
1230                "__spg_pg_amop" => {
1231                    let (schema, rows) =
1232                        crate::system_catalog::synth_pg_amop(self.active_catalog());
1233                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1234                }
1235                "__spg_pg_amproc" => {
1236                    let (schema, rows) =
1237                        crate::system_catalog::synth_pg_amproc(self.active_catalog());
1238                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1239                }
1240                // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1241                // ORM reflection + pg_dump read the deparsed default text).
1242                "__spg_pg_attrdef" => {
1243                    let (schema, rows) =
1244                        crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1245                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1246                }
1247                // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1248                "__spg_pg_policy" => {
1249                    let (schema, rows) =
1250                        crate::system_catalog::synth_pg_policy(self.active_catalog());
1251                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1252                }
1253                "__spg_pg_policies" => {
1254                    let (schema, rows) =
1255                        crate::system_catalog::synth_pg_policies(self.active_catalog());
1256                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1257                }
1258                // v7.37.24 (24.14) — pg_catalog.pg_collation.
1259                "__spg_pg_collation" => {
1260                    let (schema, rows) =
1261                        crate::system_catalog::synth_pg_collation(self.active_catalog());
1262                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1263                }
1264                // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1265                "__spg_pg_tablespace" => {
1266                    let (schema, rows) =
1267                        crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1268                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1269                }
1270                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1271                // for pgAdmin / DataGrip "indexes per table" listings.
1272                "__spg_pg_indexes" => {
1273                    let (schema, rows) = synth_pg_indexes(self.active_catalog());
1274                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1275                }
1276                // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1277                // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1278                "__spg_pg_description" => {
1279                    let (schema, rows) =
1280                        crate::system_catalog::synth_pg_description(self.active_catalog());
1281                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1282                }
1283                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1284                // for index introspection by ORM compilers.
1285                "__spg_pg_index" => {
1286                    let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1287                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1288                }
1289                // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1290                // for FK / UNIQUE / PK / CHECK introspection.
1291                "__spg_pg_constraint" => {
1292                    let (schema, rows) = synth_pg_constraint(self.active_catalog());
1293                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1294                }
1295                // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1296                // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1297                "__spg_pg_sequence" => {
1298                    let (schema, rows) = synth_pg_sequence(self.active_catalog());
1299                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1300                }
1301                // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1302                // pg_roles / pg_user. SPG is single-database so
1303                // pg_database surfaces just `postgres`; pg_roles
1304                // / pg_user walk the engine's UserStore.
1305                "__spg_pg_database" => {
1306                    let (schema, rows) = synth_pg_database(self);
1307                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1308                }
1309                "__spg_pg_roles" => {
1310                    let (schema, rows) = synth_pg_roles(self);
1311                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1312                }
1313                // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1314                // same roles, with PG's own `use*` column names. It used to
1315                // publish pg_roles' columns under this name.
1316                "__spg_pg_user" => {
1317                    let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1318                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1319                }
1320                // v7.39 (read01 round 58) — role membership.
1321                "__spg_pg_auth_members" => {
1322                    let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1323                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1324                }
1325                // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1326                // pg_views surfaces every CREATE VIEW result; SPG
1327                // ships one row per declared view from the catalog.
1328                "__spg_pg_views" => {
1329                    let (schema, rows) = synth_pg_views(self.active_catalog());
1330                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1331                }
1332                // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1333                // catalogued query-rewrite RULE.
1334                "__spg_pg_rules" => {
1335                    let (schema, rows) =
1336                        crate::system_catalog::synth_pg_rules(self.active_catalog());
1337                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1338                }
1339                // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1340                // catalogue `pg_get_ruledef(oid)` resolves against.
1341                "__spg_pg_rewrite" => {
1342                    let (schema, rows) =
1343                        crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1344                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1345                }
1346                // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1347                // and PG's own column names.
1348                "__spg_pg_matviews" => {
1349                    let (schema, rows) =
1350                        crate::system_catalog::synth_pg_matviews(self.active_catalog());
1351                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1352                }
1353                // pg_catalog.pg_extension — native capability list
1354                // (mailrs embed round-12).
1355                // v7.39 (round 546) — the catalogs SPG has real content
1356                // for, from the facts it already holds.
1357                "__spg_pg_db_role_setting" => {
1358                    let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1359                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1360                }
1361                "__spg_pg_language" => {
1362                    let (schema, rows) = crate::system_catalog::synth_pg_language();
1363                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1364                }
1365                "__spg_pg_sequences" => {
1366                    let (schema, rows) =
1367                        crate::system_catalog::synth_pg_sequences(self.active_catalog());
1368                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1369                }
1370                "__spg_pg_range" => {
1371                    let (schema, rows) = crate::system_catalog::synth_pg_range();
1372                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1373                }
1374                "__spg_pg_partitioned_table" => {
1375                    let (schema, rows) =
1376                        crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1377                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1378                }
1379                "__spg_pg_authid" => {
1380                    let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1381                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1382                }
1383                "__spg_pg_group" => {
1384                    let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1385                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1386                }
1387                "__spg_pg_shadow" => {
1388                    let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1389                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1390                }
1391                // v7.39 (round 544) — pg_cast, probed from the real
1392                // cast implementation.
1393                "__spg_pg_cast" => {
1394                    let (schema, rows) = crate::system_catalog::synth_pg_cast();
1395                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1396                }
1397                // v7.39 (round 541) — an empty catalog that exists.
1398                "__spg_pg_foreign_table" => {
1399                    let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1400                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1401                }
1402                "__spg_pg_extension" => {
1403                    let (schema, rows) = synth_pg_extension();
1404                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1405                }
1406                // v7.39 (round 502) — the timezone catalogues.
1407                "__spg_pg_timezone_names" => {
1408                    let (schema, rows) = synth_pg_timezone_names(self);
1409                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1410                }
1411                "__spg_pg_timezone_abbrevs" => {
1412                    let (schema, rows) = synth_pg_timezone_abbrevs(self);
1413                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1414                }
1415                // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1416                "__spg_pg_settings" => {
1417                    let (schema, rows) = synth_pg_settings(self);
1418                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1419                }
1420                // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1421                // v7.39 (read01 round 51) — information_schema.role_table_grants
1422                // and .table_privileges. Both report the owner's seven implicit
1423                // table privileges; SPG's single role owns everything.
1424                // v7.39 (read01 round 59) — information_schema.column_privileges.
1425                "__spg_info_column_privileges" => {
1426                    let (schema, rows) =
1427                        crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1428                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1429                }
1430                "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1431                    let grantee = self.current_role().to_string();
1432                    let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1433                        self.active_catalog(),
1434                        &grantee,
1435                    );
1436                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1437                }
1438                "__spg_info_key_column_usage" => {
1439                    // v7.39.11 — the session's dialect decides the
1440                    // column list; see the synthesiser.
1441                    let mysql = self.in_mysql_dialect();
1442                    let (schema, rows) = synth_info_key_column_usage(self.active_catalog(), mysql);
1443                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1444                }
1445                // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1446                "__spg_info_referential_constraints" => {
1447                    let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1448                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1449                }
1450                // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1451                "__spg_info_statistics" => {
1452                    let (schema, rows) = synth_info_statistics(self.active_catalog());
1453                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1454                }
1455                // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1456                "__spg_info_routines" => {
1457                    let (schema, rows) = synth_info_routines();
1458                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1459                }
1460                // v7.37.24 (24.3) — information_schema.attributes.
1461                "__spg_info_attributes" => {
1462                    let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1463                        self.active_catalog(),
1464                    );
1465                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1466                }
1467                // v7.37.24 (24.2) — information_schema.domains.
1468                "__spg_info_domains" => {
1469                    let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1470                        self.active_catalog(),
1471                    );
1472                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1473                }
1474                // v7.37.24 (24.9) — information_schema.schemata.
1475                "__spg_info_schemata" => {
1476                    let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1477                        self.active_catalog(),
1478                        self.speaks_mysql,
1479                        &self.listed_database_names(),
1480                    );
1481                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1482                }
1483                // v7.37.24 (24.9) — information_schema.views.
1484                "__spg_info_views" => {
1485                    let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1486                        self.active_catalog(),
1487                    );
1488                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1489                }
1490                // v7.37.24 (24.9) — information_schema.table_constraints.
1491                "__spg_info_table_constraints" => {
1492                    let (schema, rows) =
1493                        crate::system_catalog::synth_information_schema_table_constraints(
1494                            self.active_catalog(),
1495                            self.speaks_mysql,
1496                        );
1497                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1498                }
1499                // v7.37.17 — information_schema.constraint_column_usage.
1500                "__spg_info_constraint_column_usage" => {
1501                    let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1502                        self.active_catalog(),
1503                    );
1504                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1505                }
1506                // v7.37.17 — information_schema.triggers.
1507                "__spg_info_triggers" => {
1508                    let (schema, rows) =
1509                        crate::system_catalog::synth_info_triggers(self.active_catalog());
1510                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1511                }
1512                // v7.37.17 — information_schema.check_constraints.
1513                "__spg_info_check_constraints" => {
1514                    let (schema, rows) =
1515                        crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1516                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1517                }
1518                // v7.37.17 — information_schema.sequences.
1519                "__spg_info_sequences" => {
1520                    let (schema, rows) =
1521                        crate::system_catalog::synth_info_sequences(self.active_catalog());
1522                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1523                }
1524                // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1525                "__spg_mysql_user" => {
1526                    let (schema, rows) = synth_mysql_user(self);
1527                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1528                }
1529                "__spg_mysql_db" => {
1530                    let (schema, rows) = synth_mysql_db();
1531                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1532                }
1533                // v7.39 (round 541) — the catalogs PG has that SPG is
1534                // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1535                other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1536                    let (schema, rows) =
1537                        crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1538                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1539                }
1540                _ => {
1541                    return Err(EngineError::Unsupported(alloc::format!(
1542                        "meta view {view:?} is not yet materialisable; \
1543                         v7.16.2 covers information_schema.columns / .tables \
1544                         and pg_catalog.pg_class / pg_attribute; \
1545                         v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1546                         pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1547                         pg_user / pg_views / pg_matviews / pg_settings"
1548                    )));
1549                }
1550            }
1551        }
1552        Ok(catalog)
1553    }
1554
1555    pub(crate) fn exec_with_ctes(
1556        &self,
1557        stmt: &SelectStatement,
1558        cancel: CancelToken<'_>,
1559    ) -> Result<QueryResult, EngineError> {
1560        cancel.check()?;
1561        // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1562        // bodies are supported here. Writable CTEs on a SELECT
1563        // outer require `&mut self` and route through the
1564        // top-level `exec_select_cancel_mut` entry; sentori
1565        // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1566        // INSERT, not a SELECT, so this restriction is harmless
1567        // in practice.
1568        if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1569            // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1570            // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1571            // of a statement, not nested inside a subquery; this path is
1572            // reached exactly when one is nested. The old text described SPG's
1573            // own executor plumbing ("the top-level mutable entry"), which
1574            // means nothing to a client.
1575            return Err(EngineError::Unsupported(
1576                "WITH clause containing a data-modifying statement must be at the top level".into(),
1577            ));
1578        }
1579        let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1580        // Strip CTEs from the body before running on the temp engine
1581        // so we don't recurse forever.
1582        let mut body = stmt.clone();
1583        body.ctes = Vec::new();
1584        let mut temp = Engine::restore(catalog);
1585        if let Some(c) = self.clock {
1586            temp = temp.with_clock(c);
1587        }
1588        if let Some(f) = self.salt_fn {
1589            temp = temp.with_salt_fn(f);
1590        }
1591        temp.exec_select_cancel(&body, cancel)
1592    }
1593
1594    /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1595    /// `&self` SELECT path. Caller guarantees no modifying CTE
1596    /// bodies are present.
1597    pub(crate) fn materialise_ctes_readonly(
1598        &self,
1599        ctes: &[spg_sql::ast::Cte],
1600        cancel: CancelToken<'_>,
1601    ) -> Result<crate::Catalog, EngineError> {
1602        cancel.check()?;
1603        let mut catalog = self.active_catalog().clone();
1604        for cte in ctes {
1605            let body_select = cte.body.as_select().ok_or_else(|| {
1606                EngineError::Unsupported(alloc::format!(
1607                    "data-modifying CTE not supported on this SELECT entry"
1608                ))
1609            })?;
1610            // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1611            // (PG scoping: the WITH name wins for the outer query and later
1612            // CTEs, while THIS body still sees the real table — a
1613            // non-recursive body's self-name is the table, probe P2). This
1614            // materialiser works on a CLONE, so the shadow is simply: run
1615            // the body against the untouched clone, then drop the real
1616            // table from the clone before installing the CTE's temp. A
1617            // RECURSIVE self-reference is the CTE itself (P6), so there the
1618            // drop happens before the iterating materialiser runs.
1619            let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1620                let synthetic = spg_sql::ast::Cte {
1621                    name: cte.name.clone(),
1622                    body: spg_sql::ast::CteBody::Select(body_select.clone()),
1623                    recursive: true,
1624                    column_overrides: cte.column_overrides.clone(),
1625                    search: None,
1626                    cycle: None,
1627                };
1628                if catalog.get(&cte.name).is_some() {
1629                    let _ = catalog.drop_table(&cte.name);
1630                }
1631                self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1632            } else {
1633                let mut cte_engine = Engine::restore(catalog.clone());
1634                if let Some(c) = self.clock {
1635                    cte_engine = cte_engine.with_clock(c);
1636                }
1637                if let Some(f) = self.salt_fn {
1638                    cte_engine = cte_engine.with_salt_fn(f);
1639                }
1640                let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1641                let QueryResult::Rows { columns, rows } = body_result else {
1642                    return Err(EngineError::Unsupported(alloc::format!(
1643                        "CTE {:?} body did not return rows",
1644                        cte.name
1645                    )));
1646                };
1647                (columns, rows)
1648            };
1649            let inferred = infer_column_types(&columns, &rows);
1650            let mut columns = inferred;
1651            if !cte.column_overrides.is_empty() {
1652                if cte.column_overrides.len() != columns.len() {
1653                    return Err(EngineError::Unsupported(alloc::format!(
1654                        "CTE {:?} column list has {} names but body returns {} columns",
1655                        cte.name,
1656                        cte.column_overrides.len(),
1657                        columns.len()
1658                    )));
1659                }
1660                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1661                    col.name.clone_from(name);
1662                }
1663            }
1664            let schema = TableSchema::new(cte.name.clone(), columns);
1665            // v7.39 (round 156) — the body ran against the untouched clone;
1666            // from here on the CTE name resolves to the temp (PG scoping).
1667            if catalog.get(&cte.name).is_some() {
1668                let _ = catalog.drop_table(&cte.name);
1669            }
1670            catalog.create_table(schema).map_err(EngineError::Storage)?;
1671            let table = catalog
1672                .get_mut(&cte.name)
1673                .expect("just-created CTE table must exist");
1674            for row in rows {
1675                table.insert(row).map_err(EngineError::Storage)?;
1676            }
1677        }
1678        Ok(catalog)
1679    }
1680
1681    /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1682    /// Retained for non-DML callers; the DML path (writable CTE on
1683    /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1684    /// `dml.rs` which installs the CTE temps directly on the
1685    /// active catalog so the outer statement's writes hit real
1686    /// tables.
1687    #[allow(dead_code)]
1688    pub(crate) fn materialise_ctes(
1689        &mut self,
1690        ctes: &[spg_sql::ast::Cte],
1691        cancel: CancelToken<'_>,
1692    ) -> Result<crate::Catalog, EngineError> {
1693        cancel.check()?;
1694        // v7.37.43-T4.4 — modifying CTEs need to write through the
1695        // SAME catalog as the outer statement, not a clone (PG's
1696        // writable CTE puts all modifications in one transaction).
1697        // For the read-only case the original logic cloned, but
1698        // since the outer statement also goes through the cloned
1699        // engine and ALL writes must converge, we now drive the
1700        // accumulator off `self.active_catalog().clone()` and
1701        // commit the modifying writes directly to `self`'s active
1702        // catalog so the surface is consistent.
1703        let mut catalog = self.active_catalog().clone();
1704        // v7.39 (round 149) — a modifying CTE body's target must be a
1705        // real relation, never a sibling CTE (PG: relation does not
1706        // exist); checked before any alias lands in the accumulator.
1707        for cte in ctes {
1708            let body_target = match &cte.body {
1709                spg_sql::ast::CteBody::Select(_) => None,
1710                spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1711                spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1712                spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1713                spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1714            };
1715            if let Some(t) = body_target
1716                && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1717                && catalog.get(t).is_none()
1718            {
1719                return Err(EngineError::Storage(
1720                    spg_storage::StorageError::TableNotFound { name: t.into() },
1721                ));
1722            }
1723        }
1724        for cte in ctes {
1725            if catalog.get(&cte.name).is_some() {
1726                return Err(EngineError::Unsupported(alloc::format!(
1727                    "CTE name {:?} shadows an existing table; rename the CTE",
1728                    cte.name
1729                )));
1730            }
1731            let (columns, rows) = match &cte.body {
1732                // v7.39 (round 145) — see the sibling site: only a body that
1733                // truly self-references takes the iterating materialiser.
1734                spg_sql::ast::CteBody::Select(body)
1735                    if cte.recursive && select_refers_to(body, &cte.name) =>
1736                {
1737                    // Recursive CTE — the existing helper takes a
1738                    // SELECT body and the snapshot catalog.
1739                    let synthetic = spg_sql::ast::Cte {
1740                        name: cte.name.clone(),
1741                        body: spg_sql::ast::CteBody::Select(body.clone()),
1742                        recursive: true,
1743                        column_overrides: cte.column_overrides.clone(),
1744                        search: None,
1745                        cycle: None,
1746                    };
1747                    self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1748                }
1749                spg_sql::ast::CteBody::Select(body) => {
1750                    // v7.25 (round-17) — run against the accumulated
1751                    // catalog so later CTEs can reference earlier
1752                    // ones in the same WITH clause.
1753                    let mut cte_engine = Engine::restore(catalog.clone());
1754                    if let Some(c) = self.clock {
1755                        cte_engine = cte_engine.with_clock(c);
1756                    }
1757                    if let Some(f) = self.salt_fn {
1758                        cte_engine = cte_engine.with_salt_fn(f);
1759                    }
1760                    let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1761                    let QueryResult::Rows { columns, rows } = body_result else {
1762                        return Err(EngineError::Unsupported(alloc::format!(
1763                            "CTE {:?} body did not return rows",
1764                            cte.name
1765                        )));
1766                    };
1767                    (columns, rows)
1768                }
1769                spg_sql::ast::CteBody::Insert(body) => {
1770                    self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1771                }
1772                spg_sql::ast::CteBody::Update(body) => {
1773                    self.exec_modifying_cte_update(&cte.name, body, cancel)?
1774                }
1775                spg_sql::ast::CteBody::Delete(body) => {
1776                    self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1777                }
1778                spg_sql::ast::CteBody::Merge(body) => {
1779                    self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1780                }
1781            };
1782            // v4.22: the projection builder labels any non-column
1783            // expression as Text — including literal SELECT 1.
1784            // Promote each column's type to whatever the rows
1785            // actually carry so the CTE storage table accepts them.
1786            let inferred = infer_column_types(&columns, &rows);
1787            let mut columns = inferred;
1788            if !cte.column_overrides.is_empty() {
1789                if cte.column_overrides.len() != columns.len() {
1790                    return Err(EngineError::Unsupported(alloc::format!(
1791                        "CTE {:?} column list has {} names but body returns {} columns",
1792                        cte.name,
1793                        cte.column_overrides.len(),
1794                        columns.len()
1795                    )));
1796                }
1797                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1798                    col.name.clone_from(name);
1799                }
1800            }
1801            let schema = TableSchema::new(cte.name.clone(), columns);
1802            catalog.create_table(schema).map_err(EngineError::Storage)?;
1803            let table = catalog
1804                .get_mut(&cte.name)
1805                .expect("just-created CTE table must exist");
1806            for row in rows {
1807                table.insert(row).map_err(EngineError::Storage)?;
1808            }
1809        }
1810        Ok(catalog)
1811    }
1812
1813    /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1814    /// against `self` (so the mutation lands in the active catalog
1815    /// inside the current transaction) and captures the RETURNING
1816    /// projection — column schema + rows — to materialise as the
1817    /// CTE alias's table. An INSERT without RETURNING produces a
1818    /// 0-row table with a synthetic single-column placeholder
1819    /// (matches PG: the CTE alias is still defined, but referencing
1820    /// it from the outer query without RETURNING raises a
1821    /// column-resolution error at scan time).
1822    fn exec_modifying_cte_insert(
1823        &mut self,
1824        cte_name: &str,
1825        body: &spg_sql::ast::InsertStatement,
1826        _cancel: CancelToken<'_>,
1827    ) -> Result<
1828        (
1829            Vec<spg_storage::ColumnSchema>,
1830            Vec<spg_storage::Row<'static>>,
1831        ),
1832        EngineError,
1833    > {
1834        // round 151 — a WITH-headed body keeps its own ctes; the body
1835        // statement routes through its writable-CTE entry (outer CTEs
1836        // are never copied into bodies, so no recursion risk).
1837        let body = body.clone();
1838        let result = self.exec_insert(body)?;
1839        match result {
1840            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1841            QueryResult::CommandOk { .. } => {
1842                // No RETURNING — emit a sentinel single-column
1843                // schema with zero rows so the alias is defined.
1844                let placeholder = spg_storage::ColumnSchema::new(
1845                    alloc::format!("{cte_name}_returning_absent"),
1846                    spg_storage::DataType::Text,
1847                    true,
1848                );
1849                Ok((alloc::vec![placeholder], Vec::new()))
1850            }
1851        }
1852    }
1853
1854    /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1855    /// as INSERT above.
1856    fn exec_modifying_cte_update(
1857        &mut self,
1858        cte_name: &str,
1859        body: &spg_sql::ast::UpdateStatement,
1860        cancel: CancelToken<'_>,
1861    ) -> Result<
1862        (
1863            Vec<spg_storage::ColumnSchema>,
1864            Vec<spg_storage::Row<'static>>,
1865        ),
1866        EngineError,
1867    > {
1868        let body = body.clone();
1869        let result = self.exec_update_cancel(&body, cancel)?;
1870        match result {
1871            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1872            QueryResult::CommandOk { .. } => {
1873                let placeholder = spg_storage::ColumnSchema::new(
1874                    alloc::format!("{cte_name}_returning_absent"),
1875                    spg_storage::DataType::Text,
1876                    true,
1877                );
1878                Ok((alloc::vec![placeholder], Vec::new()))
1879            }
1880        }
1881    }
1882
1883    /// v7.37.43-T4.4 — execute a DELETE CTE body.
1884    fn exec_modifying_cte_delete(
1885        &mut self,
1886        cte_name: &str,
1887        body: &spg_sql::ast::DeleteStatement,
1888        cancel: CancelToken<'_>,
1889    ) -> Result<
1890        (
1891            Vec<spg_storage::ColumnSchema>,
1892            Vec<spg_storage::Row<'static>>,
1893        ),
1894        EngineError,
1895    > {
1896        let body = body.clone();
1897        let result = self.exec_delete_cancel(&body, cancel)?;
1898        match result {
1899            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1900            QueryResult::CommandOk { .. } => {
1901                let placeholder = spg_storage::ColumnSchema::new(
1902                    alloc::format!("{cte_name}_returning_absent"),
1903                    spg_storage::DataType::Text,
1904                    true,
1905                );
1906                Ok((alloc::vec![placeholder], Vec::new()))
1907            }
1908        }
1909    }
1910
1911    /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1912    fn exec_modifying_cte_merge(
1913        &mut self,
1914        cte_name: &str,
1915        body: &spg_sql::ast::MergeStatement,
1916        cancel: CancelToken<'_>,
1917    ) -> Result<
1918        (
1919            Vec<spg_storage::ColumnSchema>,
1920            Vec<spg_storage::Row<'static>>,
1921        ),
1922        EngineError,
1923    > {
1924        let body = body.clone();
1925        let result = self.exec_merge_cancel(&body, cancel)?;
1926        match result {
1927            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1928            QueryResult::CommandOk { .. } => {
1929                let placeholder = spg_storage::ColumnSchema::new(
1930                    alloc::format!("{cte_name}_returning_absent"),
1931                    spg_storage::DataType::Text,
1932                    true,
1933                );
1934                Ok((alloc::vec![placeholder], Vec::new()))
1935            }
1936        }
1937    }
1938
1939    /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1940    /// UNION (or UNION ALL) of an anchor that does not reference
1941    /// the CTE name, and one or more recursive terms that do. The
1942    /// anchor runs first; each subsequent iteration runs the
1943    /// recursive term against a temp catalog where the CTE name is
1944    /// bound to the *previous* iteration's output. Iteration stops
1945    /// when the recursive term yields no rows; UNION (DISTINCT)
1946    /// deduplicates against the accumulated result, UNION ALL does
1947    /// not. A hard cap on total rows prevents runaway queries.
1948    #[allow(clippy::too_many_lines)]
1949    pub(crate) fn materialise_recursive_cte(
1950        &self,
1951        cte: &spg_sql::ast::Cte,
1952        base_catalog: &Catalog,
1953        cancel: CancelToken<'_>,
1954    ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1955        const MAX_TOTAL_ROWS: usize = 1_000_000;
1956        const MAX_ITERATIONS: usize = 100_000;
1957        cancel.check()?;
1958        // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1959        // a modifying recursive CTE is parser-rejectable but we
1960        // guard here defensively.
1961        let body_select = cte.body.as_select().ok_or_else(|| {
1962            EngineError::Unsupported(alloc::format!(
1963                "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1964                cte.name
1965            ))
1966        })?;
1967        if body_select.unions.is_empty() {
1968            return Err(EngineError::Unsupported(alloc::format!(
1969                "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1970                cte.name
1971            )));
1972        }
1973        // Anchor: the body's leading SELECT, with unions stripped.
1974        let mut anchor = body_select.clone();
1975        let all_union_terms = core::mem::take(&mut anchor.unions);
1976        anchor.ctes = Vec::new();
1977        // v7.37 D.42 — split the UNION members: those that do NOT reference the
1978        // CTE are additional ANCHOR terms, only the ones that do recurse. A
1979        // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1980        // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1981        // treating the non-recursive `SELECT r2` as a recursive term made it
1982        // re-emit its constant row every iteration → runaway loop.
1983        let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1984            .into_iter()
1985            .partition(|(_, t)| !select_refers_to(t, &cte.name));
1986        let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1987        let QueryResult::Rows {
1988            columns: anchor_cols,
1989            rows: mut anchor_rows,
1990        } = anchor_result
1991        else {
1992            return Err(EngineError::Unsupported(alloc::format!(
1993                "WITH RECURSIVE {:?}: anchor did not return rows",
1994                cte.name
1995            )));
1996        };
1997        // Append every non-recursive UNION member's rows to the anchor set.
1998        for (_, term) in &anchor_terms {
1999            let mut term = term.clone();
2000            term.ctes = Vec::new();
2001            if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
2002                anchor_rows.extend(rows);
2003            }
2004        }
2005        // The projection builder labels non-column expressions Text;
2006        // refine column types from the anchor's actual values so the
2007        // intermediate iter-catalog tables accept them.
2008        let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
2009        if !cte.column_overrides.is_empty() {
2010            if cte.column_overrides.len() != columns.len() {
2011                return Err(EngineError::Unsupported(alloc::format!(
2012                    "CTE {:?} column list has {} names but anchor returns {} columns",
2013                    cte.name,
2014                    cte.column_overrides.len(),
2015                    columns.len()
2016                )));
2017            }
2018            for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
2019                col.name.clone_from(name);
2020            }
2021        }
2022        let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
2023        let mut working_set: Vec<Row<'static>> = anchor_rows;
2024        let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
2025        // Track at least one "all UNION ALL" flag — if every union
2026        // kind is ALL we skip the dedup step (faster + matches PG).
2027        let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
2028        if !all_union_all {
2029            for r in &all_rows {
2030                seen.insert(encode_row_key(r));
2031            }
2032        }
2033        // v7.39 (round 598) — the engine and its catalog are built ONCE.
2034        // Each iteration used to clone the catalog, create the CTE table,
2035        // and construct a whole `Engine` — which initialises 82 fields — to
2036        // hold that round's working set. A counting allocator put the loop
2037        // at 63 allocations and 104 kB per iteration, or 1 GB for a
2038        // 10,000-row recursive CTE, and none of it varied with how much
2039        // else was in the catalog: the per-round rebuild WAS the cost. The
2040        // table is emptied and refilled instead.
2041        let mut iter_catalog = base_catalog.clone();
2042        let schema = TableSchema::new(cte.name.clone(), columns.clone());
2043        iter_catalog
2044            .create_table(schema)
2045            .map_err(EngineError::Storage)?;
2046        let mut iter_engine = Engine::restore(iter_catalog);
2047        if let Some(c) = self.clock {
2048            iter_engine = iter_engine.with_clock(c);
2049        }
2050        if let Some(f) = self.salt_fn {
2051            iter_engine = iter_engine.with_salt_fn(f);
2052        }
2053        // The recursive terms are cloned once too — the clone stripped the
2054        // CTE list off each of them, per term per iteration.
2055        let recursive_terms: Vec<SelectStatement> = union_terms
2056            .iter()
2057            .map(|(_, t)| {
2058                let mut t = t.clone();
2059                t.ctes = Vec::new();
2060                t
2061            })
2062            .collect();
2063        // v7.39 (round 618) — plan every recursive term once. Taken only if
2064        // ALL of them plan, so a query never runs half on each path.
2065        let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2066            .iter()
2067            .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2068            .collect();
2069        let fast_ctx = term_plans.as_ref().map(|plans| {
2070            let alias = plans[0].alias.clone();
2071            (alias, ())
2072        });
2073        for iter in 0..MAX_ITERATIONS {
2074            cancel.check()?;
2075            if working_set.is_empty() {
2076                break;
2077            }
2078            if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2079                // The worktable IS the working set: no table to empty and
2080                // refill, and no query execution per round.
2081                let mut next_set: Vec<Row<'static>> = Vec::new();
2082                for plan in plans {
2083                    let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2084                    for row in &working_set {
2085                        cancel.check()?;
2086                        if let Some(w) = plan.where_ {
2087                            let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2088                            if !matches!(v, Value::Bool(true)) {
2089                                continue;
2090                            }
2091                        }
2092                        let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2093                        for it in &plan.items {
2094                            vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2095                        }
2096                        let out = Row::new(vals);
2097                        if !all_union_all {
2098                            let key = encode_row_key(&out);
2099                            if !seen.insert(key) {
2100                                continue;
2101                            }
2102                        }
2103                        next_set.push(out);
2104                    }
2105                }
2106                if next_set.is_empty() {
2107                    break;
2108                }
2109                all_rows.extend(next_set.iter().cloned());
2110                working_set = next_set;
2111                if all_rows.len() > MAX_TOTAL_ROWS {
2112                    return Err(EngineError::Unsupported(alloc::format!(
2113                        "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2114                        cte.name
2115                    )));
2116                }
2117                if iter + 1 == MAX_ITERATIONS {
2118                    return Err(EngineError::Unsupported(alloc::format!(
2119                        "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2120                        cte.name
2121                    )));
2122                }
2123                continue;
2124            }
2125            {
2126                // Truncated rather than dropped and recreated: the table's
2127                // own structure is what dropping it throws away, and it is
2128                // identical every round.
2129                let cat = iter_engine.base_catalog_mut();
2130                let table = cat.get_mut(&cte.name).expect("created above");
2131                table.truncate();
2132                for row in &working_set {
2133                    table.insert(row.clone()).map_err(EngineError::Storage)?;
2134                }
2135            }
2136            // Run each recursive term in sequence and collect new rows.
2137            let mut next_set: Vec<Row<'static>> = Vec::new();
2138            for term in &recursive_terms {
2139                let r = iter_engine.exec_select_cancel(term, cancel)?;
2140                let QueryResult::Rows {
2141                    columns: rc,
2142                    rows: rs,
2143                } = r
2144                else {
2145                    return Err(EngineError::Unsupported(alloc::format!(
2146                        "WITH RECURSIVE {:?}: recursive term did not return rows",
2147                        cte.name
2148                    )));
2149                };
2150                if rc.len() != columns.len() {
2151                    return Err(EngineError::Unsupported(alloc::format!(
2152                        "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2153                        cte.name,
2154                        rc.len(),
2155                        columns.len()
2156                    )));
2157                }
2158                for row in rs {
2159                    if !all_union_all {
2160                        let key = encode_row_key(&row);
2161                        if !seen.insert(key) {
2162                            continue;
2163                        }
2164                    }
2165                    next_set.push(row);
2166                }
2167            }
2168            if next_set.is_empty() {
2169                break;
2170            }
2171            all_rows.extend(next_set.iter().cloned());
2172            working_set = next_set;
2173            if all_rows.len() > MAX_TOTAL_ROWS {
2174                return Err(EngineError::Unsupported(alloc::format!(
2175                    "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2176                    cte.name
2177                )));
2178            }
2179            if iter + 1 == MAX_ITERATIONS {
2180                return Err(EngineError::Unsupported(alloc::format!(
2181                    "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2182                    cte.name
2183                )));
2184            }
2185        }
2186        Ok((columns, all_rows))
2187    }
2188
2189    pub(crate) fn resolve_select_subqueries(
2190        &self,
2191        stmt: &mut SelectStatement,
2192        cancel: CancelToken<'_>,
2193    ) -> Result<(), EngineError> {
2194        for item in &mut stmt.items {
2195            if let SelectItem::Expr { expr, alias } = item {
2196                // An UNCORRELATED subquery is replaced by its value right
2197                // here, and the shape the column was named for goes with
2198                // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2199                // boolean literal, so SPG answered `?column?` where PG18
2200                // answers `exists`. Only a subquery at the TOP of the item
2201                // loses its name this way — one nested inside a call still
2202                // reports the call.
2203                if alias.is_none()
2204                    && matches!(
2205                        expr,
2206                        Expr::ScalarSubquery(_)
2207                            | Expr::Exists { .. }
2208                            | Expr::InSubquery { .. }
2209                            | Expr::RowInSubquery { .. }
2210                            | Expr::RowCmpSubquery { .. }
2211                    )
2212                {
2213                    *alias = Some(default_output_name(expr, self.speaks_mysql));
2214                }
2215                self.resolve_expr_subqueries(expr, cancel)?;
2216            }
2217        }
2218        if let Some(w) = &mut stmt.where_ {
2219            self.resolve_expr_subqueries(w, cancel)?;
2220        }
2221        // v7.24.1 — JOIN ON conditions can carry subqueries too;
2222        // they were never walked, so even an UNCORRELATED subquery
2223        // in ON hit "subquery reached row eval".
2224        if let Some(from) = &mut stmt.from {
2225            for j in &mut from.joins {
2226                if let Some(on) = &mut j.on {
2227                    self.resolve_expr_subqueries(on, cancel)?;
2228                }
2229            }
2230        }
2231        if let Some(gs) = &mut stmt.group_by {
2232            for g in gs {
2233                self.resolve_expr_subqueries(g, cancel)?;
2234            }
2235        }
2236        if let Some(h) = &mut stmt.having {
2237            self.resolve_expr_subqueries(h, cancel)?;
2238        }
2239        for o in &mut stmt.order_by {
2240            self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2241        }
2242        for (_, peer) in &mut stmt.unions {
2243            self.resolve_select_subqueries(peer, cancel)?;
2244        }
2245        Ok(())
2246    }
2247
2248    #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2249    pub(crate) fn resolve_expr_subqueries(
2250        &self,
2251        e: &mut Expr,
2252        cancel: CancelToken<'_>,
2253    ) -> Result<(), EngineError> {
2254        // Replace-on-this-node cases first.
2255        if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2256            *e = replacement;
2257            return Ok(());
2258        }
2259        match e {
2260            Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } => {
2261                self.resolve_expr_subqueries(expr, cancel)?
2262            }
2263            Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2264            Expr::AggregateOrdered { call, order_by, .. } => {
2265                self.resolve_expr_subqueries(call, cancel)?;
2266                for o in order_by.iter_mut() {
2267                    self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2268                }
2269            }
2270            Expr::Binary { lhs, rhs, .. } => {
2271                self.resolve_expr_subqueries(lhs, cancel)?;
2272                self.resolve_expr_subqueries(rhs, cancel)?;
2273            }
2274            Expr::Unary { expr, .. }
2275            | Expr::Cast { expr, .. }
2276            | Expr::IsNull { expr, .. }
2277            | Expr::BoolTest { expr, .. }
2278            | Expr::FieldAccess { base: expr, .. } => {
2279                self.resolve_expr_subqueries(expr, cancel)?;
2280            }
2281            Expr::FunctionCall { args, .. } => {
2282                for a in args {
2283                    self.resolve_expr_subqueries(a, cancel)?;
2284                }
2285            }
2286            Expr::Like { expr, pattern, .. } => {
2287                self.resolve_expr_subqueries(expr, cancel)?;
2288                self.resolve_expr_subqueries(pattern, cancel)?;
2289            }
2290            Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2291            // v4.12 window functions — recurse into args + ORDER BY
2292            // + PARTITION BY in case they carry inner subqueries.
2293            Expr::WindowFunction {
2294                args,
2295                partition_by,
2296                order_by,
2297                ..
2298            } => {
2299                for a in args {
2300                    self.resolve_expr_subqueries(a, cancel)?;
2301                }
2302                for p in partition_by {
2303                    self.resolve_expr_subqueries(p, cancel)?;
2304                }
2305                for (e, _, _) in order_by {
2306                    self.resolve_expr_subqueries(e, cancel)?;
2307                }
2308            }
2309            // Subquery nodes are handled in subquery_replacement
2310            // (which returned None — defensive no-op); Literal /
2311            // Column are leaves.
2312            Expr::ScalarSubquery(_)
2313            | Expr::Exists { .. }
2314            | Expr::InSubquery { .. }
2315            | Expr::RowInSubquery { .. }
2316            | Expr::RowCmpSubquery { .. }
2317            | Expr::Literal(_)
2318            | Expr::Placeholder(_)
2319            | Expr::Column(_) => {}
2320            // v7.30.2 — list elements can carry scalar subqueries
2321            // (`x IN (1, (SELECT …))`).
2322            Expr::InList { expr, list, .. } => {
2323                self.resolve_expr_subqueries(expr, cancel)?;
2324                for item in list {
2325                    self.resolve_expr_subqueries(item, cancel)?;
2326                }
2327            }
2328            // v7.10.10 — recurse children.
2329            Expr::Array(items) => {
2330                for elem in items {
2331                    self.resolve_expr_subqueries(elem, cancel)?;
2332                }
2333            }
2334            Expr::ArraySubscript { target, index } => {
2335                self.resolve_expr_subqueries(target, cancel)?;
2336                self.resolve_expr_subqueries(index, cancel)?;
2337            }
2338            Expr::ArraySlice { target, lo, hi } => {
2339                self.resolve_expr_subqueries(target, cancel)?;
2340                if let Some(l) = lo {
2341                    self.resolve_expr_subqueries(l, cancel)?;
2342                }
2343                if let Some(h) = hi {
2344                    self.resolve_expr_subqueries(h, cancel)?;
2345                }
2346            }
2347            Expr::AnyAll { expr, array, .. } => {
2348                self.resolve_expr_subqueries(expr, cancel)?;
2349                // Quantified subquery — an uncorrelated one
2350                // materialises up front; a correlated one stays for
2351                // the per-row resolver.
2352                if let Expr::ScalarSubquery(inner) = array.as_mut() {
2353                    if !crate::subquery::select_is_correlated(inner) {
2354                        let s = (**inner).clone();
2355                        **array = self.materialize_quantified_rows(&s, cancel)?;
2356                    }
2357                } else {
2358                    self.resolve_expr_subqueries(array, cancel)?;
2359                }
2360            }
2361            Expr::Case {
2362                operand,
2363                branches,
2364                else_branch,
2365            } => {
2366                if let Some(o) = operand {
2367                    self.resolve_expr_subqueries(o, cancel)?;
2368                }
2369                for (w, t) in branches {
2370                    self.resolve_expr_subqueries(w, cancel)?;
2371                    self.resolve_expr_subqueries(t, cancel)?;
2372                }
2373                if let Some(e) = else_branch {
2374                    self.resolve_expr_subqueries(e, cancel)?;
2375                }
2376            }
2377        }
2378        Ok(())
2379    }
2380}
2381
2382impl Engine {
2383    /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2384    /// `SelectItem::Wildcard` to all schema columns and
2385    /// `SelectItem::Expr` via the regular eval path.
2386    pub(crate) fn project_row_simple(
2387        &self,
2388        row: &Row<'static>,
2389        items: &[SelectItem],
2390        schema_cols: &[ColumnSchema],
2391        alias: &str,
2392    ) -> Result<Row<'static>, EngineError> {
2393        let ctx = self.ev_ctx(schema_cols, Some(alias));
2394        let cancel = CancelToken::none();
2395        let mut out_vals = Vec::new();
2396        for item in items {
2397            match item {
2398                // In a single-table projection (AS OF SEGMENT / RETURNING) a
2399                // qualified `t.*` covers exactly the same columns as a bare `*`.
2400                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2401                    out_vals.extend(row.values.iter().cloned());
2402                }
2403                SelectItem::Expr { expr, .. } => {
2404                    let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2405                    out_vals.push(v);
2406                }
2407            }
2408        }
2409        Ok(Row::new(out_vals))
2410    }
2411
2412    /// v6.10.2 — derive the output `ColumnSchema` list for an
2413    /// AS OF SEGMENT projection. Wildcards take the full schema;
2414    /// expressions take the alias if present or a synthetic
2415    /// `?column?` (PG convention) otherwise.
2416    pub(crate) fn derive_output_columns(
2417        &self,
2418        items: &[SelectItem],
2419        schema_cols: &[ColumnSchema],
2420        table_alias: &str,
2421    ) -> Vec<ColumnSchema> {
2422        let mut out = Vec::new();
2423        for item in items {
2424            match item {
2425                // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2426                // a single-table projection.
2427                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2428                    out.extend(schema_cols.iter().cloned());
2429                }
2430                SelectItem::Expr { expr, alias } => {
2431                    // Bare column references inherit the schema
2432                    // column's name + type — PG names `RETURNING id`
2433                    // "id" and types it BIGINT, and the sqlx embed
2434                    // path type-checks RowDescription against the
2435                    // Rust target (mailrs embed round-12).
2436                    if let Expr::Column(col) = expr
2437                        && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2438                    {
2439                        let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2440                        let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2441                        // v7.39 (read01 round 54) — carry the enum identity:
2442                        // it lives outside the DataType lattice, so a derived
2443                        // table built from this schema otherwise forgets it and
2444                        // the OUTER `ORDER BY <enum col>` silently sorts by the
2445                        // label's TEXT instead of member order.
2446                        c.user_enum_type = sc.user_enum_type.clone();
2447                        out.push(c);
2448                        continue;
2449                    }
2450                    let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2451                    // v7.30.4 (mailrs round-27, P0) — type the
2452                    // expression with the same inference the SELECT
2453                    // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2454                    // The old Text default broke every typed decode
2455                    // of `RETURNING uidnext - 1 AS uid`: four days
2456                    // of inbound mail indexed nowhere. Inference
2457                    // failure keeps the old Text fallback rather
2458                    // than inventing new error paths here.
2459                    // v7.39 (round 258) — take the enum identity from the
2460                    // same projection build, not just the type: a constant
2461                    // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2462                    // VALUES row lowers to) is an EXPRESSION, so it landed
2463                    // here and the derived table forgot the enum.
2464                    let (ty, nullable) = build_projection(
2465                        core::slice::from_ref(item),
2466                        schema_cols,
2467                        table_alias,
2468                        self.speaks_mysql,
2469                        Some(self.active_catalog()),
2470                    )
2471                    .ok()
2472                    .and_then(|p| p.into_iter().next())
2473                    .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2474                    out.push(ColumnSchema::new(name, ty, nullable));
2475                }
2476            }
2477        }
2478        out
2479    }
2480
2481    /// v4.5: SELECT with cooperative cancellation. The token is
2482    /// honoured between UNION peers and inside the bare-SELECT row
2483    /// loop; HNSW kNN graph walks and the aggregate executor don't
2484    /// honour it yet (deferred — those paths bound their work
2485    /// internally by `LIMIT k` and `GROUP BY` cardinality).
2486    /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2487    /// its (lowercased) name, or None if the name isn't a virtual view.
2488    /// Callers decide whether to return it directly (`SELECT *`) or stage
2489    /// it as a temp table for the full query pipeline.
2490    fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2491        Some(match name {
2492            "spg_statistic" => self.exec_spg_statistic(),
2493            "spg_stat_replication" => self.exec_spg_stat_replication(),
2494            "spg_stat_segment" => self.exec_spg_stat_segment(),
2495            "spg_memory_stats" => self.exec_spg_memory_stats(),
2496            "spg_stat_query" => self.exec_spg_stat_query(),
2497            "pg_stat_statements" => self.exec_pg_stat_statements(),
2498            "spg_stat_activity" => self.exec_spg_stat_activity(),
2499            "pg_stat_activity" => self.exec_pg_stat_activity(),
2500            "pg_locks" => self.exec_pg_locks(),
2501            "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2502            "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2503            "spg_partition_health" => self.exec_spg_partition_health(),
2504            "spg_audit_chain" => self.exec_spg_audit_chain(),
2505            "spg_audit_verify" => self.exec_spg_audit_verify(),
2506            "spg_table_ddl" => self.exec_spg_table_ddl(),
2507            "spg_role_ddl" => self.exec_spg_role_ddl(),
2508            "spg_database_ddl" => self.exec_spg_database_ddl(),
2509            _ => return None,
2510        })
2511    }
2512
2513    /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2514    /// describes against: this engine's catalog with the view staged as a
2515    /// table, exactly as `exec_select_cancel_as` stages it for a
2516    /// non-bare query.
2517    ///
2518    /// These views never reach the catalog — each is a fixed row set built
2519    /// inside its own `exec_*` — so Describe reported no columns for all
2520    /// seventeen of them. Rows are deliberately not inserted: Describe
2521    /// only needs the shape, and `infer_column_types` reads the rows we
2522    /// already have in hand.
2523    pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2524        let from = stmt.from.as_ref()?;
2525        if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2526            return None;
2527        }
2528        let lower = from.primary.name.to_ascii_lowercase();
2529        let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2530            return None;
2531        };
2532        let mut catalog = self.active_catalog().clone();
2533        let cols = infer_column_types(&columns, &rows);
2534        catalog
2535            .create_table(TableSchema::new(from.primary.name.clone(), cols))
2536            .ok()?;
2537        Some(catalog)
2538    }
2539
2540    pub(crate) fn exec_select_cancel(
2541        &self,
2542        stmt: &SelectStatement,
2543        cancel: CancelToken<'_>,
2544    ) -> Result<QueryResult, EngineError> {
2545        self.exec_select_cancel_as(stmt, cancel, None)
2546    }
2547
2548    /// v7.39 (round 334, V55) — the same read core, authorised as
2549    /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2550    /// function's OWNER: that is the entire point of the form, and without
2551    /// it every definer function failed with "permission denied" on the
2552    /// very table it exists to expose.
2553    /// v7.39 (round 559) — see the call site. `None` for anything but
2554    /// the bare shape, so every other query keeps its old path.
2555    fn try_bare_count_star(
2556        &self,
2557        stmt: &SelectStatement,
2558        as_role: Option<&str>,
2559    ) -> Result<Option<QueryResult>, EngineError> {
2560        use spg_sql::ast::SelectItem;
2561        if as_role.is_some()
2562            || !stmt.ctes.is_empty()
2563            || !stmt.unions.is_empty()
2564            || stmt.where_.is_some()
2565            || stmt.group_by.is_some()
2566            || stmt.having.is_some()
2567            || stmt.distinct
2568            || !stmt.order_by.is_empty()
2569            || stmt.limit.is_some()
2570            || stmt.offset.is_some()
2571            || stmt.items.len() != 1
2572        {
2573            return Ok(None);
2574        }
2575        let Some(from) = &stmt.from else {
2576            return Ok(None);
2577        };
2578        if !from.joins.is_empty()
2579            || stmt.locking.is_some()
2580            || from.primary.lateral_subquery.is_some()
2581            || from.primary.unnest_expr.is_some()
2582            || from.primary.generate_series_args.is_some()
2583            || from.primary.name.is_empty()
2584            || from.primary.name.starts_with("__spg_")
2585        {
2586            return Ok(None);
2587        }
2588        // A partition PARENT holds no rows of its own — they live in the
2589        // children — so its header count is 0 and the ordinary path has
2590        // to fan out. Caught by the partition conformance cases.
2591        //
2592        // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2593        // of them, which is worse: its header count is a real number,
2594        // just not the answer. `SELECT count(*) FROM par` returned 1
2595        // where PG returns 2, because this shortcut fired before the
2596        // fan-out could. The question is "does anything descend from
2597        // this", not "was it declared a partition parent".
2598        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2599            return Ok(None);
2600        }
2601        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2602            return Ok(None);
2603        };
2604        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2605            return Ok(None);
2606        };
2607        if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2608            return Ok(None);
2609        }
2610        // A row-security policy filters rows, so the header count is not
2611        // the answer; the ordinary path applies the policy.
2612        let Some(table) = self.active_catalog().get(&from.primary.name) else {
2613            return Ok(None);
2614        };
2615        if table.schema().row_security {
2616            return Ok(None);
2617        }
2618        // Rows frozen to the cold tier are not in `headers`, so the
2619        // header count would miss them. Caught by the cold-tier e2e.
2620        if table.has_cold_rows_fast() {
2621            return Ok(None);
2622        }
2623        let n = table.count_visible(&self.current_snapshot());
2624        let col = alias.clone().unwrap_or_else(|| String::from("count"));
2625        Ok(Some(QueryResult::Rows {
2626            columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2627            rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2628                i64::try_from(n).unwrap_or(i64::MAX)
2629            )])],
2630        }))
2631    }
2632
2633    /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2634    /// that col>` served from the index, never reading a row.
2635    ///
2636    /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2637    /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2638    /// count (2x at 1k). PG needs its visibility map for this — a heap
2639    /// tuple carries its own visibility, so an index entry alone cannot
2640    /// say whether the row is live, and PG reads the heap for any page
2641    /// the map does not mark all-visible. SPG keeps a header array
2642    /// beside the rows, so the locator answers it directly and there is
2643    /// no map to be stale.
2644    /// v7.39 (round 564) — the shape test, once, for both the
2645    /// materialising scan and the streaming one.
2646    ///
2647    /// Two callers asking the same question in two places is how a fact
2648    /// starts drifting; the answer here is the single copy. Returns the
2649    /// table, the alias the predicate is written against, the projected
2650    /// column's position, and the name the single output column takes.
2651    pub(crate) fn index_only_shape<'s>(
2652        &'s self,
2653        stmt: &'s SelectStatement,
2654    ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2655        use spg_sql::ast::SelectItem;
2656        if !stmt.ctes.is_empty()
2657            || !stmt.unions.is_empty()
2658            || stmt.group_by.is_some()
2659            || stmt.having.is_some()
2660            || stmt.distinct
2661            || stmt.locking.is_some()
2662            || !stmt.order_by.is_empty()
2663            || stmt.limit.is_some()
2664            || stmt.offset.is_some()
2665            || stmt.items.len() != 1
2666        {
2667            return None;
2668        }
2669        let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2670            return None;
2671        };
2672        if !from.joins.is_empty()
2673            || from.primary.lateral_subquery.is_some()
2674            || from.primary.unnest_expr.is_some()
2675            || from.primary.generate_series_args.is_some()
2676            || from.primary.name.is_empty()
2677            || from.primary.name.starts_with("__spg_")
2678        {
2679            return None;
2680        }
2681        // v7.39 (round 645) — see the note on the sibling shortcut above:
2682        // an inheritance parent's own header count is not the answer.
2683        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2684            return None;
2685        }
2686        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2687            return None;
2688        };
2689        let spg_sql::ast::Expr::Column(c) = expr else {
2690            return None;
2691        };
2692        let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2693        if let Some(q) = c.qualifier.as_deref()
2694            && !q.eq_ignore_ascii_case(alias_name)
2695        {
2696            return None;
2697        }
2698        let table = self.active_catalog().get(&from.primary.name)?;
2699        if table.schema().row_security {
2700            return None;
2701        }
2702        let cols = &table.schema().columns;
2703        let pos = cols
2704            .iter()
2705            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2706        let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2707        Some((table, alias_name, pos, out))
2708    }
2709
2710    /// v7.39 (round 565) — would this statement be answered out of the
2711    /// index alone?
2712    ///
2713    /// EXPLAIN has to name the node the executor will actually run, and
2714    /// the only honest way to know is to ask the same two questions the
2715    /// executor asks: the statement's shape, and everything decidable
2716    /// about the scan before it walks. Neither is re-stated here.
2717    pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2718        let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2719            return false;
2720        };
2721        let Some(where_) = stmt.where_.as_ref() else {
2722            return false;
2723        };
2724        crate::index_access::index_only_precheck(
2725            where_,
2726            &table.schema().columns,
2727            table,
2728            alias_name,
2729            pos,
2730            self.speaks_mysql,
2731        )
2732        .is_some()
2733    }
2734
2735    fn try_index_only_scan(
2736        &self,
2737        stmt: &SelectStatement,
2738    ) -> Result<Option<QueryResult>, EngineError> {
2739        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2740            return Ok(None);
2741        };
2742        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2743        // are not materialised here, and a partition parent's own
2744        // heap/indexes are empty (its rows live in the children).
2745        if !stmt.ctes.is_empty() {
2746            return Ok(None);
2747        }
2748        if let Some(from) = &stmt.from
2749            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2750        {
2751            return Ok(None);
2752        }
2753        let where_ = stmt.where_.as_ref().expect("shape checked it");
2754        let cols = &table.schema().columns;
2755        let Some(values) = crate::index_access::try_index_only_range(
2756            where_,
2757            cols,
2758            table,
2759            alias_name,
2760            &self.current_snapshot(),
2761            pos,
2762            self.speaks_mysql,
2763        ) else {
2764            return Ok(None);
2765        };
2766        let schema = alloc::vec![ColumnSchema::new(
2767            out_name,
2768            cols[pos].ty,
2769            cols[pos].nullable
2770        )];
2771        Ok(Some(QueryResult::Rows {
2772            columns: schema,
2773            rows: values
2774                .into_iter()
2775                .map(|v| Row::new(alloc::vec![v]))
2776                .collect(),
2777        }))
2778    }
2779
2780    /// v7.39 (round 564) — the same scan, emitting each value instead of
2781    /// building a `Vec<Row>` for the encoder to walk once and drop.
2782    ///
2783    /// A profile of the server serving a 50k-row range put 10.2% of the
2784    /// connection thread's CPU on BUILDING that vector and another 9.7%
2785    /// on dropping it — a fifth of the query, spent allocating and
2786    /// freeing one single-element `Vec` per output row so that the wire
2787    /// encoder could borrow each value for a few nanoseconds. The
2788    /// streaming interface it then hands them to takes `&[Value]`
2789    /// already.
2790    ///
2791    /// Returns `None` when the shape does not apply, so the caller falls
2792    /// back before anything has been emitted.
2793    pub(crate) fn try_index_only_stream<F>(
2794        &self,
2795        stmt: &SelectStatement,
2796        emit: &mut F,
2797    ) -> Result<Option<usize>, EngineError>
2798    where
2799        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2800    {
2801        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2802            return Ok(None);
2803        };
2804        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2805        // are not materialised here, and a partition parent's own
2806        // heap/indexes are empty (its rows live in the children).
2807        if !stmt.ctes.is_empty() {
2808            return Ok(None);
2809        }
2810        if let Some(from) = &stmt.from
2811            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2812        {
2813            return Ok(None);
2814        }
2815        let where_ = stmt.where_.as_ref().expect("shape checked it");
2816        let cols = &table.schema().columns;
2817        let schema = alloc::vec![ColumnSchema::new(
2818            out_name,
2819            cols[pos].ty,
2820            cols[pos].nullable
2821        )];
2822        let snapshot = self.current_snapshot();
2823        // The header goes out only once the walk has agreed to run — a
2824        // shape rejection after it would leave the client with a
2825        // RowDescription for a result that never comes.
2826        let mut wrote_header = false;
2827        let counted = crate::index_access::index_only_range_each(
2828            where_,
2829            cols,
2830            table,
2831            alias_name,
2832            &snapshot,
2833            pos,
2834            self.speaks_mysql,
2835            &mut |v: spg_storage::Value<'_>| {
2836                if !wrote_header {
2837                    emit(crate::StreamItem::Header(&schema))?;
2838                    wrote_header = true;
2839                }
2840                emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2841            },
2842        );
2843        match counted {
2844            None => Ok(None),
2845            Some(Err(e)) => Err(e),
2846            Some(Ok(n)) => {
2847                if !wrote_header {
2848                    emit(crate::StreamItem::Header(&schema))?;
2849                }
2850                Ok(Some(n))
2851            }
2852        }
2853    }
2854
2855    /// `DISTINCT ON`'s de-duplication, which runs after the inner
2856    /// SELECT has produced its rows.
2857    ///
2858    /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2859    /// reason round 848 established: a debug build gives every branch's
2860    /// locals a slot in the frame whichever branch runs, and this one is
2861    /// eighty lines of hashing, key slicing and survivor sorting that a
2862    /// statement without `DISTINCT ON` never touches. Round 867
2863    /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2864    /// reaches none of it — the segment that had been blamed on
2865    /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2866    #[inline(never)]
2867    fn apply_distinct_on(
2868        &self,
2869        result: QueryResult,
2870        don_hidden: usize,
2871        don_limit: &(
2872            Option<spg_sql::ast::LimitExpr>,
2873            Option<spg_sql::ast::LimitExpr>,
2874        ),
2875        don_top1: usize,
2876        orig_order_by: &[spg_sql::ast::OrderBy],
2877    ) -> Result<QueryResult, EngineError> {
2878        let QueryResult::Rows { columns, rows } = result else {
2879            return Ok(result);
2880        };
2881        // The keys are the hidden trailing columns appended above.
2882        // v7.39 (round 729) — top-1 mode: the trailing columns are the
2883        // DON keys plus the ORDER tail; keep each group's best in one
2884        // hash pass, then sort the SURVIVORS with the original spec.
2885        let mut kept: alloc::vec::Vec<Row<'static>>;
2886        let key_start;
2887        if don_top1 > 0 {
2888            let tail = don_top1 - 1;
2889            key_start = columns.len().saturating_sub(don_hidden + tail);
2890            let ord_start = key_start + don_hidden;
2891            let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2892                .iter()
2893                .map(|o| (o.desc, o.nulls_first))
2894                .collect();
2895            let mysql = self.speaks_mysql;
2896            let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2897                for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2898                    let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2899                    let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2900                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2901                        core::cmp::Ordering::Less => return true,
2902                        core::cmp::Ordering::Greater => return false,
2903                        core::cmp::Ordering::Equal => {}
2904                    }
2905                }
2906                false
2907            };
2908            let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2909            let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2910            let mut keybuf = String::new();
2911            for row in rows {
2912                keybuf.clear();
2913                for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2914                    aggregate::push_canonical_key(&mut keybuf, v);
2915                }
2916                match slot.get(keybuf.as_str()) {
2917                    Some(&i) => {
2918                        if better(&row, &best[i]) {
2919                            best[i] = row;
2920                        }
2921                    }
2922                    None => {
2923                        slot.insert(keybuf.clone(), best.len());
2924                        best.push(row);
2925                    }
2926                }
2927            }
2928            // Survivors sort with the FULL original spec (keys are still
2929            // aboard as hidden columns).
2930            let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2931                .iter()
2932                .map(|o| (o.desc, o.nulls_first))
2933                .collect();
2934            best.sort_by(|a, b| {
2935                for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2936                    let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2937                    let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2938                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2939                        core::cmp::Ordering::Equal => {}
2940                        o => return o,
2941                    }
2942                }
2943                core::cmp::Ordering::Equal
2944            });
2945            for r in &mut best {
2946                r.values.truncate(key_start);
2947            }
2948            kept = best;
2949        } else {
2950            key_start = columns.len().saturating_sub(don_hidden);
2951            let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2952            kept = alloc::vec::Vec::new();
2953            for mut row in rows {
2954                let key: alloc::vec::Vec<Value<'static>> =
2955                    row.values.get(key_start..).unwrap_or(&[]).to_vec();
2956                if seen.iter().any(|k| k == &key) {
2957                    continue;
2958                }
2959                seen.push(key);
2960                row.values.truncate(key_start);
2961                kept.push(row);
2962            }
2963        }
2964        let mut columns = columns;
2965        columns.truncate(key_start);
2966        // PG limits what DISTINCT ON left, not what fed it.
2967        let kept = apply_deferred_limit(kept, don_limit);
2968        Ok(QueryResult::Rows {
2969            columns,
2970            rows: kept,
2971        })
2972    }
2973
2974    pub(crate) fn exec_select_cancel_as(
2975        &self,
2976        stmt: &SelectStatement,
2977        cancel: CancelToken<'_>,
2978        as_role: Option<&str>,
2979    ) -> Result<QueryResult, EngineError> {
2980        // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2981        // <all columns>` is legal PG (the wildcard expands to grouped
2982        // columns); SPG refused the whole shape. Expand the wildcard
2983        // into explicit column refs up front — the aggregate layer's
2984        // existing "must appear in the GROUP BY clause" validation
2985        // then answers PG's sentence for any non-grouped column.
2986        if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2987            return self.exec_select_cancel_as(&expanded, cancel, as_role);
2988        }
2989        // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2990        // a row.
2991        //
2992        // The aggregate layer already short-circuits this to
2993        // `rows.len()`, so the O(1) part was never the problem — the
2994        // cost is UPSTREAM, materialising every visible row so that
2995        // layer can take its length. Measured over pgwire on 500k rows:
2996        // PG18 8.2 ms with two parallel workers, 10.3 ms with
2997        // parallelism off, SPG 16.5 ms — 1.6x slower than a
2998        // single-threaded PG on the commonest aggregate there is, and no
2999        // ledger entry recorded it.
3000        //
3001        // Counting visible HEADERS needs no row at all. PG cannot do
3002        // this: its visibility lives in the heap tuples themselves, so
3003        // it has to read them (that is why its own count(*) is a full
3004        // scan, parallel or not).
3005        // v7.39 (read01 round 57) — the table-privilege gate on the common
3006        // read core. A superuser session returns from it immediately.
3007        // v7.39 (round 529) — resolve an ORDER BY that names an output
3008        // ALIAS. The statement-level pass never reached a SELECT nested in
3009        // a FROM clause, a CTE or a scalar subquery, so the same query
3010        // worked on its own and failed the moment anything wrapped it —
3011        // which is what generated SQL does constantly.
3012        let aliased;
3013        let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
3014            let mut s = stmt.clone();
3015            crate::orderby::resolve_order_by_position(&mut s);
3016            aliased = s;
3017            &aliased
3018        } else {
3019            stmt
3020        };
3021        // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
3022        //
3023        // Its keys were evaluated against the PROJECTED row, so a key that
3024        // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
3025        // ORDER BY g, v DESC`, the canonical "latest row per group" — could
3026        // not be read at all and the query failed. PG evaluates them on the
3027        // input. They are projected as hidden columns here and stripped
3028        // again below, the same way the grouping-set ordering columns
3029        // already travel.
3030        //
3031        // And the dedup ran AFTER the inner statement's LIMIT, so
3032        // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
3033        // PG answers two: the limit had already taken two rows of the same
3034        // group before anything deduplicated them. A paginated DISTINCT ON
3035        // returned short pages, with no error. The limit is deferred to
3036        // after the dedup, which is PG's order.
3037        let don_stmt;
3038        // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
3039        // order spec (the rewritten stmt's is emptied).
3040        let orig_order_by = stmt.order_by.clone();
3041        let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
3042            (stmt, 0, (None, None), 0usize)
3043        } else {
3044            let mut s = stmt.clone();
3045            let hidden = s.distinct_on.len();
3046            for (i, e) in stmt.distinct_on.iter().enumerate() {
3047                s.items.push(SelectItem::Expr {
3048                    expr: e.clone(),
3049                    alias: Some(alloc::format!("__distinct_on_{i}")),
3050                });
3051            }
3052            // v7.39 (round 729) — group-top-1 short circuit. When the
3053            // DISTINCT ON keys are exactly the ORDER BY's leading keys,
3054            // the answer is "per group, the row that wins the remaining
3055            // order" — a single O(n) hash pass. The old path sorted the
3056            // ENTIRE input first (500k rows, ~180 ms on the panel cell)
3057            // to keep 100. The inner query runs UNSORTED with every
3058            // order key appended as a hidden column; the dedup below
3059            // keeps each group's best, then sorts the SURVIVORS.
3060            // Declared-collation order keys stay on the sorting path
3061            // (the value comparator here is collation-blind).
3062            let prefix_matches = s.order_by.len() >= hidden
3063                && stmt
3064                    .distinct_on
3065                    .iter()
3066                    .zip(s.order_by.iter())
3067                    .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3068            let colls_plain =
3069                crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3070                    .map(|cs| cs.iter().all(Option::is_none))
3071                    .unwrap_or(false);
3072            let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3073                let tail = s.order_by.len() - hidden;
3074                for (j, o) in s.order_by[hidden..].iter().enumerate() {
3075                    s.items.push(SelectItem::Expr {
3076                        expr: o.expr.clone(),
3077                        alias: Some(alloc::format!("__don_ord_{j}")),
3078                    });
3079                }
3080                // Carry the tail's direction flags through the aliases'
3081                // ORDER; the survivors re-sort below with the full spec.
3082                s.order_by = Vec::new();
3083                tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3084            } else {
3085                0
3086            };
3087            // Only a folded literal is deferred; a placeholder or an
3088            // expression keeps the path it has today rather than being
3089            // resolved a second way here.
3090            let deferrable = matches!(
3091                (&s.limit, &s.offset),
3092                (
3093                    None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3094                    None | Some(spg_sql::ast::LimitExpr::Literal(_))
3095                )
3096            );
3097            let deferred = if deferrable {
3098                (s.limit.take(), s.offset.take())
3099            } else {
3100                (None, None)
3101            };
3102            don_stmt = s;
3103            (&don_stmt, hidden, deferred, top1_tail)
3104        };
3105        self.acl_check_select_as(stmt, as_role)?;
3106        validate_aggregate_placement(stmt)?;
3107        // BEFORE the fast paths below, not after: a name that resolves to
3108        // nothing is not a question the count fast path or the index-only
3109        // scan should get to answer first. Placed after them at first,
3110        // and the two of them swallowed `WHERE` and `ORDER BY` while
3111        // `GROUP BY` and `HAVING`, which cannot take those routes, raised
3112        // — the same statement answering two ways depending on the plan.
3113        self.validate_clause_columns(stmt)?;
3114        self.validate_function_arity(stmt)?;
3115        // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3116        // privilege gate above. Placed before it at first, and the
3117        // security-definer e2e caught it immediately: a SECURITY INVOKER
3118        // function whose body is `SELECT count(*) FROM t` answered
3119        // instead of being refused, because the fast path never reached
3120        // the check.
3121        if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3122            return Ok(r);
3123        }
3124        // v7.39 (round 560) — an index-only range scan. Same placement
3125        // reasoning as the count above: after the privilege gate.
3126        if let Some(r) = self.try_index_only_scan(stmt)? {
3127            return Ok(r);
3128        }
3129        validate_locking_clause(stmt)?;
3130        let result = self.exec_select_cancel_inner(stmt, cancel)?;
3131        // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3132        // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3133        // They carry the per-branch mask through the UNION-ALL sort and must not
3134        // appear in the output. Stripped per SELECT level (grouping-set queries
3135        // are often wrapped in a derived subquery), before DISTINCT ON.
3136        let result = strip_synthetic_order_cols(result);
3137        // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3138        // rows arrive here already ORDER BY'd; keep the FIRST row of
3139        // each group the expressions define (PG semantics). The
3140        // expressions evaluate against the projected schema — an
3141        // expression that isn't in the select list errors honestly.
3142        if stmt.distinct_on.is_empty() {
3143            return Ok(result);
3144        }
3145        self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3146    }
3147
3148    /// The UNION chain: execute the head as a bare block, then fold each
3149    /// peer in with left-associative dedup.
3150    ///
3151    /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3152    /// reason round 848 established. A statement with no unions returns
3153    /// one line above the call — and every nested subquery on a deep
3154    /// path is such a statement, so each level of the recursion carried
3155    /// 170 lines of locals it could not reach. Round 867 measured that
3156    /// frame at 34,800 bytes, the largest single one on the descent,
3157    /// after two earlier attributions had blamed its caller and then its
3158    /// callee: the gap between two marks is the frame of everything
3159    /// BETWEEN them, and this function had no mark of its own.
3160    #[inline(never)]
3161    fn exec_union_chain(
3162        &self,
3163        stmt_ref: &SelectStatement,
3164        stmt: &SelectStatement,
3165        cancel: CancelToken<'_>,
3166    ) -> Result<QueryResult, EngineError> {
3167        // UNION path: clone-strip the head into a bare block (its own
3168        // DISTINCT and any inner ORDER BY are dropped by parser rule —
3169        // the wrapper SelectStatement carries them), execute, then chain
3170        // peers with left-associative dedup semantics.
3171        // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3172        // output columns; a position past their count is PG's 42P10.
3173        crate::orderby::check_order_by_positions(stmt_ref)?;
3174        let mut head_unknown = branch_unknown_mask(stmt_ref);
3175        let head_regcast = branch_regcast_mask(stmt_ref);
3176        let mut head = stmt_ref.clone();
3177        head.unions = Vec::new();
3178        head.order_by = Vec::new();
3179        head.limit = None;
3180        let QueryResult::Rows {
3181            mut columns,
3182            mut rows,
3183        } = self.exec_bare_select_cancel(&head, cancel)?
3184        else {
3185            unreachable!("bare SELECT cannot return CommandOk")
3186        };
3187        for (kind, peer) in &stmt_ref.unions {
3188            // v7.37.17 (17.6 siblings) — a peer carrying its own
3189            // unions is a nested INTERSECT group (the parser's
3190            // precedence regrouping); recurse through the
3191            // union-aware wrapper for it.
3192            let peer_result = if peer.unions.is_empty() {
3193                self.exec_bare_select_cancel(peer, cancel)?
3194            } else {
3195                self.exec_select_cancel(peer, cancel)?
3196            };
3197            let QueryResult::Rows {
3198                columns: peer_cols,
3199                rows: mut peer_rows,
3200            } = peer_result
3201            else {
3202                unreachable!("bare SELECT cannot return CommandOk")
3203            };
3204            if peer_cols.len() != columns.len() {
3205                // v7.39 (round 232) — PG's wording, which clients match on.
3206                return Err(EngineError::Unsupported(alloc::format!(
3207                    "each {} query must have the same number of columns",
3208                    set_op_name(*kind)
3209                )));
3210            }
3211            // v7.39 (round 232+233) — PG resolves each result column to one
3212            // type before it merges anything, and refuses the query when the
3213            // two branches have no common type. SPG's unifier
3214            // (`unify_union_columns`) is value-driven and deliberately
3215            // conservative — "a column where any cell fails to coerce is left
3216            // exactly as it was" — so a mismatch produced a column holding
3217            // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3218            // back with integers and text interleaved) instead of an error.
3219            //
3220            // The check has to read the branch ASTs, not just their schemas:
3221            // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3222            // as TEXT and is indistinguishable from a real text column by
3223            // schema alone — yet PG treats the two completely differently
3224            // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3225            // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3226            let peer_unknown = branch_unknown_mask(peer);
3227            let peer_regcast = branch_regcast_mask(peer);
3228            for i in 0..columns.len() {
3229                let hu = head_unknown.get(i).copied().unwrap_or(false);
3230                let pu = peer_unknown.get(i).copied().unwrap_or(false);
3231                let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3232                let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3233                    || head_regcast.get(i).copied().unwrap_or(false);
3234                match (hu, pu) {
3235                    // Both sides carry a real type: they must share a category.
3236                    (false, false) => {
3237                        if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3238                            return Err(EngineError::Unsupported(alloc::format!(
3239                                "{} types {} and {} cannot be matched",
3240                                set_op_name(*kind),
3241                                crate::conversions::pg_type_name_for_error(ht),
3242                                crate::conversions::pg_type_name_for_error(pt),
3243                            )));
3244                        }
3245                    }
3246                    // One side is an untyped literal: it takes the other's
3247                    // type, and failing to convert is the error PG reports.
3248                    (true, false) => {
3249                        coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3250                        columns[i].ty = pt;
3251                        head_unknown[i] = false;
3252                    }
3253                    (false, true) => {
3254                        coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3255                    }
3256                    // Both untyped — nothing to resolve against yet.
3257                    (true, true) => {}
3258                }
3259            }
3260            // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3261            // nullable (PG semantics). Previously the result kept only the head's
3262            // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3263            // non-null `1`) wrongly reported the column NOT NULL, which let
3264            // `count(col)`'s NOT-NULL fast-path count the NULL row.
3265            for (i, pc) in peer_cols.iter().enumerate() {
3266                if pc.nullable {
3267                    columns[i].nullable = true;
3268                }
3269            }
3270            // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3271            // text by the session collation (CI + accent + PAD SPACE), like
3272            // GROUP BY. PG stays byte-exact.
3273            let mysql = self.speaks_mysql;
3274            // v7.38.14 — the mask, which 7.38.13 recorded as impossible here
3275            // and was wrong about. `columns` and `peer_cols` are both in
3276            // scope; what was actually missing is that the branches' output
3277            // schemas did not CARRY the collation, so a mask built from them
3278            // would have marked every column byte-wise. Unifying the
3279            // projection-to-schema conversion fixed the supply side, and the
3280            // mask is now buildable from what was always there.
3281            //
3282            // Either side byte-wise keeps the position byte-wise, mirroring
3283            // `eval::resolve::mysql_text_fold_applies`: a set operation
3284            // between a folding column and a declared-binary one must not
3285            // quietly fold the binary one's values away.
3286            let set_mask: alloc::vec::Vec<bool> = columns
3287                .iter()
3288                .zip(peer_cols.iter())
3289                .map(|(l, r)| {
3290                    matches!(l.collation, spg_storage::Collation::Binary)
3291                        || matches!(r.collation, spg_storage::Collation::Binary)
3292                })
3293                .collect();
3294            let fold = FoldSpec::of(mysql, &set_mask);
3295            match kind {
3296                UnionKind::All => rows.extend(peer_rows),
3297                UnionKind::Distinct => {
3298                    rows.extend(peer_rows);
3299                    rows = dedup_rows(rows, fold);
3300                }
3301                // v7.37.17 (17.6 siblings) — PG set semantics.
3302                // v7.39 (round 591) — all four ask the same question of the
3303                // right side, and all four used to answer it by scanning it
3304                // once per left row. `PeerIndex` buckets it by the hash
3305                // DISTINCT already uses, so the answer is a lookup.
3306                // INTERSECT: distinct rows present on both sides.
3307                UnionKind::Intersect => {
3308                    let idx = PeerIndex::build(&peer_rows, fold);
3309                    rows = dedup_rows(rows, fold)
3310                        .into_iter()
3311                        .filter(|r| idx.contains(r))
3312                        .collect();
3313                }
3314                // INTERSECT ALL: multiset intersection — each row
3315                // keeps min(left count, right count) occurrences.
3316                UnionKind::IntersectAll => {
3317                    let mut idx = PeerIndex::build(&peer_rows, fold);
3318                    let mut kept: Vec<Row<'static>> = Vec::new();
3319                    for r in rows {
3320                        if idx.take_one(&r) {
3321                            kept.push(r);
3322                        }
3323                    }
3324                    rows = kept;
3325                }
3326                // EXCEPT: distinct left rows absent from the right.
3327                UnionKind::Except => {
3328                    let idx = PeerIndex::build(&peer_rows, fold);
3329                    rows = dedup_rows(rows, fold)
3330                        .into_iter()
3331                        .filter(|r| !idx.contains(r))
3332                        .collect();
3333                }
3334                // EXCEPT ALL: multiset subtraction — each right
3335                // occurrence cancels one left occurrence.
3336                UnionKind::ExceptAll => {
3337                    let mut idx = PeerIndex::build(&peer_rows, fold);
3338                    let mut kept: Vec<Row<'static>> = Vec::new();
3339                    for r in rows {
3340                        if !idx.take_one(&r) {
3341                            kept.push(r);
3342                        }
3343                    }
3344                    rows = kept;
3345                }
3346            }
3347        }
3348        // PG resolves a UNION / VALUES result column to one common type
3349        // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3350        // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3351        // built each branch independently, leaving mixed-type columns
3352        // that broke ORDER BY, comparisons, and value-based window
3353        // frames. Unify + coerce before the combined ORDER BY sees them.
3354        unify_union_columns(&mut columns, &mut rows);
3355        // ORDER BY at the top of a UNION applies to the combined result.
3356        // Eval against the projected schema (NOT the source table).
3357        if !stmt.order_by.is_empty() {
3358            // v7.39 (read01 round 54) — the combined-result ctx must carry the
3359            // catalog, and the projected columns must keep their enum identity
3360            // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3361            // by TEXT instead of member order — silently wrong rows, not an
3362            // error. (Same shape as the enum-order knife's GROUP BY fix.)
3363            let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3364            // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3365            // survive to here when the head projects a Wildcard (the
3366            // group-tail wrapper shape): map them onto the Nth
3367            // projected column so the combined sort works.
3368            let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3369                .order_by
3370                .iter()
3371                .map(|o| {
3372                    let mut o = o.clone();
3373                    if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3374                        && *n >= 1
3375                        && let Ok(idx) = usize::try_from(*n - 1)
3376                        && idx < columns.len()
3377                    {
3378                        o.expr = Expr::Column(spg_sql::ast::ColumnName {
3379                            qualifier: None,
3380                            name: columns[idx].name.clone(),
3381                        });
3382                    }
3383                    o
3384                })
3385                .collect();
3386            let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3387            let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3388            for r in rows {
3389                // v7.39.12 — a correlated subquery in ORDER BY is resolved
3390                // for this row before the key is built; see
3391                // `Engine::order_by_resolved_for_row`.
3392                let per_row =
3393                    self.order_by_resolved_for_row(&resolved_order, &r, &synth_ctx, cancel)?;
3394                let keys = build_order_keys(
3395                    per_row.as_deref().unwrap_or(&resolved_order),
3396                    &r,
3397                    &synth_ctx,
3398                )?;
3399                tagged.push((keys, r));
3400            }
3401            sort_by_keys(&mut tagged, &descs);
3402            rows = tagged.into_iter().map(|(_, r)| r).collect();
3403        }
3404        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3405        Ok(QueryResult::Rows { columns, rows })
3406    }
3407
3408    fn exec_select_cancel_inner(
3409        &self,
3410        stmt: &SelectStatement,
3411        cancel: CancelToken<'_>,
3412    ) -> Result<QueryResult, EngineError> {
3413        cancel.check()?;
3414        // v7.38 P0 元机制 A — first observable point inside the
3415        // planner / executor. Tests use this to inject a delay or
3416        // a cancellation race before any row is produced. Release
3417        // build expands to `let _ = (...);` — zero cost.
3418        crate::injection_point!("planner_first_row_fetch", &stmt.from);
3419        // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3420        // PG analyses every definition, referenced or not, so `SELECT i FROM
3421        // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3422        // succeeded here (the parser used to drop the unreferenced defs
3423        // whole). The check is the CREATE VIEW check's shape (round 700): a
3424        // LIMIT-0 run of the same FROM with the definitions' key
3425        // expressions as the projection — it cannot disagree with what a
3426        // referencing window would have done, because it resolves the same
3427        // names the same way. Zero cost for the ordinary statement: the
3428        // list is empty unless a WINDOW clause left unreferenced defs.
3429        if !stmt.window_check_exprs.is_empty() {
3430            let mut probe = stmt.clone();
3431            probe.items = stmt
3432                .window_check_exprs
3433                .iter()
3434                .map(|e| spg_sql::ast::SelectItem::Expr {
3435                    expr: e.clone(),
3436                    alias: None,
3437                })
3438                .collect();
3439            probe.window_check_exprs = Vec::new();
3440            probe.distinct = false;
3441            probe.distinct_on = Vec::new();
3442            probe.group_by = None;
3443            probe.group_by_all = false;
3444            probe.having = None;
3445            probe.unions = Vec::new();
3446            probe.order_by = Vec::new();
3447            probe.locking = None;
3448            probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3449            probe.offset = None;
3450            probe.limit_with_ties = false;
3451            self.exec_select_cancel_inner(&probe, cancel)?;
3452        }
3453        // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3454        // takes the catalog, so the parser leaves a marker and the rewrite lands
3455        // here: the call moves into a LATERAL FROM item and the item becomes one
3456        // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3457        // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3458        // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3459        // second one.
3460        if let Some(lowered) = self.lower_record_expansion(stmt)? {
3461            return self.exec_select_cancel_inner(&lowered, cancel);
3462        }
3463        // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3464        // FROM / JOIN graph references any catalogued view name,
3465        // re-parse the view body and prepend it as a synthetic
3466        // CTE. Recurses on views-in-views via the regular CTE
3467        // dispatch below. Fast-path: skip the walker entirely when
3468        // the catalog has no views (the typical OLTP load).
3469        if !self.active_catalog().views_all().is_empty() {
3470            if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3471                return self.exec_select_cancel(&rewritten, cancel);
3472            }
3473        }
3474        // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3475        // gets rewritten to a UNION-ALL over the children that overlap
3476        // the WHERE-derived key range. Uses the same CTE-injection
3477        // trick as VIEW expansion above so downstream resolution
3478        // doesn't need a partition-aware code path.
3479        if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3480            return self.exec_select_cancel(&rewritten, cancel);
3481        }
3482        // v7.16.2 — information_schema / pg_catalog virtual
3483        // views (mailrs round-10 A.3). If the SELECT touches a
3484        // synthetic meta-table name (`__spg_info_*` /
3485        // `__spg_pg_*` — produced by the parser for
3486        // `information_schema.X` / `pg_catalog.X`), clone the
3487        // catalog, materialise the requested view as a real
3488        // temporary table, and re-execute against an enriched
3489        // engine. Same pattern as `exec_with_ctes` for CTEs.
3490        if !self.meta_views_materialised && select_references_meta_view(stmt) {
3491            return self.exec_select_with_meta_views(stmt, cancel);
3492        }
3493        // v6.10.2 — cold-tier time-travel short-circuit. When the
3494        // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3495        // dedicated cold-segment scan instead of the regular
3496        // hot+index path. The scope is intentionally narrow for
3497        // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3498        // optionally with a single-column-equality WHERE. JOINs /
3499        // aggregates / ORDER BY / subqueries on top of a time-
3500        // travelled scan are STABILITY § "Out of v6.10".
3501        if let Some(from) = &stmt.from
3502            && let Some(seg_id) = from.primary.as_of_segment
3503        {
3504            return self.exec_select_as_of_segment(stmt, from, seg_id);
3505        }
3506        // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3507        // pre-CTE because they don't read from the catalog and
3508        // shouldn't participate in regular FROM resolution.
3509        // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3510        // short-circuits. A meta-view FROM materialises to a fixed row
3511        // set. For a bare `SELECT *` we return it directly; otherwise we
3512        // stage it as a temp table and run the normal pipeline, so
3513        // projection / WHERE / ORDER BY / aggregates work over these views
3514        // (they were `SELECT *`-only before). A real table shadowing the
3515        // name wins (checked first), which also stops the staged re-run
3516        // from recursing back into meta-view detection.
3517        if let Some(from) = &stmt.from
3518            && from.joins.is_empty()
3519            && self.active_catalog().get(&from.primary.name).is_none()
3520        {
3521            let lower = from.primary.name.to_ascii_lowercase();
3522            if let Some(result) = self.meta_view_result(&lower) {
3523                let bare = stmt.where_.is_none()
3524                    && stmt.group_by.is_none()
3525                    && stmt.having.is_none()
3526                    && stmt.unions.is_empty()
3527                    && stmt.order_by.is_empty()
3528                    && stmt.limit.is_none()
3529                    && stmt.offset.is_none()
3530                    && !stmt.distinct
3531                    && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3532                if bare {
3533                    return Ok(result);
3534                }
3535                if let QueryResult::Rows { columns, rows } = result {
3536                    let mut catalog = self.active_catalog().clone();
3537                    let cols = infer_column_types(&columns, &rows);
3538                    let schema = TableSchema::new(from.primary.name.clone(), cols);
3539                    catalog.create_table(schema).map_err(EngineError::Storage)?;
3540                    let t = catalog
3541                        .get_mut(&from.primary.name)
3542                        .expect("just-created meta-view table must exist");
3543                    for row in rows {
3544                        t.insert(row).map_err(EngineError::Storage)?;
3545                    }
3546                    let mut eng = Engine::restore(catalog);
3547                    if let Some(c) = self.clock {
3548                        eng = eng.with_clock(c);
3549                    }
3550                    if let Some(f) = self.salt_fn {
3551                        eng = eng.with_salt_fn(f);
3552                    }
3553                    // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3554                    // connection identity so `WHERE pid = pg_backend_pid()`
3555                    // matches inside the staged meta-view run.
3556                    if let Some(f) = self.backend_pid_fn {
3557                        eng.set_backend_pid_fn(f);
3558                    }
3559                    return eng.exec_select_cancel(stmt, cancel);
3560                }
3561                return Ok(result);
3562            }
3563        }
3564        // v4.11: CTEs materialise into a temporary enriched catalog
3565        // *before* anything else — the body SELECT can then refer
3566        // to CTE names via the regular FROM-clause resolution.
3567        // Uncorrelated only: each CTE body runs once against the
3568        // current catalog, not against later CTEs' results (left-
3569        // to-right materialisation would relax this, but we keep
3570        // it simple for v4.11 MVP).
3571        if !stmt.ctes.is_empty() {
3572            return self.exec_with_ctes(stmt, cancel);
3573        }
3574        // v4.10: subqueries (uncorrelated) are resolved here, before
3575        // the executor sees the row loop. We clone the statement so
3576        // we can mutate without disturbing the caller's AST — most
3577        // queries pass through with no subquery nodes and the clone
3578        // is cheap; with subqueries the materialisation cost
3579        // dominates anyway.
3580        let mut stmt_owned;
3581        let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3582            stmt_owned = stmt.clone();
3583            // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3584            // aggregate-wrapped correlated scalar subquery whose
3585            // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3586            // executor streams one join instead of splicing a per-row
3587            // subplan. Runs before the per-row/batch resolver, which then
3588            // only sees the subqueries the pull-up left behind.
3589            self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3590            // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3591            // the "per-key latest" scalar subquery shape (inbox / feed
3592            // / timeline applications) becomes a CTE + LEFT JOIN
3593            // against a GROUP BY pre-aggregation that reuses the v7.33
3594            // first_ordered argmax executor. Runs AFTER unique-key
3595            // pull-up (so the unique-key fast path still wins for
3596            // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3597            // Phase 1 (this commit) is skeleton only — no-op pass.
3598            self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3599            // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3600            // sublink pull-up to semi/anti-join, before the resolver gets
3601            // a chance to walk per-row.
3602            self.pull_up_exists_sublinks(&mut stmt_owned);
3603            // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3604            // exec_with_ctes so they materialise once before the body
3605            // SELECT runs. exec_with_ctes strips ctes from the body
3606            // clone, then re-enters select.
3607            if !stmt_owned.ctes.is_empty() {
3608                return self.exec_with_ctes(&stmt_owned, cancel);
3609            }
3610            // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3611            //   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3612            // BEFORE `resolve_select_subqueries` materialises the inner
3613            // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3614            // INSUBQ benchmark). Run the inner once, collect the result
3615            // values into a `HashSet<i64>` directly, then probe A.pk per
3616            // value and tally. Returns `Some` when the shape matches.
3617            if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3618                return Ok(out);
3619            }
3620            self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3621            &stmt_owned
3622        } else {
3623            stmt
3624        };
3625        if stmt_ref.unions.is_empty() {
3626            return self.exec_bare_select_cancel(stmt_ref, cancel);
3627        }
3628        self.exec_union_chain(stmt_ref, stmt, cancel)
3629    }
3630
3631    #[allow(clippy::too_many_lines)]
3632    #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3633    /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3634    /// Synthesises a single-column virtual table whose column type
3635    /// is TEXT and whose rows are the array elements. Routes
3636    /// through the regular projection / WHERE / ORDER BY / LIMIT
3637    /// machinery so set-returning UNNEST composes naturally with
3638    /// the rest of the SELECT surface.
3639    fn exec_select_unnest(
3640        &self,
3641        stmt: &SelectStatement,
3642        primary: &TableRef,
3643        cancel: CancelToken<'_>,
3644    ) -> Result<QueryResult, EngineError> {
3645        let expr = primary
3646            .unnest_expr
3647            .as_deref()
3648            .expect("caller guards unnest_expr.is_some()");
3649        // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3650        // N value columns instead of one; the shared builder does
3651        // the work and the tail below (WHERE / agg / projection)
3652        // runs against the wider schema.
3653        let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3654            match unnest_zip_args(expr) {
3655                Some(args) => Some(unnest_zip_rows(args)?),
3656                None => None,
3657            };
3658        // Evaluate the array expression once. Empty schema / empty
3659        // row — uncorrelated UNNEST cannot reference outer columns.
3660        // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3661        // introspection family (enum_range / enum_first / enum_last) resolves
3662        // its labels from the argument's STATIC enum type against the
3663        // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3664        // fell through to the generic arm, got NULL, and expanded to zero rows
3665        // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3666        // carry the catalog) worked.
3667        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3668        let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3669        let dummy_row = Row::new(alloc::vec::Vec::new());
3670        // v7.11.13 — unnest dispatches per array element type so
3671        // INT[] / BIGINT[] surface their PG types in projection.
3672        // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3673        // columns (PG: lexeme | positions | weights); everything else
3674        // keeps the alias / "unnest" defaults below.
3675        let mut composite_names: Option<&[&str]> = None;
3676        let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3677            if let Some(m) = multi {
3678                m
3679            } else {
3680                // v7.39 (round 236) — flatten a multidimensional array into
3681                // its row-major elements (PG) before the 1-D-only match.
3682                let unnest_src = {
3683                    let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3684                    crate::eval::values::flatten_2d(&v).unwrap_or(v)
3685                };
3686                let mut return_multi: Option<(
3687                    alloc::vec::Vec<DataType>,
3688                    alloc::vec::Vec<Row<'static>>,
3689                )> = None;
3690                let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3691                {
3692                    Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3693                    Value::TextArray(items) => {
3694                        let rows = items
3695                            .into_iter()
3696                            .map(|item| {
3697                                Row::new(alloc::vec![match item {
3698                                    Some(s) => Value::text(s),
3699                                    None => Value::Null,
3700                                }])
3701                            })
3702                            .collect();
3703                        (DataType::Text, rows)
3704                    }
3705                    Value::IntArray(items) => {
3706                        let rows = items
3707                            .into_iter()
3708                            .map(|item| {
3709                                Row::new(alloc::vec![match item {
3710                                    Some(n) => Value::Int(n),
3711                                    None => Value::Null,
3712                                }])
3713                            })
3714                            .collect();
3715                        (DataType::Int, rows)
3716                    }
3717                    Value::BigIntArray(items) => {
3718                        let rows = items
3719                            .into_iter()
3720                            .map(|item| {
3721                                Row::new(alloc::vec![match item {
3722                                    Some(n) => Value::BigInt(n),
3723                                    None => Value::Null,
3724                                }])
3725                            })
3726                            .collect();
3727                        (DataType::BigInt, rows)
3728                    }
3729                    Value::Multirange { kind, ranges } => {
3730                        let rows = ranges
3731                            .iter()
3732                            .map(|sp| {
3733                                Row::new(alloc::vec![Value::Range {
3734                                    kind,
3735                                    lower: sp.lower.clone(),
3736                                    upper: sp.upper.clone(),
3737                                    lower_inc: sp.lower_inc,
3738                                    upper_inc: sp.upper_inc,
3739                                    empty: false,
3740                                }])
3741                            })
3742                            .collect();
3743                        (DataType::Range(kind), rows)
3744                    }
3745                    // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3746                    // one row per lexeme, PG18-measured columns
3747                    // lexeme | positions | weights (`a | {1,3} |
3748                    // {D,D}`); a position-less lexeme (a stripped
3749                    // vector) reads NULL in both array columns.
3750                    Value::TsVector(lexemes) => {
3751                        composite_names = Some(&["lexeme", "positions", "weights"]);
3752                        let rows = lexemes
3753                            .iter()
3754                            .map(|l| {
3755                                let (pos, wts) = if l.positions.is_empty() {
3756                                    (Value::Null, Value::Null)
3757                                } else {
3758                                    let letter = match l.weight {
3759                                        3 => "A",
3760                                        2 => "B",
3761                                        1 => "C",
3762                                        _ => "D",
3763                                    };
3764                                    (
3765                                        Value::SmallIntArray(
3766                                            l.positions
3767                                                .iter()
3768                                                .map(|p| {
3769                                                    Some(i16::try_from(*p).unwrap_or(i16::MAX))
3770                                                })
3771                                                .collect(),
3772                                        ),
3773                                        Value::TextArray(
3774                                            l.positions
3775                                                .iter()
3776                                                .map(|_| Some(letter.into()))
3777                                                .collect(),
3778                                        ),
3779                                    )
3780                                };
3781                                Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3782                            })
3783                            .collect();
3784                        return_multi = Some((
3785                            alloc::vec![
3786                                DataType::Text,
3787                                DataType::SmallIntArray,
3788                                DataType::TextArray
3789                            ],
3790                            rows,
3791                        ));
3792                        (DataType::Text, alloc::vec::Vec::new())
3793                    }
3794                    // v7.39.11 — every remaining array-family value,
3795                    // through the one element menu, so a type does not
3796                    // have to be written out here a second time to be
3797                    // unnestable. `unnest(ARRAY[1,2]::smallint[])`
3798                    // raised "expects an array argument, got
3799                    // smallint[]" until this arm — the arms above name
3800                    // int / bigint / text / json and stop — and so did
3801                    // every catalog vector. Found while closing
3802                    // sentori's §4 against 7.39.10.
3803                    ref v if crate::eval::values::array_len(v).is_some() => {
3804                        let elems = crate::eval::values::array_elements(v).unwrap_or_default();
3805                        let dt = elems
3806                            .iter()
3807                            .find_map(spg_storage::Value::data_type)
3808                            .unwrap_or(DataType::Text);
3809                        let rows = elems
3810                            .into_iter()
3811                            .map(|e| Row::new(alloc::vec![e]))
3812                            .collect();
3813                        (dt, rows)
3814                    }
3815                    other => {
3816                        // v7.39 (round 622, S05a) — see table_access.rs:
3817                        // the same sentence, and it is a type mismatch.
3818                        return Err(EngineError::Eval(EvalError::TypeMismatch {
3819                            detail: alloc::format!(
3820                                "unnest() expects an array argument, got {}",
3821                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3822                            ),
3823                        }));
3824                    }
3825                };
3826                if let Some(m) = return_multi {
3827                    m
3828                } else {
3829                    (alloc::vec![elem_dtype], rows)
3830                }
3831            };
3832        let alias = primary
3833            .alias
3834            .clone()
3835            .unwrap_or_else(|| "unnest".to_string());
3836        // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3837        // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3838        // entries map positionally over the value columns. Without
3839        // the column list, a single column falls back to the table
3840        // alias (pre-v7.13.2 behaviour); multi-arg columns default
3841        // to PG's `unnest`.
3842        let n_vals = dtypes.len();
3843        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3844            .iter()
3845            .enumerate()
3846            .map(|(i, dt)| {
3847                let name = primary
3848                    .unnest_column_aliases
3849                    .get(i)
3850                    .cloned()
3851                    .unwrap_or_else(|| {
3852                        if let Some(names) = composite_names {
3853                            names
3854                                .get(i)
3855                                .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3856                        } else if n_vals == 1 {
3857                            alias.clone()
3858                        } else {
3859                            "unnest".to_string()
3860                        }
3861                    });
3862                ColumnSchema::new(name, *dt, true)
3863            })
3864            .collect();
3865        // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3866        // parser desugared a base-type-returning function here (see
3867        // TableRef::scalar_fn_item); the marker rides the column so it survives
3868        // every EvalContext an inner stage rebuilds.
3869        if primary.scalar_fn_item && schema_cols.len() == 1 {
3870            schema_cols[0].scalar_row_source = true;
3871        }
3872        // WITH ORDINALITY — trailing BIGINT counting rows from 1
3873        // in element order. The alias entry after the value
3874        // columns renames it (PG default: `ordinality`).
3875        let rows = if primary.with_ordinality {
3876            let ord_name = primary
3877                .unnest_column_aliases
3878                .get(n_vals)
3879                .cloned()
3880                .unwrap_or_else(|| "ordinality".to_string());
3881            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3882            rows.into_iter()
3883                .enumerate()
3884                .map(|(i, row)| {
3885                    let mut vals = row.values.clone();
3886                    vals.push(Value::BigInt(i as i64 + 1));
3887                    Row::new(vals)
3888                })
3889                .collect()
3890        } else {
3891            rows
3892        };
3893        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3894        // `EvalContext::new` drops it and every catalog-dependent cast
3895        // (regclass / enum / composite / domain) silently degrades.
3896        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3897        // Apply WHERE.
3898        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3899            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3900            for row in rows {
3901                cancel.check()?;
3902                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3903                if matches!(v, Value::Bool(true)) {
3904                    out.push(row);
3905                }
3906            }
3907            out
3908        } else {
3909            rows
3910        };
3911        // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3912        // unnest source. Same routing the relational scan path
3913        // already takes — without it `SELECT COUNT(*) FROM
3914        // unnest(ARRAY[…])` either errored at projection time or
3915        // returned the wrong shape.
3916        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
3917            // v7.29 — a per-query memo so correlated scalar
3918            // subqueries batch-evaluate once (group map) instead of
3919            // executing per group.
3920            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3921            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3922                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3923                    .map_err(|err| match err {
3924                        EngineError::Eval(ev) => ev,
3925                        other => eval::EvalError::TypeMismatch {
3926                            detail: alloc::format!("{other}"),
3927                        },
3928                    })
3929            };
3930            // v7.39 (round 656) — hand the rows over as they are rather than
3931            // collecting a second vector of `RowRef` wrappers. Note this is
3932            // a set-returning-function path, NOT the relational scan: the
3933            // measured O(rows) cost lived in `run_single_table_aggregate`,
3934            // and converting these four first was a miss that cost a full
3935            // round — every test stayed green and the number did not move.
3936            let agg = aggregate::run(
3937                stmt,
3938                crate::join::AggRows::Owned(&filtered),
3939                &schema_cols,
3940                Some(&alias),
3941                Some(&agg_correlated),
3942                self.parallel_runner.0.as_deref(),
3943                Some(self.active_catalog()),
3944                Some(self),
3945            )?;
3946            return self.finish_agg_result(agg, stmt, cancel);
3947        }
3948        // Projection.
3949        let projection = build_projection(
3950            &stmt.items,
3951            &schema_cols,
3952            &alias,
3953            self.speaks_mysql,
3954            Some(self.active_catalog()),
3955        )?;
3956        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3957            alloc::vec::Vec::with_capacity(filtered.len());
3958        // v7.19 P5 — Set-Returning-Function in projection
3959        // position (PG `SELECT unnest(arr) FROM t` shape). When a
3960        // SELECT item evaluates to a top-level unnest(arr) call,
3961        // expand it: for each input row, evaluate the array, emit
3962        // one output row per element, broadcasting non-SRF
3963        // projections from the same input row. Multi-SRF + LCM
3964        // padding stays a documented carve-out; mailrs uses
3965        // single-SRF for redirect_uris.
3966        // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3967        // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3968        let srf_idxs = self.srf_target_idxs(&projection);
3969        // v7.39 (round 621) — which input row each output row came from. An
3970        // SRF turns one input row into many, and the ORDER BY below used to
3971        // index the EXPANDED rows by the INPUT row's position: the result was
3972        // silently truncated to the input row count and left unsorted, so
3973        // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3974        // answered three of its six rows, in no order. Without the ORDER BY
3975        // the same query was already right.
3976        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3977        if !srf_idxs.is_empty() {
3978            let (rows, src) =
3979                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3980            projected_rows = rows;
3981            src_of_row = src;
3982        } else {
3983            // v7.24 (round-16 B) — select-list subqueries resolve
3984            // per row (correlated-aware; plain exprs take the fast
3985            // path inside).
3986            let mut proj_memo = memoize::MemoizeCache::default();
3987            for row in &filtered {
3988                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3989                for p in &projection {
3990                    vals.push(self.eval_expr_with_correlated(
3991                        &p.expr,
3992                        row,
3993                        &scan_ctx,
3994                        cancel,
3995                        Some(&mut proj_memo),
3996                    )?);
3997                }
3998                projected_rows.push(Row::new(vals));
3999            }
4000        }
4001        // ORDER BY / LIMIT — apply on the projected rows (cheap;
4002        // unnest result sets are small by design).
4003        let columns: alloc::vec::Vec<ColumnSchema> = projection
4004            .iter()
4005            // v7.39 (read01 round 54) — keep the column's enum identity through
4006            // the projection (it lives outside the DataType lattice), or a
4007            // derived table / UNION / windowed result forgets it and any outer
4008            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4009            .map(|p| p.to_column_schema())
4010            .collect();
4011        // Re-evaluate ORDER BY against the source schema (pre-projection
4012        // so col refs by name still resolve through `scan_ctx`).
4013        // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
4014        // column. Evaluated as an expression it is just the constant N: the same
4015        // key for every row, so the sort ran and changed nothing.
4016        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4017        if !order_by.is_empty() {
4018            // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
4019            // A key that names a select-list item reads it out of the expanded
4020            // row (PG sorts AFTER the expansion); one that names a source
4021            // column the query does not project is evaluated on the input row
4022            // it came from, which is what `srf_order_output_cols` decides.
4023            let out_cols = if srf_idxs.is_empty() {
4024                alloc::vec![None; order_by.len()]
4025            } else {
4026                srf_order_output_cols(&order_by, &projection)
4027            };
4028            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4029                .iter()
4030                .enumerate()
4031                .map(|(k, out)| -> Result<_, EngineError> {
4032                    let src = src_of_row.get(k).copied().unwrap_or(k);
4033                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4034                        .iter()
4035                        .zip(out_cols.iter())
4036                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
4037                        .collect();
4038                    Ok((k, keys?))
4039                })
4040                .collect::<Result<_, _>>()?;
4041            indexed.sort_by(|a, b| {
4042                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4043                    let o = &order_by[idx];
4044                    let cmp = order_by_value_cmp_in(
4045                        o.desc,
4046                        o.nulls_first,
4047                        ka,
4048                        kb,
4049                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4050                    );
4051                    if cmp != core::cmp::Ordering::Equal {
4052                        return cmp;
4053                    }
4054                }
4055                core::cmp::Ordering::Equal
4056            });
4057            projected_rows = indexed
4058                .into_iter()
4059                .map(|(i, _)| projected_rows[i].clone())
4060                .collect();
4061        }
4062        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4063        if stmt.distinct {
4064            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4065            // spec folds EVERY text position, so a column declared
4066            // `COLLATE utf8mb4_bin` had its values merged here exactly the
4067            // way 3b494b6e fixed on the main scan path. The projection is
4068            // already in scope at each of these sites, so the mask needs no
4069            // new plumbing -- it was simply never asked for.
4070            projected_rows = dedup_rows(
4071                projected_rows,
4072                FoldSpec::of_masks(
4073                    scan_ctx.mysql_dialect,
4074                    &fold_mask(&projection),
4075                    &pad_mask(&projection),
4076                ),
4077            );
4078        }
4079        // LIMIT / OFFSET — apply at the tail.
4080        if let Some(offset) = stmt.offset_literal() {
4081            let off = (offset as usize).min(projected_rows.len());
4082            projected_rows.drain(..off);
4083        }
4084        if let Some(limit) = stmt.limit_literal() {
4085            projected_rows.truncate(limit as usize);
4086        }
4087        Ok(QueryResult::Rows {
4088            columns,
4089            rows: projected_rows,
4090        })
4091    }
4092
4093    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
4094    /// step])` set-returning source. Mirrors `exec_select_unnest`'s
4095    /// shape: evaluate the arg list once against an empty row,
4096    /// materialise the row stream by stepping start → stop, then
4097    /// route through the standard WHERE / projection / ORDER BY /
4098    /// LIMIT pipeline. Two arg-type combos in v7.17:
4099    ///   * integer / integer [/ integer] — SmallInt, Int, BigInt
4100    ///     (widened to BigInt internally; step defaults to 1)
4101    ///   * timestamp / timestamp / interval — date-range
4102    ///     iteration (mailrs's daily-report pattern)
4103    fn exec_select_generate_series(
4104        &self,
4105        stmt: &SelectStatement,
4106        primary: &TableRef,
4107        cancel: CancelToken<'_>,
4108    ) -> Result<QueryResult, EngineError> {
4109        let args = primary
4110            .generate_series_args
4111            .as_ref()
4112            .expect("caller guards generate_series_args.is_some()");
4113        let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
4114        let alias = primary
4115            .alias
4116            .clone()
4117            .unwrap_or_else(|| "generate_series".to_string());
4118        // `AS t(n)` — the first column-alias entry renames the
4119        // series column (PG semantics); bare alias keeps the
4120        // pre-existing behaviour of naming the column after it.
4121        let col_name = primary
4122            .unnest_column_aliases
4123            .first()
4124            .cloned()
4125            .unwrap_or_else(|| alias.clone());
4126        let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
4127        let mut schema_cols = alloc::vec![col_schema.clone()];
4128        // WITH ORDINALITY — trailing BIGINT counting rows from 1;
4129        // the second column-alias entry renames it.
4130        let rows = if primary.with_ordinality {
4131            let ord_name = primary
4132                .unnest_column_aliases
4133                .get(1)
4134                .cloned()
4135                .unwrap_or_else(|| "ordinality".to_string());
4136            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4137            rows.into_iter()
4138                .enumerate()
4139                .map(|(i, row)| {
4140                    let mut vals = row.values.clone();
4141                    vals.push(Value::BigInt(i as i64 + 1));
4142                    Row::new(vals)
4143                })
4144                .collect()
4145        } else {
4146            rows
4147        };
4148        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4149        // `EvalContext::new` drops it and every catalog-dependent cast
4150        // (regclass / enum / composite / domain) silently degrades.
4151        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4152        // WHERE.
4153        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4154            let mut out = alloc::vec::Vec::with_capacity(rows.len());
4155            for row in rows {
4156                cancel.check()?;
4157                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4158                if matches!(v, Value::Bool(true)) {
4159                    out.push(row);
4160                }
4161            }
4162            out
4163        } else {
4164            rows
4165        };
4166        // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4167        // returning sources. When the SELECT projection contains
4168        // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4169        // …) we route the filtered row stream through the same
4170        // aggregate executor the relational scan path uses, so
4171        // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4172        // a single 100 row instead of erroring at projection
4173        // time. GROUP BY / HAVING / ORDER BY over the aggregate
4174        // output all ride through `aggregate::run`.
4175        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
4176            // v7.29 — a per-query memo so correlated scalar
4177            // subqueries batch-evaluate once (group map) instead of
4178            // executing per group.
4179            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4180            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4181                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4182                    .map_err(|err| match err {
4183                        EngineError::Eval(ev) => ev,
4184                        other => eval::EvalError::TypeMismatch {
4185                            detail: alloc::format!("{other}"),
4186                        },
4187                    })
4188            };
4189            // v7.39 (round 656) — hand the rows over as they are rather than
4190            // collecting a second vector of `RowRef` wrappers. Note this is
4191            // a set-returning-function path, NOT the relational scan: the
4192            // measured O(rows) cost lived in `run_single_table_aggregate`,
4193            // and converting these four first was a miss that cost a full
4194            // round — every test stayed green and the number did not move.
4195            let agg = aggregate::run(
4196                stmt,
4197                crate::join::AggRows::Owned(&filtered),
4198                &schema_cols,
4199                Some(&alias),
4200                Some(&agg_correlated),
4201                self.parallel_runner.0.as_deref(),
4202                Some(self.active_catalog()),
4203                Some(self),
4204            )?;
4205            return self.finish_agg_result(agg, stmt, cancel);
4206        }
4207        // Projection.
4208        let projection = build_projection(
4209            &stmt.items,
4210            &schema_cols,
4211            &alias,
4212            self.speaks_mysql,
4213            Some(self.active_catalog()),
4214        )?;
4215        // v7.39 (round 621) — and here, for the same reason.
4216        let srf_idxs = self.srf_target_idxs(&projection);
4217        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4218        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4219            alloc::vec::Vec::with_capacity(filtered.len());
4220        let mut proj_memo = memoize::MemoizeCache::default();
4221        if !srf_idxs.is_empty() {
4222            let (rows, src) =
4223                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4224            projected_rows = rows;
4225            src_of_row = src;
4226        } else {
4227            for row in &filtered {
4228                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4229                for p in &projection {
4230                    // v7.24 (round-16 B) — correlated-aware.
4231                    vals.push(self.eval_expr_with_correlated(
4232                        &p.expr,
4233                        row,
4234                        &scan_ctx,
4235                        cancel,
4236                        Some(&mut proj_memo),
4237                    )?);
4238                }
4239                projected_rows.push(Row::new(vals));
4240            }
4241        }
4242        let columns: alloc::vec::Vec<ColumnSchema> = projection
4243            .iter()
4244            // v7.39 (read01 round 54) — keep the column's enum identity through
4245            // the projection (it lives outside the DataType lattice), or a
4246            // derived table / UNION / windowed result forgets it and any outer
4247            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4248            .map(|p| p.to_column_schema())
4249            .collect();
4250        // ORDER BY against the source schema.
4251        // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4252        // more of them than there were inputs), and a positional key means the
4253        // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4254        // and what the other two synthetic-source tails already did.
4255        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4256        if !order_by.is_empty() {
4257            let out_cols = if srf_idxs.is_empty() {
4258                alloc::vec![None; order_by.len()]
4259            } else {
4260                srf_order_output_cols(&order_by, &projection)
4261            };
4262            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4263                .iter()
4264                .enumerate()
4265                .map(|(k, out)| -> Result<_, EngineError> {
4266                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4267                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4268                        .iter()
4269                        .zip(out_cols.iter())
4270                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4271                        .collect();
4272                    Ok((k, keys?))
4273                })
4274                .collect::<Result<_, _>>()?;
4275            indexed.sort_by(|a, b| {
4276                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4277                    let o = &stmt.order_by[idx];
4278                    let cmp = order_by_value_cmp_in(
4279                        o.desc,
4280                        o.nulls_first,
4281                        ka,
4282                        kb,
4283                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4284                    );
4285                    if cmp != core::cmp::Ordering::Equal {
4286                        return cmp;
4287                    }
4288                }
4289                core::cmp::Ordering::Equal
4290            });
4291            projected_rows = indexed
4292                .into_iter()
4293                .map(|(i, _)| projected_rows[i].clone())
4294                .collect();
4295        }
4296        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4297        if stmt.distinct {
4298            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4299            // spec folds EVERY text position, so a column declared
4300            // `COLLATE utf8mb4_bin` had its values merged here exactly the
4301            // way 3b494b6e fixed on the main scan path. The projection is
4302            // already in scope at each of these sites, so the mask needs no
4303            // new plumbing -- it was simply never asked for.
4304            projected_rows = dedup_rows(
4305                projected_rows,
4306                FoldSpec::of_masks(
4307                    scan_ctx.mysql_dialect,
4308                    &fold_mask(&projection),
4309                    &pad_mask(&projection),
4310                ),
4311            );
4312        }
4313        if let Some(offset) = stmt.offset_literal() {
4314            let off = (offset as usize).min(projected_rows.len());
4315            projected_rows.drain(..off);
4316        }
4317        if let Some(limit) = stmt.limit_literal() {
4318            projected_rows.truncate(limit as usize);
4319        }
4320        Ok(QueryResult::Rows {
4321            columns,
4322            rows: projected_rows,
4323        })
4324    }
4325
4326    /// The FROM shapes that are not an ordinary table scan — joins, the
4327    /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4328    ///
4329    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4330    /// reason round 848 established in the parser: a debug build gives
4331    /// EVERY branch's locals a slot in the frame, whichever branch runs.
4332    /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4333    /// stacks several of them; a plain scan reaches none of these
4334    /// branches. Moving them out took the frame to 52,336.
4335    ///
4336    /// `Ok(None)` means "not one of these shapes, carry on".
4337    #[inline(never)]
4338    fn try_from_shape_paths(
4339        &self,
4340        stmt: &SelectStatement,
4341        from: &spg_sql::ast::FromClause,
4342        cancel: CancelToken<'_>,
4343    ) -> Result<Option<QueryResult>, EngineError> {
4344        if !from.joins.is_empty() {
4345            // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4346            // elimination: when a LEFT JOIN's right side is referenced
4347            // ONLY in the ON equality and the right-side join key is
4348            // UNIQUE/PK, the join preserves outer cardinality exactly
4349            // and contributes no values used downstream. Drop the
4350            // entire join. PG does this on the
4351            // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4352            // — A's row count is what survives, B never has to be
4353            // touched.
4354            if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4355                return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4356            }
4357            // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4358            // the v7.32 joinfold rewrite that turns inner JOINs into a
4359            // single-table scan when the catalogue can prove key-only
4360            // dependency. Tests use this to assert "without joinfold,
4361            // the join still executes correctly" (joinfold is a
4362            // semantically-equivalent rewrite, not a correctness fix).
4363            if !self.env_cfg().disable_joinfold {
4364                if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4365                    return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4366                }
4367            }
4368            return self.exec_joined_select(stmt, from, cancel).map(Some);
4369        }
4370        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4371        // single-column table at SELECT entry by evaluating the
4372        // expression once against the empty row (UNNEST is
4373        // uncorrelated in v7.11; correlated / LATERAL unnest is a
4374        // v7.12 carve-out). Build a virtual `Table` in a heap-only
4375        // catalog, then route to the regular scan path.
4376        if from.primary.unnest_expr.is_some() {
4377            return self
4378                .exec_select_unnest(stmt, &from.primary, cancel)
4379                .map(Some);
4380        }
4381        // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4382        // returning function. Same dispatch shape as unnest but
4383        // emits a two-column (key TEXT, value TEXT) row stream.
4384        if from.primary.jsonb_each_text_arg.is_some() {
4385            return self
4386                .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4387                .map(Some);
4388        }
4389        // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4390        // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4391        // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4392        // array form. Each function runs; the results zip in LOCKSTEP with the
4393        // shorter padded to NULL — the SAME rule the target-list SRFs follow
4394        // (round 67), which is why `srf_values` is what evaluates each entry.
4395        if from.primary.rows_from.is_some() {
4396            let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4397            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4398                if let Some(col) = schema_cols.get_mut(i) {
4399                    col.name = new_name.clone();
4400                }
4401            }
4402            let alias = from
4403                .primary
4404                .alias
4405                .clone()
4406                .unwrap_or_else(|| from.primary.name.clone());
4407            return self
4408                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4409                .map(Some);
4410        }
4411        // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4412        // COLUMNS (...))`. Materialise the row stream + schema by
4413        // walking the row path, then run the regular pipeline over it.
4414        if let Some(jt) = &from.primary.json_table {
4415            let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4416            let alias = from
4417                .primary
4418                .alias
4419                .clone()
4420                .unwrap_or_else(|| from.primary.name.clone());
4421            return self
4422                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4423                .map(Some);
4424        }
4425        if from.primary.table_fn_call.is_some() {
4426            let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4427            // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4428            // (from 1, in output order) AFTER the function's own columns. The
4429            // alias list names it like any other, which is why it is appended
4430            // BEFORE the renaming pass below.
4431            let rows = if from.primary.with_ordinality {
4432                schema_cols.push(ColumnSchema::new(
4433                    "ordinality".to_string(),
4434                    DataType::BigInt,
4435                    false,
4436                ));
4437                rows.into_iter()
4438                    .enumerate()
4439                    .map(|(i, r)| {
4440                        let mut vals = r.values;
4441                        vals.push(Value::BigInt(i as i64 + 1));
4442                        Row::new(vals)
4443                    })
4444                    .collect()
4445            } else {
4446                rows
4447            };
4448            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4449                if let Some(col) = schema_cols.get_mut(i) {
4450                    col.name = new_name.clone();
4451                }
4452            }
4453            let alias = from
4454                .primary
4455                .alias
4456                .clone()
4457                .unwrap_or_else(|| from.primary.name.clone());
4458            return self
4459                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4460                .map(Some);
4461        }
4462        // v7.37.17 (17.6 siblings) — plain derived table in primary
4463        // position: `FROM ( SELECT … ) alias` (no joins). The inner
4464        // SELECT materialises once (it is uncorrelated by
4465        // construction), then the outer projection / WHERE /
4466        // aggregate / ORDER BY pipeline runs over the synthetic
4467        // table. Joined derived tables keep riding the LATERAL
4468        // machinery in join.rs.
4469        if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4470            // v7.39 (round 727) — flatten first. A simple derived table
4471            // (bare-column projection over one stored table, nothing that
4472            // changes cardinality or order) used to force the inner
4473            // SELECT through the SERIAL row-at-a-time projection pipeline
4474            // just to materialise a synthetic table the outer query then
4475            // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4476            // measured 18.6 ms against PG's 5 — and bare count over the
4477            // same filter WITHOUT the wrapper is 2 ms here, because it
4478            // rides the fused parallel lane. Rewriting to the unwrapped
4479            // form is PG's subquery pull-up; the whole tree gets the
4480            // fast lanes back.
4481            if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4482                return self.exec_select_cancel(&flat, cancel).map(Some);
4483            }
4484            // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4485            // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4486            // ORDER BY never changes the row count, and OFFSET drops
4487            // exactly k. The materialising path sorted 500k rows to
4488            // count 10k (57 ms); PG runs its parallel sort anyway
4489            // (28 ms). The rewrite skips the sort entirely on both
4490            // counts — a plan PG itself does not have.
4491            if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4492                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4493            }
4494            // v7.39 (round 743) — `count(*) OVER a derived whose only
4495            // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4496            // a constant-length array unnests to exactly k rows per
4497            // input row, NULL elements included. PG expands the set to
4498            // count it (6.6 ms on the panel cell); the identity doesn't.
4499            if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4500                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4501            }
4502            return self
4503                .exec_select_derived(stmt, &from.primary, cancel)
4504                .map(Some);
4505        }
4506        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4507        // [, step])` set-returning source. Dispatch mirrors UNNEST:
4508        // materialise the row stream from a single eval pass, then
4509        // run the regular projection / WHERE / ORDER BY / LIMIT
4510        // pipeline over the synthetic single-column table.
4511        if from.primary.generate_series_args.is_some() {
4512            return self
4513                .exec_select_generate_series(stmt, &from.primary, cancel)
4514                .map(Some);
4515        }
4516        Ok(None)
4517    }
4518
4519    /// Pick an index seek for this WHERE, if any of the four apply:
4520    /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4521    ///
4522    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4523    /// frame reason on `try_from_shape_paths`: in a debug build a
4524    /// closure's locals belong to the enclosing frame, and this one is
4525    /// four seek attempts wide on a function that nests.
4526    #[inline(never)]
4527    fn pick_indexed_rows<'r>(
4528        &'r self,
4529        stmt: &SelectStatement,
4530        table: &'r spg_storage::Table,
4531        schema_cols: &[spg_storage::ColumnSchema],
4532        alias: &str,
4533        ctx: &crate::eval::EvalContext<'_>,
4534        seek_snapshot: &crate::Snapshot,
4535    ) -> Option<crate::index_access::Seeked<'r>> {
4536        stmt.where_.as_ref().and_then(|w| {
4537            // BTree / col=literal seek first — covers the v7.11.3 multi-
4538            // column AND case and the leading-column equality lookup.
4539            try_index_seek(
4540                w,
4541                schema_cols,
4542                self.active_catalog(),
4543                table,
4544                alias,
4545                seek_snapshot,
4546                ctx.mysql_dialect,
4547            )
4548            .or_else(|| {
4549                // v7.12.3 — GIN-accelerated `WHERE col @@
4550                // tsquery` when the column has a `USING gin`
4551                // index. Returns an over-approximate candidate
4552                // set; the WHERE re-eval loop below verifies
4553                // the full `@@` predicate per row.
4554                try_gin_seek(
4555                    w,
4556                    schema_cols,
4557                    self.active_catalog(),
4558                    table,
4559                    alias,
4560                    ctx,
4561                    seek_snapshot,
4562                )
4563                .map(crate::index_access::Seeked::over_approximate)
4564            })
4565            .or_else(|| {
4566                // v7.15.0 — trigram-GIN-accelerated
4567                // `WHERE col LIKE / ILIKE '<pat>'` when the
4568                // column has a `gin_trgm_ops` GIN index.
4569                // Over-approximate candidate set; the WHERE
4570                // re-eval verifies the LIKE per row.
4571                try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4572                    .map(crate::index_access::Seeked::over_approximate)
4573            })
4574            .or_else(|| {
4575                // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4576                // accelerated `WHERE col @> <jsonb_literal>`
4577                // when the column has a `USING gin` index. The
4578                // posting-list intersection returns an over-
4579                // approximate candidate set; the WHERE re-eval
4580                // verifies the full `@>` predicate per row.
4581                try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4582                    .map(crate::index_access::Seeked::over_approximate)
4583            })
4584        })
4585    }
4586
4587    /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4588    /// the two `count(*)` short-circuits. Out-of-line for the frame
4589    /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4590    /// of them, and in a debug build their locals sit in the frame
4591    /// regardless.
4592    #[inline(never)]
4593    fn try_seek_fast_paths(
4594        &self,
4595        stmt: &SelectStatement,
4596        table: &spg_storage::Table,
4597        schema_cols: &[spg_storage::ColumnSchema],
4598        alias: &str,
4599        seek_snapshot: &crate::Snapshot,
4600        cancel: CancelToken<'_>,
4601    ) -> Result<Option<QueryResult>, EngineError> {
4602        if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4603            // NSW kNN dispatches against the hot-tier vector index only
4604            // (vector cells aren't promoted to cold segments), so wrap
4605            // the returned row indices as `Cow::Borrowed` for the
4606            // unified `materialise_in_order` shape.
4607            let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4608                .into_iter()
4609                .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4610                .collect();
4611            return materialise_in_order(stmt, schema_cols, alias, &ordered, self.speaks_mysql)
4612                .map(Some);
4613        }
4614
4615        // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4616        // the scan via the BTree iterator in the requested direction
4617        // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4618        // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4619        // the load-bearing consumer; this skips the materialise-every-
4620        // row + partial-sort tail entirely. Walker output is already
4621        // in ORDER BY order so `materialise_in_order` (no extra sort)
4622        // is the natural sink.
4623        if let Some(walked) = try_pk_walk_top_n(
4624            stmt,
4625            self.active_catalog(),
4626            table,
4627            schema_cols,
4628            alias,
4629            self,
4630            cancel,
4631            self.speaks_mysql,
4632        ) {
4633            return materialise_in_order(stmt, schema_cols, alias, &walked, self.speaks_mysql)
4634                .map(Some);
4635        }
4636
4637        // Index seek: if WHERE is `col = literal` (or commuted) and the
4638        // referenced column has an index, dispatch each locator through
4639        // the catalog (hot tier → borrow, cold tier → page-read +
4640        // decode) and iterate just those rows. Otherwise fall back to a
4641        // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4642        // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4643        // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4644        // we don't pay the row materialisation cost twice. Returns
4645        // a bare `Rows{count}` if the shape matches.
4646        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
4647            && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4648        {
4649            return Ok(Some(out));
4650        }
4651        // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4652        // locators directly, skipping row materialisation + WHERE re-eval.
4653        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
4654            && let Some(out) = self.try_count_star_indexed_range_fast(
4655                stmt,
4656                table,
4657                schema_cols,
4658                alias,
4659                seek_snapshot,
4660            )
4661        {
4662            return Ok(Some(out));
4663        }
4664        Ok(None)
4665    }
4666
4667    /// The two rewrites that must happen before the FROM clause is even
4668    /// looked at: a meta-view reference needs the catalog views
4669    /// materialised, and a windowed projection belongs to the window
4670    /// executor. Out-of-line for the frame reason on
4671    /// `try_from_shape_paths`.
4672    #[inline(never)]
4673    fn try_pre_from_paths(
4674        &self,
4675        stmt: &SelectStatement,
4676        cancel: CancelToken<'_>,
4677    ) -> Result<Option<QueryResult>, EngineError> {
4678        if !self.meta_views_materialised && select_references_meta_view(stmt) {
4679            return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4680        }
4681        // v4.12: window-function path. When the projection contains
4682        // any `name(args) OVER (...)` we route to the dedicated
4683        // executor — partition + sort + per-row window value before
4684        // the regular projection.
4685        if select_has_window(stmt) {
4686            // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4687            // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4688            // needs the aggregation done first, then windows over the grouped
4689            // rows. Rewrite to an aggregate derived subquery + outer window query
4690            // (which the window-over-derived path, D.13, executes). Only fires on
4691            // the currently-erroring agg+window+GROUP BY shape, so it can't
4692            // regress working window-only or aggregate-only queries.
4693            if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4694                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4695            }
4696            return self.exec_select_with_window(stmt, cancel).map(Some);
4697        }
4698        Ok(None)
4699    }
4700
4701    /// A projection naming `ctid` or another system column: the schema
4702    /// has to be widened with them before the scan. Out-of-line for the
4703    /// frame reason on `try_from_shape_paths`.
4704    #[inline(never)]
4705    fn try_ctid_projection(
4706        &self,
4707        stmt: &SelectStatement,
4708        primary: &spg_sql::ast::TableRef,
4709        table: &spg_storage::Table,
4710        schema_cols: &[spg_storage::ColumnSchema],
4711        alias: &str,
4712        cancel: CancelToken<'_>,
4713    ) -> Result<Option<QueryResult>, EngineError> {
4714        if references_ctid(stmt) {
4715            let snapshot = self.current_snapshot();
4716            let mut ext_cols = schema_cols.to_vec();
4717            for name in SYSTEM_COLUMNS {
4718                ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4719            }
4720            let table_oid =
4721                crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4722                    .unwrap_or(0);
4723            let headers = table.headers();
4724            let rows: Vec<Row<'static>> = table
4725                .scan_visible(&snapshot)
4726                .map(|(i, r)| {
4727                    let mut vals = r.values.clone();
4728                    // One block, offsets from 1, as PG numbers them.
4729                    vals.push(Value::Tid(0, i as u32 + 1));
4730                    let h = headers.get(i);
4731                    vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4732                    vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4733                    // SPG keeps no per-statement command ids; PG shows 0 for
4734                    // every row a reader can see, which is every row here.
4735                    vals.push(Value::Cid(0));
4736                    vals.push(Value::Cid(0));
4737                    vals.push(Value::BigInt(table_oid));
4738                    Row::new(vals)
4739                })
4740                .collect();
4741            return self
4742                .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4743                .map(Some);
4744        }
4745        Ok(None)
4746    }
4747
4748    /// A sequence read as a one-row relation (`SELECT last_value FROM
4749    /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4750    /// the frame reason on `try_from_shape_paths`.
4751    #[inline(never)]
4752    fn try_sequence_relation(
4753        &self,
4754        stmt: &SelectStatement,
4755        primary: &spg_sql::ast::TableRef,
4756        cancel: CancelToken<'_>,
4757    ) -> Result<Option<QueryResult>, EngineError> {
4758        if self.active_catalog().get(&primary.name).is_none()
4759            && let Some(seq) = self.active_catalog().sequence(&primary.name)
4760        {
4761            let rows = alloc::vec![Row::new(alloc::vec![
4762                Value::BigInt(seq.last_value),
4763                Value::BigInt(0),
4764                Value::Bool(seq.is_called),
4765            ])];
4766            let schema_cols = alloc::vec![
4767                ColumnSchema::new("last_value", DataType::BigInt, false),
4768                ColumnSchema::new("log_cnt", DataType::BigInt, false),
4769                ColumnSchema::new("is_called", DataType::Bool, false),
4770            ];
4771            let alias = primary
4772                .alias
4773                .clone()
4774                .unwrap_or_else(|| primary.name.clone());
4775            return self
4776                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4777                .map(Some);
4778        }
4779        Ok(None)
4780    }
4781
4782    pub(crate) fn exec_bare_select_cancel(
4783        &self,
4784        stmt: &SelectStatement,
4785        cancel: CancelToken<'_>,
4786    ) -> Result<QueryResult, EngineError> {
4787        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4788        // is meaningless without an ORDER BY; PG raises a hard
4789        // error and SPG mirrors the surface so the same DDL/app
4790        // path behaves identically on cutover.
4791        check_with_ties_requires_order_by(stmt)?;
4792        // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4793        // PG rejects window calls there outright. Checked here rather than
4794        // on the window path: `HAVING row_number() OVER () = 1` has no
4795        // window in its projection at all.
4796        crate::window::reject_window_in_row_clauses(stmt)?;
4797        // v7.39 (round 232) — the ORDER BY legality rules (positional
4798        // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4799        // check: before anything scans.
4800        crate::orderby::check_order_by_legality(stmt)?;
4801        // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4802        // equivalent statement the regular executor handles (merged join
4803        // columns collapse to a single unqualified output column; NATURAL
4804        // gets its common-column ON synthesised). The rewrite clears the
4805        // flags, so this re-entrant call is a no-op on the second pass.
4806        if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4807            return self.exec_bare_select_cancel(&rewritten, cancel);
4808        }
4809        // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4810        // exactly the group keys, IS a DISTINCT and was paying for the
4811        // aggregate executor to find that out. Same placement and shape
4812        // as the desugar above; the rewrite clears `group_by`, so the
4813        // re-entry is a no-op on the second pass. See `baregroup` for
4814        // what the gate rules out.
4815        if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4816            return self.exec_bare_select_cancel(&rewritten, cancel);
4817        }
4818        // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4819        // operand in a security-barrier subquery, then re-enter (the wrapped
4820        // operands are no longer bare RLS tables, so this is a no-op on the
4821        // second pass).
4822        if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4823            return self.exec_bare_select_cancel(&rewritten, cancel);
4824        }
4825        // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4826        // AND the RLS USING predicate into a single-table SELECT's WHERE.
4827        // Superuser sessions and non-RLS tables get `None` (no clone, no
4828        // change). Applied inline (shadowing `stmt`) rather than via re-entry
4829        // so it can't re-inject on a recursive pass.
4830        let rls_stmt;
4831        let stmt = match self.rls_select_predicate(stmt)? {
4832            Some(pred) => {
4833                let mut s = stmt.clone();
4834                s.where_ = Some(match s.where_.take() {
4835                    Some(existing) => spg_sql::ast::Expr::Binary {
4836                        lhs: alloc::boxed::Box::new(existing),
4837                        op: spg_sql::ast::BinOp::And,
4838                        rhs: alloc::boxed::Box::new(pred),
4839                    },
4840                    None => pred,
4841                });
4842                rls_stmt = s;
4843                &rls_stmt
4844            }
4845            None => stmt,
4846        };
4847        // v7.16.2 — same meta-view dispatch as
4848        // `exec_select_cancel`, applied here too because
4849        // `subquery_replacement` enters this function directly
4850        // for Exists / ScalarSubquery / InSubquery resolution
4851        // (bypassing the top-level entry to avoid double
4852        // subquery walking). Without this dispatch the subquery
4853        // hits `__spg_info_columns` and reports TableNotFound.
4854        if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4855            return Ok(done);
4856        }
4857        // Constant SELECT (no FROM) — evaluate each item once against an
4858        // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4859        // `SELECT '7'::INT`. Column references will surface as
4860        // ColumnNotFound on eval since the schema is empty.
4861        let Some(from) = &stmt.from else {
4862            return self.exec_constant_select(stmt);
4863        };
4864        // Multi-table FROM (one or more joined peers) goes through the
4865        // nested-loop join executor. Single-table FROM stays on the
4866        // existing scan + index-seek path.
4867        if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4868            return Ok(done);
4869        }
4870        // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4871        // tested — eight ORDER BY shapes byte-identical spilled against
4872        // in-memory, with 103 runs opened to prove the spill ran — and it
4873        // loses on wall clock, which is a hard stop whatever the memory
4874        // buys. Measured round 865, same psql client both sides, same
4875        // machine, row counts verified, and both sides confirmed to be
4876        // doing an external merge rather than an indexed walk:
4877        //
4878        //   PG18        178.7 - 187.0 ms   Sort Method: external merge, 85 MB
4879        //   SPG spilled 269.7 - 299.6 ms   33 spill files at peak
4880        //
4881        // Non-overlapping, about 1.55x. Re-enable by restoring the call
4882        // below once that closes; nothing else has to change, which is
4883        // the point of it being a separate path.
4884        //
4885        //   if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4886        //       return Ok(done);
4887        //   }
4888        //
4889        // v7.37 (round 882) — this walk stays unhooked, but its streaming
4890        // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4891        // bail in `try_exec_joined_streaming`. Collecting the answer was
4892        // most of what this one cost: handing rows over as the merge
4893        // produces them holds peak to the budget plus one row, and the
4894        // wall clock lands inside PG18's range rather than 1.55x outside
4895        // it. Numbers in `extsort.rs`'s header.
4896        let primary = &from.primary;
4897        // v7.39 (round 244) — a sequence is selectable as a one-row relation
4898        // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4899        // read it). Synthesize PG's three columns.
4900        if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4901            return Ok(done);
4902        }
4903        let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4904            StorageError::TableNotFound {
4905                name: primary.name.clone(),
4906            }
4907        })?;
4908        let schema_cols = &table.schema().columns;
4909        // The qualifier accepted on column refs is the alias (if any) else the
4910        // bare table name.
4911        let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4912        // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4913        // system columns at all: `SELECT ctid FROM t` answered "column
4914        // \"ctid\" does not exist", which takes out the dedup idiom every
4915        // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4916        // GROUP BY key)`.
4917        //
4918        // The value comes from the row's position, which the scan already
4919        // yields; the column is appended to the schema and the rows only
4920        // when the statement asks for it, so nothing else pays for it. That
4921        // also routes the query down the general path, past the index fast
4922        // paths below — they hand back rows without positions, and a ctid
4923        // that was sometimes right would be worse than none.
4924        if let Some(done) =
4925            self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4926        {
4927            return Ok(done);
4928        }
4929        let ctx = self.ev_ctx(schema_cols, Some(alias));
4930
4931        // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4932        // WHERE and an NSW index on `col` skips the full scan. The
4933        // walk returns rows already in ascending-distance order, so
4934        // ORDER BY / LIMIT are honoured implicitly.
4935        // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4936        // and thread it into every index-seek fast path below. No-op
4937        // today (every hot header is committed-alive).
4938        let seek_snapshot = self.current_snapshot();
4939        if let Some(done) =
4940            self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4941        {
4942            return Ok(done);
4943        }
4944        // full scan over the hot tier (cold-tier rows are only reached
4945        // via index seek in v5.1 — full table scans against cold-tier
4946        // data ship in v5.2 with the freezer's per-segment scan API).
4947        let indexed_rows =
4948            self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4949
4950        // Aggregate path: filter rows first, then hand off to the
4951        // aggregate executor which does its own projection + ORDER BY.
4952        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
4953            return self.run_single_table_aggregate(
4954                stmt,
4955                table,
4956                schema_cols,
4957                alias,
4958                indexed_rows,
4959                cancel,
4960            );
4961        }
4962        self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4963    }
4964
4965    /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4966    /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4967    /// uncorrelated FROM-primary case is the simpler shape, used by
4968    /// e2e pins. Materialises the (key, value) pair stream into a
4969    /// synthetic two-column TEXT table, then routes through the
4970    /// regular projection / WHERE / ORDER BY pipeline.
4971    /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4972    /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4973    /// item into (rows, schema). `outer_doc` is `Some` only when this
4974    /// is a NESTED level being expanded against a parent row item's
4975    /// already-parsed sub-document; the top-level call parses the doc
4976    /// expr itself. Row/column paths reuse the existing jsonpath
4977    /// evaluator (`json::json_table_path`); coercion reuses
4978    /// `coerce_value` on the JSON scalar text, so a json string
4979    /// coerces to DATE by its content, matching PG.
4980    #[allow(clippy::type_complexity)]
4981    pub(crate) fn json_table_rows(
4982        &self,
4983        jt: &spg_sql::ast::JsonTable,
4984        outer_doc: Option<&crate::json::JsonValue>,
4985    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4986        // Column schema is static (independent of data): flatten the
4987        // COLUMNS tree in declaration order (NESTED contributes its
4988        // children inline, the PG output shape).
4989        let schema = json_table_schema(&jt.columns);
4990
4991        // PASSING variables → a single JsonValue object the jsonpath
4992        // engine reads `$name` from.
4993        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4994        let ctx = EvalContext::new(&empty_schema, None);
4995        let dummy = Row::new(alloc::vec::Vec::new());
4996        let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4997            None
4998        } else {
4999            let mut entries = alloc::vec::Vec::new();
5000            for (name, e) in &jt.passing {
5001                let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
5002                entries.push((name.clone(), value_to_json_value(&v)));
5003            }
5004            Some(crate::json::JsonValue::Object(entries))
5005        };
5006
5007        // The document root: a NESTED level gets it from the parent;
5008        // the top level parses its doc expr.
5009        let root_owned;
5010        let root: &crate::json::JsonValue = match outer_doc {
5011            Some(d) => d,
5012            None => {
5013                let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
5014                let src = match &doc_val {
5015                    Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
5016                    Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
5017                    other => {
5018                        return Err(EngineError::Unsupported(alloc::format!(
5019                            "JSON_TABLE document must be json/text, got {}",
5020                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
5021                        )));
5022                    }
5023                };
5024                root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
5025                &root_owned
5026            }
5027        };
5028
5029        let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
5030            .map_err(EngineError::Eval)?;
5031        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5032        for (idx, item) in items.iter().enumerate() {
5033            self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
5034        }
5035        Ok((rows, schema))
5036    }
5037
5038    /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
5039    /// Regular columns produce one value each; a NESTED column expands
5040    /// as an outer join (each nested match → one row sharing the
5041    /// parent cells; no nested match → one row with the nested cells
5042    /// NULL). Sibling NESTED at one level cross by concatenation of
5043    /// their independent expansions (PG's UNION-of-outer shape).
5044    fn json_table_emit_item(
5045        &self,
5046        jt: &spg_sql::ast::JsonTable,
5047        item: &crate::json::JsonValue,
5048        ordinality: usize,
5049        vars: Option<&crate::json::JsonValue>,
5050        out: &mut alloc::vec::Vec<Row<'static>>,
5051    ) -> Result<(), EngineError> {
5052        use spg_sql::ast::JsonTableColumn as C;
5053        // Parent cells (regular + ordinality), left-to-right; NESTED
5054        // columns contribute a run of child cells appended after.
5055        let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5056        let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
5057            alloc::vec::Vec::new();
5058        let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5059        for col in &jt.columns {
5060            match col {
5061                C::Ordinality { .. } => {
5062                    parent_cells.push(Value::BigInt(ordinality as i64 + 1));
5063                }
5064                C::Regular { .. } => {
5065                    parent_cells.push(self.json_table_column_value(col, item, vars)?);
5066                }
5067                C::Nested { path, columns } => {
5068                    // Recurse: a nested JSON_TABLE over `item` filtered
5069                    // by `path`, with the same PASSING vars.
5070                    let sub = spg_sql::ast::JsonTable {
5071                        doc: jt.doc.clone(), // unused (outer_doc provided)
5072                        row_path: path.clone(),
5073                        columns: columns.clone(),
5074                        passing: alloc::vec::Vec::new(),
5075                    };
5076                    let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
5077                    nested_widths.push(nschema.len());
5078                    nested_runs.push(nrows);
5079                }
5080            }
5081        }
5082        if nested_runs.is_empty() {
5083            out.push(Row::new(parent_cells));
5084            return Ok(());
5085        }
5086        // PG sibling-NESTED semantics: each sibling expands
5087        // INDEPENDENTLY and the results CONCATENATE — a row from
5088        // sibling s fills only s's cells, every other sibling's cells
5089        // NULL. An empty sibling contributes ZERO rows (not a NULL
5090        // row). Only when EVERY sibling is empty does the parent still
5091        // emit one all-NULL row (the outer-join guarantee that a parent
5092        // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
5093        // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
5094        let before = out.len();
5095        for (s_idx, run) in nested_runs.iter().enumerate() {
5096            for nrow in run {
5097                let mut cells = parent_cells.clone();
5098                for (o_idx, w) in nested_widths.iter().enumerate() {
5099                    if o_idx == s_idx {
5100                        cells.extend(nrow.values.iter().cloned());
5101                    } else {
5102                        for _ in 0..*w {
5103                            cells.push(Value::Null);
5104                        }
5105                    }
5106                }
5107                out.push(Row::new(cells));
5108            }
5109        }
5110        if out.len() == before {
5111            // Every sibling empty → one all-NULL nested row.
5112            let mut cells = parent_cells.clone();
5113            for w in &nested_widths {
5114                for _ in 0..*w {
5115                    cells.push(Value::Null);
5116                }
5117            }
5118            out.push(Row::new(cells));
5119        }
5120        Ok(())
5121    }
5122
5123    /// v7.39 (round 205) — evaluate one Regular column against a row
5124    /// item: EXISTS → bool; else path → at most one value, coerced to
5125    /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
5126    fn json_table_column_value(
5127        &self,
5128        col: &spg_sql::ast::JsonTableColumn,
5129        item: &crate::json::JsonValue,
5130        vars: Option<&crate::json::JsonValue>,
5131    ) -> Result<Value<'static>, EngineError> {
5132        use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
5133        let C::Regular {
5134            name,
5135            ty,
5136            path,
5137            exists,
5138            format_json,
5139            wrapper,
5140            on_empty,
5141            on_error,
5142        } = col
5143        else {
5144            unreachable!("caller guards Regular");
5145        };
5146        let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5147        if *exists {
5148            return Ok(Value::Bool(!matches.is_empty()));
5149        }
5150        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5151        let ctx = EvalContext::new(&empty_schema, None);
5152        let dummy = Row::new(alloc::vec::Vec::new());
5153        let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5154            match b {
5155                B::Null => Ok(Some(Value::Null)),
5156                B::Error => Ok(None),
5157                B::Default(e) => Ok(Some(
5158                    eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5159                )),
5160            }
5161        };
5162        // Empty match set → ON EMPTY.
5163        if matches.is_empty() {
5164            return match default_of(on_empty)? {
5165                Some(v) => coerce_json_table_default(v, *ty, name),
5166                None => Err(EngineError::Unsupported(alloc::format!(
5167                    "no SQL/JSON item found for JSON_TABLE column {name:?}"
5168                ))),
5169            };
5170        }
5171        let first = &matches[0];
5172        // FORMAT JSON: return the PG-canonical json representation.
5173        // WITH WRAPPER wraps the whole match SET in an array (even a
5174        // single scalar → `[5]`); without it, the single match's json.
5175        if *format_json {
5176            let text = if *wrapper {
5177                crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5178            } else {
5179                first.canonical_json_text()
5180            };
5181            return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5182        }
5183        if first.is_json_null() {
5184            return Ok(Value::Null);
5185        }
5186        // Coerce the scalar text to the declared type; on failure → ON
5187        // ERROR (default NULL, DEFAULT expr, or raise).
5188        let dt = crate::conversions::column_type_to_data_type(*ty);
5189        let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5190        match crate::conversions::coerce_value(scalar, dt, name, 0) {
5191            Ok(v) => Ok(v),
5192            Err(e) => match default_of(on_error)? {
5193                Some(v) => coerce_json_table_default(v, *ty, name),
5194                None => Err(e),
5195            },
5196        }
5197    }
5198
5199    /// table function into (rows, default schema). Dispatch by name.
5200    pub(crate) fn table_fn_rows(
5201        &self,
5202        primary: &TableRef,
5203    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5204        let (fn_name, args) = primary
5205            .table_fn_call
5206            .as_deref()
5207            .expect("caller guards table_fn_call.is_some()");
5208        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5209        let ctx = EvalContext::new(&empty_schema, None);
5210        let dummy_row = Row::new(alloc::vec::Vec::new());
5211        let arg0: Option<Value<'static>> = match args.first() {
5212            Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5213            None => None,
5214        };
5215        match fn_name.as_str() {
5216            // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5217            // `…_recordset` (+ json_ variants). The row shape is the BASE
5218            // argument's declared type — a table's or a composite type's
5219            // column list — which only the catalog knows, so the parser hands
5220            // the raw arguments here rather than desugaring blind.
5221            "jsonb_populate_record"
5222            | "json_populate_record"
5223            | "jsonb_populate_recordset"
5224            | "json_populate_recordset" => {
5225                let type_name = match args.first() {
5226                    Some(Expr::Cast {
5227                        target: spg_sql::ast::CastTarget::Named(n),
5228                        ..
5229                    }) => n.clone(),
5230                    _ => {
5231                        return Err(EngineError::Unsupported(alloc::format!(
5232                            "{fn_name}(): first argument must name a row type, \
5233                             e.g. NULL::mytable"
5234                        )));
5235                    }
5236                };
5237                let cat = self.active_catalog();
5238                let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5239                    t.schema().columns.clone()
5240                } else if let Some(c) = cat.composite_types().get(&type_name) {
5241                    c.fields
5242                        .iter()
5243                        .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5244                        .collect()
5245                } else {
5246                    return Err(EngineError::Unsupported(alloc::format!(
5247                        "type \"{type_name}\" does not exist"
5248                    )));
5249                };
5250                let json_arg = match args.get(1) {
5251                    Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5252                    None => Value::Null,
5253                };
5254                // The set form iterates the JSON array; the scalar form is
5255                // the one-element case of the same walk.
5256                let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5257                    crate::json::array_element_rows(&json_arg, false, fn_name)
5258                        .map_err(EngineError::Eval)?
5259                        .into_iter()
5260                        .map(|s| s.map_or(Value::Null, Value::json))
5261                        .collect()
5262                } else if matches!(json_arg, Value::Null) {
5263                    alloc::vec::Vec::new()
5264                } else {
5265                    alloc::vec![json_arg]
5266                };
5267                let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5268                for doc in &docs {
5269                    let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5270                    for c in &cols {
5271                        // `->>` semantics: a missing key is NULL, present keys
5272                        // arrive as text and cast to the declared column type.
5273                        let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5274                            .map_err(EngineError::Eval)?;
5275                        let v = if matches!(raw, Value::Null) {
5276                            Value::Null
5277                        } else {
5278                            crate::conversions::coerce_value(raw, c.ty, "", 0)
5279                                .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5280                        };
5281                        vals.push(v);
5282                    }
5283                    rows.push(Row::new(vals));
5284                }
5285                Ok((rows, cols))
5286            }
5287            // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5288            // a text[] of 'name=value' reloptions/fdw options → one
5289            // (option_name, option_value) row per element. NULL or an
5290            // empty array yields zero rows (PG); an element without
5291            // '=' carries a NULL option_value, matching PG's split.
5292            "pg_options_to_table" => {
5293                let schema = alloc::vec![
5294                    ColumnSchema::new("option_name", DataType::Text, true),
5295                    ColumnSchema::new("option_value", DataType::Text, true),
5296                ];
5297                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5298                if let Some(Value::TextArray(items)) = arg0 {
5299                    for item in items.into_iter().flatten() {
5300                        let (name, value) = match item.split_once('=') {
5301                            Some((n, v)) => (Value::text(n), Value::text(v)),
5302                            None => (Value::text(item.as_str()), Value::Null),
5303                        };
5304                        rows.push(Row::new(alloc::vec![name, value]));
5305                    }
5306                }
5307                Ok((rows, schema))
5308            }
5309            // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5310            // PG18's per-sequence state SRF, (last_value, is_called).
5311            // pg_dump reads it joined to pg_sequence for every dumped
5312            // sequence's setval line. The oid resolves through the
5313            // same relation_oid mapping seqrelid publishes.
5314            "pg_get_sequence_data" => {
5315                let schema = alloc::vec![
5316                    ColumnSchema::new("last_value", DataType::BigInt, false),
5317                    ColumnSchema::new("is_called", DataType::Bool, false),
5318                ];
5319                let want = match arg0 {
5320                    Some(Value::Int(n)) => i64::from(n),
5321                    Some(Value::BigInt(n)) => n,
5322                    _ => {
5323                        return Err(EngineError::Unsupported(
5324                            "pg_get_sequence_data(): argument must be a sequence oid".into(),
5325                        ));
5326                    }
5327                };
5328                let cat = self.active_catalog();
5329                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5330                for (name, def) in cat.sequences_all() {
5331                    if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5332                        rows.push(Row::new(alloc::vec![
5333                            Value::BigInt(def.last_value),
5334                            Value::Bool(def.is_called),
5335                        ]));
5336                        break;
5337                    }
5338                }
5339                Ok((rows, schema))
5340            }
5341            "pg_partition_tree" => {
5342                let cols = alloc::vec![
5343                    ColumnSchema::new("relid".to_string(), DataType::Text, true),
5344                    ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5345                    ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5346                    ColumnSchema::new("level".to_string(), DataType::Int, true),
5347                ];
5348                let Some(Value::Text(name)) = &arg0 else {
5349                    // NULL (or missing) argument → zero rows (PG).
5350                    return Ok((alloc::vec::Vec::new(), cols));
5351                };
5352                let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5353                if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5354                    return Err(EngineError::Unsupported(alloc::format!(
5355                        "relation \"{name}\" does not exist"
5356                    )));
5357                }
5358                let rows = entries
5359                    .into_iter()
5360                    .map(|(relid, parent, isleaf, level)| {
5361                        Row::new(alloc::vec![
5362                            Value::text(relid),
5363                            parent.map_or(Value::Null, Value::text),
5364                            Value::Bool(isleaf),
5365                            #[allow(clippy::cast_possible_truncation)]
5366                            Value::Int(level as i32),
5367                        ])
5368                    })
5369                    .collect();
5370                Ok((rows, cols))
5371            }
5372            "pg_partition_ancestors" => {
5373                let cols =
5374                    alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5375                let Some(Value::Text(name)) = &arg0 else {
5376                    return Ok((alloc::vec::Vec::new(), cols));
5377                };
5378                let cat = self.active_catalog();
5379                if cat.get(name.as_ref()).is_none() {
5380                    return Err(EngineError::Unsupported(alloc::format!(
5381                        "relation \"{name}\" does not exist"
5382                    )));
5383                }
5384                // A relation outside any partition tree yields no rows (PG).
5385                let in_tree = cat
5386                    .get(name.as_ref())
5387                    .is_some_and(|t| t.schema().partition_role.is_some());
5388                let rows = if in_tree {
5389                    crate::partition_walks::ancestors_of(cat, name.as_ref())
5390                        .into_iter()
5391                        .map(|n| Row::new(alloc::vec![Value::text(n)]))
5392                        .collect()
5393                } else {
5394                    alloc::vec::Vec::new()
5395                };
5396                Ok((rows, cols))
5397            }
5398            // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5399            // saw, what each token was called, which dictionary took it
5400            // and what came out. It is a projection of the same tokenizer
5401            // and the same map the indexer uses, so it cannot describe a
5402            // pipeline other than the one that runs.
5403            "ts_debug" => {
5404                use crate::fts::{TokenType, TsDict};
5405                let cols = alloc::vec![
5406                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5407                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5408                    ColumnSchema::new("token".to_string(), DataType::Text, false),
5409                    ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5410                    ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5411                    ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5412                ];
5413                // PG's one-arg form uses the session configuration; the
5414                // two-arg form names one.
5415                let (cfg_name, text) = match (&arg0, args.get(1)) {
5416                    (Some(Value::Text(c)), Some(t)) => {
5417                        let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5418                        (c.to_string(), crate::eval::value_to_text(&v))
5419                    }
5420                    (Some(v), None) => (
5421                        alloc::string::String::from("english"),
5422                        crate::eval::value_to_text(v),
5423                    ),
5424                    _ => return Ok((alloc::vec::Vec::new(), cols)),
5425                };
5426                let english = match cfg_name
5427                    .trim()
5428                    .trim_start_matches("pg_catalog.")
5429                    .to_ascii_lowercase()
5430                    .as_str()
5431                {
5432                    "english" => true,
5433                    "simple" => false,
5434                    other => {
5435                        return Err(EngineError::Unsupported(alloc::format!(
5436                            "text search configuration \"{other}\" does not exist"
5437                        )));
5438                    }
5439                };
5440                let rows = crate::fts::tokenize_typed(&text)
5441                    .into_iter()
5442                    .map(|tok| {
5443                        let dict = tok.ty.dictionary(english);
5444                        let dname = dict.map(|d| match d {
5445                            TsDict::Simple => "simple",
5446                            TsDict::EnglishStem => "english_stem",
5447                        });
5448                        let folded = tok.text.to_lowercase();
5449                        let lexemes = dict.map(|d| match d {
5450                            TsDict::Simple => alloc::vec![Some(folded.clone())],
5451                            TsDict::EnglishStem => {
5452                                if crate::fts::is_english_stopword(&folded) {
5453                                    alloc::vec::Vec::new()
5454                                } else {
5455                                    alloc::vec![Some(crate::fts::porter_stem(&folded))]
5456                                }
5457                            }
5458                        });
5459                        Row::new(alloc::vec![
5460                            Value::text(tok.ty.alias()),
5461                            Value::text(tok.ty.description()),
5462                            Value::text(tok.text),
5463                            Value::TextArray(
5464                                dname
5465                                    .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5466                                    .unwrap_or_default(),
5467                            ),
5468                            dname.map_or(Value::Null, Value::text),
5469                            lexemes.map_or(Value::Null, Value::TextArray),
5470                        ])
5471                    })
5472                    .collect();
5473                let _ = TokenType::AsciiWord;
5474                Ok((rows, cols))
5475            }
5476            // v7.39 (round 651) — `ts_token_type('default')`, the list the
5477            // parser actually produces. It is a projection of the
5478            // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5479            // read, so the three cannot disagree about what a token is.
5480            "ts_token_type" => {
5481                use crate::fts::TokenType as T;
5482                let cols = alloc::vec![
5483                    ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5484                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5485                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5486                ];
5487                // PG takes the parser by name or oid; SPG has the one.
5488                if let Some(Value::Text(p)) = &arg0
5489                    && !p.eq_ignore_ascii_case("default")
5490                    && !p.eq_ignore_ascii_case("pg_catalog.default")
5491                {
5492                    return Err(EngineError::Unsupported(alloc::format!(
5493                        "text search parser \"{p}\" does not exist"
5494                    )));
5495                }
5496                const TYPES: &[T] = &[
5497                    T::AsciiWord,
5498                    T::Word,
5499                    T::NumWord,
5500                    T::Email,
5501                    T::Url,
5502                    T::Host,
5503                    T::SFloat,
5504                    T::Version,
5505                    T::HwordNumPart,
5506                    T::HwordPart,
5507                    T::HwordAsciiPart,
5508                    T::Blank,
5509                    T::Tag,
5510                    T::Protocol,
5511                    T::NumHword,
5512                    T::AsciiHword,
5513                    T::Hword,
5514                    T::UrlPath,
5515                    T::File,
5516                    T::Float,
5517                    T::Int,
5518                    T::Uint,
5519                    T::Entity,
5520                ];
5521                let rows = TYPES
5522                    .iter()
5523                    .map(|t| {
5524                        Row::new(alloc::vec![
5525                            Value::Int(*t as i32),
5526                            Value::text(t.alias()),
5527                            Value::text(t.description()),
5528                        ])
5529                    })
5530                    .collect();
5531                Ok((rows, cols))
5532            }
5533            // v7.39 (read01 round 65) — a set-returning USER function in FROM
5534            // (`FROM rows_of(2)`). Its body runs through the real executor, like
5535            // every other function body since round 63.
5536            other => {
5537                if !self.active_catalog().functions_named(other).is_empty() {
5538                    return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5539                }
5540                Err(EngineError::Unsupported(alloc::format!(
5541                    "table function {other}() is not supported in FROM"
5542                )))
5543            }
5544        }
5545    }
5546
5547    /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5548    /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5549    /// are bound into it as literals and it goes through the read path, so the
5550    /// rows it yields are exactly the rows a hand-written query would see.
5551    ///
5552    /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5553    /// text)` names them, and a `SETOF <scalar>` yields a single column named
5554    /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5555    /// shows.
5556    fn exec_setof_user_function(
5557        &self,
5558        name: &str,
5559        args: &[spg_sql::ast::Expr],
5560        // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5561        // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5562        alias: Option<&str>,
5563    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5564        // The call's arguments belong to the ENCLOSING query, so they are
5565        // evaluated here and the body sees values.
5566        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5567        let arg_ctx = self.ev_ctx(&empty, None);
5568        let dummy = Row::new(alloc::vec::Vec::new());
5569        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5570        for a in args {
5571            vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5572        }
5573        self.setof_rows_of(name, &vals, alias)
5574    }
5575
5576    /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5577    /// arguments. Shared by the FROM position and the target-list expansion, so
5578    /// a function cannot behave differently depending on where it is called.
5579    pub(crate) fn setof_rows_of(
5580        &self,
5581        name: &str,
5582        arg_values: &[Value<'static>],
5583        alias: Option<&str>,
5584    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5585        let cat = self.active_catalog();
5586        let overloads = cat.functions_named(name);
5587        let def = overloads
5588            .iter()
5589            .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5590            .ok_or_else(|| {
5591                EngineError::Unsupported(alloc::format!(
5592                    "function {name} does not exist with {} argument(s)",
5593                    arg_values.len()
5594                ))
5595            })?;
5596        let declared = def.returns.trim().to_string();
5597        let upper = declared.to_ascii_uppercase();
5598        if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5599            return Err(EngineError::Unsupported(alloc::format!(
5600                "function {name}() does not return a set — it cannot be used in FROM"
5601            )));
5602        }
5603
5604        let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5605        // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5606        // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5607        if def.language.eq_ignore_ascii_case("plpgsql") {
5608            let out_rows = self
5609                .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5610                .map_err(EngineError::Eval)?;
5611            let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5612            let rows = out_rows.into_iter().map(Row::new).collect();
5613            return Ok((rows, cols));
5614        }
5615        let body = def.body.trim().trim_end_matches(';');
5616        let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5617            EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5618        })?;
5619        let spg_sql::ast::Statement::Select(body_select) = stmt else {
5620            return Err(EngineError::Unsupported(alloc::format!(
5621                "function {name}(): a set-returning body must be a SELECT"
5622            )));
5623        };
5624        let arg_names = spg_storage::function_arg_names(&def.args_repr);
5625        let bound = crate::eval::bind_user_fn_args(
5626            self.active_catalog(),
5627            &body_select,
5628            &arg_names,
5629            arg_values,
5630        )
5631        .map_err(EngineError::Eval)?;
5632        let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5633        let QueryResult::Rows { columns, rows } = out else {
5634            return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5635        };
5636        // Name the columns from the DECLARED shape — the same rule the plpgsql
5637        // path above uses, so a body's language cannot change the row shape.
5638        let cols = setof_column_shape_from(&declared, name, alias, &columns);
5639        Ok((rows, cols))
5640    }
5641
5642    fn exec_select_jsonb_each_text(
5643        &self,
5644        stmt: &SelectStatement,
5645        primary: &TableRef,
5646        cancel: CancelToken<'_>,
5647    ) -> Result<QueryResult, EngineError> {
5648        let (each_fn, arg_expr) = primary
5649            .jsonb_each_text_arg
5650            .as_ref()
5651            .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5652            .expect("caller guards jsonb_each_text_arg.is_some()");
5653        // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5654        // forms keep JSON rendering in the value column (JSON null
5655        // stays jsonb 'null', strings keep their quotes).
5656        let as_text = each_fn.ends_with("_text");
5657        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5658        let ctx = EvalContext::new(&empty_schema, None);
5659        let dummy_row = Row::new(alloc::vec::Vec::new());
5660        let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5661        let pairs =
5662            crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5663        let rows: alloc::vec::Vec<Row<'static>> = pairs
5664            .into_iter()
5665            .map(|(k, v)| {
5666                let key_val = Value::text(k);
5667                let value_val = match v {
5668                    Some(s) if as_text => Value::text(s),
5669                    Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5670                    None => Value::Null,
5671                };
5672                Row::new(alloc::vec![key_val, value_val])
5673            })
5674            .collect();
5675        let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5676        let value_dtype = if as_text {
5677            spg_storage::DataType::Text
5678        } else {
5679            spg_storage::DataType::Json
5680        };
5681        let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5682        let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5683        let mut schema_cols = alloc::vec![key_col, value_col];
5684        // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5685        // LATERAL-position form of the same call already honours it.
5686        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5687            if let Some(col) = schema_cols.get_mut(i) {
5688                col.name = new_name.clone();
5689            }
5690        }
5691        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5692        // `EvalContext::new` drops it and every catalog-dependent cast
5693        // (regclass / enum / composite / domain) silently degrades.
5694        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5695        // WHERE.
5696        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5697            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5698            for row in rows {
5699                cancel.check()?;
5700                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5701                if matches!(v, Value::Bool(true)) {
5702                    out.push(row);
5703                }
5704            }
5705            out
5706        } else {
5707            rows
5708        };
5709        // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5710        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
5711            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5712            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5713                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5714                    .map_err(|err| match err {
5715                        EngineError::Eval(ev) => ev,
5716                        other => eval::EvalError::TypeMismatch {
5717                            detail: alloc::format!("{other}"),
5718                        },
5719                    })
5720            };
5721            // v7.39 (round 656) — hand the rows over as they are rather than
5722            // collecting a second vector of `RowRef` wrappers. Note this is
5723            // a set-returning-function path, NOT the relational scan: the
5724            // measured O(rows) cost lived in `run_single_table_aggregate`,
5725            // and converting these four first was a miss that cost a full
5726            // round — every test stayed green and the number did not move.
5727            let agg = aggregate::run(
5728                stmt,
5729                crate::join::AggRows::Owned(&filtered),
5730                &schema_cols,
5731                Some(&alias),
5732                Some(&agg_correlated),
5733                self.parallel_runner.0.as_deref(),
5734                Some(self.active_catalog()),
5735                Some(self),
5736            )?;
5737            return self.finish_agg_result(agg, stmt, cancel);
5738        }
5739        // Projection.
5740        let projection = build_projection(
5741            &stmt.items,
5742            &schema_cols,
5743            &alias,
5744            self.speaks_mysql,
5745            Some(self.active_catalog()),
5746        )?;
5747        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5748            alloc::vec::Vec::with_capacity(filtered.len());
5749        for row in &filtered {
5750            let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5751            for p in &projection {
5752                let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5753                vals.push(v);
5754            }
5755            projected_rows.push(Row::new(vals));
5756        }
5757        let columns: alloc::vec::Vec<ColumnSchema> = projection
5758            .iter()
5759            // v7.39 (read01 round 54) — keep the column's enum identity through
5760            // the projection (it lives outside the DataType lattice), or a
5761            // derived table / UNION / windowed result forgets it and any outer
5762            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5763            .map(|p| p.to_column_schema())
5764            .collect();
5765        // ORDER BY.
5766        if !stmt.order_by.is_empty() {
5767            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5768                .iter()
5769                .enumerate()
5770                .map(|(i, r)| -> Result<_, EngineError> {
5771                    let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5772                        .order_by
5773                        .iter()
5774                        .map(|ob| {
5775                            eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5776                        })
5777                        .collect();
5778                    Ok((i, keys?))
5779                })
5780                .collect::<Result<_, _>>()?;
5781            indexed.sort_by(|a, b| {
5782                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5783                    let o = &stmt.order_by[idx];
5784                    let cmp = order_by_value_cmp_in(
5785                        o.desc,
5786                        o.nulls_first,
5787                        ka,
5788                        kb,
5789                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5790                    );
5791                    if cmp != core::cmp::Ordering::Equal {
5792                        return cmp;
5793                    }
5794                }
5795                core::cmp::Ordering::Equal
5796            });
5797            projected_rows = indexed
5798                .into_iter()
5799                .map(|(i, _)| projected_rows[i].clone())
5800                .collect();
5801        }
5802        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5803        if stmt.distinct {
5804            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
5805            // spec folds EVERY text position, so a column declared
5806            // `COLLATE utf8mb4_bin` had its values merged here exactly the
5807            // way 3b494b6e fixed on the main scan path. The projection is
5808            // already in scope at each of these sites, so the mask needs no
5809            // new plumbing -- it was simply never asked for.
5810            projected_rows = dedup_rows(
5811                projected_rows,
5812                FoldSpec::of_masks(
5813                    scan_ctx.mysql_dialect,
5814                    &fold_mask(&projection),
5815                    &pad_mask(&projection),
5816                ),
5817            );
5818        }
5819        if let Some(offset) = stmt.offset_literal() {
5820            let off = (offset as usize).min(projected_rows.len());
5821            projected_rows.drain(..off);
5822        }
5823        if let Some(limit) = stmt.limit_literal() {
5824            projected_rows.truncate(limit as usize);
5825        }
5826        Ok(QueryResult::Rows {
5827            columns,
5828            rows: projected_rows,
5829        })
5830    }
5831
5832    /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5833    /// ( SELECT … ) alias` in primary position. The inner SELECT
5834    /// materialises once through the regular bare-select executor
5835    /// (UNION tails included), then the outer WHERE / aggregate /
5836    /// projection / ORDER BY / LIMIT pipeline runs over the
5837    /// synthetic table — the same post-materialisation shape as
5838    /// exec_select_jsonb_each_text, generalised to N columns.
5839    fn exec_select_derived(
5840        &self,
5841        stmt: &SelectStatement,
5842        primary: &TableRef,
5843        cancel: CancelToken<'_>,
5844    ) -> Result<QueryResult, EngineError> {
5845        let inner = primary
5846            .lateral_subquery
5847            .as_deref()
5848            .expect("caller guards lateral_subquery.is_some()");
5849        // exec_select_cancel is the union-aware wrapper — the inner
5850        // SELECT may carry UNION tails on stmt.unions.
5851        let QueryResult::Rows {
5852            columns: inner_cols,
5853            rows,
5854        } = self.exec_select_cancel(inner, cancel)?
5855        else {
5856            return Err(EngineError::Unsupported(
5857                "derived table subquery must return rows".into(),
5858            ));
5859        };
5860        let alias = primary
5861            .alias
5862            .clone()
5863            .unwrap_or_else(|| primary.name.clone());
5864        // `AS t(a, b)` renames the materialised columns positionally
5865        // (extra inner columns keep their own names, PG behaviour).
5866        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5867        // v7.39 (read01 round 78) — a column-alias list longer than the item is
5868        // the error PG reports; SPG used to let the extra names through and then
5869        // fail two layers downstream with "column not found: <the extra name>".
5870        let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5871        if primary.unnest_column_aliases.len() > n_out {
5872            return Err(EngineError::Unsupported(alloc::format!(
5873                "table \"{alias}\" has {n_out} columns available but {} columns specified",
5874                primary.unnest_column_aliases.len()
5875            )));
5876        }
5877        if primary.scalar_fn_item && schema_cols.len() == 1 {
5878            schema_cols[0].scalar_row_source = true;
5879        }
5880        // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5881        // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5882        // The column-alias list, if given, names it like any other column.
5883        let mut rows = rows;
5884        if primary.with_ordinality {
5885            schema_cols.push(ColumnSchema::new(
5886                "ordinality".to_string(),
5887                DataType::BigInt,
5888                false,
5889            ));
5890            rows = rows
5891                .into_iter()
5892                .enumerate()
5893                .map(|(i, r)| {
5894                    let mut v = r.values;
5895                    #[allow(clippy::cast_possible_wrap)]
5896                    v.push(Value::BigInt(i as i64 + 1));
5897                    Row::new(v)
5898                })
5899                .collect();
5900        }
5901        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5902            if let Some(col) = schema_cols.get_mut(i) {
5903                col.name = new_name.clone();
5904            }
5905        }
5906        self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5907    }
5908
5909    /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5910    /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5911    /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5912    /// derived-table executor and the FROM-position table functions.
5913    fn exec_select_over_rows(
5914        &self,
5915        stmt: &SelectStatement,
5916        rows: alloc::vec::Vec<Row<'static>>,
5917        schema_cols: alloc::vec::Vec<ColumnSchema>,
5918        alias: &str,
5919        cancel: CancelToken<'_>,
5920    ) -> Result<QueryResult, EngineError> {
5921        let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5922        // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5923        // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5924        // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5925        // (the same path the aggregate branch uses); the old plain eval_expr let
5926        // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5927        let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5928        // WHERE.
5929        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5930            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5931            for row in rows {
5932                cancel.check()?;
5933                let v = self.eval_expr_with_correlated(
5934                    w,
5935                    &row,
5936                    &scan_ctx,
5937                    cancel,
5938                    Some(&mut corr_memo.borrow_mut()),
5939                )?;
5940                if matches!(v, Value::Bool(true)) {
5941                    out.push(row);
5942                }
5943            }
5944            out
5945        } else {
5946            rows
5947        };
5948        // Aggregate dispatch.
5949        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
5950            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5951            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5952                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5953                    .map_err(|err| match err {
5954                        EngineError::Eval(ev) => ev,
5955                        other => eval::EvalError::TypeMismatch {
5956                            detail: alloc::format!("{other}"),
5957                        },
5958                    })
5959            };
5960            // v7.39 (round 656) — hand the rows over as they are rather than
5961            // collecting a second vector of `RowRef` wrappers. Note this is
5962            // a set-returning-function path, NOT the relational scan: the
5963            // measured O(rows) cost lived in `run_single_table_aggregate`,
5964            // and converting these four first was a miss that cost a full
5965            // round — every test stayed green and the number did not move.
5966            let agg = aggregate::run(
5967                stmt,
5968                crate::join::AggRows::Owned(&filtered),
5969                &schema_cols,
5970                Some(alias),
5971                Some(&agg_correlated),
5972                self.parallel_runner.0.as_deref(),
5973                Some(self.active_catalog()),
5974                Some(self),
5975            )?;
5976            return self.finish_agg_result(agg, stmt, cancel);
5977        }
5978        // Projection.
5979        let projection = build_projection(
5980            &stmt.items,
5981            &schema_cols,
5982            alias,
5983            self.speaks_mysql,
5984            Some(self.active_catalog()),
5985        )?;
5986        // v7.39 (round 621) — a target-list SRF expands here too. This tail
5987        // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5988        // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5989        // answered `function unnest(integer[]) does not exist` for a query PG
5990        // answers.
5991        let srf_idxs = self.srf_target_idxs(&projection);
5992        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5993        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5994            alloc::vec::Vec::with_capacity(filtered.len());
5995        if !srf_idxs.is_empty() {
5996            let (rows, src) =
5997                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5998            projected_rows = rows;
5999            src_of_row = src;
6000        } else {
6001            for row in &filtered {
6002                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
6003                for p in &projection {
6004                    let v = self.eval_expr_with_correlated(
6005                        &p.expr,
6006                        row,
6007                        &scan_ctx,
6008                        cancel,
6009                        Some(&mut corr_memo.borrow_mut()),
6010                    )?;
6011                    vals.push(v);
6012                }
6013                projected_rows.push(Row::new(vals));
6014            }
6015        }
6016        let columns: alloc::vec::Vec<ColumnSchema> = projection
6017            .iter()
6018            // v7.39 (read01 round 54) — keep the column's enum identity through
6019            // the projection (it lives outside the DataType lattice), or a
6020            // derived table / UNION / windowed result forgets it and any outer
6021            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
6022            .map(|p| p.to_column_schema())
6023            .collect();
6024        // ORDER BY over the source rows (same shape as the other
6025        // synthetic-table executors).
6026        // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
6027        // OUTPUT column. Evaluated as an expression, as it was here, the literal
6028        // `1` is just the constant 1: the same sort key for every row, so the
6029        // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
6030        // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
6031        // landing on this executor) came back in input order.
6032        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6033        if !order_by.is_empty() {
6034            // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
6035            // SRF makes more of them than there were inputs.
6036            let out_cols = if srf_idxs.is_empty() {
6037                alloc::vec![None; order_by.len()]
6038            } else {
6039                srf_order_output_cols(&order_by, &projection)
6040            };
6041            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
6042                .iter()
6043                .enumerate()
6044                .map(|(k, out)| -> Result<_, EngineError> {
6045                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
6046                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
6047                        .iter()
6048                        .zip(out_cols.iter())
6049                        .map(|(ob, oc)| {
6050                            // v7.39 (read01 round 54) — this path builds its
6051                            // sort keys itself instead of going through
6052                            // `build_order_keys`, so it skipped the enum-ordinal
6053                            // substitution: an OUTER `ORDER BY <enum col>` over
6054                            // a DERIVED TABLE sorted by the label TEXT, not by
6055                            // member order. Silently wrong rows, not an error.
6056                            let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
6057                            Ok(
6058                                match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
6059                                    Some(ord) => Value::Float(ord),
6060                                    None => v,
6061                                },
6062                            )
6063                        })
6064                        .collect();
6065                    Ok((k, keys?))
6066                })
6067                .collect::<Result<_, _>>()?;
6068            indexed.sort_by(|a, b| {
6069                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
6070                    let o = &stmt.order_by[idx];
6071                    let cmp = order_by_value_cmp_in(
6072                        o.desc,
6073                        o.nulls_first,
6074                        ka,
6075                        kb,
6076                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
6077                    );
6078                    if cmp != core::cmp::Ordering::Equal {
6079                        return cmp;
6080                    }
6081                }
6082                core::cmp::Ordering::Equal
6083            });
6084            projected_rows = indexed
6085                .into_iter()
6086                .map(|(i, _)| projected_rows[i].clone())
6087                .collect();
6088        }
6089        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
6090        if stmt.distinct {
6091            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
6092            // spec folds EVERY text position, so a column declared
6093            // `COLLATE utf8mb4_bin` had its values merged here exactly the
6094            // way 3b494b6e fixed on the main scan path. The projection is
6095            // already in scope at each of these sites, so the mask needs no
6096            // new plumbing -- it was simply never asked for.
6097            projected_rows = dedup_rows(
6098                projected_rows,
6099                FoldSpec::of_masks(
6100                    scan_ctx.mysql_dialect,
6101                    &fold_mask(&projection),
6102                    &pad_mask(&projection),
6103                ),
6104            );
6105        }
6106        if let Some(offset) = stmt.offset_literal() {
6107            let off = (offset as usize).min(projected_rows.len());
6108            projected_rows.drain(..off);
6109        }
6110        if let Some(limit) = stmt.limit_literal() {
6111            projected_rows.truncate(limit as usize);
6112        }
6113        Ok(QueryResult::Rows {
6114            columns,
6115            rows: projected_rows,
6116        })
6117    }
6118
6119    /// Constant `SELECT` with no FROM: evaluate each projection item
6120    /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
6121    fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
6122        let empty_schema: Vec<ColumnSchema> = Vec::new();
6123        let ctx = self.ev_ctx(&empty_schema, None);
6124        // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
6125        // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
6126        // `SELECT string_agg('x',',')` → x). Before this it fell through to the
6127        // scalar projection, where the aggregate name looked like an unknown
6128        // function. The WHERE filters that one row, so `… WHERE false` leaves
6129        // the aggregate zero input rows (`count(*)` → 0).
6130        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
6131            let dummy = Row::new(Vec::new());
6132            let passes = match &stmt.where_ {
6133                Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
6134                None => true,
6135            };
6136            let rows: Vec<RowRef<'_>> = if passes {
6137                alloc::vec![RowRef::Owned(&dummy)]
6138            } else {
6139                Vec::new()
6140            };
6141            let agg = aggregate::run(
6142                stmt,
6143                crate::join::AggRows::Refs(&rows),
6144                &empty_schema,
6145                None,
6146                None,
6147                self.parallel_runner.0.as_deref(),
6148                Some(self.active_catalog()),
6149                Some(self),
6150            )?;
6151            return self.finish_agg_result(agg, stmt, CancelToken::none());
6152        }
6153        let projection = build_projection(
6154            &stmt.items,
6155            &empty_schema,
6156            "",
6157            self.speaks_mysql,
6158            Some(self.active_catalog()),
6159        )?;
6160        // `SELECT … WHERE cond` with no FROM — the one conceptual
6161        // row survives only when the condition is true (previously
6162        // the WHERE was silently ignored: `SELECT 1 WHERE false`
6163        // returned a row).
6164        let dummy_row = Row::new(Vec::new());
6165        if let Some(w) = &stmt.where_ {
6166            let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
6167            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6168                let columns: Vec<ColumnSchema> = projection
6169                    .into_iter()
6170                    .map(|p| p.to_column_schema())
6171                    .collect();
6172                return Ok(QueryResult::Rows {
6173                    columns,
6174                    rows: Vec::new(),
6175                });
6176            }
6177        }
6178        // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6179        // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6180        // desugar to unnest) expands here: one output row per SRF row, sibling
6181        // scalar columns repeated. unnest / array_elements / path_query reach a
6182        // real FROM via the parser rewrite and never land here.
6183        // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6184        let srf_idxs = self.srf_target_idxs(&projection);
6185        if !srf_idxs.is_empty() {
6186            let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6187            let columns: Vec<ColumnSchema> = projection
6188                .into_iter()
6189                .map(|p| p.to_column_schema())
6190                .collect();
6191            // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6192            // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6193            // to. This returned straight out of the expansion, so
6194            // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6195            // input order — the sort was not wrong, it never ran. (There is
6196            // exactly one conceptual input row here, which is why the ordinary
6197            // scan pipeline is not on this path at all.)
6198            if !stmt.order_by.is_empty() {
6199                let synth_ctx =
6200                    EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6201                let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6202                    .order_by
6203                    .iter()
6204                    .map(|o| {
6205                        let mut o = o.clone();
6206                        if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6207                            && *n >= 1
6208                            && let Ok(idx) = usize::try_from(*n - 1)
6209                            && idx < columns.len()
6210                        {
6211                            o.expr = Expr::Column(spg_sql::ast::ColumnName {
6212                                qualifier: None,
6213                                name: columns[idx].name.clone(),
6214                            });
6215                        }
6216                        o
6217                    })
6218                    .collect();
6219                let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6220                let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6221                for r in rows {
6222                    // v7.39.12 — a correlated subquery in ORDER BY is resolved
6223                    // for this row before the key is built; see
6224                    // `Engine::order_by_resolved_for_row`.
6225                    let per_row = self.order_by_resolved_for_row(
6226                        &resolved,
6227                        &r,
6228                        &synth_ctx,
6229                        CancelToken::none(),
6230                    )?;
6231                    let keys =
6232                        build_order_keys(per_row.as_deref().unwrap_or(&resolved), &r, &synth_ctx)?;
6233                    tagged.push((keys, r));
6234                }
6235                sort_by_keys(&mut tagged, &descs);
6236                rows = tagged.into_iter().map(|(_, r)| r).collect();
6237            }
6238            apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6239            return Ok(QueryResult::Rows { columns, rows });
6240        }
6241        let mut values = Vec::with_capacity(projection.len());
6242        for p in &projection {
6243            values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6244        }
6245        let columns: Vec<ColumnSchema> = projection
6246            .into_iter()
6247            .map(|p| p.to_column_schema())
6248            .collect();
6249        // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6250        // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6251        // returns none. (The SRF and aggregate arms above already applied
6252        // them; this tail was the one that didn't.)
6253        let mut rows = alloc::vec![Row::new(values)];
6254        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6255        Ok(QueryResult::Rows { columns, rows })
6256    }
6257
6258    /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6259    /// circuit. Catches
6260    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6261    /// BEFORE `resolve_select_subqueries` materialises the inner result
6262    /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6263    /// values into a `HashSet<i64>` directly, then probes A.pk per
6264    /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6265    /// (~150 µs / query at INSUBQ benchmark scale).
6266    pub(crate) fn try_count_star_pk_in_subquery_fast(
6267        &self,
6268        stmt: &SelectStatement,
6269        cancel: CancelToken<'_>,
6270    ) -> Result<Option<QueryResult>, EngineError> {
6271        use spg_sql::ast::SelectItem;
6272        if stmt.distinct
6273            || stmt.limit_with_ties
6274            || stmt.group_by.is_some()
6275            || stmt.having.is_some()
6276            || !stmt.unions.is_empty()
6277            || !stmt.order_by.is_empty()
6278            || stmt.limit.is_some()
6279            || stmt.offset.is_some()
6280            || stmt.items.len() != 1
6281        {
6282            return Ok(None);
6283        }
6284        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6285            return Ok(None);
6286        };
6287        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6288            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6289        if !is_count_star {
6290            return Ok(None);
6291        }
6292        let Some(from) = stmt.from.as_ref() else {
6293            return Ok(None);
6294        };
6295        if !from.joins.is_empty()
6296            || from.primary.lateral_subquery.is_some()
6297            || from.primary.unnest_expr.is_some()
6298            || from.primary.generate_series_args.is_some()
6299            || from.primary.table_fn_call.is_some()
6300            || from.primary.as_of_segment.is_some()
6301        {
6302            return Ok(None);
6303        }
6304        let Some(where_expr) = stmt.where_.as_ref() else {
6305            return Ok(None);
6306        };
6307        // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6308        // negated=false; no other predicates.
6309        let Expr::InSubquery {
6310            expr: col_expr,
6311            subquery,
6312            negated: false,
6313        } = where_expr
6314        else {
6315            return Ok(None);
6316        };
6317        let Expr::Column(c) = col_expr.as_ref() else {
6318            return Ok(None);
6319        };
6320        let outer_alias = from
6321            .primary
6322            .alias
6323            .as_deref()
6324            .unwrap_or(from.primary.name.as_str());
6325        if let Some(q) = c.qualifier.as_deref()
6326            && !q.eq_ignore_ascii_case(outer_alias)
6327        {
6328            return Ok(None);
6329        }
6330        // Outer column must be a single-column PK on integer family.
6331        let catalog = self.active_catalog();
6332        let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6333            return Ok(None);
6334        };
6335        let outer_schema = outer_table.schema();
6336        let Some(outer_pos) = outer_schema
6337            .columns
6338            .iter()
6339            .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6340        else {
6341            return Ok(None);
6342        };
6343        if !matches!(
6344            outer_schema.columns[outer_pos].ty,
6345            spg_storage::DataType::BigInt
6346                | spg_storage::DataType::Int
6347                | spg_storage::DataType::SmallInt
6348        ) {
6349            return Ok(None);
6350        }
6351        if !outer_schema
6352            .uniqueness_constraints
6353            .iter()
6354            .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6355        {
6356            return Ok(None);
6357        }
6358        let Some(idx) = outer_table.index_on(outer_pos) else {
6359            return Ok(None);
6360        };
6361        // Inner must be uncorrelated. The cheap-correlation pre-check
6362        // exists upstream; here we just attempt the bare exec.
6363        if crate::subquery::select_is_correlated(subquery) {
6364            return Ok(None);
6365        }
6366        let mut inner = (**subquery).clone();
6367        self.resolve_select_subqueries(&mut inner, cancel)?;
6368        let r = match self.exec_bare_select_cancel(&inner, cancel) {
6369            Ok(r) => r,
6370            Err(_) => return Ok(None),
6371        };
6372        let QueryResult::Rows { columns, rows, .. } = r else {
6373            return Ok(None);
6374        };
6375        if columns.len() != 1 {
6376            return Ok(None);
6377        }
6378        // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6379        // subquery projects a column known to be UNIQUE/PK on its table
6380        // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6381        // in `tbl.uniqueness_constraints`), survivor values are
6382        // guaranteed distinct and the per-survivor `HashSet::insert`
6383        // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6384        //
6385        // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6386        // projection that is a bare Column ref, table-column lookup in
6387        // catalog confirms the column appears as a unique constraint's
6388        // sole member. UNIQUE NOT NULL is required — a nullable unique
6389        // column may have multiple NULLs, but NULLs are already skipped
6390        // above (`Value::Null => continue`), so a UNIQUE-only column is
6391        // still safe to dedup-skip.
6392        let inner_unique = (|| -> bool {
6393            if inner.distinct
6394                || inner.group_by.is_some()
6395                || !inner.unions.is_empty()
6396                || inner.having.is_some()
6397                || inner.items.len() != 1
6398            {
6399                return false;
6400            }
6401            let Some(inner_from) = inner.from.as_ref() else {
6402                return false;
6403            };
6404            if !inner_from.joins.is_empty()
6405                || inner_from.primary.lateral_subquery.is_some()
6406                || inner_from.primary.unnest_expr.is_some()
6407                || inner_from.primary.generate_series_args.is_some()
6408                || inner_from.primary.table_fn_call.is_some()
6409            {
6410                return false;
6411            }
6412            let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6413                return false;
6414            };
6415            let Expr::Column(pc) = proj else {
6416                return false;
6417            };
6418            let inner_alias = inner_from
6419                .primary
6420                .alias
6421                .as_deref()
6422                .unwrap_or(inner_from.primary.name.as_str());
6423            if let Some(q) = pc.qualifier.as_deref()
6424                && !q.eq_ignore_ascii_case(inner_alias)
6425            {
6426                return false;
6427            }
6428            let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6429                return false;
6430            };
6431            let isch = inner_table.schema();
6432            let Some(ipos) = isch
6433                .columns
6434                .iter()
6435                .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6436            else {
6437                return false;
6438            };
6439            isch.uniqueness_constraints
6440                .iter()
6441                .any(|u| u.columns.as_slice() == [ipos])
6442        })();
6443        // Collect inner i64 values directly into a HashSet, then probe.
6444        let mut count: i64 = 0;
6445        let mut probed = if inner_unique {
6446            hashbrown::HashSet::<i64>::new()
6447        } else {
6448            hashbrown::HashSet::<i64>::with_capacity(rows.len())
6449        };
6450        for row in &rows {
6451            let v = row.values.first().cloned().unwrap_or(Value::Null);
6452            let n = match v {
6453                Value::BigInt(n) => n,
6454                Value::Int(n) => i64::from(n),
6455                Value::SmallInt(n) => i64::from(n),
6456                Value::Null => continue,
6457                _ => return Ok(None),
6458            };
6459            // De-duplicate inner key set so a duplicate inner value
6460            // doesn't double-count the same outer row. Skipped when
6461            // the inner projection is statically unique.
6462            if !inner_unique && !probed.insert(n) {
6463                continue;
6464            }
6465            // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6466            // the `IndexKey::from_value` enum-dispatch and the per-call
6467            // `IndexKey` wrapper construction. The outer column is
6468            // already gated to integer-family above, so an i64 key
6469            // always corresponds to a valid PK lookup.
6470            if !idx.lookup_eq_i64(n).is_empty() {
6471                count += 1;
6472            }
6473        }
6474        let columns_out = alloc::vec![ColumnSchema::new(
6475            "count".to_string(),
6476            spg_storage::DataType::BigInt,
6477            false,
6478        )];
6479        let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6480        Ok(Some(QueryResult::Rows {
6481            columns: columns_out,
6482            rows: rows_out,
6483        }))
6484    }
6485
6486    /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6487    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6488    /// (the post-subquery-replacement shape of the INSUBQ probe
6489    /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6490    /// The general aggregate path materialises every seeked row into
6491    /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6492    /// For COUNT(*) we only care how many keys hit; iterate the list
6493    /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6494    /// row materialisation, the aggregate state machine, and the per-
6495    /// row WHERE re-eval (the seek already filtered by the same list).
6496    /// Returns `None` when the shape doesn't match.
6497    fn try_count_star_pk_in_list_fast(
6498        &self,
6499        stmt: &SelectStatement,
6500        table: &spg_storage::Table,
6501        schema_cols: &[ColumnSchema],
6502        alias: &str,
6503    ) -> Option<QueryResult> {
6504        use spg_sql::ast::{ColumnName, SelectItem};
6505        // Gates on the SELECT shape.
6506        if stmt.distinct
6507            || stmt.limit_with_ties
6508            || stmt.group_by.is_some()
6509            || stmt.having.is_some()
6510            || !stmt.unions.is_empty()
6511            || !stmt.order_by.is_empty()
6512            || stmt.limit.is_some()
6513            || stmt.offset.is_some()
6514            || stmt.items.len() != 1
6515        {
6516            return None;
6517        }
6518        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6519            return None;
6520        };
6521        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6522            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6523        if !is_count_star {
6524            return None;
6525        }
6526        // WHERE must be `<col> IN (literal list)` with no other
6527        // conjuncts (the seek result is a true subset of the row
6528        // population for this predicate).
6529        let where_expr = stmt.where_.as_ref()?;
6530        let Expr::InList {
6531            expr: col_expr,
6532            list,
6533            negated: false,
6534        } = where_expr
6535        else {
6536            return None;
6537        };
6538        let Expr::Column(c) = col_expr.as_ref() else {
6539            return None;
6540        };
6541        if let Some(q) = c.qualifier.as_deref()
6542            && !q.eq_ignore_ascii_case(alias)
6543        {
6544            return None;
6545        }
6546        let col_pos = schema_cols
6547            .iter()
6548            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6549        // The column must be a single-column PK on an integer family
6550        // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6551        // so the antiset stays collision-free under `HashSet<i64>`.
6552        let schema = table.schema();
6553        if !matches!(
6554            schema.columns[col_pos].ty,
6555            spg_storage::DataType::BigInt
6556                | spg_storage::DataType::Int
6557                | spg_storage::DataType::SmallInt
6558        ) {
6559            return None;
6560        }
6561        if !schema
6562            .uniqueness_constraints
6563            .iter()
6564            .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6565        {
6566            return None;
6567        }
6568        let idx = table.index_on(col_pos)?;
6569        // Tally non-empty seek results across all literal values.
6570        let mut count: i64 = 0;
6571        for lit in list {
6572            let Expr::Literal(l) = lit else {
6573                return None;
6574            };
6575            // r1039 — through the shared resolver, so a literal spelled
6576            // in another type ('5' against an integer PK) is read as the
6577            // column's before it becomes a key. This tally answers from
6578            // the index alone, so a key in the wrong space would return a
6579            // COUNT of zero rather than fall back to a scan.
6580            let col = schema.columns.get(col_pos)?;
6581            let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6582            let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6583            if !idx.lookup_eq(&key).is_empty() {
6584                count += 1;
6585            }
6586        }
6587        let columns = alloc::vec![ColumnSchema::new(
6588            "count".to_string(),
6589            spg_storage::DataType::BigInt,
6590            false,
6591        )];
6592        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6593        let _ = ColumnName {
6594            qualifier: None,
6595            name: String::new(),
6596        };
6597        Some(QueryResult::Rows { columns, rows })
6598    }
6599
6600    /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6601    /// BETWEEN a AND b` on an indexed column. The index range walk yields
6602    /// exactly the matching (visible) rows, so we count locators directly —
6603    /// skipping the row materialisation, the aggregate state machine, and the
6604    /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6605    /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6606    /// when the shape doesn't match.
6607    fn try_count_star_indexed_range_fast(
6608        &self,
6609        stmt: &SelectStatement,
6610        table: &spg_storage::Table,
6611        schema_cols: &[ColumnSchema],
6612        alias: &str,
6613        snapshot: &spg_storage::snapshot::Snapshot,
6614    ) -> Option<QueryResult> {
6615        use spg_sql::ast::SelectItem;
6616        if stmt.distinct
6617            || stmt.limit_with_ties
6618            || stmt.group_by.is_some()
6619            || stmt.having.is_some()
6620            || !stmt.unions.is_empty()
6621            || !stmt.order_by.is_empty()
6622            || stmt.limit.is_some()
6623            || stmt.offset.is_some()
6624            || stmt.items.len() != 1
6625        {
6626            return None;
6627        }
6628        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6629            return None;
6630        };
6631        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6632            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6633        if !is_count_star {
6634            return None;
6635        }
6636        let where_expr = stmt.where_.as_ref()?;
6637        let count = crate::index_access::try_range_count(
6638            where_expr,
6639            schema_cols,
6640            table,
6641            alias,
6642            snapshot,
6643            self.speaks_mysql,
6644        )?;
6645        let columns = alloc::vec![ColumnSchema::new(
6646            "count".to_string(),
6647            spg_storage::DataType::BigInt,
6648            false,
6649        )];
6650        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6651        Some(QueryResult::Rows { columns, rows })
6652    }
6653
6654    /// Single-table aggregate path: filter the (optionally index-seeked)
6655    /// rows, then hand off to the aggregate executor which does its own
6656    /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6657    fn run_single_table_aggregate<'a>(
6658        &self,
6659        stmt: &SelectStatement,
6660        table: &'a spg_storage::Table,
6661        schema_cols: &'a [ColumnSchema],
6662        alias: &str,
6663        indexed_rows: Option<crate::index_access::Seeked<'a>>,
6664        cancel: CancelToken<'_>,
6665    ) -> Result<QueryResult, EngineError> {
6666        // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6667        // REPEATABLE (see run_single_table_scan). Aggregates
6668        // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6669        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6670        let ctx = self
6671            .ev_ctx(schema_cols, Some(alias))
6672            .with_sample_rng(&sample_cell);
6673        // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6674        // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6675        // and every abandoned buffer on the way stays resident: RSS is a
6676        // high-water mark, so the intermediates are paid for even though
6677        // they are freed. Round 656 measured the scan at 17 bytes/row
6678        // where the survivor list itself only needs 8.
6679        let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6680            Vec::with_capacity(table.rows().len())
6681        } else {
6682            // With a WHERE, the row count is an UPPER bound and reserving it
6683            // is the worse trade: `… WHERE id = 5` over 50M rows would take
6684            // 400 MB of pointers to hold one survivor. Let it grow.
6685            Vec::new()
6686        };
6687        // v6.2.6 — Memoize: per-query LRU cache for correlated
6688        // scalar subqueries. Fresh per row-loop entry so each
6689        // SELECT execution gets an isolated cache.
6690        let mut memo = memoize::MemoizeCache::new();
6691        // v7.37 (perf) — single-table aggregate's WHERE filter
6692        // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6693        // correlated`) per row, even for subquery-free WHEREs that
6694        // the single-table SCAN path has compiled since v7.32
6695        // (perf knife D). The asymmetry meant a fold-to-filter
6696        // rewrite (joinfold) that swapped a JOIN for a single-table
6697        // aggregate over a compiled WHERE saw the tree-walker
6698        // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6699        // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6700        // step. Compile once if eligible; fall back to the walker
6701        // for subquery-bearing or non-compilable WHEREs.
6702        let compiled_where: Option<eval::CompiledExpr> = stmt
6703            .where_
6704            .as_ref()
6705            .filter(|w| eval::fully_compilable(w))
6706            .map(|w| {
6707                // v7.38.8 — the scan filter runs the cheap half of its
6708                // conjunction first. Called from HERE and not from
6709                // `eval::compiled`, deliberately: the row loop lives in
6710                // that file, and adding a function to it cost this
6711                // query 11 % through layout alone while doing no work
6712                // for it. See `crate::qualorder`.
6713                match crate::qualorder::reordered(w) {
6714                    Some(r) => eval::compile_expr(&r, &ctx),
6715                    None => eval::compile_expr(w, &ctx),
6716                }
6717            });
6718        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6719        let mut row_passes_where = |row: &Row<'static>,
6720                                    eval_stack: &mut Vec<Value<'static>>,
6721                                    memo: &mut memoize::MemoizeCache|
6722         -> Result<bool, EngineError> {
6723            match (&compiled_where, &stmt.where_) {
6724                (Some(cw), _) => {
6725                    // v7.39 (round 479) — the predicate wants a bool, not a
6726                    // Value. The owned entry ended in `Value::into_owned`
6727                    // and the caller then dropped it, once per row; round
6728                    // 478's profile put that pair above the comparison
6729                    // itself.
6730                    Ok(eval::compiled::eval_compiled_pred(
6731                        cw,
6732                        row,
6733                        &ctx,
6734                        eval_stack,
6735                        ctx.mysql_dialect,
6736                    )
6737                    .map_err(EngineError::Eval)?)
6738                }
6739                (None, Some(w)) => {
6740                    let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6741                    Ok(crate::eval::predicate_is_true(
6742                        &cond,
6743                        "WHERE",
6744                        ctx.mysql_dialect,
6745                    )?)
6746                }
6747                (None, None) => Ok(true),
6748            }
6749        };
6750        if let Some(seeked) = &indexed_rows {
6751            // v7.38.19 — an EXACT seek has already applied the whole
6752            // predicate, so asking again is asking the index's question
6753            // a second time, once per row.
6754            //
6755            // Profiled on `count(*) FROM events WHERE project_id = 3`
6756            // over 200,000 rows: `try_index_seek` 1,814 leaf samples and
6757            // `binop::compare` 1,633 — and `compare`'s first arm is
6758            // `(Int, Int) => a.cmp(b)`, so it was never that a
6759            // comparison is expensive. It was that 25,000 of them were
6760            // re-deciding what the walk had decided. The same query with
6761            // `GROUP BY project_id` bolted on ran in half the time,
6762            // doing strictly more work, because that path reached the
6763            // rows differently.
6764            //
6765            // `exact` is false for every arm that has not proven it —
6766            // the GIN, trigram and jsonb walks, an `AND` whose other
6767            // conjuncts went unapplied, a collated key, a type whose key
6768            // cannot name it. See `index_access::Seeked`.
6769            if seeked.exact {
6770                filtered.extend(seeked.rows.iter().map(Cow::as_ref));
6771            } else {
6772                for cow in &seeked.rows {
6773                    let row = cow.as_ref();
6774                    if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6775                        continue;
6776                    }
6777                    filtered.push(row);
6778                }
6779            }
6780        }
6781        // v7.36 (cold-tier coverage) — single-table aggregate's
6782        // non-indexed full scan was hot-only and silently lost cold
6783        // rows on COUNT/SUM/etc. Materialise cold rows once into
6784        // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6785        // shape stays unchanged; the cold rows live until the end of
6786        // the aggregate run.
6787        let cold_rows_storage = if indexed_rows.is_none() {
6788            self.iter_cold_rows_of_table(table)
6789        } else {
6790            Vec::new()
6791        };
6792        if indexed_rows.is_none() {
6793            // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6794            // single-table aggregate full-scan path. Mirrors the gate on
6795            // `run_single_table_scan`: this is a user-query result path,
6796            // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6797            // reader's snapshot cannot see (e.g. tombstoned versions),
6798            // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6799            // under the default gate-off: every hot row is frozen or
6800            // committed-and-alive, so `is_row_visible` returns true.
6801            // Cold-tier rows are frozen (visible) by definition — left
6802            // ungated, matching the plain-scan path.
6803            let scan_snapshot = self.current_snapshot();
6804            // v7.39 (pg_stat knife B) — this full-scan branch walks
6805            // headers directly (serial and sharded alike); count the
6806            // sequential scan here.
6807            table.note_seq_scan();
6808            // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6809            // filter dominate the pre-aggregate wall time on big
6810            // scans (P1's ground truth: accumulation is only ~17%).
6811            // Shard THAT work when the host injected an executor and
6812            // the WHERE is compiled (the compiled evaluator is pure
6813            // over &row; the tree-walker fallback can hit correlated
6814            // subqueries and stays serial). Shards return surviving
6815            // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6816            // 'static bound — and the main thread only dereferences.
6817            let n = table.row_count();
6818            let par = self.parallel_runner.0.as_deref().filter(|_| {
6819                n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6820            });
6821            // v7.38.11 — ask the BRIN summary first. When it prunes,
6822            // the work left is a few thousand rows and sharding it
6823            // costs more than it saves, so the serial pruned loop below
6824            // takes it; the shard machinery is left exactly as it was
6825            // rather than taught about slots.
6826            let brin_slots = stmt
6827                .where_
6828                .as_ref()
6829                .and_then(|w| crate::brin::candidate_slots(w, table));
6830            let brin_prunes = brin_slots
6831                .as_ref()
6832                .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6833            if let Some(r) = par
6834                && !brin_prunes
6835            {
6836                let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6837                let chunk = n.div_ceil(n_shards);
6838                type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6839                let cw = &compiled_where;
6840                let snap_ref = &scan_snapshot;
6841                let results = r.run_shards(n_shards, &|s| {
6842                    let lo = s * chunk;
6843                    let hi = ((s + 1) * chunk).min(n);
6844                    let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6845                    // EvalContext carries Cells (sampler / row counters)
6846                    // and is !Sync — each shard builds its own from the
6847                    // same Sync inputs. The compiled WHERE is gated to
6848                    // the pure-scalar whitelist, which reads none of the
6849                    // session state the engine-built ctx would add
6850                    // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6851                    // sampled scans never take this branch).
6852                    let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6853                    let mut stack: Vec<Value<'static>> = Vec::new();
6854                    let out: ShardOut = (|| {
6855                        for i in lo..hi {
6856                            if !table.is_row_visible(i, snap_ref) {
6857                                continue;
6858                            }
6859                            let row = &table.rows()[i];
6860                            // v7.39 (round 480) — the parallel full-scan
6861                            // shard is the path the aggregate benchmark
6862                            // actually takes, and it was still on the OWNED
6863                            // entry: round 480's profile attributed 68.7 %
6864                            // of `drop_glue<Value>` to this closure, which
6865                            // is why round 479's fix to the indexed path
6866                            // barely moved the total.
6867                            //
6868                            // The `matches!(…, Value::Bool(true))` form was
6869                            // also a narrower reading than the rest of the
6870                            // engine uses — `predicate_is_true` is what
6871                            // handles NULL and MySQL truthiness — so the
6872                            // bool entry fixes the shape as well as the cost.
6873                            let pass = match cw {
6874                                Some(c) => eval::compiled::eval_compiled_pred(
6875                                    c,
6876                                    row,
6877                                    &shard_ctx,
6878                                    &mut stack,
6879                                    shard_ctx.mysql_dialect,
6880                                )
6881                                .map_err(EngineError::Eval)?,
6882                                None => true,
6883                            };
6884                            if pass {
6885                                keep.push(i);
6886                            }
6887                        }
6888                        Ok(keep)
6889                    })();
6890                    alloc::boxed::Box::new(out)
6891                });
6892                // v7.39 (round 567) — `rows()` is a 32-way trie, so
6893                // indexing it is four dependent loads and a scan that
6894                // reads every row paid them every row. A profile of
6895                // `SELECT sum(id)` over 500k rows put 37.8% of the
6896                // connection thread's CPU on THIS ONE LINE. The cursor
6897                // holds the leaf, making that one descent per 32.
6898                let mut rows_cur = table.rows().run_cursor();
6899                for boxed in results {
6900                    let shard = boxed
6901                        .downcast::<ShardOut>()
6902                        .expect("runner echoes the closure's box");
6903                    for i in (*shard)? {
6904                        if let Some(row) = rows_cur.get(i) {
6905                            filtered.push(row);
6906                        }
6907                    }
6908                }
6909            } else {
6910                let mut rows_cur = table.rows().run_cursor();
6911                // v7.38.11 — the slots the BRIN summary could not rule
6912                // out. The predicate still runs on every row that
6913                // survives: the summary decides what to SKIP, never
6914                // what to return.
6915                let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6916                for range in ranges {
6917                    for i in range {
6918                        if !table.is_row_visible(i, &scan_snapshot) {
6919                            continue;
6920                        }
6921                        let Some(row) = rows_cur.get(i) else { continue };
6922                        if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6923                            continue;
6924                        }
6925                        filtered.push(row);
6926                    }
6927                }
6928            }
6929            for row in &cold_rows_storage {
6930                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6931                    continue;
6932                }
6933                filtered.push(row);
6934            }
6935        }
6936        // v7.29 — a per-query memo so correlated scalar
6937        // subqueries batch-evaluate once (group map) instead of
6938        // executing per group.
6939        let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6940        let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6941            self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6942                .map_err(|err| match err {
6943                    EngineError::Eval(ev) => ev,
6944                    other => eval::EvalError::TypeMismatch {
6945                        detail: alloc::format!("{other}"),
6946                    },
6947                })
6948        };
6949        // v7.39 (round 656) — the plain relational scan. This collect() was
6950        // the measured defect: one 64-byte `RowRef` per surviving row to
6951        // wrap an 8-byte pointer `filtered` already holds. Scalar
6952        // aggregates measured ~81 bytes/row of working memory because of
6953        // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6954        // one number. `AggRows::Ptrs` reads the pointers directly.
6955        let agg = aggregate::run(
6956            stmt,
6957            crate::join::AggRows::Ptrs(&filtered),
6958            schema_cols,
6959            Some(alias),
6960            Some(&agg_correlated),
6961            self.parallel_runner.0.as_deref(),
6962            Some(self.active_catalog()),
6963            Some(self),
6964        )?;
6965        self.finish_agg_result(agg, stmt, cancel)
6966    }
6967
6968    /// Single-table scan + projection path: WHERE filter (compiled when
6969    /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6970    /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6971    fn run_single_table_scan<'a>(
6972        &self,
6973        stmt: &SelectStatement,
6974        table: &'a spg_storage::Table,
6975        schema_cols: &'a [ColumnSchema],
6976        alias: &str,
6977        indexed_rows: Option<crate::index_access::Seeked<'a>>,
6978        cancel: CancelToken<'_>,
6979    ) -> Result<QueryResult, EngineError> {
6980        // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6981        // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6982        // deterministic `__tsm_fract(seed)` draws share one scan-local
6983        // state (isolated from the global random() PRNG); a fresh cell per
6984        // scan makes a repeat / rescan reproduce the same sample. Unused
6985        // and cheap when the query carries no sample.
6986        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6987        let ctx = self
6988            .ev_ctx(schema_cols, Some(alias))
6989            .with_sample_rng(&sample_cell);
6990        let projection = build_projection(
6991            &stmt.items,
6992            schema_cols,
6993            alias,
6994            self.speaks_mysql,
6995            Some(self.active_catalog()),
6996        )?;
6997        // v7.19 P5 — single-table SELECT path for SRF
6998        // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6999        // unnest in the projection list. When present, the
7000        // per-row processor emits one output row per array
7001        // element (broadcasting non-SRF projections from the
7002        // same input row). Empty / NULL arrays emit zero rows
7003        // for that input — PG semantics.
7004        // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
7005        let srf_idxs = self.srf_target_idxs(&projection);
7006        let srf_position = srf_idxs.first().copied();
7007        // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
7008        let mut srf_plan = if srf_position.is_some() {
7009            Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
7010        } else {
7011            None
7012        };
7013
7014        // Materialise the filter pass into `(order_key, projected_row)`
7015        // tuples. The order key is `None` when there's no ORDER BY clause.
7016        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
7017        // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
7018        // output row to the per-query byte budget as it is built, so a
7019        // fat single-table scan / sort REJECTS with QueryBytesExceeded
7020        // at ~the ceiling instead of materialising the whole table and
7021        // only noticing at the final enforce_row_limit check. Without
7022        // this, N concurrent fat scans peak at N×table and OOM the host.
7023        // `max_query_bytes = None` (the embedded default) = no ceiling,
7024        // so existing unbudgeted behaviour is byte-identical.
7025        let mut budget = ByteBudget::new(self.max_query_bytes);
7026        // v6.2.6 — Memoize per-row WHERE eval shares one cache.
7027        let mut memo = memoize::MemoizeCache::new();
7028        // v7.32 (perf knife D) — subquery-free WHERE compiles once;
7029        // the row loop then runs a flat step program instead of a
7030        // tree interpretation per row.
7031        let compiled_where: Option<eval::CompiledExpr> = stmt
7032            .where_
7033            .as_ref()
7034            .filter(|w| eval::fully_compilable(w))
7035            .map(|w| {
7036                // v7.38.8 — the scan filter runs the cheap half of its
7037                // conjunction first. Called from HERE and not from
7038                // `eval::compiled`, deliberately: the row loop lives in
7039                // that file, and adding a function to it cost this
7040                // query 11 % through layout alone while doing no work
7041                // for it. See `crate::qualorder`.
7042                match crate::qualorder::reordered(w) {
7043                    Some(r) => eval::compile_expr(&r, &ctx),
7044                    None => eval::compile_expr(w, &ctx),
7045                }
7046            });
7047        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7048        // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
7049        // SELECT-item scalar subquery for the PK-probe fast path. The
7050        // analysis (gate checks + catalog lookups) takes ~500 ns; doing
7051        // it once per query instead of once per row × 100 rows saves
7052        // ~50 µs and lets the per-row evaluation reduce to a single
7053        // index probe + outer-column read.
7054        let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
7055            .iter()
7056            .map(|p| {
7057                if let Expr::ScalarSubquery(inner) = &p.expr {
7058                    self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
7059                } else {
7060                    None
7061                }
7062            })
7063            .collect();
7064        let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
7065        // v7.39 (round 487) — a projection item that is a bare column
7066        // reference binds its position ONCE per query.
7067        //
7068        // Per row it used to walk `eval_expr_with_correlated` (a memo
7069        // lookup for "does this have a subquery", then an un-memoised
7070        // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
7071        // then `resolve_column`, which finds the column by scanning the
7072        // schema and comparing NAMES. On `SELECT g FROM h` that chain was
7073        // 19 % of self time for what is ultimately one cell read.
7074        //
7075        // `compile_column_pos` is the Step VM's resolver, already
7076        // `pub(crate)` and already reused by the aggregate's bind-once
7077        // path: it mirrors `resolve_column`'s happy layers and returns
7078        // None for anything that would reach an error, an ambiguity, or a
7079        // miss, so those still go the interpreter's way and keep its
7080        // exact message. A composite column is excluded for the same
7081        // reason `compile_into` excludes it — it must be rehydrated from
7082        // stored JSON, which is not a cell read.
7083        let proj_direct = bind_direct_columns(&projection, &ctx);
7084        let any_proj_direct = proj_direct.iter().any(Option::is_some);
7085        // v7.39 (round 605) — a projection item that cannot depend on the row
7086        // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
7087        // allocations a row against one for a plain column, `'abc' || 'def'`
7088        // six and `upper('abc')` five, all of them producing the same value
7089        // 50,000 times. An item that fails to evaluate is left alone, so its
7090        // error still comes from the row loop in the interpreter's wording.
7091        let proj_const: Vec<Option<Value<'static>>> = projection
7092            .iter()
7093            .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
7094            .collect();
7095        let any_proj_const = proj_const.iter().any(Option::is_some);
7096        crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
7097        // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
7098        // projection. Statement prep (`resolve_order_by_position`) can only map
7099        // `ORDER BY 1` onto the first SELECT item when that item is an
7100        // expression; a `*` is not one, so the literal survived to here and was
7101        // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
7102        // at all. The parser rewrites `SELECT unnest(a) x` into
7103        // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
7104        // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
7105        // back in input order. The projection is built by now, so the Nth output
7106        // column is known — resolve against it.
7107        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
7108        // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
7109        // EXPANDED rows, so a key naming a select-list item reads that item.
7110        let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
7111            srf_order_output_cols(&order_by, &projection)
7112        } else {
7113            Vec::new()
7114        };
7115        let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
7116        // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
7117        // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
7118        // Hoisted above the closure so the projection-eval path can
7119        // gate `memo` passing on it: the SELECT-item correlated-scalar
7120        // batch path scans the FULL inner table once (~5 ms for 12.5 k
7121        // rows) and is only a win when N outer rows is large; for small
7122        // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
7123        let early_cap: Option<usize> = if order_by.is_empty()
7124            && !stmt.distinct
7125            && !stmt.limit_with_ties
7126            && srf_position.is_none()
7127            && stmt.where_.is_none()
7128        {
7129            stmt.limit_literal()
7130                .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
7131        } else {
7132            None
7133        };
7134        // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
7135        // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
7136        // full-sort by the test gate) keep only the running top-`keep`
7137        // rows in memory instead of materialising every projected row,
7138        // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
7139        // space, not O(rows). `None` = accumulate everything (the prior
7140        // behaviour). The final `partial_sort_tagged(keep)` below still
7141        // runs and produces the identical rows.
7142        // v7.39 (round 683) — the declared collation for each ORDER BY
7143        // position, resolved once and carried beside `descs` for the same
7144        // reason `descs` is carried: it is per key position, not per row.
7145        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
7146        let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
7147            && !stmt.distinct
7148            && !stmt.limit_with_ties
7149            && srf_position.is_none()
7150            && !self.env_cfg().disable_topk
7151        {
7152            stmt.limit_literal().and_then(|l| {
7153                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
7154                (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
7155            })
7156        } else {
7157            None
7158        };
7159        // v7.38.19 — when the sort column is one the projection already
7160        // carries, build no key at all and sort by reading it.
7161        //
7162        // Restricted to the FULL sort: a top-N compares against a stored
7163        // boundary key and `WITH TIES` extends past the limit through the
7164        // keys, both of which need one to exist. DISTINCT keys on them
7165        // too, and an SRF's keys come from the EXPANDED row.
7166        // A COLLATION does not rule it out, but it has to be one that
7167        // orders these values the way bytes do -- decided on the values
7168        // themselves, further down, once they exist.
7169        let sort_by_output: Option<Vec<usize>> = if stmt.distinct
7170            || stmt.limit_with_ties
7171            || srf_position.is_some()
7172            || topk_stream.is_some()
7173        {
7174            None
7175        } else {
7176            order_by_output_cols_if_identical(&order_by, &projection, schema_cols)
7177        };
7178        // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
7179        // kept rows in `tagged`. Probing on the PROJECTED row as soon as
7180        // it is built means a duplicate costs neither a build_order_keys
7181        // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
7182        // a tagged slot, and the sort below runs over u survivors, not
7183        // n input rows — PG's hash-distinct-then-sort plan shape.
7184        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
7185            hashbrown::HashMap::new();
7186        let distinct_hb = hashbrown::DefaultHashBuilder::default();
7187        // v7.38.13 — which output positions must NOT fold. Built once per
7188        // scan from the projection, which carries the source column's
7189        // byte-wise-ness; see `FoldSpec`.
7190        let distinct_mask = fold_mask(&projection);
7191        // v7.39 (round 485) — one projection buffer for the whole scan
7192        // rather than a fresh `Vec` per input row. A row that survives
7193        // the DISTINCT probe takes the buffer with it (`mem::take`) and
7194        // the next row allocates a new one; a row that duplicates an
7195        // earlier one leaves the buffer — and its capacity — in place.
7196        // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
7197        // projected rows are duplicates, so that is 49 900 allocate /
7198        // free pairs the scan no longer performs. Shapes where every row
7199        // survives (plain projection, `DISTINCT` over a unique column)
7200        // allocate exactly as often as before.
7201        let mut proj_buf: Vec<Value<'static>> = Vec::new();
7202        // v7.39 (round 571) — buffers handed back by the top-N trim.
7203        // Round 485 made the scan share ONE projection buffer, but a
7204        // surviving row takes it (`mem::take`) and without DISTINCT
7205        // almost every row survives, so the next one starts from zero
7206        // capacity and allocates. The trim drops `keep` rows at a time
7207        // and their buffers come back here instead of being freed.
7208        let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
7209        let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
7210        // v7.39 (round 581) — the worst row the accumulator is currently
7211        // keeping. Anything that loses to it cannot reach the answer, so
7212        // it is dropped before its projection is ever built.
7213        let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
7214        // v7.38.20 — the boundary's own leading eight bytes, so a losing
7215        // row can be turned away before a key is built for it. Kept
7216        // beside the boundary and refreshed with it; `None` whenever the
7217        // boundary's first key is not one this can read, which sends
7218        // every row down the ordinary path.
7219        // v7.38.21 — and whether those bytes may be trusted under the
7220        // collation in force, which is the boundary's own text to answer.
7221        let mut topk_boundary_prefix: Option<(crate::orderby::PrefixKind, u64, bool)> = None;
7222        // v7.39 (round 582) — resolve each ORDER BY column once, not
7223        // once per row. See `order_by_bound_positions`.
7224        let order_bound =
7225            crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
7226        // v7.39.12 — a correlated scalar subquery in ORDER BY is
7227        // resolved for the row before its key is built.
7228        //
7229        // Uncorrelated subqueries are replaced by a literal before
7230        // execution; a correlated one cannot be, so it reached the
7231        // per-row evaluator — the one place that cannot run a subquery
7232        // — and the statement raised "subquery reached row eval".
7233        // Reported by sentori against 7.39.11; see
7234        // `Engine::order_by_resolved_for_row`.
7235        //
7236        // The `any` runs once, here, so an ordinary ORDER BY pays one
7237        // bool per row and nothing else.
7238        let order_has_subquery = order_by
7239            .iter()
7240            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
7241        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
7242        // v7.39 (round 581) — and it stops asking when the answer is
7243        // always "keep".
7244        //
7245        // The check earns its place only on rows it rejects. Over
7246        // ascending ids, `ORDER BY id DESC` never rejects one — every
7247        // row beats the current worst — so the comparison is pure
7248        // overhead there, measured at +5.5% in three batches out of
7249        // three. After a window of rows it looks at what it has
7250        // actually rejected and switches itself off if the shape is not
7251        // paying. The answers do not depend on it either way.
7252        // v7.38.21 — resolved once per query, not per row.
7253        //
7254        // No collation at all is the case v7.38.20 shipped. A DECLARED
7255        // one may still be answered by bytes, and which collations those
7256        // are is `Collated::ascii_byte_order`'s to say — the same
7257        // allowlist `byte_order_answers_the_collation` consults, so the
7258        // two cannot come to disagree about a collation. What that
7259        // allowlist requires of the TEXT is checked per row and on the
7260        // boundary, because a streaming top-N has no batch to check.
7261        let boundary_no_collation = order_colls.iter().all(Option::is_none);
7262        let boundary_collations_permit = boundary_no_collation
7263            || order_colls
7264                .iter()
7265                .flatten()
7266                .all(crate::collate::Collated::ascii_byte_order);
7267        const BOUNDARY_WINDOW: u32 = 8192;
7268        let mut boundary_checks: u32 = 0;
7269        let mut boundary_rejects: u32 = 0;
7270        let mut boundary_check_on = true;
7271        // Inline the per-row work in a closure so the indexed and full-
7272        // scan branches share the body.
7273        // v7.38.19 — `check_where` is per CALL SITE, not per closure: the
7274        // full-scan loops below must apply the predicate, and the
7275        // indexed loop must not when the seek already did. A captured
7276        // flag would have to be right for both.
7277        let mut process_row = |row: &Row<'static>,
7278                               loop_idx: usize,
7279                               check_where: bool|
7280         -> Result<(), EngineError> {
7281            if loop_idx.is_multiple_of(256) {
7282                cancel.check()?;
7283            }
7284            if !check_where {
7285                // The seek answered the whole predicate. See
7286                // `index_access::Seeked`.
7287            } else if let Some(cw) = &compiled_where {
7288                let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7289                    .map_err(EngineError::Eval)?;
7290                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7291                    return Ok(());
7292                }
7293            } else if let Some(where_expr) = &stmt.where_ {
7294                let cond =
7295                    self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7296                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7297                    return Ok(());
7298                }
7299            }
7300            // Under DISTINCT the keys are built AFTER the dup probe
7301            // (survivors only); the non-distinct order is unchanged.
7302            // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7303            // row further down, and building them here would evaluate the
7304            // ORDER BY against the INPUT row: a key naming the SRF's own
7305            // output became a scalar call to it, which is where
7306            // "function unnest(integer[]) does not exist" came from.
7307            let order_keys = if order_by.is_empty()
7308                || stmt.distinct
7309                || srf_position.is_some()
7310                // v7.38.19 — the branch below builds whatever key it
7311                // needs from the projected values, collation included,
7312                // so nothing has to be built here for it.
7313                //
7314                // A draft that skipped them here but still let the
7315                // COLLATED case fall through to the key-based sort put a
7316                // mixed column back in INSERT order: every key empty,
7317                // every row equal, a stable sort faithfully preserving
7318                // nothing. The rule is one decision, not two.
7319                || sort_by_output.is_some()
7320            {
7321                Vec::new()
7322            } else {
7323                // v7.38.20 — turn a decisively losing row away before
7324                // its key is built. Only the FIRST key is read, and only
7325                // its leading eight bytes; a tie there decides nothing
7326                // and falls through to the full path below.
7327                //
7328                // ASC only: under DESC the boundary is the largest kept
7329                // key and the comparison flips, which this deliberately
7330                // does not try to express — a second direction in a
7331                // fast-path predicate is how one of them ends up wrong.
7332                if boundary_check_on
7333                    && let Some((_, descs)) = &topk_stream
7334                    && !descs.first().copied().unwrap_or(false)
7335                    && order_by.len() == 1
7336                    && boundary_collations_permit
7337                    && let Some((bkind, bp, boundary_is_ascii)) = topk_boundary_prefix
7338                    && let Some((rkind, rp, row_is_ascii)) =
7339                        crate::orderby::first_key_prefix(&order_bound, row)
7340                    && bkind == rkind
7341                    && (boundary_no_collation || (boundary_is_ascii && row_is_ascii))
7342                    && rp > bp
7343                {
7344                    boundary_checks += 1;
7345                    boundary_rejects += 1;
7346                    if boundary_checks == BOUNDARY_WINDOW {
7347                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7348                    }
7349                    return Ok(());
7350                }
7351                let mut buf = key_pool.pop().unwrap_or_default();
7352                if order_has_subquery {
7353                    // A substituted literal is no longer a bound column.
7354                    let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7355                    crate::orderby::build_order_keys_bound(
7356                        per_row.as_deref().unwrap_or(&order_by),
7357                        &unbound,
7358                        &order_colls,
7359                        row,
7360                        &ctx,
7361                        &mut buf,
7362                    )?;
7363                } else {
7364                    crate::orderby::build_order_keys_bound(
7365                        &order_by,
7366                        &order_bound,
7367                        &order_colls,
7368                        row,
7369                        &ctx,
7370                        &mut buf,
7371                    )?;
7372                }
7373                // v7.39 (round 581) — reject before projecting.
7374                //
7375                // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7376                // 50 distinct `g` decides nearly every row on the FIRST
7377                // key, and PG answers it FASTER than the single-key form
7378                // (7.4 ms against 10.4) because a rejected row costs it
7379                // one comparison. SPG built both keys AND the projected
7380                // row for all 500k before throwing them away. The keys
7381                // are needed to compare; the projection is not.
7382                if boundary_check_on
7383                    && let Some((_, descs)) = &topk_stream
7384                    && let Some(b) = &topk_boundary
7385                {
7386                    boundary_checks += 1;
7387                    let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7388                        == core::cmp::Ordering::Greater;
7389                    if loses {
7390                        boundary_rejects += 1;
7391                    }
7392                    if boundary_checks == BOUNDARY_WINDOW {
7393                        // Keep asking only if it has been rejecting at
7394                        // least a quarter of what it saw.
7395                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7396                    }
7397                    if loses {
7398                        buf.clear();
7399                        key_pool.push(buf);
7400                        return Ok(());
7401                    }
7402                }
7403                buf
7404            };
7405            if srf_position.is_some() {
7406                let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7407                for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7408                    if stmt.distinct {
7409                        let bucket = seen_distinct
7410                            .entry(norm_hash_row(
7411                                &out,
7412                                &distinct_hb,
7413                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7414                            ))
7415                            .or_default();
7416                        if bucket.iter().any(|i| {
7417                            row_eq_norm(
7418                                &tagged[i].1,
7419                                &out,
7420                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7421                            )
7422                        }) {
7423                            continue;
7424                        }
7425                        bucket.push(tagged.len());
7426                    }
7427                    budget.charge(approx_row_bytes(&out))?;
7428                    // The keys come from THIS expanded row: a key naming a
7429                    // select-list item reads its value, anything else is
7430                    // still evaluated against the input row.
7431                    let keys = if order_by.is_empty() {
7432                        Vec::new()
7433                    } else {
7434                        let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7435                        for (k, ob) in order_by.iter().enumerate() {
7436                            kv.push(match srf_order_cols.get(k).copied().flatten() {
7437                                Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7438                                None => eval::eval_expr(&ob.expr, row, &ctx)
7439                                    .map_err(EngineError::Eval)?,
7440                            });
7441                        }
7442                        // Packed by the same code every other ORDER BY uses,
7443                        // so DESC / NULLS FIRST / the MySQL rule are not
7444                        // restated here.
7445                        let key_row = Row::new(kv);
7446                        let mut buf = Vec::new();
7447                        crate::orderby::build_order_keys_bound(
7448                            &order_by,
7449                            &srf_key_bound,
7450                            &order_colls,
7451                            &key_row,
7452                            &ctx,
7453                            &mut buf,
7454                        )?;
7455                        buf
7456                    };
7457                    tagged.push((keys, out));
7458                }
7459            } else {
7460                let values = &mut proj_buf;
7461                values.clear();
7462                values.reserve(projection.len());
7463                for (i, p) in projection.iter().enumerate() {
7464                    // v7.37.x (docker-fair SCALARSQ attack) — pre-
7465                    // analysed PK-probe fast path. The per-row work is
7466                    // a read of outer.col from the row plus an index
7467                    // probe — no Expr clone, no walker, no
7468                    // `eval_expr_with_correlated` framework.
7469                    if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7470                        values.push(self.probe_with_pk_fast_path(fp, row));
7471                        continue;
7472                    }
7473                    // v7.39 (round 605) — the same value every row.
7474                    if any_proj_const && let Some(v) = &proj_const[i] {
7475                        values.push(v.clone());
7476                        continue;
7477                    }
7478                    // v7.39 (round 487) — bound column: read the cell.
7479                    // This is `rehydrate_cell`'s body for a non-composite
7480                    // column, which is what the whole chain below reduces
7481                    // to once the name has been resolved.
7482                    if any_proj_direct && let Some(pos) = proj_direct[i] {
7483                        crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7484                        values.push(row.values[pos].clone().into_owned());
7485                        continue;
7486                    }
7487                    // v7.24 (round-16 B) — correlated-aware.
7488                    // v7.37.x (docker-fair SCALARSQ attack) — share the
7489                    // per-row memo with projection. Required for the
7490                    // batch-evaluated correlated-scalar path to fire on
7491                    // SELECT-item scalar subqueries; otherwise each row
7492                    // re-executes the inner.
7493                    //
7494                    // Skip the memo when the outer row count is small
7495                    // (early-limited): the batch path scans the FULL
7496                    // inner table to build a GroupMap (~5 ms for a
7497                    // 12.5 k-row inner), while per-row execution with a
7498                    // PK index seek is ~5 µs per call — much cheaper for
7499                    // N ≤ ~1000 outer rows.
7500                    let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7501                    let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7502                    values.push(
7503                        self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7504                    );
7505                }
7506                crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7507                if stmt.distinct {
7508                    let bucket = seen_distinct
7509                        .entry(norm_hash_values(
7510                            &proj_buf,
7511                            &distinct_hb,
7512                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7513                        ))
7514                        .or_default();
7515                    if bucket.iter().any(|i| {
7516                        values_eq_norm(
7517                            &tagged[i].1.values,
7518                            &proj_buf,
7519                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7520                        )
7521                    }) {
7522                        crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7523                        return Ok(());
7524                    }
7525                    bucket.push(tagged.len());
7526                }
7527                let out = Row::new(core::mem::replace(
7528                    &mut proj_buf,
7529                    proj_pool.pop().unwrap_or_default(),
7530                ));
7531                let order_keys = if stmt.distinct && !order_by.is_empty() {
7532                    // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7533                    // the bound-cell path precisely so an ORDER BY key that
7534                    // names a column is READ instead of evaluated, and the
7535                    // non-DISTINCT branch above has passed it ever since;
7536                    // this branch never did, so `SELECT DISTINCT k .. ORDER
7537                    // BY k` resolved "k" by string for every surviving row.
7538                    let mut buf = key_pool.pop().unwrap_or_default();
7539                    if order_has_subquery {
7540                        // A substituted literal is no longer a bound column.
7541                        let per_row =
7542                            self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7543                        crate::orderby::build_order_keys_bound(
7544                            per_row.as_deref().unwrap_or(&order_by),
7545                            &unbound,
7546                            &order_colls,
7547                            row,
7548                            &ctx,
7549                            &mut buf,
7550                        )?;
7551                    } else {
7552                        crate::orderby::build_order_keys_bound(
7553                            &order_by,
7554                            &order_bound,
7555                            &order_colls,
7556                            row,
7557                            &ctx,
7558                            &mut buf,
7559                        )?;
7560                    }
7561                    buf
7562                } else {
7563                    order_keys
7564                };
7565                budget.charge(approx_row_bytes(&out))?;
7566                tagged.push((order_keys, out));
7567            }
7568            // Streaming top-N: bound the accumulator to O(keep) rows.
7569            if let Some((k, descs)) = &topk_stream {
7570                crate::orderby::topk_trim_recycling(
7571                    &mut tagged,
7572                    *k,
7573                    descs,
7574                    &mut proj_pool,
7575                    &mut key_pool,
7576                    &mut topk_boundary,
7577                );
7578                // The prefix follows the boundary it summarises.
7579                topk_boundary_prefix = topk_boundary
7580                    .as_ref()
7581                    .and_then(|b| b.first())
7582                    .and_then(crate::orderby::order_key_prefix);
7583            }
7584            Ok(())
7585        };
7586        // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7587        // load-bearing full-scan path. This is the primary single-table
7588        // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7589        // in-place writers retain dead/old versions, an ungated scan
7590        // here would return them, so the gate must land BEFORE the
7591        // writers flip (see the plan's activation-order rule). A no-op
7592        // today: every hot row is frozen or committed-and-alive under
7593        // the reader's snapshot, so `is_row_visible` returns true for
7594        // all of them (verified by the full e2e suite staying green).
7595        let scan_snapshot = self.current_snapshot();
7596        let mut emitted: usize = 0;
7597        if let Some(seeked) = &indexed_rows {
7598            let recheck = !seeked.exact;
7599            for (loop_idx, cow) in seeked.rows.iter().enumerate() {
7600                if let Some(cap) = early_cap
7601                    && emitted >= cap
7602                {
7603                    break;
7604                }
7605                process_row(cow.as_ref(), loop_idx, recheck)?;
7606                emitted = emitted.saturating_add(1);
7607            }
7608        } else {
7609            // v7.39 (round 570) — the row store is a 32-way trie, so
7610            // indexing it is four dependent loads. Round 567 measured
7611            // -18% on the aggregate scan from holding the leaf between
7612            // rows; this is the same loop for the projecting scan.
7613            let mut rows_cur = table.rows().run_cursor();
7614            // v7.38.11 — see the aggregate scan above: a BRIN index on a
7615            // column this WHERE bounds says which slots cannot match.
7616            let brin_slots = stmt
7617                .where_
7618                .as_ref()
7619                .and_then(|w| crate::brin::candidate_slots(w, table))
7620                .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7621            for i in brin_slots.into_iter().flatten() {
7622                if let Some(cap) = early_cap
7623                    && emitted >= cap
7624                {
7625                    break;
7626                }
7627                // Skip rows this snapshot cannot see (invisible rows do
7628                // not count toward the LIMIT).
7629                if !table.is_row_visible(i, &scan_snapshot) {
7630                    continue;
7631                }
7632                let Some(row) = rows_cur.get(i) else { continue };
7633                process_row(row, i, true)?;
7634                emitted = emitted.saturating_add(1);
7635            }
7636            // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7637            // rows into the same loop. The full-scan path here is the
7638            // load-bearing single-table SELECT executor, and pre-
7639            // 7.35.1 it only walked `table.rows()` (hot), so any
7640            // `SELECT … FROM t` against a table with cold segments
7641            // silently returned a subset.
7642            let cold_rows = self.iter_cold_rows_of_table(table);
7643            for (offset, row) in cold_rows.iter().enumerate() {
7644                if let Some(cap) = early_cap
7645                    && emitted >= cap
7646                {
7647                    break;
7648                }
7649                process_row(row, table.row_count() + offset, true)?;
7650                emitted = emitted.saturating_add(1);
7651            }
7652        }
7653
7654        // (DISTINCT already de-duped STREAMING inside process_row, so the
7655        // sort below only sees the u survivors and the partial-sort
7656        // budget applies to DISTINCT too.)
7657        if !order_by.is_empty() {
7658            // Partial-sort fast path: when LIMIT is small relative to
7659            // the row count, select_nth_unstable + sort just the
7660            // prefix is O(n + k log k) instead of O(n log n).
7661            // WITH TIES needs the full sort so the tie extension can
7662            // scan past `limit` to find rows that share the last-kept
7663            // row's key.
7664            let keep = if stmt.limit_with_ties
7665                // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7666                // forces the full-sort fallback by suppressing the
7667                // partial-sort `keep` budget. See
7668                // `xtests/sigil/test-mode-gucs.md`.
7669                || self.env_cfg().disable_topk
7670            {
7671                None
7672            } else {
7673                stmt.limit_literal()
7674                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7675            };
7676            let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7677            if let Some(cols) = &sort_by_output {
7678                // No keys were built; the sort reads the projected row.
7679                // The comparator is the value-level one the window
7680                // functions and the key path both defer to, so DESC,
7681                // NULLS placement, the MySQL fold and the collation are
7682                // not restated here.
7683                let terms: Vec<(usize, bool, Option<bool>)> = cols
7684                    .iter()
7685                    .zip(order_by.iter())
7686                    .map(|(c, o)| (*c, o.desc, o.nulls_first))
7687                    .collect();
7688                let mysql = ctx.mysql_dialect;
7689                // v7.38.19 — sort a PERMUTATION carrying the first eight
7690                // bytes, not the rows.
7691                //
7692                // The elements above are `(Vec<OrderKey>, Row)`, 48 bytes,
7693                // and driftsort moves them ~n log n times: 7.4 M moves at
7694                // 400,000 rows. Worse, every comparison chases three
7695                // dependent loads PER SIDE to reach the byte it wants --
7696                // the row's `Vec`, the `Value`, then the string's own
7697                // buffer -- and a profile of this sort put 35% of its
7698                // working samples in the sort machinery around that.
7699                //
7700                // A `(u64, u32)` is 16 bytes and the comparison reads it
7701                // straight out of the array. The u64 is the first eight
7702                // bytes big-endian, zero-padded, which ORDERS THE SAME as
7703                // the string: if two differ inside those bytes they differ
7704                // at the same index either way, and a string shorter than
7705                // eight pads with zeros exactly where `[u8]`'s own
7706                // comparison runs out. Equal prefixes fall through to the
7707                // full comparator, so nothing rests on the padding being
7708                // clever.
7709                //
7710                // The tail-break on the index is what keeps the sort
7711                // STABLE, which `sort_by` was giving for free and an
7712                // unstable sort over a permutation would not.
7713                // v7.38.19 — three ways to sort these rows, and which
7714                // one is right turns on the values, which is why it is
7715                // decided here rather than at plan time.
7716                //
7717                //   * the collation orders these values the way bytes do
7718                //     -- take the eight-byte key below
7719                //   * it does not, but there IS a collation -- build its
7720                //     sort key once per row and order the permutation on
7721                //     those, which is what the key path did, done from
7722                //     the projected value instead of during the scan
7723                //   * no collation at all -- the eight-byte key again
7724                //
7725                // The middle case is the one a draft got wrong by
7726                // leaving the rows to a key path whose keys it had just
7727                // skipped building.
7728                let mut keep_sorted = false;
7729                let bytes_answer = byte_order_answers_the_collation(&tagged, &terms, &order_colls);
7730                if !bytes_answer && let Some(coll) = order_colls.first().and_then(Option::as_ref) {
7731                    let (first_col, first_desc, _) = terms[0];
7732                    let mut order: Vec<(Vec<u8>, u32)> = Vec::with_capacity(tagged.len());
7733                    for (i, row) in tagged.iter().enumerate() {
7734                        let k = match row.1.values.get(first_col) {
7735                            Some(Value::Text(t)) => coll.sort_key_of(t).unwrap_or_else(|| {
7736                                let mut v = Vec::with_capacity(t.len() + 1);
7737                                v.push(0);
7738                                v.extend_from_slice(t.as_bytes());
7739                                v
7740                            }),
7741                            _ => Vec::new(),
7742                        };
7743                        order.push((k, u32::try_from(i).unwrap_or(u32::MAX)));
7744                    }
7745                    order.sort_by(|(ka, ia), (kb, ib)| {
7746                        let c = ka.cmp(kb);
7747                        let c = if first_desc { c.reverse() } else { c };
7748                        if c != core::cmp::Ordering::Equal {
7749                            return c;
7750                        }
7751                        row_cmp_by_index(&tagged, &terms, &order_colls, mysql, *ia, *ib)
7752                            .then_with(|| ia.cmp(ib))
7753                    });
7754                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7755                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7756                    tagged = order
7757                        .iter()
7758                        .map(|&(_, i)| {
7759                            slots[i as usize]
7760                                .take()
7761                                .expect("the permutation names each row once")
7762                        })
7763                        .collect();
7764                    keep_sorted = true;
7765                }
7766                // v7.38.20 — a key that does NOT discriminate is still
7767                // worth sorting on, as long as the runs it leaves are
7768                // handled once instead of n log n times.
7769                //
7770                // `text (26 values)` is two hundred identical characters
7771                // drawn from twenty-six letters, so every eight-byte
7772                // prefix inside a letter is the same and 15,384 rows tie
7773                // on it. A comparison sort then asks ~7.4 M questions of
7774                // which nearly all are a two-hundred-byte `memcmp`
7775                // answering EQUAL: profiled, 30% of the working samples
7776                // sat in `memcmp` and 37% in the sort machinery.
7777                //
7778                // Sorting the integer keys is cheap. What each run needs
7779                // afterwards is ONE pass: if every value in it is equal,
7780                // input order already IS the stable answer, and proving
7781                // that costs n-1 comparisons rather than n log n. Only a
7782                // run that is not all-equal gets sorted.
7783                //
7784                // Single-term only. With a second ORDER BY column an
7785                // all-equal first term does not settle the row order --
7786                // the later terms still speak -- and the shortcut would
7787                // drop them.
7788                let all_keys = if keep_sorted {
7789                    None
7790                } else {
7791                    sort_keys_of(&tagged, terms[0].0)
7792                };
7793                let (worth_it, key_exact) = match all_keys.as_ref() {
7794                    Some(PrefixKeys::Narrow(k, e)) => (*e || key_discriminates(k), *e),
7795                    Some(PrefixKeys::Wide(k, e)) => (*e || key_discriminates(k), *e),
7796                    None => (false, false),
7797                };
7798                let low_card = !keep_sorted && terms.len() == 1 && !key_exact && !worth_it;
7799                let keyed = all_keys.filter(|_| worth_it || low_card);
7800                if keep_sorted {
7801                    // The collated permutation above already placed every
7802                    // row. A draft let the byte-order fallback run after
7803                    // it and undo the whole thing.
7804                } else if let Some(keys) = keyed {
7805                    let exact = key_exact;
7806                    let (first_col, first_desc, _) = terms[0];
7807                    let row_cmp = |ia: u32, ib: u32| -> core::cmp::Ordering {
7808                        let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
7809                        for (col, desc, nf) in &terms {
7810                            let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col))
7811                            else {
7812                                continue;
7813                            };
7814                            let ord = match (va, vb) {
7815                                (Value::Text(x), Value::Text(y)) if !mysql => {
7816                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7817                                    if *desc { c.reverse() } else { c }
7818                                }
7819                                _ => {
7820                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7821                                }
7822                            };
7823                            if ord != core::cmp::Ordering::Equal {
7824                                return ord;
7825                            }
7826                        }
7827                        core::cmp::Ordering::Equal
7828                    };
7829                    // v7.40.2 — the uniform check reads a COMPACT column,
7830                    // not the rows.
7831                    //
7832                    // `low_card` sorts the prefix keys and then proves,
7833                    // once per run, whether every value in it is equal.
7834                    // That proof is n-1 comparisons, and each one used to
7835                    // walk `order[i] -> tagged[i] -> values -> Value ->
7836                    // &str -> bytes`: four dependent reads, the first of
7837                    // them scattered across a 400,000-element array of fat
7838                    // rows. This file's own note at `key_discriminates`
7839                    // warned about that random read; here is its price.
7840                    //
7841                    // Measured, 400,000 rows, twenty-six distinct values,
7842                    // server-reported, against the same binary with
7843                    // `low_card` forced off (md5-witnessed, order digests
7844                    // identical):
7845                    //
7846                    //   8-byte values     54.98 ms on   54.91 ms off
7847                    //   200-byte values   89.64 ms on  163.18 ms off
7848                    //
7849                    // So the path earns 1.82x and is not in question. What
7850                    // the 8-byte and 200-byte cells say together is where
7851                    // the rest goes: the extra 192 bytes a row cost
7852                    // 34.6 ms, which is 80 MB compared at 2.3 GB/s — an
7853                    // order of magnitude under this machine's memory
7854                    // bandwidth, because the cost is the misses and not
7855                    // the compare.
7856                    //
7857                    // Collecting the column first is one sequential pass
7858                    // over `tagged` and leaves the comparison two reads:
7859                    // a 16-byte slice header, then its bytes.
7860                    // Built ONLY for the branch that uses it. `same_value`
7861                    // is called from the `low_card` run walk and nowhere
7862                    // else, so an exact key -- every value inside sixteen
7863                    // bytes -- never asks the question. The first version
7864                    // built the column unconditionally and charged 2.1 ms
7865                    // to a shape that never reads it:
7866                    //
7867                    //   8-byte values     56.45 -> 58.55 ms   (a tax)
7868                    //   200-byte values   97.18 -> 84.70 ms   (the point)
7869                    let col_strs: Option<Vec<&str>> = if low_card {
7870                        tagged
7871                            .iter()
7872                            .map(|t| match t.1.values.get(first_col) {
7873                                Some(Value::Text(x)) => Some(x.as_ref()),
7874                                _ => None,
7875                            })
7876                            .collect()
7877                    } else {
7878                        None
7879                    };
7880                    let same_value = |ia: u32, ib: u32| -> bool {
7881                        col_strs.as_ref().map_or_else(
7882                            || {
7883                                tagged[ia as usize].1.values.get(first_col)
7884                                    == tagged[ib as usize].1.values.get(first_col)
7885                            },
7886                            |c| c[ia as usize] == c[ib as usize],
7887                        )
7888                    };
7889                    let how = PrefixSort {
7890                        first_desc,
7891                        low_card,
7892                        exact,
7893                        single_term: terms.len() == 1,
7894                    };
7895                    let order: Vec<u32> = match keys {
7896                        PrefixKeys::Narrow(v, _) => {
7897                            sort_prefix_permutation(v, &how, &row_cmp, &same_value)
7898                        }
7899                        PrefixKeys::Wide(v, _) => {
7900                            sort_prefix_permutation(v, &how, &row_cmp, &same_value)
7901                        }
7902                    };
7903                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7904                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7905                    tagged = order
7906                        .iter()
7907                        .map(|&i| {
7908                            slots[i as usize]
7909                                .take()
7910                                .expect("the permutation names each row once")
7911                        })
7912                        .collect();
7913                } else {
7914                    tagged.sort_by(|a, b| {
7915                        for (i, (col, desc, nf)) in terms.iter().enumerate() {
7916                            let va = a.1.values.get(*col);
7917                            let vb = b.1.values.get(*col);
7918                            let (Some(va), Some(vb)) = (va, vb) else {
7919                                continue;
7920                            };
7921                            let _ = i;
7922                            // v7.38.19 — two non-NULL strings, no MySQL fold, is
7923                            // where a text sort spends every one of its ~7 M
7924                            // comparisons, and the shared comparator cannot be
7925                            // inlined into this loop: it carries NULL placement,
7926                            // the fold, the NUMERIC bignum gate and the float
7927                            // total order. Answering that one pair here is the
7928                            // same answer by the same route — `value_cmp`'s
7929                            // leading same-variant arm is `x.cmp(y)`, and the
7930                            // raw comparator's last act is this reverse.
7931                            let ord = match (va, vb) {
7932                                (Value::Text(x), Value::Text(y)) if !mysql => {
7933                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7934                                    if *desc { c.reverse() } else { c }
7935                                }
7936                                _ => {
7937                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7938                                }
7939                            };
7940                            if ord != core::cmp::Ordering::Equal {
7941                                return ord;
7942                            }
7943                        }
7944                        core::cmp::Ordering::Equal
7945                    });
7946                }
7947            } else {
7948                crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7949            }
7950        }
7951
7952        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7953        // past the truncated tail through every row that shares the
7954        // last-kept row's ORDER BY key. The tie check uses the
7955        // already-computed `(order_keys, row)` pairs so it matches
7956        // the sort comparator exactly. DISTINCT + WITH TIES falls
7957        // through to the no-ties path (PG also disallows their
7958        // combination; SPG silently drops the tie extension here so
7959        // the customer doesn't see a hard error mid-query — the
7960        // user-visible result is still correct, just narrower).
7961        let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7962            apply_offset_and_limit_tagged(
7963                &mut tagged,
7964                stmt.offset_literal(),
7965                stmt.limit_literal(),
7966                true,
7967            );
7968            tagged.into_iter().map(|(_, r)| r).collect()
7969        } else {
7970            // DISTINCT already de-duped pre-sort above.
7971            let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7972            apply_offset_and_limit(
7973                &mut output_rows,
7974                stmt.offset_literal(),
7975                stmt.limit_literal(),
7976            );
7977            output_rows
7978        };
7979
7980        let columns: Vec<ColumnSchema> = projection
7981            .into_iter()
7982            .map(|p| p.to_column_schema())
7983            .collect();
7984
7985        Ok(QueryResult::Rows {
7986            columns,
7987            rows: output_rows,
7988        })
7989    }
7990
7991    /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7992    /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7993    /// select items for the surviving rows only — PG's Result-above-
7994    /// Limit shape, where SubPlan loops equal the OUTPUT row count
7995    /// (50) instead of the group count (24k).
7996    fn finish_agg_result(
7997        &self,
7998        mut agg: aggregate::AggResult,
7999        stmt: &SelectStatement,
8000        cancel: CancelToken<'_>,
8001    ) -> Result<QueryResult, EngineError> {
8002        apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
8003        if !agg.deferred.is_empty() {
8004            apply_offset_and_limit(
8005                &mut agg.synth_rows,
8006                stmt.offset_literal(),
8007                stmt.limit_literal(),
8008            );
8009            let ctx = EvalContext::new(&agg.synth_schema, None);
8010            let mut memo = memoize::MemoizeCache::default();
8011            // v7.32 (architecture v2 P3) — keyed index-probe seeding.
8012            // Deferred subqueries are referenced only by surviving
8013            // select-list rows (≤ LIMIT), so their correlation keys are
8014            // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
8015            // each batchable subquery's group map over just those keys
8016            // via per-key index seek; the per-row splice loop below then
8017            // reuses the seeded map. A join-shaped or un-indexed inner
8018            // falls through to the all-keys batch inside the call (built
8019            // eagerly here instead of lazily on row 0 — same cost), so
8020            // it still pays the full scan, never the 715 ms per-row
8021            // direct eval; its index-nested-loop probe is the next
8022            // knife. Genuinely non-batchable shapes return None and are
8023            // left unseeded for the loop's per-row resolver, as before.
8024            for (_, expr) in &agg.deferred {
8025                let mut subs: Vec<&SelectStatement> = Vec::new();
8026                collect_scalar_subqueries(expr, &mut subs);
8027                for sub in subs {
8028                    let repr = alloc::format!("{sub}");
8029                    if memo.group_maps.contains_key(&repr) {
8030                        continue;
8031                    }
8032                    if let Some(gm) = self.try_batch_correlated_scalar(
8033                        sub,
8034                        Some((&agg.synth_rows, &ctx)),
8035                        cancel,
8036                    )? {
8037                        memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
8038                    }
8039                }
8040            }
8041            for (ri, srow) in agg.synth_rows.iter().enumerate() {
8042                cancel.check()?;
8043                for (col, expr) in &agg.deferred {
8044                    let v =
8045                        self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
8046                    if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
8047                        *cell = v;
8048                    }
8049                }
8050            }
8051        }
8052        Ok(QueryResult::Rows {
8053            columns: agg.columns,
8054            rows: agg.rows,
8055        })
8056    }
8057
8058    /// v7.37 — streaming projection for the joined-non-aggregate
8059    /// shape (multi-table FROM, all projection items bound, no
8060    /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
8061    /// UNION). Walks the deferred join survivors and emits
8062    /// `&[&Value]` borrowed straight out of the source tables — no
8063    /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
8064    /// on the mailrs `PROJ` shape (about 4 ms saved).
8065    ///
8066    /// Returns `Ok(None)` when the shape doesn't qualify; the caller
8067    /// then falls back to the materialising path.
8068    /// v7.37 (round 831) — stream a joinless SELECT straight off the
8069    /// stored table, one row at a time, without ever building a row set.
8070    ///
8071    /// Returns `Ok(None)` for anything this cannot serve, and the caller
8072    /// falls through to the deferred-join path exactly as before: a
8073    /// missing table, or a cold tier whose hydration the fallback handles.
8074    /// Sort a single-table scan through the external sorter, so the
8075    /// answer's size is bounded by `work_mem` and not by the input.
8076    ///
8077    /// Sorting held every row twice — the scan's `Vec<Row>` and the
8078    /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
8079    /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
8080    /// enough ORDER BY took the server down, which is a liveness
8081    /// problem before it is a performance one.
8082    ///
8083    /// A SEPARATE walk rather than a change to `run_single_table_scan`,
8084    /// following what round 831 did for the joinless shape. That
8085    /// function is 552 lines whose projection loop is entangled with
8086    /// DISTINCT (which indexes back into the tagged vector) and with
8087    /// streaming top-N (whose boundary moves as the scan runs); both
8088    /// assume the projection has already happened when a row is
8089    /// pushed, which is exactly what spilling has to defer. Two earlier
8090    /// attempts tried to rework that loop and were reverted. Here the
8091    /// existing path is untouched and this one only claims shapes it
8092    /// can serve, so a decline costs nothing.
8093    ///
8094    /// Records are SOURCE rows, not projected ones: `finish` re-derives
8095    /// keys from what it decodes, and an ORDER BY key need not be in
8096    /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
8097    fn try_spill_sorted_scan(
8098        &self,
8099        stmt: &SelectStatement,
8100        from: &FromClause,
8101        cancel: CancelToken<'_>,
8102    ) -> Result<Option<QueryResult>, EngineError> {
8103        // Shapes this walk does not serve. Each one either needs the
8104        // whole tagged vector addressable (DISTINCT probes back into
8105        // it, WITH TIES re-reads its tail) or is already bounded
8106        // without spilling (a LIMIT makes the partial sort O(keep)).
8107        if !self.can_spill()
8108            || stmt.order_by.is_empty()
8109            || stmt.distinct
8110            || stmt.limit_with_ties
8111            || stmt.limit_literal().is_some()
8112            || !from.joins.is_empty()
8113            || from.primary.lateral_subquery.is_some()
8114            || from.primary.unnest_expr.is_some()
8115            || from.primary.generate_series_args.is_some()
8116            || select_has_window(stmt)
8117        {
8118            return Ok(None);
8119        }
8120        // A parent's rows are its children's. These walks scan the named
8121        // relation alone, so a partitioned or inherited parent comes back
8122        // short — and silently: the corpus caught `SELECT id FROM pr
8123        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8124        // parent's own rows instead of the partitions'. `ONLY` is exactly
8125        // the case that does not fan out, so it stays, which is the test
8126        // the FROM-clause fan-out itself makes.
8127        if !from.primary.only
8128            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8129        {
8130            return Ok(None);
8131        }
8132        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8133            return Ok(None);
8134        };
8135        // Cold-tier rows live outside `rows()`; this walk would drop
8136        // them silently, the same reason round 831's walk declines.
8137        if table.has_cold_rows_fast() {
8138            return Ok(None);
8139        }
8140
8141        let alias = from
8142            .primary
8143            .alias
8144            .as_deref()
8145            .unwrap_or(from.primary.name.as_str());
8146        let cols = table.schema().columns.clone();
8147        let sess = self.dml_session();
8148        let ctx = EvalContext::new(&cols, Some(alias))
8149            .with_catalog(self.active_catalog())
8150            .with_session(&sess);
8151        let projection = build_projection(
8152            &stmt.items,
8153            &cols,
8154            alias,
8155            self.speaks_mysql,
8156            Some(self.active_catalog()),
8157        )?;
8158        let order_by = stmt.order_by.clone();
8159        // The same one-shot resolution the general path does (round
8160        // 582): each ORDER BY column is bound once, not once per row.
8161        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8162        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8163        // Resolved BEFORE the scan, because it now decides what the sort
8164        // STORES and not just what it decodes (round 995).
8165        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8166
8167        // v7.38.22 — resolved HERE, because this path did not resolve
8168        // them at all.
8169        //
8170        // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8171        // "en_US.utf8"` in BYTE order on this path — and swallowed an
8172        // unknown collation name rather than raising — because the sorter
8173        // below compared with an empty collation slice. The materialising
8174        // path honoured both. Which answer a query got depended on which
8175        // path the planner took, and this is the path a plain single-table
8176        // SELECT takes.
8177        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
8178        // v7.39.12 — a correlated scalar subquery in ORDER BY is
8179        // resolved for the row before its key is built.
8180        //
8181        // Uncorrelated subqueries are replaced by a literal before
8182        // execution; a correlated one cannot be, so it reached the
8183        // per-row evaluator — the one place that cannot run a subquery
8184        // — and the statement raised "subquery reached row eval".
8185        // Reported by sentori against 7.39.11; see
8186        // `Engine::order_by_resolved_for_row`.
8187        //
8188        // The `any` runs once, here, so an ordinary ORDER BY pays one
8189        // bool per row and nothing else.
8190        let order_has_subquery = order_by
8191            .iter()
8192            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
8193        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
8194        let mut sorter = crate::extsort::ExternalSorter::new(
8195            self.temp_run_factory,
8196            self.session_work_mem_bytes(),
8197            cols.clone(),
8198            &descs,
8199            &order_colls,
8200        )
8201        .with_stats(&self.spill_stats)
8202        .with_pruned(&needed);
8203        let snapshot = self.current_snapshot();
8204        // One key buffer for the whole scan: `push` drains it and leaves
8205        // the capacity behind.
8206        let mut keys: Vec<OrderKey> = Vec::new();
8207        // r1024 — compile the predicate once for the scan.
8208        //
8209        // These two sorted-spill scans are the paths a single-table SELECT
8210        // with an ORDER BY takes, and they were the last row-returning ones
8211        // still walking the expression tree per row. r1023 did the
8212        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8213        // exactly this shape.
8214        //
8215        // Found from the profile's CALL TREE rather than its leaves. The
8216        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8217        // 261, `mod_op` 178 — and two attempts at reasoning out which
8218        // function asked for it were both wrong. The tree names the caller
8219        // chain, and it named this one.
8220        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8221            .where_
8222            .as_ref()
8223            .filter(|w| crate::eval::fully_compilable(w))
8224            .map(|w| crate::eval::compile_expr(w, &ctx));
8225        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8226        for (i, row) in table.scan_visible_from(0, &snapshot) {
8227            if i.is_multiple_of(256) {
8228                cancel.check()?;
8229            }
8230            if let Some(c) = &compiled_where {
8231                if !crate::eval::compiled::eval_compiled_pred(
8232                    c,
8233                    row,
8234                    &ctx,
8235                    &mut eval_stack,
8236                    ctx.mysql_dialect,
8237                )? {
8238                    continue;
8239                }
8240            } else if let Some(w) = &stmt.where_ {
8241                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8242                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8243                    continue;
8244                }
8245            }
8246            keys.clear();
8247            // The same collations the sorter compares with, and the
8248            // re-derivation below is handed the same ones. `finish`'s
8249            // contract is that a key comes back the way it was pushed;
8250            // a collation is part of the way it was pushed.
8251            if order_has_subquery {
8252                // A substituted literal is no longer a bound column.
8253                let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
8254                crate::orderby::build_order_keys_bound(
8255                    per_row.as_deref().unwrap_or(&order_by),
8256                    &unbound,
8257                    &order_colls,
8258                    row,
8259                    &ctx,
8260                    &mut keys,
8261                )?;
8262            } else {
8263                crate::orderby::build_order_keys_bound(
8264                    &order_by,
8265                    &order_bound,
8266                    &order_colls,
8267                    row,
8268                    &ctx,
8269                    &mut keys,
8270                )?;
8271            }
8272            sorter.push(&mut keys, row)?;
8273        }
8274
8275        let key_ctx = &ctx;
8276        let rows = sorter.finish(
8277            |src, buf| {
8278                crate::orderby::build_order_keys_rederived(
8279                    &order_by,
8280                    &order_bound,
8281                    &order_colls,
8282                    src,
8283                    key_ctx,
8284                    buf,
8285                )
8286            },
8287            |src| {
8288                let mut values = Vec::with_capacity(projection.len());
8289                for p in &projection {
8290                    values.push(
8291                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8292                    );
8293                }
8294                Ok(Row::new(values))
8295            },
8296        )?;
8297
8298        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8299        Ok(Some(QueryResult::Rows { columns, rows }))
8300    }
8301
8302    /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
8303    /// handing each row to the consumer instead of collecting the answer.
8304    ///
8305    /// That walk bounds the SORT and then returns `QueryResult::Rows`,
8306    /// which holds every output row. Measured at `work_mem = 4 MB` over
8307    /// 200-byte rows, RSS above the server's own baseline while the
8308    /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
8309    /// at 400k — linear — while the spill underneath worked correctly
8310    /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
8311    /// removes each file, so a count taken afterwards reads 0 whatever
8312    /// happened, and an earlier reading of "no spill at all" was that
8313    /// blind witness). The growth is the collected result, not the sort.
8314    ///
8315    /// Emitting makes peak the budget, one buffer per run and a single
8316    /// row — the state a merge already holds at every step. It also
8317    /// frees each projected row as the next is built rather than
8318    /// accumulating them, which is where the time is: a profile of the
8319    /// collecting walk put the allocator at 586 samples, more than every
8320    /// sort comparison combined (420), against 19 for `push` itself.
8321    /// v7.37 (round 923) — which of a sort record's columns the output half
8322    /// reads. The record is the SOURCE row (round 836), so a narrow projection
8323    /// decoded every column: skipping one 200-byte text halves a decode
8324    /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
8325    ///
8326    /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
8327    /// column reads NULL. Answers only when every projection item is a bare
8328    /// column reference AND every ORDER BY key is a bound column; anything
8329    /// else returns empty, decoding everything as before.
8330    /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
8331    /// drops references from expression kinds it does not enumerate.
8332    ///
8333    /// ORDER BY columns are included — the merge re-derives keys from the
8334    /// decoded row on the spilled path, so pruning one would sort NULLs.
8335    pub(crate) fn sort_record_columns_needed(
8336        items: &[SelectItem],
8337        order_bound: &[Option<usize>],
8338        arity: usize,
8339        ctx: &EvalContext,
8340    ) -> Vec<bool> {
8341        let all_bare = items.iter().all(|i| {
8342            matches!(
8343                i,
8344                SelectItem::Expr {
8345                    expr: Expr::Column(_),
8346                    ..
8347                }
8348            )
8349        });
8350        if !all_bare || order_bound.iter().any(Option::is_none) {
8351            return Vec::new();
8352        }
8353        let mut mask = alloc::vec![false; arity];
8354        for item in items {
8355            if let SelectItem::Expr {
8356                expr: Expr::Column(c),
8357                ..
8358            } = item
8359            {
8360                match crate::eval::find_column_pos(c, ctx) {
8361                    Some(p) if p < arity => mask[p] = true,
8362                    _ => return Vec::new(),
8363                }
8364            }
8365        }
8366        for p in order_bound.iter().flatten() {
8367            if *p < arity {
8368                mask[*p] = true;
8369            } else {
8370                return Vec::new();
8371            }
8372        }
8373        mask
8374    }
8375
8376    /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
8377    /// of sorting.
8378    ///
8379    /// PG serves such an ordering from the index and never sorts. We sorted:
8380    /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
8381    /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
8382    /// the sorter's own round trip — `ExternalSorter::finish_each` →
8383    /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
8384    /// Every row is encoded into the sorter's arena and decoded back out,
8385    /// for an order the index already holds.
8386    ///
8387    /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
8388    /// because it was built for top-N. This is the unbounded sibling.
8389    ///
8390    /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
8391    /// from a btree, so walking one would silently drop those rows. That is
8392    /// exactly the defect r1020 fixed on the top-N path, where it had
8393    /// shipped.
8394    /// r1044 — the index this statement's ORDER BY can be WALKED on,
8395    /// instead of sorted, or `None`.
8396    ///
8397    /// Extracted so `EXPLAIN` can ask the same question the executor
8398    /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
8399    /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
8400    /// while the executor walked the primary key — 34.9 ms against
8401    /// 147.0 for the same query ordered by an unindexed column, so the
8402    /// walk was plainly running. Round 551 fixed a different case of
8403    /// this and wrote the reason down: EXPLAIN is the first thing any
8404    /// performance question opens, and an instrument that misnames the
8405    /// access path is worse than one that says nothing.
8406    ///
8407    /// The gate is here once. Two copies of it is how the plan and the
8408    /// executor come to disagree again.
8409    /// v7.39.13 — the shape refusals both ordered-walk gates make.
8410    ///
8411    /// One list, because two of them would be two answers to "can this
8412    /// statement walk an index", and a walk that runs where EXPLAIN says
8413    /// it does not is the defect r1044 exists to prevent.
8414    /// v7.39.13 — `WHERE lead = <literal> ORDER BY next [DESC] LIMIT n`
8415    /// behind an index on `(lead, next, …)`: one seek to the key prefix,
8416    /// then n steps inside it.
8417    ///
8418    /// Sentori's busiest read, and the one shape they have reported
8419    /// unchanged for three versions: `WHERE project_id = ? ORDER BY
8420    /// received_at DESC LIMIT 20`. PostgreSQL 18 answers it with
8421    /// `Limit -> Index Scan`; SPG planned `Sort -> Seq Scan` and sorted
8422    /// the table to return twenty rows, roughly 250x behind.
8423    ///
8424    /// The ordered walk that existed could only start at an index's
8425    /// LEADING column, so an index on `(project_id, received_at)` could
8426    /// serve `ORDER BY project_id` and nothing else. What was missing is
8427    /// below it: a tree walk bounded by a key prefix, which
8428    /// `Index::iter_prefix_desc` now provides.
8429    ///
8430    /// The equality conjunct only NARROWS the walk — the statement's own
8431    /// `WHERE` still runs per row — so picking the wrong conjunct can
8432    /// cost time and cannot change an answer.
8433    pub(crate) fn index_prefix_walk_target(
8434        &self,
8435        stmt: &SelectStatement,
8436        from: &FromClause,
8437    ) -> Option<(String, usize, alloc::vec::Vec<spg_storage::IndexKey>)> {
8438        if self.walk_shape_refused(stmt, from) {
8439            return None;
8440        }
8441        // One ORDER BY term for now: a second one would have to be the
8442        // next key column again, and the tree walks one direction.
8443        if stmt.order_by.len() != 1 || stmt.distinct {
8444            return None;
8445        }
8446        let table = self.active_catalog().get(&from.primary.name)?;
8447        let alias = from
8448            .primary
8449            .alias
8450            .as_deref()
8451            .unwrap_or(from.primary.name.as_str());
8452        let cols = &table.schema().columns;
8453        let order = &stmt.order_by[0];
8454        let Expr::Column(oc) = &order.expr else {
8455            return None;
8456        };
8457        if let Some(q) = &oc.qualifier
8458            && !q.eq_ignore_ascii_case(alias)
8459        {
8460            return None;
8461        }
8462        let order_pos = cols
8463            .iter()
8464            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8465        // The walk comes out in the tree's order, so it may only take an
8466        // ORDER BY whose order that IS — the same question the leading-
8467        // column gate asks, for the same reason.
8468        let order_col = cols.get(order_pos)?;
8469        if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8470            && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8471        {
8472            return None;
8473        }
8474        // A NULL key is not in the tree, and this walk has no separate
8475        // pass for those rows the way the leading-column one does.
8476        if order_col.nullable {
8477            return None;
8478        }
8479        let where_ = stmt.where_.as_ref()?;
8480        for index in table.indices() {
8481            if !matches!(index.kind, spg_storage::IndexKind::BTreeMulti(_))
8482                || index.expression.is_some()
8483                || index.partial_predicate.is_some()
8484            {
8485                continue;
8486            }
8487            // The ORDER BY column must be the key component that follows
8488            // the equality-bound prefix.
8489            if index.extra_column_positions.first() != Some(&order_pos) {
8490                continue;
8491            }
8492            let lead_pos = index.column_position;
8493            let lead_col = cols.get(lead_pos)?;
8494            // The prefix is compared with the tree's own ordering, so the
8495            // leading column has to be one the tree orders bytewise too.
8496            if crate::index_access::collated_column(lead_col, table.db_collation()).is_none()
8497                && !crate::collate::column_key_is_bytewise(lead_col, self.speaks_mysql)
8498            {
8499                continue;
8500            }
8501            let Some(key) = self.eq_literal_key_for(where_, lead_pos, cols, alias) else {
8502                continue;
8503            };
8504            return Some((index.name.clone(), order_pos, alloc::vec![key]));
8505        }
8506        None
8507    }
8508
8509    /// The index key a top-level `AND` conjunct binds `col_pos` to, when
8510    /// one of them is `col = <literal>` (either way round).
8511    ///
8512    /// Only literals: a column reference or a function would have to be
8513    /// evaluated per row, and this runs once for the whole statement.
8514    fn eq_literal_key_for(
8515        &self,
8516        where_: &Expr,
8517        col_pos: usize,
8518        cols: &[ColumnSchema],
8519        alias: &str,
8520    ) -> Option<spg_storage::IndexKey> {
8521        let col = cols.get(col_pos)?;
8522        let mut found: Option<spg_storage::IndexKey> = None;
8523        let mut stack: alloc::vec::Vec<&Expr> = alloc::vec![where_];
8524        while let Some(e) = stack.pop() {
8525            match e {
8526                Expr::Binary {
8527                    lhs,
8528                    op: spg_sql::ast::BinOp::And,
8529                    rhs,
8530                } => {
8531                    stack.push(lhs);
8532                    stack.push(rhs);
8533                }
8534                Expr::Binary {
8535                    lhs,
8536                    op: spg_sql::ast::BinOp::Eq,
8537                    rhs,
8538                } => {
8539                    let names_col = |x: &Expr| match x {
8540                        Expr::Column(c) => {
8541                            c.name.eq_ignore_ascii_case(&col.name)
8542                                && c.qualifier
8543                                    .as_ref()
8544                                    .is_none_or(|q| q.eq_ignore_ascii_case(alias))
8545                        }
8546                        _ => false,
8547                    };
8548                    let lit = if names_col(lhs) {
8549                        Some(&**rhs)
8550                    } else if names_col(rhs) {
8551                        Some(&**lhs)
8552                    } else {
8553                        None
8554                    };
8555                    // v7.39.13 — a BARE literal means whatever the
8556                    // COLUMN says it means, and
8557                    // `literal_as_column_value` is the one place that
8558                    // decision is made. Asking
8559                    // `literal_expr_to_value` instead made this the
8560                    // fifth copy of it, and it read every string
8561                    // literal as text: `WHERE k = '\x07'` on a `bytea`
8562                    // column built no key at all, so the walk declined
8563                    // and the plan went back to sorting the table —
8564                    // while the EQUALITY seek beside it, which does ask
8565                    // the one funnel, used the very same index.
8566                    //
8567                    // Anything that is not a bare literal — a cast, a
8568                    // negation — already carries its own type, and
8569                    // `from_value_for_column` decides whether that type
8570                    // keys for this column.
8571                    let v = match lit {
8572                        Some(Expr::Literal(l)) => {
8573                            crate::index_access::literal_as_column_value(l, col, col_pos)
8574                        }
8575                        Some(other) => {
8576                            crate::conversions::literal_expr_to_value(other.clone()).ok()
8577                        }
8578                        None => None,
8579                    };
8580                    if let Some(v) = v
8581                        && !v.is_null()
8582                        && let Some(k) = spg_storage::IndexKey::from_value_for_column(&v, col.ty)
8583                    {
8584                        found = Some(k);
8585                    }
8586                }
8587                _ => {}
8588            }
8589        }
8590        found
8591    }
8592
8593    fn walk_shape_refused(&self, stmt: &SelectStatement, from: &FromClause) -> bool {
8594        // A non-literal count is refused: `LIMIT $1` is rewritten to a
8595        // literal by `resolve_limit_exprs` before dispatch, so anything
8596        // still carrying a placeholder here has not been through it.
8597        let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8598            matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8599        };
8600        if stmt.order_by.is_empty()
8601            || !stmt.distinct_on.is_empty()
8602            || stmt.limit_with_ties
8603            || !literal_count(&stmt.limit)
8604            || !literal_count(&stmt.offset)
8605            || stmt.having.is_some()
8606            || stmt.group_by.is_some()
8607            || !stmt.unions.is_empty()
8608            || !from.joins.is_empty()
8609            || from.primary.lateral_subquery.is_some()
8610            || from.primary.unnest_expr.is_some()
8611            || from.primary.as_of_segment.is_some()
8612            || from.primary.generate_series_args.is_some()
8613            || select_has_window(stmt)
8614            || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
8615        {
8616            return true;
8617        }
8618        if stmt
8619            .items
8620            .iter()
8621            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8622        {
8623            return true;
8624        }
8625        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8626            return true;
8627        };
8628        if table.has_cold_rows_fast() {
8629            return true;
8630        }
8631        !from.primary.only
8632            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8633    }
8634
8635    pub(crate) fn index_order_walk_target(
8636        &self,
8637        stmt: &SelectStatement,
8638        from: &FromClause,
8639    ) -> Option<(String, usize)> {
8640        // v7.39.11 — LIMIT / OFFSET join the walk instead of refusing it.
8641        //
8642        // Reported by sentori against 7.39.10 and measured on their own
8643        // busiest read: "the most recent N events for this project",
8644        // backed by an index on exactly that ordering. PostgreSQL 18
8645        // answered it with `Limit -> Index Scan`; SPG with
8646        // `Limit -> Sort -> Seq Scan`, 4.998 ms against 0.021 — the
8647        // whole table sorted to return twenty rows.
8648        //
8649        // The walk was built for this shape — `iter_desc`'s own doc says
8650        // "the ORDER BY <indexed col> DESC + LIMIT N executor path" —
8651        // and then the gate refused every statement that had a LIMIT, so
8652        // the one query it was written for could never reach it. The
8653        // capability was here; the routing was not.
8654        //
8655        // A non-literal count is refused: `LIMIT $1` is rewritten to a
8656        // literal by `resolve_limit_exprs` before dispatch, so anything
8657        // still carrying a placeholder here has not been through it.
8658        let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8659            matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8660        };
8661        if self.walk_shape_refused(stmt, from) {
8662            return None;
8663        }
8664        let table = self.active_catalog().get(&from.primary.name)?;
8665        let alias = from
8666            .primary
8667            .alias
8668            .as_deref()
8669            .unwrap_or(from.primary.name.as_str());
8670        let cols = &table.schema().columns;
8671        let order = &stmt.order_by[0];
8672        let Expr::Column(oc) = &order.expr else {
8673            return None;
8674        };
8675        if let Some(q) = &oc.qualifier
8676            && !q.eq_ignore_ascii_case(alias)
8677        {
8678            return None;
8679        }
8680        let order_pos = cols
8681            .iter()
8682            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8683        // r1047 — DISTINCT joins the walk when the projection IS the
8684        // order column, and only then. The index's keys are canonical
8685        // (r1039: representation equality is value equality — the
8686        // property every seek already depends on), so one key is one
8687        // distinct value and the walk can emit the first passing row of
8688        // each key group instead of hashing every row. On the release
8689        // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
8690        // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
8691        // with an ablation floor of 14.8, because the hash must
8692        // normalize and probe ALL the rows; the walk visits each key
8693        // once. A wider projection makes DISTINCT about the whole tuple,
8694        // not the key, so anything else still declines.
8695        if stmt.distinct {
8696            let only_the_order_column = stmt.items.len() == 1
8697                && match &stmt.items[0] {
8698                    SelectItem::Expr {
8699                        expr: Expr::Column(c),
8700                        ..
8701                    } => {
8702                        c.name.eq_ignore_ascii_case(&oc.name)
8703                            && match &c.qualifier {
8704                                Some(q) => q.eq_ignore_ascii_case(alias),
8705                                None => true,
8706                            }
8707                    }
8708                    _ => false,
8709                };
8710            if !only_the_order_column {
8711                return None;
8712            }
8713        }
8714        // r1046 — a nullable key no longer refuses the walk; it changes
8715        // what the walk has to do. A NULL key is not in the btree, so
8716        // walking alone would silently drop those rows — the r1020
8717        // defect, which shipped once. The walk emits them separately, at
8718        // the end SQL puts them.
8719        //
8720        // Refusing was costing every nullable indexed column a 3.4x:
8721        // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
8722        // 72.0 ms with the column nullable and 20.2 with the same data
8723        // under NOT NULL. `NOT NULL` is not the default, so that was the
8724        // common case paying for the uncommon one.
8725        // v7.39.11 — the walk comes out in the tree's order, so it may
8726        // only take an ORDER BY whose order that IS.
8727        //
8728        // The B-tree walks in BYTE order unless the column's keys are
8729        // ICU sort keys. `try_pk_walk_top_n` has asked this since
8730        // v7.38.18; this gate never did, and the answer changed when an
8731        // index appeared. Measured on `alpha / Beta / GAMMA / delta`
8732        // over a MySQL-dialect session, `SELECT t FROM s ORDER BY t`:
8733        //
8734        //   no index   alpha Beta delta GAMMA   (MySQL's own order)
8735        //   indexed    Beta GAMMA alpha delta   (bytes)
8736        //
8737        // No row is wrong and nothing raises; only the order changes,
8738        // and it changes because an index exists. Ordering is the one
8739        // thing a walk contributes, so when it is the wrong ordering
8740        // there is nothing left to keep.
8741        let order_col = cols.get(order_pos)?;
8742        if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8743            && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8744        {
8745            return None;
8746        }
8747        // v7.39.11 — a composite B-tree LEADING on the ORDER BY column
8748        // walks it too, which is what `try_pk_walk_top_n` has always
8749        // done and what this gate did not know.
8750        //
8751        // Keys sort by the whole tuple, so the leading component comes
8752        // out in order — `Index::iter_asc` says so, and the materialising
8753        // top-N walk has relied on it since v7.38.1. The consequence of
8754        // the two gates disagreeing was the thing r1044 exists to
8755        // prevent: measured on a table indexed `(a, b)`, `SELECT a FROM
8756        // m ORDER BY a LIMIT 2` planned as `Limit -> Sort -> Seq Scan`
8757        // while the executor plainly walked the index — a projection
8758        // that divides by zero on the last row in key order returned two
8759        // rows instead of raising. EXPLAIN is the first thing any
8760        // performance question opens, and an instrument that misnames
8761        // the access path is worse than one that says nothing.
8762        let index = table
8763            .index_on(order_pos)
8764            .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8765            .or_else(|| {
8766                table.indices().iter().find(|i| {
8767                    matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8768                        && i.column_position == order_pos
8769                })
8770            })?;
8771        if index.expression.is_some() || index.partial_predicate.is_some() {
8772            return None;
8773        }
8774        // v7.39.11 — more than one ORDER BY term walks when the index
8775        // holds exactly that ordering.
8776        //
8777        // Keys sort by the whole tuple, so `iter_asc` over a composite
8778        // B-tree IS `ORDER BY a, b` — the walk needs no new machinery,
8779        // only permission. Reported by sentori against 7.39.10:
8780        // `ORDER BY a, b LIMIT 10` planned as `Seq Scan -> Sort` here
8781        // against an `Incremental Sort` over an index scan on
8782        // PostgreSQL 18, on a table indexed for it.
8783        //
8784        // Three things have to hold, and each of them is the tree's
8785        // limitation rather than a conservative choice:
8786        //
8787        //   * the terms are the index's key columns, in its order, from
8788        //     the leading one — a suffix or a permutation is a different
8789        //     ordering;
8790        //   * every term runs the same direction, because the tree is
8791        //     walked one way for all of them. `(a, b DESC)` is what
8792        //     PostgreSQL serves from an index whose SECOND key is
8793        //     descending, and SPG's tree does not scan per column;
8794        //   * every key column is NOT NULL. A NULL key is not in the
8795        //     tree at all, and the separate pass that emits those rows
8796        //     (r1046) knows how to place them for ONE column, not for a
8797        //     tuple.
8798        if stmt.order_by.len() > 1 {
8799            let keys: Vec<usize> = core::iter::once(index.column_position)
8800                .chain(index.extra_column_positions.iter().copied())
8801                .collect();
8802            if stmt.order_by.len() > keys.len() {
8803                return None;
8804            }
8805            let desc = stmt.order_by[0].desc;
8806            for (term, &key_pos) in stmt.order_by.iter().zip(keys.iter()) {
8807                if term.desc != desc {
8808                    return None;
8809                }
8810                let Expr::Column(c) = &term.expr else {
8811                    return None;
8812                };
8813                if let Some(q) = &c.qualifier
8814                    && !q.eq_ignore_ascii_case(alias)
8815                {
8816                    return None;
8817                }
8818                let pos = cols
8819                    .iter()
8820                    .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
8821                if pos != key_pos {
8822                    return None;
8823                }
8824                let col = cols.get(pos)?;
8825                if col.nullable {
8826                    return None;
8827                }
8828                if crate::index_access::collated_column(col, table.db_collation()).is_none()
8829                    && !crate::collate::column_key_is_bytewise(col, self.speaks_mysql)
8830                {
8831                    return None;
8832                }
8833            }
8834        }
8835        Some((index.name.clone(), order_pos))
8836    }
8837
8838    fn try_index_order_stream<F>(
8839        &self,
8840        stmt: &SelectStatement,
8841        from: &FromClause,
8842        cancel: CancelToken<'_>,
8843        emit: &mut F,
8844    ) -> Result<Option<usize>, EngineError>
8845    where
8846        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8847    {
8848        // r1044 — the shape gate lives in `index_order_walk_target`, so
8849        // `EXPLAIN` answers the same question. What stays here is the
8850        // part that RAISES (an illegal ORDER BY has to keep erroring
8851        // from where it did) and the bindings the walk needs.
8852        crate::orderby::check_order_by_legality(stmt)?;
8853        crate::orderby::check_order_by_positions(stmt)?;
8854        crate::window::reject_window_in_row_clauses(stmt)?;
8855        // v7.39.13 — the prefix walk first: it serves a shape the
8856        // leading-column walk cannot, and refuses everything that one
8857        // takes.
8858        let (order_pos, prefix) = match self.index_prefix_walk_target(stmt, from) {
8859            Some((_, pos, keys)) => (pos, Some(keys)),
8860            None => match self.index_order_walk_target(stmt, from) {
8861                Some((_, pos)) => (pos, None),
8862                None => return Ok(None),
8863            },
8864        };
8865        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8866            return Ok(None);
8867        };
8868        let alias = from
8869            .primary
8870            .alias
8871            .as_deref()
8872            .unwrap_or(from.primary.name.as_str());
8873        let cols = table.schema().columns.clone();
8874        let order = &stmt.order_by[0];
8875        // v7.39.11 — the same lookup the gate made; see
8876        // `index_order_walk_target`.
8877        let Some(index) = (if prefix.is_some() {
8878            // The prefix planner named an index whose FIRST extra key
8879            // column is the order column; the lookup below looks for one
8880            // whose LEADING column is, and would find the wrong tree.
8881            table.indices().iter().find(|i| {
8882                matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8883                    && i.extra_column_positions.first() == Some(&order_pos)
8884                    && i.expression.is_none()
8885                    && i.partial_predicate.is_none()
8886            })
8887        } else {
8888            table
8889                .index_on(order_pos)
8890                .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8891                .or_else(|| {
8892                    table.indices().iter().find(|i| {
8893                        matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8894                            && i.column_position == order_pos
8895                    })
8896                })
8897        }) else {
8898            return Ok(None);
8899        };
8900
8901        let sess = self.dml_session();
8902        let ctx = EvalContext::new(&cols, Some(alias))
8903            .with_catalog(self.active_catalog())
8904            .with_session(&sess);
8905        let projection = build_projection(
8906            &stmt.items,
8907            &cols,
8908            alias,
8909            self.speaks_mysql,
8910            Some(self.active_catalog()),
8911        )?;
8912        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8913        emit(crate::StreamItem::Header(&columns))?;
8914        let bound_pos: Vec<Option<usize>> = projection
8915            .iter()
8916            .map(|p| match &p.expr {
8917                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8918                    Ok(Some(pos)) => Some(pos),
8919                    _ => None,
8920                },
8921                _ => None,
8922            })
8923            .collect();
8924
8925        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8926            .where_
8927            .as_ref()
8928            .filter(|w| crate::eval::fully_compilable(w))
8929            .map(|w| crate::eval::compile_expr(w, &ctx));
8930        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8931        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8932        let snapshot = self.current_snapshot();
8933
8934        // A btree holds one locator per row VERSION, so a row whose key was
8935        // updated can sit under two keys and a dead one can sit beside its
8936        // replacement. The visibility gate drops the dead; `seen` drops a
8937        // live row that the walk reaches twice, which would otherwise be a
8938        // duplicated output row rather than a slow one.
8939        let mut emitted_rows = alloc::vec![false; table.rows().len()];
8940
8941        // r1046 — the rows the index cannot hold.
8942        //
8943        // A NULL key is not in the btree, so the walk below never reaches
8944        // those rows; they are emitted here, at the end SQL puts them.
8945        // PG's default is NULLS LAST ascending and NULLS FIRST
8946        // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
8947        // the same rule `order_by_value_cmp_raw` applies to the sort this
8948        // replaces, so the two orders agree.
8949        //
8950        // Finding them costs one pass over the column. That pass is why
8951        // this is still worth doing: the sort it replaces encodes and
8952        // decodes every row, and the walk plus the pass measured 72.0 ms
8953        // down to about 22 on 400,000 rows.
8954        let nulls_first = order.nulls_first.unwrap_or(order.desc);
8955        // r1047 — under DISTINCT the walk emits the FIRST passing row of
8956        // each key group and skips the rest; the gate admits DISTINCT
8957        // only when the projection is the order column itself, so one
8958        // canonical key is one output row. NULL is one distinct value,
8959        // so the NULL pass stops at its first emit too.
8960        let distinct = stmt.distinct;
8961        let mut count = 0usize;
8962        let mut visited = 0usize;
8963        // v7.39.11 — OFFSET and LIMIT, applied as the walk goes.
8964        //
8965        // Both count PASSING rows, so a skipped row still has to run the
8966        // predicate and the projection — `stream_filter_project` is
8967        // `stream_project_row` without the emit, which is exactly that.
8968        // Stopping at `remaining == 0` is the whole point: twenty rows
8969        // off the end of an index instead of a sorted table.
8970        let mut to_skip = stmt.offset_literal().unwrap_or(0) as usize;
8971        let mut remaining: Option<usize> = stmt.limit_literal().map(|l| l as usize);
8972        let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
8973                                  eval_stack: &mut Vec<Value<'static>>,
8974                                  values: &mut Vec<Value<'static>>,
8975                                  visited: &mut usize,
8976                                  to_skip: &mut usize,
8977                                  remaining: &mut Option<usize>,
8978                                  emit: &mut F|
8979         -> Result<usize, EngineError> {
8980            if !cols[order_pos].nullable {
8981                return Ok(0);
8982            }
8983            // v7.39.11 — nothing to emit once the LIMIT is met, and
8984            // finding that out must not cost a scan.
8985            //
8986            // This pass looks for NULL-keyed rows by walking the whole
8987            // heap, because they are not in the tree. That is the price
8988            // r1046 measured and accepted for an UNBOUNDED order. With
8989            // a LIMIT the walk above has usually already produced every
8990            // row the caller asked for, and scanning 400,000 rows to
8991            // add none of them is the whole cost of the query: the
8992            // release sweep's `SELECT pad FROM t ORDER BY n LIMIT 10`
8993            // over a nullable indexed NUMERIC went 0.237 ms at 50,000
8994            // rows and 2.251 at 400,000 — linear, against PostgreSQL's
8995            // 0.155 and 0.182 — the moment this gate started accepting
8996            // LIMIT. The `remaining` check below sits after the
8997            // per-row filters, so it could never be reached.
8998            if *remaining == Some(0) {
8999                return Ok(0);
9000            }
9001            let mut n = 0usize;
9002            for (ri, row) in table.rows().iter().enumerate() {
9003                if !matches!(row.values.get(order_pos), Some(Value::Null)) {
9004                    continue;
9005                }
9006                if emitted_rows.get(ri).copied().unwrap_or(true) {
9007                    continue;
9008                }
9009                if !table.is_row_visible(ri, &snapshot) {
9010                    continue;
9011                }
9012                *visited += 1;
9013                if visited.is_multiple_of(256) {
9014                    cancel.check()?;
9015                }
9016                emitted_rows[ri] = true;
9017                if *remaining == Some(0) {
9018                    break;
9019                }
9020                let passed = if *to_skip > 0 {
9021                    let p = Self::stream_filter_project(
9022                        row,
9023                        stmt.where_.as_ref(),
9024                        compiled_where.as_ref(),
9025                        eval_stack,
9026                        &projection,
9027                        &bound_pos,
9028                        &ctx,
9029                        values,
9030                    )?;
9031                    if p {
9032                        *to_skip -= 1;
9033                    }
9034                    false
9035                } else {
9036                    Self::stream_project_row(
9037                        row,
9038                        stmt.where_.as_ref(),
9039                        compiled_where.as_ref(),
9040                        eval_stack,
9041                        &projection,
9042                        &bound_pos,
9043                        &ctx,
9044                        values,
9045                        emit,
9046                    )?
9047                };
9048                if passed {
9049                    n += 1;
9050                    if let Some(r) = remaining.as_mut() {
9051                        *r -= 1;
9052                        if *r == 0 {
9053                            break;
9054                        }
9055                    }
9056                    if distinct {
9057                        break;
9058                    }
9059                }
9060            }
9061            Ok(n)
9062        };
9063
9064        if nulls_first {
9065            count += emit_null_rows(
9066                &mut emitted_rows,
9067                &mut eval_stack,
9068                &mut values,
9069                &mut visited,
9070                &mut to_skip,
9071                &mut remaining,
9072                emit,
9073            )?;
9074        }
9075
9076        // v7.39.13 — a prefix walk when the statement binds the index's
9077        // leading column, the whole tree otherwise. The key is not read
9078        // by the loop, so the two shapes meet as posting lists.
9079        let walker: alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> =
9080            match prefix.as_ref().and_then(|p| {
9081                if order.desc {
9082                    index.iter_prefix_desc(p).map(
9083                        |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9084                            alloc::boxed::Box::new(it.map(|(_, l)| l))
9085                        },
9086                    )
9087                } else {
9088                    index.iter_prefix_asc(p).map(
9089                        |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9090                            alloc::boxed::Box::new(it.map(|(_, l)| l))
9091                        },
9092                    )
9093                }
9094            }) {
9095                Some(it) => it,
9096                None if order.desc => alloc::boxed::Box::new(index.iter_desc().map(|(_, l)| l)),
9097                None => alloc::boxed::Box::new(index.iter_asc().map(|(_, l)| l)),
9098            };
9099        'walk: for locators in walker {
9100            if remaining == Some(0) {
9101                break;
9102            }
9103            for loc in locators {
9104                let spg_storage::RowLocator::Hot(ri) = *loc else {
9105                    continue;
9106                };
9107                if emitted_rows.get(ri).copied().unwrap_or(true) {
9108                    continue;
9109                }
9110                if !table.is_row_visible(ri, &snapshot) {
9111                    continue;
9112                }
9113                let Some(row) = table.rows().get(ri) else {
9114                    continue;
9115                };
9116                visited += 1;
9117                if visited.is_multiple_of(256) {
9118                    cancel.check()?;
9119                }
9120                emitted_rows[ri] = true;
9121                // v7.39.11 — a skipped row still runs the predicate and
9122                // the projection, because OFFSET counts rows that PASS;
9123                // it just does not reach the client.
9124                let passed = if to_skip > 0 {
9125                    let p = Self::stream_filter_project(
9126                        row,
9127                        stmt.where_.as_ref(),
9128                        compiled_where.as_ref(),
9129                        &mut eval_stack,
9130                        &projection,
9131                        &bound_pos,
9132                        &ctx,
9133                        &mut values,
9134                    )?;
9135                    if p {
9136                        to_skip -= 1;
9137                    }
9138                    false
9139                } else {
9140                    Self::stream_project_row(
9141                        row,
9142                        stmt.where_.as_ref(),
9143                        compiled_where.as_ref(),
9144                        &mut eval_stack,
9145                        &projection,
9146                        &bound_pos,
9147                        &ctx,
9148                        &mut values,
9149                        emit,
9150                    )?
9151                };
9152                if passed {
9153                    count += 1;
9154                    if let Some(r) = remaining.as_mut() {
9155                        *r -= 1;
9156                        if *r == 0 {
9157                            break 'walk;
9158                        }
9159                    }
9160                    // One row per key group: the rest are the same value.
9161                    if distinct {
9162                        break;
9163                    }
9164                }
9165            }
9166        }
9167
9168        if !nulls_first {
9169            count += emit_null_rows(
9170                &mut emitted_rows,
9171                &mut eval_stack,
9172                &mut values,
9173                &mut visited,
9174                &mut to_skip,
9175                &mut remaining,
9176                emit,
9177            )?;
9178        }
9179        Ok(Some(count))
9180    }
9181
9182    /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
9183    /// building an `OrderKey` vector per row.
9184    ///
9185    /// The row-returning sorted scan allocates twice per row: one
9186    /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
9187    /// projection. Counted over 400 k rows (r1030,
9188    /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
9189    /// allocations and 208 MB of traffic for an answer of four hundred
9190    /// thousand integers.
9191    ///
9192    /// The key half is pure ceremony on this shape.
9193    /// `sort_tagged_by_inline_int_key` already sorts indices rather than
9194    /// rows, so the per-row vector is built, has one integer taken out of
9195    /// it, and is then dragged through the permutation — it exists to carry
9196    /// a number the row's column already held. This lane carries the number
9197    /// instead, in a fixed-size array that lives inside the buffer element
9198    /// and allocates nothing. Same idea as the predicate VM's integer lane.
9199    ///
9200    /// Declines to `None` for anything it does not cover, and every caller
9201    /// falls through to the general path, so the gate list is the
9202    /// specification.
9203    ///
9204    /// Ties: equal keys keep scan order, as the stable sort on the general
9205    /// path does. Rows that tie on every ORDER BY term are entitled to any
9206    /// order among themselves either way — see `STABILITY.md`.
9207    fn try_int_key_sorted_stream<F>(
9208        &self,
9209        stmt: &SelectStatement,
9210        from: &FromClause,
9211        cancel: CancelToken<'_>,
9212        emit: &mut F,
9213    ) -> Result<Option<usize>, EngineError>
9214    where
9215        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9216    {
9217        /// Sort terms this lane carries inline. Four covers every ORDER BY
9218        /// in the endpoint sweep and in the dogfood corpus; wider ones fall
9219        /// through rather than growing the buffer element for everybody.
9220        const MAX_KEYS: usize = 4;
9221
9222        if stmt.order_by.is_empty()
9223            || stmt.order_by.len() > MAX_KEYS
9224            // v7.38.14 — DISTINCT is admitted when the projected set is
9225            // exactly the ORDER BY set, and only then. This lane sorts, and
9226            // when the sort key determines the projected row every duplicate
9227            // lands ADJACENT to its twin -- so the de-duplication is a
9228            // comparison with the previous row rather than a hash table, and
9229            // the reason this lane declined DISTINCT disappears with it. The
9230            // seen-set it could not offer held indices into a materialised
9231            // vector; there is no seen-set now.
9232            //
9233            // The gate is as narrow as the bare-GROUP-BY rewrite's for the
9234            // same reason: `ORDER BY a` over a projection of `a, b` does NOT
9235            // place duplicates of the PAIR adjacent, so set EQUALITY, never
9236            // overlap.
9237            || (stmt.distinct && !Self::distinct_is_adjacent_after_sort(stmt))
9238            || stmt.limit_with_ties
9239            || stmt.limit.is_some()
9240            || stmt.offset.is_some()
9241            || stmt.having.is_some()
9242            || stmt.group_by.is_some()
9243            || !stmt.unions.is_empty()
9244            || !from.joins.is_empty()
9245            || from.primary.lateral_subquery.is_some()
9246            || from.primary.unnest_expr.is_some()
9247            || from.primary.as_of_segment.is_some()
9248            || from.primary.generate_series_args.is_some()
9249            || select_has_window(stmt)
9250            || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
9251        {
9252            return Ok(None);
9253        }
9254        if stmt
9255            .items
9256            .iter()
9257            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9258        {
9259            return Ok(None);
9260        }
9261        crate::orderby::check_order_by_legality(stmt)?;
9262        crate::orderby::check_order_by_positions(stmt)?;
9263        crate::window::reject_window_in_row_clauses(stmt)?;
9264        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9265            return Ok(None);
9266        };
9267        if table.has_cold_rows_fast() {
9268            return Ok(None);
9269        }
9270        if !from.primary.only
9271            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9272        {
9273            return Ok(None);
9274        }
9275        let alias = from
9276            .primary
9277            .alias
9278            .as_deref()
9279            .unwrap_or(from.primary.name.as_str());
9280        let cols = table.schema().columns.clone();
9281
9282        // Every ORDER BY term must be a NOT NULL integer column of this
9283        // table. NOT NULL is what lets the key be a bare integer: with
9284        // NULLs the lane would have to carry their ordering too, and
9285        // getting that subtly wrong is the r1020 defect.
9286        let mut key_pos = [0usize; MAX_KEYS];
9287        let mut descs = [false; MAX_KEYS];
9288        // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
9289        // which the AST records as `None`; `unwrap_or(desc)` is how the
9290        // rest of the engine resolves it.
9291        let mut nulls_first = [false; MAX_KEYS];
9292        let n_keys = stmt.order_by.len();
9293        for (slot, order) in stmt.order_by.iter().enumerate() {
9294            let Expr::Column(oc) = &order.expr else {
9295                return Ok(None);
9296            };
9297            if let Some(q) = &oc.qualifier
9298                && !q.eq_ignore_ascii_case(alias)
9299            {
9300                return Ok(None);
9301            }
9302            let Some(pos) = cols
9303                .iter()
9304                .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
9305            else {
9306                return Ok(None);
9307            };
9308            if !matches!(
9309                cols[pos].ty,
9310                spg_storage::DataType::SmallInt
9311                    | spg_storage::DataType::Int
9312                    | spg_storage::DataType::BigInt
9313            ) {
9314                return Ok(None);
9315            }
9316            key_pos[slot] = pos;
9317            descs[slot] = order.desc;
9318            nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
9319        }
9320
9321        let sess = self.dml_session();
9322        let ctx = EvalContext::new(&cols, Some(alias))
9323            .with_catalog(self.active_catalog())
9324            .with_session(&sess);
9325        let projection = build_projection(
9326            &stmt.items,
9327            &cols,
9328            alias,
9329            self.speaks_mysql,
9330            Some(self.active_catalog()),
9331        )?;
9332        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9333        let bound_pos: Vec<Option<usize>> = projection
9334            .iter()
9335            .map(|p| match &p.expr {
9336                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9337                    Ok(Some(pos)) => Some(pos),
9338                    _ => None,
9339                },
9340                _ => None,
9341            })
9342            .collect();
9343        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9344            .where_
9345            .as_ref()
9346            .filter(|w| crate::eval::fully_compilable(w))
9347            .map(|w| crate::eval::compile_expr(w, &ctx));
9348
9349        // The same first-observable point the materialising planner fires,
9350        // placed after the gates so it fires exactly once: this lane runs
9351        // BEFORE that planner and would otherwise be a hole in the
9352        // panic-isolation and cancellation-race coverage rather than a
9353        // faster path through it.
9354        crate::injection_point!("planner_first_row_fetch", &stmt.from);
9355
9356        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9357        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9358        let mut budget = ByteBudget::new(self.max_query_bytes);
9359        let snapshot = self.current_snapshot();
9360        // Keys, a NULL bit per key slot, and the row. The bitmask keeps
9361        // the element small: a nullable key still costs one bit rather
9362        // than a second array.
9363        let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
9364
9365        for (ri, row) in table.rows().iter().enumerate() {
9366            if ri.is_multiple_of(256) {
9367                cancel.check()?;
9368            }
9369            if !table.is_row_visible(ri, &snapshot) {
9370                continue;
9371            }
9372            // The key comes from the STORED row, before projection: an
9373            // ORDER BY column need not appear in the select list.
9374            let mut keys = [0i64; MAX_KEYS];
9375            let mut nulls = 0u8;
9376            let mut keyed = true;
9377            for slot in 0..n_keys {
9378                match row.values.get(key_pos[slot]) {
9379                    Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
9380                    Some(Value::Int(v)) => keys[slot] = i64::from(*v),
9381                    Some(Value::BigInt(v)) => keys[slot] = *v,
9382                    Some(Value::Null) | None => nulls |= 1 << slot,
9383                    // An integer column holding something else is a row
9384                    // this lane cannot order; hand the whole query back
9385                    // rather than guess at it.
9386                    _ => {
9387                        keyed = false;
9388                        break;
9389                    }
9390                }
9391            }
9392            if !keyed {
9393                return Ok(None);
9394            }
9395            if !Self::stream_filter_project(
9396                row,
9397                stmt.where_.as_ref(),
9398                compiled_where.as_ref(),
9399                &mut eval_stack,
9400                &projection,
9401                &bound_pos,
9402                &ctx,
9403                &mut values,
9404            )? {
9405                continue;
9406            }
9407            budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
9408            sorted.push((keys, nulls, core::mem::take(&mut values)));
9409            values.reserve(projection.len());
9410        }
9411
9412        sorted.sort_by(|a, b| {
9413            use core::cmp::Ordering;
9414            for slot in 0..n_keys {
9415                let bit = 1u8 << slot;
9416                let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
9417                    (true, true) => Ordering::Equal,
9418                    // Where the NULLs go is already decided — `nulls_first`
9419                    // resolved DESC's default when it was read. Reversing
9420                    // this for DESC as well would apply the direction
9421                    // twice and put them at the wrong end.
9422                    (true, false) => {
9423                        if nulls_first[slot] {
9424                            Ordering::Less
9425                        } else {
9426                            Ordering::Greater
9427                        }
9428                    }
9429                    (false, true) => {
9430                        if nulls_first[slot] {
9431                            Ordering::Greater
9432                        } else {
9433                            Ordering::Less
9434                        }
9435                    }
9436                    (false, false) => {
9437                        let o = a.0[slot].cmp(&b.0[slot]);
9438                        if descs[slot] { o.reverse() } else { o }
9439                    }
9440                };
9441                if ord != Ordering::Equal {
9442                    return ord;
9443                }
9444            }
9445            Ordering::Equal
9446        });
9447
9448        emit(crate::StreamItem::Header(&columns))?;
9449        // v7.38.14 — DISTINCT, de-duplicated against the PREVIOUS row.
9450        //
9451        // The gate above only admits DISTINCT when the sort key determines
9452        // the projected row, so every duplicate is adjacent to its twin by
9453        // the time this loop runs and one comparison replaces a hash table
9454        // of every row seen. Equality is `values_eq_norm` with the same mask
9455        // the materialising path builds -- deliberately the same function,
9456        // because a de-duplication that disagreed with the one on the other
9457        // path would make the answer depend on which lane a query took.
9458        //
9459        // A query that did not ask for DISTINCT pays one already-false bool
9460        // test per row: the short-circuit means the comparison never runs
9461        // and `prev` is never written.
9462        let dedup_mask = fold_mask(&projection);
9463        let fold = FoldSpec::of(self.speaks_mysql, &dedup_mask);
9464        let mut count = 0usize;
9465        let mut prev: Option<&[Value<'static>]> = None;
9466        for (_, _, vals) in &sorted {
9467            if stmt.distinct
9468                && let Some(p) = prev
9469                && values_eq_norm(p, vals, fold)
9470            {
9471                continue;
9472            }
9473            emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
9474            count += 1;
9475            if stmt.distinct {
9476                prev = Some(vals);
9477            }
9478        }
9479        Ok(Some(count))
9480    }
9481
9482    /// v7.38.14 — would sorting place every duplicate next to its twin?
9483    ///
9484    /// True when the projected expressions and the ORDER BY expressions are the
9485    /// same SET. Then the sort key determines the projected row, so equal rows
9486    /// are adjacent afterwards and an adjacent comparison de-duplicates exactly
9487    /// as a hash would -- and, because both sort paths are stable, the survivor
9488    /// is the first-seen row, which is the one the hash keeps too.
9489    ///
9490    /// A wildcard's expansion is not known here, so it is not a set this can
9491    /// compare; an ordinal ORDER BY names a select-list position rather than a
9492    /// value and is left alone.
9493    fn distinct_is_adjacent_after_sort(stmt: &SelectStatement) -> bool {
9494        if stmt.order_by.is_empty() || !stmt.distinct_on.is_empty() {
9495            return false;
9496        }
9497        let mut projected: alloc::vec::Vec<&Expr> =
9498            alloc::vec::Vec::with_capacity(stmt.items.len());
9499        for item in &stmt.items {
9500            match item {
9501                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return false,
9502                SelectItem::Expr { expr, .. } => projected.push(expr),
9503            }
9504        }
9505        if projected.is_empty() {
9506            return false;
9507        }
9508        let keys: alloc::vec::Vec<&Expr> = stmt.order_by.iter().map(|o| &o.expr).collect();
9509        if keys
9510            .iter()
9511            .any(|k| matches!(k, Expr::Literal(spg_sql::ast::Literal::Integer(_))))
9512        {
9513            return false;
9514        }
9515        projected.iter().all(|p| keys.contains(p)) && keys.iter().all(|k| projected.contains(k))
9516    }
9517
9518    fn try_spill_sorted_stream<F>(
9519        &self,
9520        stmt: &SelectStatement,
9521        from: &FromClause,
9522        cancel: CancelToken<'_>,
9523        emit: &mut F,
9524    ) -> Result<Option<usize>, EngineError>
9525    where
9526        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9527    {
9528        // The shapes `try_spill_sorted_scan` declines, plus the ones the
9529        // streaming executor does not carry (a LIMIT is already bounded
9530        // by a partial sort; the rest need the answer addressable).
9531        if !self.can_spill()
9532            || stmt.order_by.is_empty()
9533            || stmt.distinct
9534            || stmt.limit_with_ties
9535            || stmt.limit.is_some()
9536            || stmt.offset.is_some()
9537            || stmt.having.is_some()
9538            || stmt.group_by.is_some()
9539            || !stmt.unions.is_empty()
9540            || !from.joins.is_empty()
9541            || from.primary.lateral_subquery.is_some()
9542            || from.primary.unnest_expr.is_some()
9543            || from.primary.as_of_segment.is_some()
9544            || from.primary.generate_series_args.is_some()
9545            || select_has_window(stmt)
9546            || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
9547        {
9548            return Ok(None);
9549        }
9550        if stmt
9551            .items
9552            .iter()
9553            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9554        {
9555            return Ok(None);
9556        }
9557        // Everything `exec_bare_select_cancel` does before it scans runs
9558        // BELOW this path, so a statement claimed here skips it. Three of
9559        // those were missed on the way in and each was caught by a
9560        // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
9561        // ORDER BY 2` sorted happily instead of raising 42P10), the
9562        // cancellation check by another, the partition fan-out by the
9563        // differential corpus. What is reconciled, item by item: with-ties
9564        // needs ORDER BY (gated above), USING/NATURAL and RLS join
9565        // rewrites (joins gated above), the single-table RLS predicate
9566        // (the dispatcher declines a policy-subject table before this is
9567        // reached), the meta-view dispatch (those names are not in the
9568        // catalog, so the lookup below declines). These three are calls,
9569        // so the message and SQLSTATE are the ones the fall-back gives —
9570        // `select_has_window` above reads the select list and ORDER BY but
9571        // not WHERE, which is the case the third one covers.
9572        crate::orderby::check_order_by_legality(stmt)?;
9573        crate::orderby::check_order_by_positions(stmt)?;
9574        crate::window::reject_window_in_row_clauses(stmt)?;
9575        // A parent's rows are its children's. These walks scan the named
9576        // relation alone, so a partitioned or inherited parent comes back
9577        // short — and silently: the corpus caught `SELECT id FROM pr
9578        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
9579        // parent's own rows instead of the partitions'. `ONLY` is exactly
9580        // the case that does not fan out, so it stays, which is the test
9581        // the FROM-clause fan-out itself makes.
9582        if !from.primary.only
9583            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9584        {
9585            return Ok(None);
9586        }
9587        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9588            return Ok(None);
9589        };
9590        // Cold-tier rows live outside `rows()`; this walk would drop
9591        // them silently, the same reason round 831's walk declines.
9592        if table.has_cold_rows_fast() {
9593            return Ok(None);
9594        }
9595
9596        let alias = from
9597            .primary
9598            .alias
9599            .as_deref()
9600            .unwrap_or(from.primary.name.as_str());
9601        let cols = table.schema().columns.clone();
9602        let sess = self.dml_session();
9603        let ctx = EvalContext::new(&cols, Some(alias))
9604            .with_catalog(self.active_catalog())
9605            .with_session(&sess);
9606        let projection = build_projection(
9607            &stmt.items,
9608            &cols,
9609            alias,
9610            self.speaks_mysql,
9611            Some(self.active_catalog()),
9612        )?;
9613        let order_by = stmt.order_by.clone();
9614        // The same one-shot resolution the general path does (round
9615        // 582): each ORDER BY column is bound once, not once per row.
9616        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
9617        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
9618        // Resolved BEFORE the scan, because it now decides what the sort
9619        // STORES and not just what it decodes (round 995).
9620        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
9621
9622        // v7.38.22 — resolved HERE, because this path did not resolve
9623        // them at all.
9624        //
9625        // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
9626        // "en_US.utf8"` in BYTE order on this path — and swallowed an
9627        // unknown collation name rather than raising — because the sorter
9628        // below compared with an empty collation slice. The materialising
9629        // path honoured both. Which answer a query got depended on which
9630        // path the planner took, and this is the path a plain single-table
9631        // SELECT takes.
9632        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
9633        // v7.39.12 — a correlated scalar subquery in ORDER BY is
9634        // resolved for the row before its key is built.
9635        //
9636        // Uncorrelated subqueries are replaced by a literal before
9637        // execution; a correlated one cannot be, so it reached the
9638        // per-row evaluator — the one place that cannot run a subquery
9639        // — and the statement raised "subquery reached row eval".
9640        // Reported by sentori against 7.39.11; see
9641        // `Engine::order_by_resolved_for_row`.
9642        //
9643        // The `any` runs once, here, so an ordinary ORDER BY pays one
9644        // bool per row and nothing else.
9645        let order_has_subquery = order_by
9646            .iter()
9647            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
9648        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
9649        let mut sorter = crate::extsort::ExternalSorter::new(
9650            self.temp_run_factory,
9651            self.session_work_mem_bytes(),
9652            cols.clone(),
9653            &descs,
9654            &order_colls,
9655        )
9656        .with_stats(&self.spill_stats)
9657        .with_pruned(&needed);
9658        let snapshot = self.current_snapshot();
9659        // One key buffer for the whole scan: `push` drains it and leaves
9660        // the capacity behind.
9661        let mut keys: Vec<OrderKey> = Vec::new();
9662        // r1024 — compile the predicate once for the scan.
9663        //
9664        // These two sorted-spill scans are the paths a single-table SELECT
9665        // with an ORDER BY takes, and they were the last row-returning ones
9666        // still walking the expression tree per row. r1023 did the
9667        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
9668        // exactly this shape.
9669        //
9670        // Found from the profile's CALL TREE rather than its leaves. The
9671        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
9672        // 261, `mod_op` 178 — and two attempts at reasoning out which
9673        // function asked for it were both wrong. The tree names the caller
9674        // chain, and it named this one.
9675        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9676            .where_
9677            .as_ref()
9678            .filter(|w| crate::eval::fully_compilable(w))
9679            .map(|w| crate::eval::compile_expr(w, &ctx));
9680        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9681        for (i, row) in table.scan_visible_from(0, &snapshot) {
9682            if i.is_multiple_of(256) {
9683                cancel.check()?;
9684            }
9685            if let Some(c) = &compiled_where {
9686                if !crate::eval::compiled::eval_compiled_pred(
9687                    c,
9688                    row,
9689                    &ctx,
9690                    &mut eval_stack,
9691                    ctx.mysql_dialect,
9692                )? {
9693                    continue;
9694                }
9695            } else if let Some(w) = &stmt.where_ {
9696                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
9697                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9698                    continue;
9699                }
9700            }
9701            keys.clear();
9702            // The same collations the sorter compares with, and the
9703            // re-derivation below is handed the same ones. `finish`'s
9704            // contract is that a key comes back the way it was pushed;
9705            // a collation is part of the way it was pushed.
9706            if order_has_subquery {
9707                // A substituted literal is no longer a bound column.
9708                let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
9709                crate::orderby::build_order_keys_bound(
9710                    per_row.as_deref().unwrap_or(&order_by),
9711                    &unbound,
9712                    &order_colls,
9713                    row,
9714                    &ctx,
9715                    &mut keys,
9716                )?;
9717            } else {
9718                crate::orderby::build_order_keys_bound(
9719                    &order_by,
9720                    &order_bound,
9721                    &order_colls,
9722                    row,
9723                    &ctx,
9724                    &mut keys,
9725                )?;
9726            }
9727            sorter.push(&mut keys, row)?;
9728        }
9729
9730        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9731        emit(crate::StreamItem::Header(&columns))?;
9732
9733        let key_ctx = &ctx;
9734        let mut emitted_since_check = 0usize;
9735        let n = sorter.finish_each(
9736            |src, buf| {
9737                crate::orderby::build_order_keys_rederived(
9738                    &order_by,
9739                    &order_bound,
9740                    &order_colls,
9741                    src,
9742                    key_ctx,
9743                    buf,
9744                )
9745            },
9746            |src, values| {
9747                for p in &projection {
9748                    values.push(
9749                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
9750                    );
9751                }
9752                Ok(())
9753            },
9754            |cells| {
9755                // The merge is the long half of a big sort, and the scan's
9756                // check above stops running once it ends: a cancelled
9757                // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
9758                // anyway. Same stride as the scan.
9759                emitted_since_check += 1;
9760                if emitted_since_check >= 256 {
9761                    emitted_since_check = 0;
9762                    cancel.check()?;
9763                }
9764                emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
9765            },
9766        )?;
9767        Ok(Some(n))
9768    }
9769
9770    /// One row of the single-table streaming walk: the WHERE test, the
9771    /// projection, the emit. Returns whether a row was emitted.
9772    ///
9773    /// v7.39 (round 970) — factored out because the walk now has two ways
9774    /// to reach a row, the sequential scan and an index seek's candidate
9775    /// positions, and both must do IDENTICALLY this. A copy in each is how
9776    /// two paths for one job drift; this file already carries the cost of
9777    /// that lesson twice (rounds 823 and 961, both resolvers).
9778    ///
9779    /// `#[inline]` so the scan loop keeps the shape round 957 measured it
9780    /// in — a shared hot path pays for a new abstraction whether or not it
9781    /// uses it, and this one is on the scan.
9782    #[inline]
9783    #[allow(clippy::too_many_arguments)]
9784    fn stream_filter_project(
9785        row: &spg_storage::Row<'static>,
9786        where_: Option<&Expr>,
9787        // r1023 — the same WHERE, compiled once by the caller. `None` means
9788        // the expression did not qualify and `where_` is evaluated as before.
9789        compiled_where: Option<&crate::eval::CompiledExpr>,
9790        eval_stack: &mut Vec<Value<'static>>,
9791        projection: &[ProjectedItem],
9792        bound_pos: &[Option<usize>],
9793        ctx: &crate::eval::EvalContext<'_>,
9794        values: &mut Vec<Value<'static>>,
9795    ) -> Result<bool, EngineError> {
9796        // r1023 — this scan ran its predicate through the TREE INTERPRETER,
9797        // once per row, and it was the only row-returning path that did.
9798        // The aggregate path, `table_access`, and the PK walker all compile
9799        // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
9800        // server's live samples were `eval_expr` 99, `apply_binary` 81,
9801        // `mod_op` 29 — the interpreter, not delivery.
9802        //
9803        // The arithmetic accounted for it exactly. Over the wire, the same
9804        // filter costs 6.375 ms returning rows and 0.679 ms counting them;
9805        // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
9806        // which is what an interpreted predicate costs against the compiled
9807        // lane's 11.7. It was named "delivery after a filter" before this
9808        // profile, and it was never delivery.
9809        if let Some(c) = compiled_where {
9810            if !crate::eval::compiled::eval_compiled_pred(
9811                c,
9812                row,
9813                ctx,
9814                eval_stack,
9815                ctx.mysql_dialect,
9816            )? {
9817                return Ok(false);
9818            }
9819        } else if let Some(w) = where_ {
9820            let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
9821            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9822                return Ok(false);
9823            }
9824        }
9825        values.clear();
9826        for (p, bound) in projection.iter().zip(bound_pos) {
9827            values.push(match bound {
9828                Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
9829                None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
9830            });
9831        }
9832        Ok(true)
9833    }
9834
9835    /// The same filter and projection, then emit. Split from
9836    /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
9837    /// before it can emit them — a sort — runs the identical predicate and
9838    /// projection rather than a second copy of them.
9839    #[allow(clippy::too_many_arguments)]
9840    fn stream_project_row<F>(
9841        row: &spg_storage::Row<'static>,
9842        where_: Option<&Expr>,
9843        compiled_where: Option<&crate::eval::CompiledExpr>,
9844        eval_stack: &mut Vec<Value<'static>>,
9845        projection: &[ProjectedItem],
9846        bound_pos: &[Option<usize>],
9847        ctx: &crate::eval::EvalContext<'_>,
9848        values: &mut Vec<Value<'static>>,
9849        emit: &mut F,
9850    ) -> Result<bool, EngineError>
9851    where
9852        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9853    {
9854        if !Self::stream_filter_project(
9855            row,
9856            where_,
9857            compiled_where,
9858            eval_stack,
9859            projection,
9860            bound_pos,
9861            ctx,
9862            values,
9863        )? {
9864            return Ok(false);
9865        }
9866        emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
9867        Ok(true)
9868    }
9869
9870    fn try_stream_single_table<F>(
9871        &self,
9872        stmt: &SelectStatement,
9873        from: &FromClause,
9874        cancel: CancelToken<'_>,
9875        emit: &mut F,
9876    ) -> Result<Option<usize>, EngineError>
9877    where
9878        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9879    {
9880        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9881            return Ok(None);
9882        };
9883        // Cold-tier rows live outside `rows()`; the materialising fallback
9884        // covers both tiers and this walk would silently drop them.
9885        if table.has_cold_rows_fast() {
9886            return Ok(None);
9887        }
9888        let alias = from
9889            .primary
9890            .alias
9891            .as_deref()
9892            .unwrap_or(from.primary.name.as_str());
9893        let cols = table.schema().columns.clone();
9894        let sess = self.dml_session();
9895        let ctx = EvalContext::new(&cols, Some(alias))
9896            .with_catalog(self.active_catalog())
9897            .with_session(&sess);
9898        let projection = build_projection(
9899            &stmt.items,
9900            &cols,
9901            alias,
9902            self.speaks_mysql,
9903            Some(self.active_catalog()),
9904        )?;
9905
9906        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9907        emit(crate::StreamItem::Header(&columns))?;
9908
9909        // v7.37 (round 957) — resolve each bare-column projection ONCE
9910        // instead of once per row. `find_column_pos`-style resolution is a
9911        // linear walk of the schema comparing column-name strings, and the
9912        // row loop below ran it for every cell of every row: measured at
9913        // 400k rows, binding it out of the loop took `SELECT pad` from
9914        // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
9915        //
9916        // ORDER BY has bound its keys this way since round 582
9917        // (`order_by_bound_positions`); the projection never did.
9918        //
9919        // `locate_column` is the same resolution `resolve_column` performs,
9920        // returning the site instead of the value, so the two cannot drift
9921        // apart the way a second hand-written resolver would. Anything it
9922        // declines — an expression, a whole-row reference, a name that does
9923        // not resolve — binds to `None` and takes the general path below,
9924        // errors included, so an empty table still reports nothing rather
9925        // than raising at bind time.
9926        let bound_pos: Vec<Option<usize>> = projection
9927            .iter()
9928            .map(|p| match &p.expr {
9929                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9930                    Ok(Some(pos)) => Some(pos),
9931                    _ => None,
9932                },
9933                _ => None,
9934            })
9935            .collect();
9936
9937        // One snapshot for the whole scan, as the materialising path takes.
9938        let snapshot = self.current_snapshot();
9939
9940        // v7.39 (round 970) — ask the indices BEFORE walking the table.
9941        //
9942        // This walk had no index step at all, and it is preferred over the
9943        // materialising path, which does have one (`pick_indexed_rows` ->
9944        // `try_index_seek`). So a primary-key point lookup — the commonest
9945        // statement there is — read every row: measured on 500k rows,
9946        // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
9947        // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
9948        // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
9949        //
9950        // The control that named it: `... OFFSET 0` — semantically the same
9951        // query — answered in 0.159 ms, because OFFSET is one of the shape
9952        // gates that declines this walk and sends the statement to the path
9953        // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
9954        // no semantics in common; what they share is making this function
9955        // stand down.
9956        //
9957        // The seek only NARROWS: every candidate still goes through the
9958        // full WHERE below, exactly as the mutation paths use it, so a
9959        // partial index match cannot change an answer. Positions come back
9960        // already visibility-filtered and already capped at a quarter of the
9961        // table (round 490), so a seek can never cost more than the scan it
9962        // replaces, and `None` means "walk the table" as before.
9963        //
9964        // Sorted because the scan would have produced table order and the
9965        // index produces key order. Without an ORDER BY neither is promised,
9966        // but a walk that silently reorders its answer when an index happens
9967        // to exist is a difference nobody asked for.
9968        let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
9969            crate::index_access::try_index_seek_positions(
9970                w,
9971                &cols,
9972                table,
9973                alias,
9974                &snapshot,
9975                self.speaks_mysql,
9976            )
9977        });
9978
9979        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9980        // r1023 — compile the predicate once for the whole scan. Same gate
9981        // every other path uses: `fully_compilable` or keep the interpreter,
9982        // so a shape the VM cannot take answers exactly as it did before.
9983        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9984            .where_
9985            .as_ref()
9986            .filter(|w| crate::eval::fully_compilable(w))
9987            .map(|w| crate::eval::compile_expr(w, &ctx));
9988        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9989        let mut count: usize = 0;
9990        match seek_positions {
9991            Some(mut positions) => {
9992                positions.sort_unstable();
9993                for (n, pos) in positions.into_iter().enumerate() {
9994                    if n.is_multiple_of(256) {
9995                        cancel.check()?;
9996                    }
9997                    let Some(row) = table.rows().get(pos) else {
9998                        continue;
9999                    };
10000                    if Self::stream_project_row(
10001                        row,
10002                        stmt.where_.as_ref(),
10003                        compiled_where.as_ref(),
10004                        &mut eval_stack,
10005                        &projection,
10006                        &bound_pos,
10007                        &ctx,
10008                        &mut values,
10009                        emit,
10010                    )? {
10011                        count += 1;
10012                    }
10013                }
10014            }
10015            None => {
10016                // v7.38.11 — the streaming scan is the path a client
10017                // reaches over the wire, so it is the one that has to
10018                // ask the BRIN summary which slots can be skipped. The
10019                // predicate still runs on every row that survives.
10020                let slots = stmt
10021                    .where_
10022                    .as_ref()
10023                    .and_then(|w| crate::brin::candidate_slots(w, table))
10024                    .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
10025                for (i, row) in table.scan_visible_slots(slots, &snapshot) {
10026                    if i.is_multiple_of(256) {
10027                        cancel.check()?;
10028                    }
10029                    if Self::stream_project_row(
10030                        row,
10031                        stmt.where_.as_ref(),
10032                        compiled_where.as_ref(),
10033                        &mut eval_stack,
10034                        &projection,
10035                        &bound_pos,
10036                        &ctx,
10037                        &mut values,
10038                        emit,
10039                    )? {
10040                        count += 1;
10041                    }
10042                }
10043            }
10044        }
10045        Ok(Some(count))
10046    }
10047
10048    pub(crate) fn try_exec_joined_streaming<F>(
10049        &self,
10050        stmt: &SelectStatement,
10051        cancel: CancelToken<'_>,
10052        emit: &mut F,
10053    ) -> Result<Option<usize>, EngineError>
10054    where
10055        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
10056    {
10057        // Shape gates — keep the streamable surface narrow on
10058        // purpose. The fall-back path still handles everything else.
10059        let Some(from) = &stmt.from else {
10060            return Ok(None);
10061        };
10062        // v7.37 (round 830) — decline anything a row-security policy binds
10063        // for this session. Policies are injected in
10064        // `exec_bare_select_cancel`, below this path, so a statement claimed
10065        // here would read the table unfiltered: measured, `SELECT val FROM
10066        // sec` returned all three rows to a session whose policy allows two,
10067        // while `SELECT upper(val) FROM sec` — declined by the shape gates
10068        // and so materialised — returned the correct two.
10069        //
10070        // Declining sends it to the path that enforces. Teaching this one to
10071        // inject the predicate itself would keep the streaming benefit for
10072        // RLS tables and is the better end state; it is not what a
10073        // correctness fix should carry, and the fall-back is exactly as
10074        // correct, only slower.
10075        if self.select_reads_policy_subject_table(stmt) {
10076            return Ok(None);
10077        }
10078        // r1058 — a WITH list this path never materialises: the CTE
10079        // name would be resolved as a physical relation and error
10080        // ("relation \"big\" does not exist" over the extended
10081        // protocol, caught by the perm-runner's wire legs). The
10082        // materialising fallback owns CTE execution.
10083        if !stmt.ctes.is_empty() {
10084            return Ok(None);
10085        }
10086        // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
10087        // tables` and kin) exist only as synth arms on the
10088        // materialising path; claiming one here errored "relation
10089        // does not exist" over the extended protocol for a query the
10090        // simple protocol answered. Prefix test only — a genuinely
10091        // missing relation must keep erroring in-path.
10092        if from.primary.name.starts_with("__spg_")
10093            || from
10094                .joins
10095                .iter()
10096                .any(|j| j.table.name.starts_with("__spg_"))
10097        {
10098            return Ok(None);
10099        }
10100        // r1058 — decline partitioned / inheritance parents, same
10101        // shape of bug as the RLS decline above: this path scans the
10102        // named table's own (empty) heap, so `SELECT id, region FROM
10103        // cust` on a partition parent streamed ZERO rows over the wire
10104        // while COUNT(*) — an aggregate, materialised below — said 3.
10105        // Caught by the perm-runner's server permutations; the
10106        // materialising fallback expands children correctly.
10107        if crate::partition::has_children(self.active_catalog(), &from.primary.name)
10108            || from
10109                .joins
10110                .iter()
10111                .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
10112        {
10113            return Ok(None);
10114        }
10115        // v7.39 (round 790) — single-table SELECTs stream too. This
10116        // gate said "joins only" because the path was written for
10117        // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
10118        // fell to the materialising fallback, which builds the whole
10119        // `Vec<Row<'static>>` and only then iterates it. Measured on
10120        // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
10121        // reached through a one-row JOIN — 2.6x, purely for lacking a
10122        // join. The deferred-join structure handles one source as the
10123        // degenerate stride-1 case, so the walk below is unchanged.
10124        let _single_table = from.joins.is_empty();
10125        // An ORDER BY that the bounded sort can serve streams; everything
10126        // else still falls to the materialising fallback below.
10127        // r1025 — an ordering the index already holds needs no sort at all.
10128        // Tried before the spill sort, which is the path it replaces.
10129        if !stmt.order_by.is_empty()
10130            && from.joins.is_empty()
10131            && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
10132        {
10133            return Ok(Some(n));
10134        }
10135        if !stmt.order_by.is_empty()
10136            && from.joins.is_empty()
10137            && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
10138        {
10139            return Ok(Some(n));
10140        }
10141        // r1031 — integer keys carried inline instead of an `OrderKey`
10142        // vector per row. Tried AFTER the spill sort on purpose: this lane
10143        // buffers the whole answer, so anything the spill path would take
10144        // must keep taking it rather than be turned back into an in-memory
10145        // sort that answers with a budget error.
10146        if !stmt.order_by.is_empty()
10147            && from.joins.is_empty()
10148            && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
10149        {
10150            return Ok(Some(n));
10151        }
10152        if !stmt.order_by.is_empty()
10153            || stmt.limit.is_some()
10154            || stmt.offset.is_some()
10155            || stmt.having.is_some()
10156            || stmt.group_by.is_some()
10157            || stmt.distinct
10158            || !stmt.unions.is_empty()
10159            || stmt.limit_with_ties
10160        {
10161            return Ok(None);
10162        }
10163        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
10164            return Ok(None);
10165        }
10166        // No window / SRF on the streaming path.
10167        if select_has_window(stmt) {
10168            return Ok(None);
10169        }
10170        if stmt
10171            .items
10172            .iter()
10173            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
10174        {
10175            return Ok(None);
10176        }
10177        // v7.37 (round 831) — a joinless FROM over a plain stored table
10178        // never needs the deferred structure, and building one costs the
10179        // whole table. `materialise_table_ref_filtered` clones every row
10180        // into a `Vec<Row<'static>>` before anything is filtered or
10181        // projected, so peak cost tracks the TABLE, not the result:
10182        // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
10183        // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
10184        // projection saving nothing, while an arithmetic projection — which
10185        // the shape gates decline, so it materialises through the ordinary
10186        // executor — cost +21 MB.
10187        //
10188        // Scanning in batches and releasing each one is what `cursor_fill`
10189        // already does for a lazy cursor, and it is the same walk: resume
10190        // from a slot, take visible rows, evaluate, hand them over, drop
10191        // them. Round 800's finding stands and is why this reads rows OUT
10192        // rather than seeding the join by index — touching the stored
10193        // `PersistentVec` in place makes the whole table resident, which is
10194        // worse than the copy. Each batch is copied, then freed.
10195        if from.joins.is_empty()
10196            && from.primary.unnest_expr.is_none()
10197            && from.primary.lateral_subquery.is_none()
10198            && from.primary.as_of_segment.is_none()
10199            && from.primary.generate_series_args.is_none()
10200            && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
10201        {
10202            return Ok(Some(n));
10203        }
10204        // Build the deferred join under the regular byte budget.
10205        let mut budget = ByteBudget::new(self.max_query_bytes);
10206        let deferred = {
10207            let mut needed = alloc::collections::BTreeSet::new();
10208            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10209            self.build_joined_filtered_rows(
10210                from,
10211                stmt.where_.as_ref(),
10212                cancel,
10213                if prunable { Some(&needed) } else { None },
10214                &mut budget,
10215            )?
10216        };
10217        let combined_schema = &deferred.combined_schema;
10218        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10219        // `::regclass` / enum cast in a joined projection or HAVING needs it.
10220        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10221        // the same predicate the unjoined shape carries.
10222        let joined_sess = self.dml_session();
10223        // v7.38.18 — and the DIALECT. This context carried the catalog and
10224        // the session and not the one field that decides how text
10225        // compares, so a joined row was evaluated in PostgreSQL
10226        // semantics inside a MySQL session.
10227        //
10228        // It showed up only where the two sides had DIFFERENT text types:
10229        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10230        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10231        // were fine and the same comparison inside one table was fine.
10232        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10233        // so the wrong semantics were invisible until a CHAR's padding
10234        // had to be stripped and PostgreSQL's arm does not strip it.
10235        //
10236        // `with_engine` is what sets it; the next line already reaches
10237        // for `self.backslash_escapes`, so the dialect was in hand.
10238        let ctx = EvalContext::new(combined_schema, None)
10239            .with_catalog(self.active_catalog())
10240            .with_engine(self)
10241            .with_session(&joined_sess);
10242        let projection = build_projection(
10243            &stmt.items,
10244            combined_schema,
10245            "",
10246            self.speaks_mysql,
10247            Some(self.active_catalog()),
10248        )?;
10249        // Every projection item must be a bound qualified column —
10250        // anything that needs `eval_expr_with_correlated` keeps the
10251        // materialising path.
10252        let bound_pos = |e: &Expr| -> Option<usize> {
10253            match e {
10254                // v7.39 (round 822) — an UNQUALIFIED column resolves here
10255                // too. The `qualifier.is_some()` guard this replaces meant
10256                // `SELECT pad FROM big` — the commonest projection there is
10257                // — never reached the streaming walk: it fell out at this
10258                // gate and re-ran on the materialising path, after the
10259                // deferred join structure had already been built and paid
10260                // for. Measured (round 821, statement_timeout=120 over 400k
10261                // rows): `big.pad` and `b.pad` streamed and cancelled at
10262                // ~65k rows in 0.14 s, while bare `pad` ran to completion in
10263                // 0.80 s with the timeout never consulted. `find_column_pos`
10264                // has always handled the unqualified case (it falls through
10265                // to a by-name match), so the guard narrowed the gate for no
10266                // reason it recorded.
10267                Expr::Column(c) => eval::find_column_pos(c, &ctx),
10268                _ => None,
10269            }
10270        };
10271        let proj_decomposed: Vec<(usize, usize)> = {
10272            let mut out = Vec::with_capacity(projection.len());
10273            for p in &projection {
10274                let Some(abs) = bound_pos(&p.expr) else {
10275                    return Ok(None);
10276                };
10277                let Some(k) = deferred
10278                    .offsets
10279                    .partition_point(|&o| o <= abs)
10280                    .checked_sub(1)
10281                else {
10282                    return Ok(None);
10283                };
10284                out.push((k, abs - deferred.offsets[k]));
10285            }
10286            out
10287        };
10288        // Emit columns once.
10289        let columns: Vec<ColumnSchema> = projection
10290            .iter()
10291            // v7.39 (read01 round 54) — keep the column's enum identity through
10292            // the projection (it lives outside the DataType lattice), or a
10293            // derived table / UNION / windowed result forgets it and any outer
10294            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
10295            .map(|p| p.to_column_schema())
10296            .collect();
10297        emit(crate::StreamItem::Header(&columns))?;
10298        let sources_ref = &deferred.sources;
10299        let stride = deferred.stride;
10300        let survivors_ref = &deferred.survivors;
10301        let n_surv = if stride == 0 {
10302            0
10303        } else {
10304            survivors_ref.len() / stride
10305        };
10306        // Reused per-row cell-ref scratch — pushes are zero-alloc
10307        // after the first row.
10308        let null_value = Value::Null;
10309        let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
10310        let mut count: usize = 0;
10311        for surv_i in 0..n_surv {
10312            if surv_i.is_multiple_of(256) {
10313                cancel.check()?;
10314            }
10315            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10316            cell_refs.clear();
10317            for &(k, col_in_src) in &proj_decomposed {
10318                let ri = tuple[k];
10319                let v: &Value = if ri == usize::MAX {
10320                    &null_value
10321                } else {
10322                    sources_ref[k]
10323                        .get(ri)
10324                        .and_then(|r| r.values.get(col_in_src))
10325                        .unwrap_or(&null_value)
10326                };
10327                cell_refs.push(v);
10328            }
10329            emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
10330            count += 1;
10331        }
10332        Ok(Some(count))
10333    }
10334
10335    fn exec_joined_select(
10336        &self,
10337        stmt: &SelectStatement,
10338        from: &FromClause,
10339        cancel: CancelToken<'_>,
10340    ) -> Result<QueryResult, EngineError> {
10341        // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
10342        // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
10343        // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
10344        // FROM B WHERE B.k = A.k)` into
10345        //   SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
10346        //   WHERE B.k IS NULL
10347        // The general join executor builds a hash, probes every outer
10348        // tuple, materialises (left_padded_with_null) for every miss,
10349        // then runs the aggregate over the result set. For COUNT(*) we
10350        // only need the count — skip the tuple materialisation. Build
10351        // a HashSet of B's unique join values, scan A's PK index, and
10352        // increment the counter on each miss. PG's Merge Anti-Join
10353        // does roughly this; ours becomes a simple HashSet probe.
10354        if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
10355            return Ok(out);
10356        }
10357        // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
10358        // When ORDER BY is on an indexed primary column, walking the
10359        // btree in the requested direction lets the streamer break
10360        // after `LIMIT + OFFSET` survivors without ever materialising
10361        // the rest of the join — the 80 ms `mailrs_prod_not_exists`
10362        // plateau is exactly this shape.
10363        if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
10364            return Ok(out);
10365        }
10366        // v7.30.3 (mailrs round-26) — the bounded single-join path
10367        // first; peak memory scales with LIMIT instead of the table.
10368        if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
10369            return Ok(out);
10370        }
10371        // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
10372        // WHERE materialisation to the shared helper so the LATERAL
10373        // / UNNEST / regular-catalog paths route through one place.
10374        // (`build_joined_filtered_rows` carries LATERAL support as
10375        // of Phase 3.P0-41.) Downstream we still handle aggregate /
10376        // projection / ORDER BY / DISTINCT / LIMIT inline because
10377        // those depend on the SelectStatement's items list.
10378        let mut budget = ByteBudget::new(self.max_query_bytes);
10379        let deferred = {
10380            let mut needed = alloc::collections::BTreeSet::new();
10381            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10382            self.build_joined_filtered_rows(
10383                from,
10384                stmt.where_.as_ref(),
10385                cancel,
10386                if prunable { Some(&needed) } else { None },
10387                &mut budget,
10388            )?
10389        };
10390        let combined_schema = &deferred.combined_schema;
10391        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10392        // `::regclass` / enum cast in a joined projection or HAVING needs it.
10393        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10394        // the same predicate the unjoined shape carries.
10395        let joined_sess = self.dml_session();
10396        // v7.38.18 — and the DIALECT. This context carried the catalog and
10397        // the session and not the one field that decides how text
10398        // compares, so a joined row was evaluated in PostgreSQL
10399        // semantics inside a MySQL session.
10400        //
10401        // It showed up only where the two sides had DIFFERENT text types:
10402        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10403        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10404        // were fine and the same comparison inside one table was fine.
10405        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10406        // so the wrong semantics were invisible until a CHAR's padding
10407        // had to be stripped and PostgreSQL's arm does not strip it.
10408        //
10409        // `with_engine` is what sets it; the next line already reaches
10410        // for `self.backslash_escapes`, so the dialect was in hand.
10411        let ctx = EvalContext::new(combined_schema, None)
10412            .with_catalog(self.active_catalog())
10413            .with_engine(self)
10414            .with_session(&joined_sess);
10415        // Aggregate path: handle GROUP BY / aggregate calls over the
10416        // joined+filtered rows.
10417        if aggregate::uses_aggregate_in(stmt, self.speaks_mysql) {
10418            // v7.32 (P4 borrow channel, increment 2) — borrow each
10419            // surviving join tuple as a RowRef::Tuple; the aggregate
10420            // engine reads source cells by reference (bound fast path =
10421            // zero clone) instead of consuming materialised combined
10422            // Rows. This is where the +211k materialise_tuple_vals
10423            // clones disappear for the join+aggregate shape.
10424            let refs = deferred.row_refs();
10425            // v7.29 — a per-query memo so correlated scalar
10426            // subqueries batch-evaluate once (group map) instead of
10427            // executing per group.
10428            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
10429            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
10430                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
10431                    .map_err(|err| match err {
10432                        EngineError::Eval(ev) => ev,
10433                        other => eval::EvalError::TypeMismatch {
10434                            detail: alloc::format!("{other}"),
10435                        },
10436                    })
10437            };
10438            let agg = aggregate::run(
10439                stmt,
10440                crate::join::AggRows::Refs(&refs),
10441                combined_schema,
10442                None,
10443                Some(&agg_correlated),
10444                self.parallel_runner.0.as_deref(),
10445                Some(self.active_catalog()),
10446                Some(self),
10447            )?;
10448            return self.finish_agg_result(agg, stmt, cancel);
10449        }
10450
10451        let projection = build_projection(
10452            &stmt.items,
10453            combined_schema,
10454            "",
10455            self.speaks_mysql,
10456            Some(self.active_catalog()),
10457        )?;
10458        // v7.39 (round 734) — a set-returning projection over a JOIN.
10459        // This executor's projection loop treats every item as a scalar,
10460        // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
10461        // "function unnest(integer[]) does not exist" where PG expands
10462        // it. The row-set executor already carries the full SRF pipeline
10463        // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
10464        // sharding): materialise the joined survivors and hand over. The
10465        // WHERE is cleared — the join already applied it, and combined
10466        // columns resolve identically in both executors.
10467        if !self.srf_target_idxs(&projection).is_empty() {
10468            let refs = deferred.row_refs();
10469            let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
10470            let mut s2 = stmt.clone();
10471            s2.where_ = None;
10472            let schema = combined_schema.clone();
10473            return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
10474        }
10475        // v7.33 (P4 borrow channel, increment 3) — project directly off
10476        // the deferred row-index tuples instead of materialising an
10477        // intermediate combined Row per survivor. A bound qualified
10478        // column is read by reference (`RowRef::get` → `tuple_value`) and
10479        // cloned ONCE into the output row; the old `materialise()` (a full
10480        // combined Row plus a source→intermediate clone per referenced
10481        // cell, for every survivor) is gone. A row materialises on demand
10482        // only when a projection or ORDER BY expression needs the eval
10483        // path (subquery / function / arithmetic / unqualified column).
10484        // Same bind-once classification the aggregate input fast path uses
10485        // (`accumulate_groups`), reading the same `tuple_value` mapping the
10486        // differential gate already covers.
10487        let refs = deferred.row_refs();
10488        let bound_pos = |e: &Expr| -> Option<usize> {
10489            match e {
10490                Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
10491                _ => None,
10492            }
10493        };
10494        let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
10495        let all_proj_bound = proj_pos.iter().all(Option::is_some);
10496        // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
10497        // pre-decompose each bound projection position into
10498        // `(source_k, col_in_source)` so the per-row column read
10499        // skips the per-cell `tuple_value` partition_point + slice
10500        // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
10501        // calls) that walk dominated; this version reaches into
10502        // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
10503        let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
10504            .iter()
10505            .map(|p| {
10506                p.and_then(|abs| {
10507                    let k = deferred
10508                        .offsets
10509                        .partition_point(|&o| o <= abs)
10510                        .checked_sub(1)?;
10511                    Some((k, abs - deferred.offsets[k]))
10512                })
10513            })
10514            .collect();
10515        // v7.39 (round 962) — which projection items are whole-row
10516        // references, and to which join source. The test is
10517        // `locate_column` declining the name, which is the SAME resolver
10518        // the evaluation path uses, so this cannot drift from it: a real
10519        // column carrying an alias's name resolves to a position and is
10520        // not reported here. The source index comes from the alias
10521        // prefix, the way the combined schema names its columns.
10522        let whole_row_src: Vec<Option<usize>> = projection
10523            .iter()
10524            .map(|p| {
10525                let Expr::Column(c) = &p.expr else {
10526                    return None;
10527                };
10528                if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
10529                    return None;
10530                }
10531                let prefix = alloc::format!("{name}.", name = c.name);
10532                let abs = deferred
10533                    .combined_schema
10534                    .iter()
10535                    .position(|s| s.name.starts_with(&prefix))?;
10536                deferred
10537                    .offsets
10538                    .partition_point(|&o| o <= abs)
10539                    .checked_sub(1)
10540            })
10541            .collect();
10542        // ORDER BY (when present) still evaluates against a materialised
10543        // Row — keep the order-key encoder correct rather than fork it.
10544        let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
10545        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
10546        let mut proj_memo = memoize::MemoizeCache::default();
10547        let sources_ref = &deferred.sources;
10548        let stride = deferred.stride;
10549        let survivors_ref = &deferred.survivors;
10550        let n_surv = survivors_ref.len() / stride.max(1);
10551        // v7.38 (read01 B8) — streaming top-N budget (see the sibling
10552        // single-table path). Bounds this JOIN projection's accumulator
10553        // to O(keep) for `ORDER BY … LIMIT k`.
10554        let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
10555            && !stmt.distinct
10556            && !stmt.limit_with_ties
10557            && !self.env_cfg().disable_topk
10558        {
10559            stmt.limit_literal().and_then(|l| {
10560                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
10561                (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
10562            })
10563        } else {
10564            None
10565        };
10566        // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
10567        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
10568            hashbrown::HashMap::new();
10569        let distinct_hb = hashbrown::DefaultHashBuilder::default();
10570        // v7.38.13 — which output positions must NOT fold. Built once per
10571        // scan from the projection, which carries the source column's
10572        // byte-wise-ness; see `FoldSpec`.
10573        let distinct_mask = fold_mask(&projection);
10574        for surv_i in 0..n_surv {
10575            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10576            let row = &refs[surv_i];
10577            let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
10578                Some(row.as_row())
10579            } else {
10580                None
10581            };
10582            let mut values = Vec::with_capacity(projection.len());
10583            for (i, p) in projection.iter().enumerate() {
10584                if let Some((k, col_in_src)) = proj_decomposed[i] {
10585                    // v7.36 — direct (source_k, col) lookup, no
10586                    // partition_point. tuple[k] is the row index in
10587                    // sources[k]; LEFT-NULL slots are `usize::MAX`.
10588                    let ri = tuple[k];
10589                    let v: Value<'static> = if ri == usize::MAX {
10590                        Value::Null
10591                    } else {
10592                        sources_ref[k]
10593                            .get(ri)
10594                            .and_then(|r| r.values.get(col_in_src))
10595                            .cloned()
10596                            .map(Value::into_owned)
10597                            .unwrap_or(Value::Null)
10598                    };
10599                    values.push(v);
10600                } else if let Some(pos) = proj_pos[i] {
10601                    // Bound but couldn't decompose (shouldn't normally
10602                    // happen — keep as a safe path).
10603                    values.push(
10604                        row.get(pos)
10605                            .cloned()
10606                            .map(Value::into_owned)
10607                            .unwrap_or(Value::Null),
10608                    );
10609                } else if let Some(k) = whole_row_src[i]
10610                    && tuple[k] == usize::MAX
10611                {
10612                    // v7.39 (round 962) — a whole-row reference to a side
10613                    // an OUTER join null-extended is NULL, not a
10614                    // composite whose fields are all NULL. PG18.4 answers
10615                    // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
10616                    // an empty cell; round 961 answered `(,)`.
10617                    //
10618                    // The evaluator below cannot tell the two apart: it
10619                    // reads the MATERIALISED combined row, where a
10620                    // null-extended side is indistinguishable from a real
10621                    // row whose every column is NULL — and that row is
10622                    // `(,)` in PG too, so guessing by "all fields NULL"
10623                    // would trade one wrong answer for another. The
10624                    // tuple, which is still in hand here, does know:
10625                    // `usize::MAX` is the sentinel the join writes for
10626                    // exactly this.
10627                    values.push(Value::Null);
10628                } else {
10629                    // Eval path — `materialised` is Some whenever any
10630                    // projection item is non-bound (need_eval_row true).
10631                    // v7.24 (round-16 B) — select-list subqueries under a
10632                    // JOIN go through the correlated-aware evaluator too.
10633                    let mrow = materialised.as_deref().expect("materialised for eval");
10634                    values.push(self.eval_expr_with_correlated(
10635                        &p.expr,
10636                        mrow,
10637                        &ctx,
10638                        cancel,
10639                        Some(&mut proj_memo),
10640                    )?);
10641                }
10642            }
10643            let out_row = Row::new(values);
10644            // v7.37.16 — streaming DISTINCT (see the scan-path twin):
10645            // probe on the projected row; duplicates skip the
10646            // build_order_keys eval and never enter `tagged`.
10647            if stmt.distinct {
10648                let bucket = seen_distinct
10649                    .entry(norm_hash_row(
10650                        &out_row,
10651                        &distinct_hb,
10652                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10653                    ))
10654                    .or_default();
10655                if bucket.iter().any(|i| {
10656                    row_eq_norm(
10657                        &tagged[i].1,
10658                        &out_row,
10659                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10660                    )
10661                }) {
10662                    continue;
10663                }
10664                bucket.push(tagged.len());
10665            }
10666            let order_keys = if stmt.order_by.is_empty() {
10667                Vec::new()
10668            } else {
10669                let mrow = materialised.as_deref().expect("materialised for order by");
10670                build_order_keys(&stmt.order_by, mrow, &ctx)?
10671            };
10672            budget.charge(approx_row_bytes(&out_row))?;
10673            tagged.push((order_keys, out_row));
10674            if let Some((k, descs)) = &topk_stream {
10675                topk_trim(&mut tagged, *k, descs);
10676            }
10677        }
10678        if !stmt.order_by.is_empty() {
10679            // v7.38 元机制 D acceptor — see other call site above.
10680            let keep = if self.env_cfg().disable_topk {
10681                None
10682            } else {
10683                stmt.limit_literal()
10684                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
10685            };
10686            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
10687            // v7.39 (round 688) — the join's ORDER BY resolves its keys
10688            // against `ctx`, which is built from `build_combined_schema`, so
10689            // this is where a declared collation reaches the sort. There was
10690            // exactly ONE resolver call in the engine before this — the
10691            // single-table scan's — which is why every other shape sorted by
10692            // bytes no matter what the schemas carried.
10693            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
10694            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
10695        }
10696        let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
10697        apply_offset_and_limit(
10698            &mut output_rows,
10699            stmt.offset_literal(),
10700            stmt.limit_literal(),
10701        );
10702        let columns: Vec<ColumnSchema> = projection
10703            .into_iter()
10704            .map(|p| p.to_column_schema())
10705            .collect();
10706        Ok(QueryResult::Rows {
10707            columns,
10708            rows: output_rows,
10709        })
10710    }
10711}
10712
10713impl Engine {
10714    /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
10715    /// by id, decodes each row body against the table's current
10716    /// schema, applies the SELECT's projection + optional WHERE +
10717    /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
10718    /// / ORDER BY are unsupported on this path (STABILITY carve-
10719    /// out); operators wanting them should restore the segment
10720    /// into a regular table first.
10721    fn exec_select_as_of_segment(
10722        &self,
10723        stmt: &SelectStatement,
10724        from: &spg_sql::ast::FromClause,
10725        segment_id: u32,
10726    ) -> Result<QueryResult, EngineError> {
10727        // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
10728        // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
10729        if !from.joins.is_empty()
10730            || stmt.group_by.is_some()
10731            || stmt.having.is_some()
10732            || !stmt.unions.is_empty()
10733            || !stmt.order_by.is_empty()
10734            || stmt.offset.is_some()
10735            || stmt.distinct
10736            || aggregate::uses_aggregate_in(stmt, self.speaks_mysql)
10737        {
10738            return Err(EngineError::Unsupported(
10739                "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
10740                 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
10741                    .into(),
10742            ));
10743        }
10744        let table = self
10745            .active_catalog()
10746            .get(&from.primary.name)
10747            .ok_or_else(|| StorageError::TableNotFound {
10748                name: from.primary.name.clone(),
10749            })?;
10750        let schema = table.schema().clone();
10751        let schema_cols = &schema.columns;
10752        let alias = from
10753            .primary
10754            .alias
10755            .as_deref()
10756            .unwrap_or(from.primary.name.as_str());
10757        let ctx = self.ev_ctx(schema_cols, Some(alias));
10758        let seg = self
10759            .active_catalog()
10760            .cold_segment(segment_id)
10761            .ok_or_else(|| {
10762                EngineError::Unsupported(alloc::format!(
10763                    "AS OF SEGMENT: cold segment {segment_id} not registered"
10764                ))
10765            })?;
10766        let mut out_rows: Vec<Row<'static>> = Vec::new();
10767        let mut limit_remaining: Option<usize> =
10768            stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
10769        for (_key, body) in seg.scan() {
10770            let (row, _consumed) =
10771                spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
10772                    .map_err(EngineError::Storage)?;
10773            if let Some(where_expr) = &stmt.where_ {
10774                let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
10775                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
10776                    continue;
10777                }
10778            }
10779            // Projection.
10780            let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
10781            out_rows.push(projected);
10782            if let Some(rem) = limit_remaining.as_mut() {
10783                if *rem == 0 {
10784                    out_rows.pop();
10785                    break;
10786                }
10787                *rem -= 1;
10788            }
10789        }
10790        // Output column schema: derive from SELECT items.
10791        let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
10792        Ok(QueryResult::Rows {
10793            columns,
10794            rows: out_rows,
10795        })
10796    }
10797
10798    /// v6.10.2 — simple-path WHERE eval that doesn't go through
10799    /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
10800    /// scan paths predicate against a snapshot frozen segment, no
10801    /// cross-row state.
10802    fn eval_expr_simple(
10803        &self,
10804        expr: &Expr,
10805        row: &Row<'static>,
10806        ctx: &EvalContext,
10807    ) -> Result<Value<'static>, EngineError> {
10808        let cancel = CancelToken::none();
10809        self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
10810    }
10811}
10812
10813// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
10814
10815/// One row-producing projection: an expression to evaluate, the resulting
10816/// column's user-visible name, its inferred type, and nullability.
10817#[derive(Debug, Clone)]
10818pub(crate) struct ProjectedItem {
10819    pub(crate) expr: Expr,
10820    pub(crate) output_name: String,
10821    pub(crate) ty: DataType,
10822    pub(crate) nullable: bool,
10823    /// v7.39 (read01 round 54) — a projected enum column keeps its enum
10824    /// identity. Enum-ness lives outside the DataType lattice (the value is a
10825    /// Text), so a projection that dropped this made the RESULT schema forget
10826    /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
10827    /// that schema, silently fell back to TEXT order instead of member order.
10828    pub(crate) user_enum_type: Option<String>,
10829    /// v7.39 (round 425) — a projected MySQL temporal column keeps its
10830    /// declared fractional-seconds precision, so the renderer can pad to
10831    /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
10832    /// a whole second). Like `user_enum_type` this lives outside the
10833    /// DataType lattice, so a projection that dropped it made the RESULT
10834    /// schema forget how wide the fraction should print.
10835    pub(crate) mysql_fsp: Option<u8>,
10836    /// v7.39 (round 688) — and its declared collation, the third thing to
10837    /// live outside the DataType lattice and the third to be lost the same
10838    /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
10839    /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
10840    /// projection rebuilt the output column and the ORDER BY resolves
10841    /// against THAT schema.
10842    pub(crate) collation_name: Option<String>,
10843    /// v7.38.13 — and whether this position must NOT fold when DISTINCT
10844    /// de-dups it. The fourth thing to live outside the DataType lattice
10845    /// and the fourth to be lost the same way: a column declared
10846    /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
10847    /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
10848    /// returns two.
10849    ///
10850    /// A BOOL rather than the `Collation` enum on purpose. The enum's
10851    /// storage default is `Binary`, but the FOLD default under MySQL is
10852    /// case-insensitive — carrying the enum would silently mean
10853    /// "exempt" for every projected expression that is not a column.
10854    /// This field states the question it answers.
10855    pub(crate) fold_exempt: bool,
10856    /// v7.38.18 — does this column's collation make trailing spaces
10857    /// insignificant? A separate question from `fold_exempt`:
10858    /// `utf8mb4_bin` is fold-exempt AND pads, `utf8mb4_0900_ai_ci`
10859    /// folds and does not. Read off the same column, at the same
10860    /// place, so the two masks cannot drift apart.
10861    pub(crate) pads: bool,
10862}
10863
10864impl ProjectedItem {
10865    /// v7.38.14 — the output column this projected item describes.
10866    ///
10867    /// There were TWENTY-ONE places converting a `ProjectedItem` into a
10868    /// `ColumnSchema`, each written as `ColumnSchema::new(..)` followed by a
10869    /// hand-picked list of attributes to copy after it, and the lists did not
10870    /// agree: six carried enum identity, the collation NAME and MySQL fsp; ten
10871    /// carried the first and last but not the name; five carried nothing at
10872    /// all. Not one carried `collation`, the enum every MySQL text comparison
10873    /// actually reads.
10874    ///
10875    /// That is how a declared collation vanished between a subquery and the
10876    /// query that selects from it: the inner SELECT's output schema claimed
10877    /// `ColumnSchema::new`'s default, which is `Binary` — a value downstream
10878    /// reads as "byte-wise ON PURPOSE" rather than as "unknown", so the loss
10879    /// presents as a deliberate declaration.
10880    ///
10881    /// One conversion, so a field added to either type has one place to be
10882    /// remembered instead of twenty-one.
10883    pub(crate) fn to_column_schema(&self) -> ColumnSchema {
10884        let mut c = ColumnSchema::new(self.output_name.clone(), self.ty, self.nullable);
10885        c.user_enum_type.clone_from(&self.user_enum_type);
10886        c.collation_name.clone_from(&self.collation_name);
10887        c.mysql_fsp = self.mysql_fsp;
10888        // `fold_exempt` is the projection's answer to the same question
10889        // `ColumnSchema::collation` answers downstream, and it was computed
10890        // from the source column. Keeping the two in step here is what stops
10891        // a de-duplication site further on from asking the schema and being
10892        // told the opposite of what the projection knew.
10893        c.collation = if self.fold_exempt {
10894            spg_storage::Collation::Binary
10895        } else {
10896            spg_storage::Collation::CaseInsensitive
10897        };
10898        c
10899    }
10900}
10901
10902/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
10903/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
10904/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
10905/// the spec's "two NULLs are not distinct"; the second is a tolerated
10906/// quirk for v1 (no NaN literals are reachable from the SQL surface).
10907/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
10908fn expr_is_aggregate_call(e: &Expr) -> bool {
10909    match e {
10910        Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
10911        Expr::AggregateOrdered { .. } => true,
10912        _ => false,
10913    }
10914}
10915
10916/// Collect distinct top-level aggregate call expressions (dedup by value). Does
10917/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
10918/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
10919/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
10920/// than today — never a regression on a working query).
10921fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
10922    if expr_is_aggregate_call(e) {
10923        if !out.iter().any(|x| x == e) {
10924            out.push(e.clone());
10925        }
10926        return;
10927    }
10928    match e {
10929        Expr::Binary { lhs, rhs, .. } => {
10930            collect_agg_exprs(lhs, out);
10931            collect_agg_exprs(rhs, out);
10932        }
10933        Expr::Unary { expr, .. }
10934        | Expr::Cast { expr, .. }
10935        | Expr::IsNull { expr, .. }
10936        | Expr::BoolTest { expr, .. }
10937        | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
10938        Expr::FunctionCall { args, .. } => {
10939            for a in args {
10940                collect_agg_exprs(a, out);
10941            }
10942        }
10943        Expr::Like { expr, pattern, .. } => {
10944            collect_agg_exprs(expr, out);
10945            collect_agg_exprs(pattern, out);
10946        }
10947        Expr::Extract { source, .. } => collect_agg_exprs(source, out),
10948        Expr::WindowFunction {
10949            args,
10950            partition_by,
10951            order_by,
10952            ..
10953        } => {
10954            for a in args {
10955                collect_agg_exprs(a, out);
10956            }
10957            for p in partition_by {
10958                collect_agg_exprs(p, out);
10959            }
10960            for (o, _, _) in order_by {
10961                collect_agg_exprs(o, out);
10962            }
10963        }
10964        _ => {}
10965    }
10966}
10967
10968/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
10969fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
10970    if expr_is_aggregate_call(e) {
10971        if let Some(idx) = aggs.iter().position(|x| x == e) {
10972            *e = Expr::Column(ColumnName {
10973                qualifier: None,
10974                name: alloc::format!("__agg{idx}"),
10975            });
10976        }
10977        return;
10978    }
10979    match e {
10980        Expr::Binary { lhs, rhs, .. } => {
10981            replace_agg_exprs(lhs, aggs);
10982            replace_agg_exprs(rhs, aggs);
10983        }
10984        Expr::Unary { expr, .. }
10985        | Expr::Cast { expr, .. }
10986        | Expr::IsNull { expr, .. }
10987        | Expr::BoolTest { expr, .. }
10988        | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
10989        Expr::FunctionCall { args, .. } => {
10990            for a in args {
10991                replace_agg_exprs(a, aggs);
10992            }
10993        }
10994        Expr::Like { expr, pattern, .. } => {
10995            replace_agg_exprs(expr, aggs);
10996            replace_agg_exprs(pattern, aggs);
10997        }
10998        Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
10999        Expr::WindowFunction {
11000            args,
11001            partition_by,
11002            order_by,
11003            ..
11004        } => {
11005            for a in args {
11006                replace_agg_exprs(a, aggs);
11007            }
11008            for p in partition_by {
11009                replace_agg_exprs(p, aggs);
11010            }
11011            for (o, _, _) in order_by {
11012                replace_agg_exprs(o, aggs);
11013            }
11014        }
11015        _ => {}
11016    }
11017}
11018
11019/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
11020/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
11021/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
11022/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
11023/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
11024/// Returns None outside the bounded subset (leaves current behaviour). Only fires
11025/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
11026/// window-only / aggregate-only queries.
11027fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
11028    if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
11029        return None;
11030    }
11031    // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
11032    if !stmt.unions.is_empty() {
11033        return None;
11034    }
11035    let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
11036    if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
11037        return None;
11038    }
11039    stmt.from.as_ref()?;
11040    // Collect the aggregate calls to hoist from projection + outer ORDER BY.
11041    let mut aggs: Vec<Expr> = Vec::new();
11042    for item in &stmt.items {
11043        if let SelectItem::Expr { expr, .. } = item {
11044            collect_agg_exprs(expr, &mut aggs);
11045        }
11046    }
11047    for ob in &stmt.order_by {
11048        collect_agg_exprs(&ob.expr, &mut aggs);
11049    }
11050    // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
11051    let mut inner_items: Vec<SelectItem> = Vec::new();
11052    for g in &group_cols {
11053        inner_items.push(SelectItem::Expr {
11054            expr: g.clone(),
11055            alias: None,
11056        });
11057    }
11058    for (i, a) in aggs.iter().enumerate() {
11059        inner_items.push(SelectItem::Expr {
11060            expr: a.clone(),
11061            alias: Some(alloc::format!("__agg{i}")),
11062        });
11063    }
11064    let inner = SelectStatement {
11065        items: inner_items,
11066        distinct: false,
11067        distinct_on: Vec::new(),
11068        unions: Vec::new(),
11069        order_by: Vec::new(),
11070        limit: None,
11071        offset: None,
11072        limit_with_ties: false,
11073        window_check_exprs: Vec::new(),
11074        ..stmt.clone()
11075    };
11076    let derived = TableRef {
11077        name: "__aggwin".into(),
11078        alias: Some("__aggwin".into()),
11079        only: false,
11080        as_of_segment: None,
11081        unnest_expr: None,
11082        unnest_column_aliases: Vec::new(),
11083        with_ordinality: false,
11084        generate_series_args: None,
11085        lateral_subquery: Some(alloc::boxed::Box::new(inner)),
11086        jsonb_each_text_arg: None,
11087        table_fn_call: None,
11088        rows_from: None,
11089        json_table: None,
11090        scalar_fn_item: false,
11091    };
11092    // Outer window query over the derived rows: aggregates → __aggN column refs.
11093    let mut outer_items = stmt.items.clone();
11094    for item in &mut outer_items {
11095        if let SelectItem::Expr { expr, alias } = item {
11096            // Preserve PG's column label for a bare aggregate projection.
11097            if alias.is_none()
11098                && let Expr::FunctionCall { name, .. } = expr
11099                && crate::aggregate::is_aggregate_name(name)
11100            {
11101                *alias = Some(name.to_ascii_lowercase());
11102            }
11103            replace_agg_exprs(expr, &aggs);
11104        }
11105    }
11106    let mut outer_order = stmt.order_by.clone();
11107    for ob in &mut outer_order {
11108        replace_agg_exprs(&mut ob.expr, &aggs);
11109    }
11110    let mut outer_distinct_on = stmt.distinct_on.clone();
11111    for e in &mut outer_distinct_on {
11112        replace_agg_exprs(e, &aggs);
11113    }
11114    Some(SelectStatement {
11115        locking: None,
11116        ctes: Vec::new(),
11117        distinct: stmt.distinct,
11118        distinct_on: outer_distinct_on,
11119        items: outer_items,
11120        from: Some(FromClause {
11121            primary: derived,
11122            joins: Vec::new(),
11123        }),
11124        where_: None,
11125        group_by: None,
11126        group_by_all: false,
11127        having: None,
11128        unions: Vec::new(),
11129        order_by: outer_order,
11130        limit: stmt.limit.clone(),
11131        offset: stmt.offset.clone(),
11132        limit_with_ties: stmt.limit_with_ties,
11133        window_check_exprs: Vec::new(),
11134    })
11135}
11136
11137/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
11138/// membership.
11139///
11140/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
11141/// there?", and all four answered by scanning the whole right side once per
11142/// left row. The cost was (left rows x right rows), which is why
11143/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
11144/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
11145/// row that does not pays for all of it. Over 100k left rows, raising the
11146/// right side from 100 to 10,000 took 35 ms to 2848.
11147///
11148/// This is the shape round 485 already solved for DISTINCT, and it reuses
11149/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
11150/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
11151/// every bucket with the exact comparator, so a collision costs time and
11152/// never an answer.
11153struct PeerIndex<'r> {
11154    bh: hashbrown::DefaultHashBuilder,
11155    buckets: hashbrown::HashMap<u64, Vec<usize>>,
11156    rows: &'r [Row<'static>],
11157    fold: FoldSpec<'r>,
11158}
11159
11160impl<'r> PeerIndex<'r> {
11161    fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
11162        // ONE hasher for the whole pass: the default builder is seeded per
11163        // instance, so a fresh one per row would put equal rows in different
11164        // buckets.
11165        let bh = hashbrown::DefaultHashBuilder::default();
11166        let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
11167            hashbrown::HashMap::with_capacity(rows.len());
11168        for (i, r) in rows.iter().enumerate() {
11169            buckets
11170                .entry(norm_hash_row(r, &bh, fold))
11171                .or_default()
11172                .push(i);
11173        }
11174        Self {
11175            bh,
11176            buckets,
11177            rows,
11178            fold,
11179        }
11180    }
11181
11182    fn contains(&self, r: &Row<'static>) -> bool {
11183        let h = norm_hash_row(r, &self.bh, self.fold);
11184        self.buckets
11185            .get(&h)
11186            .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
11187    }
11188
11189    /// Remove ONE occurrence, so the multiset forms cancel row for row the
11190    /// way the pool they replaced did.
11191    fn take_one(&mut self, r: &Row<'static>) -> bool {
11192        let h = norm_hash_row(r, &self.bh, self.fold);
11193        let Some(b) = self.buckets.get_mut(&h) else {
11194            return false;
11195        };
11196        let Some(pos) = b
11197            .iter()
11198            .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
11199        else {
11200            return false;
11201        };
11202        b.swap_remove(pos);
11203        true
11204    }
11205}
11206
11207pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
11208    dedup_by_row(rows, |r| r, fold)
11209}
11210
11211/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
11212/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
11213/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
11214/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
11215/// order is preserved, and correctness needs only the one-way guarantee
11216/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
11217/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
11218fn dedup_by_row<T>(
11219    items: Vec<T>,
11220    row_of: impl Fn(&T) -> &Row<'static>,
11221    fold: FoldSpec<'_>,
11222) -> Vec<T> {
11223    if items.len() <= 32 {
11224        let mut out: Vec<T> = Vec::with_capacity(items.len());
11225        for it in items {
11226            if !out
11227                .iter()
11228                .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
11229            {
11230                out.push(it);
11231            }
11232        }
11233        return out;
11234    }
11235    // ONE BuildHasher instance for the whole pass — the default builder
11236    // is randomly seeded PER INSTANCE, so a fresh one per row would give
11237    // equal rows different hashes and never dedup.
11238    let bh = hashbrown::DefaultHashBuilder::default();
11239    let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
11240    let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
11241        hashbrown::HashMap::with_capacity(items.len());
11242    for it in items {
11243        let h = norm_hash_row(row_of(&it), &bh, fold);
11244        let bucket = buckets.entry(h).or_default();
11245        if !bucket
11246            .iter()
11247            .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
11248        {
11249            bucket.push(out.len());
11250            out.push(it);
11251        }
11252    }
11253    out
11254}
11255
11256/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
11257/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
11258/// rows may collide (buckets are re-checked with the exact comparator).
11259///
11260/// Domain design mirrors `value_cmp`'s equivalence classes:
11261/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
11262///   shares one domain: a value that is an integer fitting i64 hashes the
11263///   i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
11264///   anything else hashes the f64 approximation computed by THE SAME
11265///   formula the value_cmp float arms use (`numeric_to_f64`), so
11266///   `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
11267///   hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
11268///   Known un-closable corner: an integer in [2^53, 2^63) can compare
11269///   Equal to a float via value_cmp's lossy f64 arm while hashing in the
11270///   exact-i64 domain — mixed int/float rows at that magnitude may miss a
11271///   dedup (PG itself compares int8↔float8 in the lossy float8 domain).
11272/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
11273///   compares them blank-insensitively; plain Text pairs that differ only
11274///   in trailing blanks merely collide and are separated exactly).
11275/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
11276///   hash their fields under a distinct tag.
11277/// - Everything value_cmp falls back to debug-format ordering for
11278///   (Json, arrays, vectors, geometry, ranges, …) shares one constant
11279///   bucket — degrades to the exact linear scan, never wrong.
11280fn norm_hash_row(
11281    row: &Row<'static>,
11282    bh: &hashbrown::DefaultHashBuilder,
11283    fold: FoldSpec<'_>,
11284) -> u64 {
11285    norm_hash_values(&row.values, bh, fold)
11286}
11287
11288/// v7.39 (round 485) — the same hash over a bare value slice, so the
11289/// DISTINCT probe can run against a reused buffer instead of demanding a
11290/// `Row` that has to be allocated first (see `values_eq_norm`).
11291fn norm_hash_values(
11292    values: &[Value<'static>],
11293    bh: &hashbrown::DefaultHashBuilder,
11294    fold: FoldSpec<'_>,
11295) -> u64 {
11296    use core::hash::{BuildHasher, Hash, Hasher};
11297    let mut h = bh.build_hasher();
11298    for (i, v) in values.iter().enumerate() {
11299        // v7.39 (round 410) — hash the folded key when the MySQL collation
11300        // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
11301        // `'A'` vs `'a '`) share a hash bucket.
11302        //
11303        // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
11304        // byte-wise column that folded here while the comparator did not
11305        // would scatter equal rows across buckets and stop de-duplicating
11306        // at all; the hash and the comparator have to read the same mask.
11307        if fold.folds(i)
11308            && let Some(folded) = mysql_dedup_fold(v, fold.pads_at(i))
11309        {
11310            folded.hash(&mut h);
11311            continue;
11312        }
11313        norm_hash_value(v, &mut h);
11314    }
11315    h.finish()
11316}
11317
11318/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
11319///
11320/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
11321const fn pow10_i128(p: u16) -> Option<i128> {
11322    const P: [i128; 39] = {
11323        let mut t = [1i128; 39];
11324        let mut i = 1;
11325        while i < 39 {
11326            t[i] = t[i - 1] * 10;
11327            i += 1;
11328        }
11329        t
11330    };
11331    if (p as usize) < P.len() {
11332        Some(P[p as usize])
11333    } else {
11334        None
11335    }
11336}
11337
11338fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
11339    const TAG_NULL: u8 = 0;
11340    const TAG_BOOL: u8 = 1;
11341    const TAG_NUM_I64: u8 = 2;
11342    const TAG_NUM_F64: u8 = 3;
11343    const TAG_TEXT: u8 = 4;
11344    const TAG_DATE: u8 = 6;
11345    const TAG_TIME: u8 = 7;
11346    const TAG_TIMESTAMP: u8 = 8;
11347    const TAG_TIMETZ: u8 = 10;
11348    const TAG_UUID: u8 = 11;
11349    const TAG_MONEY: u8 = 12;
11350    const TAG_BYTES: u8 = 13;
11351    const TAG_INTERVAL: u8 = 14;
11352    const TAG_CHAR1: u8 = 15;
11353    const TAG_OPAQUE: u8 = 255;
11354    // One shared writer for the numeric family: an integer value
11355    // representable as i64 goes exact (round-trip probe — no_std, so no
11356    // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
11357    // through 0i64, folding it into 0.0 as value_cmp requires.
11358    let num_f64 = |h: &mut H, x: f64| {
11359        if x.is_nan() {
11360            h.write_u8(TAG_NUM_F64);
11361            h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
11362            return;
11363        }
11364        const TWO63: f64 = 9_223_372_036_854_775_808.0;
11365        if (-TWO63..TWO63).contains(&x) {
11366            #[allow(clippy::cast_possible_truncation)]
11367            let n = x as i64;
11368            #[allow(clippy::cast_precision_loss)]
11369            if (n as f64) == x {
11370                h.write_u8(TAG_NUM_I64);
11371                h.write_i64(n);
11372                return;
11373            }
11374        }
11375        h.write_u8(TAG_NUM_F64);
11376        h.write_u64(x.to_bits());
11377    };
11378    match v {
11379        Value::Null => h.write_u8(TAG_NULL),
11380        Value::Bool(b) => {
11381            h.write_u8(TAG_BOOL);
11382            h.write_u8(u8::from(*b));
11383        }
11384        Value::SmallInt(n) => {
11385            h.write_u8(TAG_NUM_I64);
11386            h.write_i64(i64::from(*n));
11387        }
11388        Value::Int(n) => {
11389            h.write_u8(TAG_NUM_I64);
11390            h.write_i64(i64::from(*n));
11391        }
11392        Value::BigInt(n) => {
11393            h.write_u8(TAG_NUM_I64);
11394            h.write_i64(*n);
11395        }
11396        Value::Float(x) => num_f64(h, *x),
11397        Value::Numeric {
11398            scaled,
11399            scale,
11400            kind,
11401        } => match kind {
11402            spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
11403            spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
11404            spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
11405            spg_storage::NumericKind::Finite => {
11406                // Reduce trailing fractional zeros so 1.50 and 1.5 share a
11407                // representation, then: exact integers fitting i64 go to the
11408                // i64 domain; everything else uses numeric_to_f64 — the SAME
11409                // formula value_cmp's Numeric↔Float arm compares with.
11410                // r1044 — the reduction is required (`1.5` and `1.50` are
11411                // one value and must land in one bucket) and it used to
11412                // walk one digit at a time. That is O(scale), and scale
11413                // is not small in practice: `n / 100` on a NUMERIC
11414                // column stores `9.1900000000000000`, scale 16, so the
11415                // loop ran fourteen times PER ROW.
11416                //
11417                // Priced by ablation rather than guessed at — removing
11418                // the loop entirely took `SELECT DISTINCT n FROM t ORDER
11419                // BY n` over 400,000 rows from 52 ms to 14.8, against
11420                // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
11421                // tried first moved it not at all, which is why this one
11422                // was measured before it was written.
11423                //
11424                // Binary search over the same powers finds the whole
11425                // run of trailing zeros in at most six tests and one
11426                // division, instead of one test and one division per
11427                // digit.
11428                let (mut s, mut sc) = (*scaled, *scale);
11429                if sc > 0 && s != 0 {
11430                    let mut lo: u16 = 0;
11431                    let mut hi: u16 = sc;
11432                    while lo < hi {
11433                        let mid = (lo + hi).div_ceil(2);
11434                        match pow10_i128(mid) {
11435                            Some(p) if s % p == 0 => lo = mid,
11436                            _ => hi = mid - 1,
11437                        }
11438                    }
11439                    if lo > 0 {
11440                        if let Some(p) = pow10_i128(lo) {
11441                            s /= p;
11442                            sc -= lo;
11443                        }
11444                    }
11445                }
11446                if sc == 0 {
11447                    if let Ok(n) = i64::try_from(s) {
11448                        h.write_u8(TAG_NUM_I64);
11449                        h.write_i64(n);
11450                    } else {
11451                        num_f64(h, crate::orderby::numeric_to_f64(s, 0));
11452                    }
11453                } else {
11454                    num_f64(h, crate::orderby::numeric_to_f64(s, sc));
11455                }
11456            }
11457        },
11458        // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
11459        // value that also fits i128 reuses the Numeric path above so
11460        // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
11461        // any i128-representable value — constant bucket is safe.
11462        Value::NumericBig(b) => match b.to_i128() {
11463            Some(s) => norm_hash_value(
11464                &Value::Numeric {
11465                    scaled: s,
11466                    scale: b.scale(),
11467                    kind: spg_storage::NumericKind::Finite,
11468                },
11469                h,
11470            ),
11471            None => h.write_u8(TAG_OPAQUE),
11472        },
11473        // value_cmp compares Text↔BpChar blank-insensitively (both sides
11474        // trimmed), so both hash the trimmed bytes. Text pairs differing
11475        // only in trailing blanks collide and are split exactly in-bucket.
11476        Value::Text(s) | Value::BpChar(s) => {
11477            h.write_u8(TAG_TEXT);
11478            h.write(s.trim_end_matches(' ').as_bytes());
11479        }
11480        Value::Char1(c) => {
11481            h.write_u8(TAG_CHAR1);
11482            h.write_u8(*c);
11483        }
11484        Value::Date(d) => {
11485            h.write_u8(TAG_DATE);
11486            h.write_i32(*d);
11487        }
11488        Value::Time(t) => {
11489            h.write_u8(TAG_TIME);
11490            h.write_i64(*t);
11491        }
11492        Value::Timestamp(t) => {
11493            h.write_u8(TAG_TIMESTAMP);
11494            h.write_i64(*t);
11495        }
11496        Value::TimeTz { us, offset_secs } => {
11497            h.write_u8(TAG_TIMETZ);
11498            h.write_i64(*us);
11499            h.write_i32(*offset_secs);
11500        }
11501        Value::Uuid(u) => {
11502            h.write_u8(TAG_UUID);
11503            h.write(u);
11504        }
11505        Value::Money(c) => {
11506            h.write_u8(TAG_MONEY);
11507            h.write_i64(*c);
11508        }
11509        Value::Bytes(b) => {
11510            h.write_u8(TAG_BYTES);
11511            h.write(b.as_ref());
11512        }
11513        Value::Interval {
11514            months,
11515            days,
11516            micros,
11517            kind,
11518        } => {
11519            h.write_u8(TAG_INTERVAL);
11520            h.write_i32(*months);
11521            h.write_i32(*days);
11522            h.write_i64(*micros);
11523        }
11524        // v7.37.16 — REAL joined the numeric value_cmp family (widened
11525        // to f64, same formulas as the arms), so it hashes in the shared
11526        // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
11527        // f32→f64 is exact, so equal-under-cmp implies equal bits here.
11528        Value::Real(x) => num_f64(h, f64::from(*x)),
11529        // Json (structural equality), vector families (float rendering),
11530        // arrays / geometry / net / ranges / composites (debug-format
11531        // fallback): one constant bucket — exact linear within.
11532        _ => h.write_u8(TAG_OPAQUE),
11533    }
11534}
11535
11536/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
11537/// treats numerically-equal exact values as one regardless of type or scale
11538/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
11539/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
11540/// `Row` `==` would keep them distinct.
11541/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
11542/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
11543/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
11544/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
11545/// the folded comparison key for a text value, None for anything else (which
11546/// keeps the byte-exact `value_cmp` path).
11547fn mysql_dedup_fold(v: &Value, pads: bool) -> Option<String> {
11548    match v {
11549        // v7.38.17 — CHAR's trailing spaces are padding; TEXT's are
11550        // data. The comment above named `utf8mb4_uca1400_ai_ci`, which
11551        // is MariaDB's default and PAD SPACE. SPG advertises MySQL 8.0,
11552        // whose default is NO PAD, so `'alpha'` and `'alpha  '` are two
11553        // rows to a `SELECT DISTINCT` and one to `count(DISTINCT)` was
11554        // the same question answered twice.
11555        // v7.38.18 — a CHAR's padding is the TYPE's and never counts; a
11556        // TEXT's is the collation's, which `pads` carries per position.
11557        Value::BpChar(s) => Some(spg_storage::mysql_compare_fold_char(s)),
11558        Value::Text(s) if pads => Some(spg_storage::mysql_compare_fold_char(s)),
11559        Value::Text(s) => Some(spg_storage::mysql_compare_fold(s)),
11560        _ => None,
11561    }
11562}
11563
11564/// v7.39 (round 485) — how many projected rows the single-table scan
11565/// builds, and how many of those the DISTINCT probe throws away again.
11566///
11567/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
11568/// 21 % of all samples in malloc/free called straight from the scan
11569/// closure. The closure's one per-row allocation is the projected
11570/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
11571/// instructions later — but "most" is a guess until it is a number, so
11572/// these count it. (Round 480 was spent acting on an inference about a
11573/// branch that turned out never to run.)
11574/// v7.39 (round 488) — reachability counters for round 487's projection
11575/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
11576/// and a never-called-function probe rules out code layout — so the
11577/// question is whether that shape reaches this code at all, which is a
11578/// number, not an inference.
11579pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11580pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11581
11582pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11583pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
11584    core::sync::atomic::AtomicU64::new(0);
11585
11586/// v7.38.13 — how DISTINCT must compare one row of output.
11587///
11588/// The MySQL default collation folds case and trailing spaces when it
11589/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
11590/// must not fold — `e2e_mysql_collate_binary_round370` calls the
11591/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
11592/// one when the schema asked to keep them apart", and names DISTINCT as
11593/// one of the sites that has to honour it.
11594///
11595/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
11596/// value in a MySQL session, because a bool cannot see a column. The
11597/// GROUP BY path consults the schema and was right all along; the test
11598/// only ever exercised that spelling, so the DISTINCT hole was never
11599/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
11600///
11601/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
11602/// which is what a caller with no schema to offer gets.
11603#[derive(Clone, Copy)]
11604pub(crate) struct FoldSpec<'c> {
11605    mysql: bool,
11606    binary: &'c [bool],
11607    /// v7.38.18 — the padding mask, in lockstep with `binary`. Read the
11608    /// note on `folds`: a hash and its comparator must consult the same
11609    /// masks or equal rows scatter across buckets.
11610    pads: &'c [bool],
11611}
11612
11613impl<'c> FoldSpec<'c> {
11614    /// No column information — every Text position folds under MySQL.
11615    pub(crate) const fn dialect(mysql: bool) -> Self {
11616        Self {
11617            mysql,
11618            binary: &[],
11619            pads: &[],
11620        }
11621    }
11622
11623    /// The mask read off the output columns.
11624    pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
11625        Self {
11626            mysql,
11627            binary,
11628            pads: &[],
11629        }
11630    }
11631
11632    /// The masks read off the output columns — fold-exemption AND
11633    /// padding, which are different questions about the same collation.
11634    pub(crate) fn of_masks(mysql: bool, binary: &'c [bool], pads: &'c [bool]) -> Self {
11635        Self {
11636            mysql,
11637            binary,
11638            pads,
11639        }
11640    }
11641
11642    /// Does position `i` treat trailing spaces as insignificant?
11643    #[inline]
11644    fn pads_at(&self, i: usize) -> bool {
11645        self.pads.get(i).copied().unwrap_or(false)
11646    }
11647
11648    /// Does position `i` fold?
11649    #[inline]
11650    fn folds(&self, i: usize) -> bool {
11651        self.mysql && !self.binary.get(i).copied().unwrap_or(false)
11652    }
11653}
11654
11655/// The fold-exempt mask for a projection.
11656///
11657/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
11658/// projection rebuilds that schema through `ColumnSchema::new`, whose
11659/// collation default is `Binary` — a mask built from it would mark
11660/// EVERY column byte-wise and stop DISTINCT folding at all.
11661/// The padding mask for a projection, read off the same items as
11662/// [`fold_mask`] so the two cannot come from different places.
11663pub(crate) fn pad_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11664    projection.iter().map(|p| p.pads).collect()
11665}
11666
11667pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11668    projection.iter().map(|p| p.fold_exempt).collect()
11669}
11670
11671/// v7.38.14 — the same mask, from an OUTPUT SCHEMA instead of a
11672/// projection.
11673///
11674/// Some de-duplication sites hold `Vec<ColumnSchema>` and never see the
11675/// `ProjectedItem`s it came from. `ProjectedItem::fold_exempt` is built
11676/// from exactly this test (`select.rs`, `build_projection`), so the two
11677/// must keep answering identically -- a site that decided "byte-wise" one
11678/// way while its neighbour decided the other is how the answer came to
11679/// depend on which executor ran the query.
11680///
11681/// The direction matters: `Collation::Binary` is `ColumnSchema::new`'s
11682/// DEFAULT, so a schema rebuilt without carrying the field reads as
11683/// "byte-wise on purpose" here. That is a real trap and it has caught
11684/// five fields so far; it is why S4 of this release exists.
11685/// v7.38.18 — the padding mask from output columns, the sibling of
11686/// [`fold_mask_of_columns`]. Whether a column folds and whether it
11687/// pads are different questions about the same collation.
11688pub(crate) fn pad_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11689    columns
11690        .iter()
11691        .map(|c| crate::collate::pads_space(c.collation_name.as_deref()))
11692        .collect()
11693}
11694
11695pub(crate) fn fold_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11696    columns
11697        .iter()
11698        .map(|c| matches!(c.collation, spg_storage::Collation::Binary))
11699        .collect()
11700}
11701
11702pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
11703    values_eq_norm(&a.values, &b.values, fold)
11704}
11705
11706/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
11707/// DISTINCT probe can compare a reused projection buffer against a kept
11708/// row without building a `Row` for it.
11709pub(crate) fn values_eq_norm(
11710    a: &[Value<'static>],
11711    b: &[Value<'static>],
11712    fold: FoldSpec<'_>,
11713) -> bool {
11714    a.len() == b.len()
11715        && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
11716            if fold.folds(i)
11717                && let (Some(fx), Some(fy)) = (
11718                    mysql_dedup_fold(x, fold.pads_at(i)),
11719                    mysql_dedup_fold(y, fold.pads_at(i)),
11720                )
11721            {
11722                return fx == fy;
11723            }
11724            crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
11725        })
11726}
11727
11728/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
11729/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
11730/// order via the byte values; vectors are not sortable.
11731pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
11732    // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
11733    // so values sharing a ≥6-byte common prefix (`product_001` vs
11734    // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
11735    // order by their exact bytes instead of the old lossy f64 coarse key.
11736    // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
11737    // matches PG's default C / binary text collation. Every other type
11738    // keeps the lossless-enough `f64` fast path below.
11739    if let Value::Text(s) = v {
11740        return Ok(OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())));
11741    }
11742    // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
11743    // byte order (PG bpcharcmp under C collation), so mixed-pad values of
11744    // the same logical string order equal.
11745    if let Value::BpChar(s) = v {
11746        return Ok(OrderKey::Text(crate::orderby::CompactText::new(
11747            s.trim_end_matches(' '),
11748        )));
11749    }
11750    // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
11751    // carry the parsed value and compare it structurally (see
11752    // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
11753    if let Value::Json(s) = v {
11754        return Ok(match crate::json::parse(s) {
11755            Ok(jv) => OrderKey::Json(jv),
11756            Err(_) => OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())),
11757        });
11758    }
11759    // v7.37 — byte-orderable types PG sorts byte-wise but that have no
11760    // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
11761    // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
11762    // matching PG's network ordering.
11763    match v {
11764        Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
11765        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
11766        Value::NumericBig(b) => {
11767            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11768                spg_storage::NumericKey::from_big(b),
11769            )));
11770        }
11771        Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
11772        Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11773        Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11774        Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
11775        Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
11776            let mut key = alloc::vec::Vec::with_capacity(18);
11777            key.push(*family);
11778            key.extend_from_slice(addr);
11779            key.push(*bits);
11780            return Ok(OrderKey::Bytes(key));
11781        }
11782        _ => {}
11783    }
11784    // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
11785    // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
11786    // own OrderKey so integer arrays sort numerically; a NULL element rides to
11787    // the end via the +INF sentinel.
11788    let inf = || OrderKey::NullBig;
11789    let arr = match v {
11790        Value::IntArray(a) => Some(
11791            a.iter()
11792                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11793                .collect(),
11794        ),
11795        Value::SmallIntArray(a) => Some(
11796            a.iter()
11797                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11798                .collect(),
11799        ),
11800        Value::BigIntArray(a) => Some(
11801            a.iter()
11802                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11803                .collect(),
11804        ),
11805        Value::BoolArray(a) => Some(
11806            a.iter()
11807                .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
11808                .collect(),
11809        ),
11810        Value::TextArray(a) => Some(
11811            a.iter()
11812                .map(|o| {
11813                    o.as_ref()
11814                        .map_or_else(inf, |s| OrderKey::Text(crate::orderby::CompactText::new(s)))
11815                })
11816                .collect(),
11817        ),
11818        #[allow(clippy::cast_precision_loss)]
11819        Value::FloatArray(a) => Some(
11820            a.iter()
11821                .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
11822                .collect(),
11823        ),
11824        // r1040 — array elements take the same exact key their scalar
11825        // form does; an f64 projection here would order `{0.1}` against
11826        // `{0.1000000000000000001}` by luck.
11827        Value::NumericArray(a) => Some(
11828            a.iter()
11829                .map(|o| {
11830                    o.map_or_else(inf, |(m, s)| {
11831                        OrderKey::Numeric(alloc::boxed::Box::new(
11832                            spg_storage::NumericKey::from_numeric(
11833                                m,
11834                                s,
11835                                spg_storage::NumericKind::Finite,
11836                            ),
11837                        ))
11838                    })
11839                })
11840                .collect(),
11841        ),
11842        Value::DateArray(a) => Some(
11843            a.iter()
11844                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11845                .collect(),
11846        ),
11847        _ => None,
11848    };
11849    if let Some(elements) = arr {
11850        return Ok(OrderKey::Array(elements));
11851    }
11852    // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
11853    // right, which is exactly the lexicographic element order an Array key
11854    // already gives: `(2,'b') < (9,'a')` because the leading field decides.
11855    if let Value::Composite(fields) = v {
11856        let elements = fields
11857            .iter()
11858            .map(|(_, fv)| value_to_order_key(fv))
11859            .collect::<Result<alloc::vec::Vec<_>, _>>()?;
11860        return Ok(OrderKey::Array(elements));
11861    }
11862    // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
11863    // Projecting these to f64 (the historic path) silently collapses BigInt /
11864    // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
11865    // the wrong order for large ids and microsecond timestamps.
11866    match v {
11867        Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11868        Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
11869        Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11870        // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
11871        // integer (days / micros / cents / calendar year); TIMETZ by the
11872        // UTC-equivalent micros (local wall - offset) so the same physical
11873        // instant in different zones sorts equal.
11874        Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
11875        Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
11876        Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
11877        Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
11878        // v7.39.13 — the UTC instant is only HALF the key.
11879        //
11880        // This ordered by the instant alone, so the values that share
11881        // one were called equal and a stable sort then returned them in
11882        // insertion order — an answer, not a tie-break. Measured on
11883        // PostgreSQL 18.6 against this engine, six rows, one column:
11884        //
11885        // ```text
11886        //   PG 18.6        SPG 7.39.12
11887        //   07:00:00+01    07:00:00+01
11888        //   06:59:59+00    06:59:59+00
11889        //   09:00:00+02    07:00:00+00   <- the four that share
11890        //   07:00:00+00    02:00:00-05      07:00 UTC, in the
11891        //   02:00:00-05    09:00:00+02      order they were written
11892        //   01:00:00-06    01:00:00-06
11893        // ```
11894        //
11895        // PostgreSQL breaks the tie by OFFSET DESCENDING, and
11896        // `'07:00:00+00' = '02:00:00-05'` is FALSE there. Shifting the
11897        // instant left by 32 bits leaves room for the offset underneath
11898        // it — `i128` holds both exactly, where `i64` could not — and
11899        // `compare` in `eval::binop` orders the same pair the same way,
11900        // from the same measurement.
11901        Value::TimeTz { us, offset_secs } => {
11902            return Ok(OrderKey::Int(i128::from(spg_storage::timetz_sort_key(
11903                *us,
11904                *offset_secs,
11905            ))));
11906        }
11907        Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
11908        _ => {}
11909    }
11910    let num = match v {
11911        // Callers without NULLS FIRST/LAST context (array elements,
11912        // histogram sampling) put NULL last, as before.
11913        Value::Null => return Ok(OrderKey::NullBig),
11914        // v7.17.0 Phase 3.P0-38 — range ordering is not supported
11915        // in v7.17.0 (needs lex-then-inclusivity tiebreak).
11916        Value::Range { .. } => {
11917            return Err(EngineError::Unsupported(
11918                "ORDER BY of a range value is not supported in v7.17.0".into(),
11919            ));
11920        }
11921        // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
11922        Value::Hstore(_) => {
11923            return Err(EngineError::Unsupported(
11924                "ORDER BY of a hstore value is not supported".into(),
11925            ));
11926        }
11927        // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
11928        Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
11929            return Err(EngineError::Unsupported(
11930                "ORDER BY of a 2D array is not supported in v7.17.0".into(),
11931            ));
11932        }
11933        // r1039/r1040 — the exact canonical key, not an f64 projection.
11934        //
11935        // r1039 fixed the three specials, which carry a canonical zero in
11936        // `scaled` and so all sorted as the number 0. The projection
11937        // itself was the rest of the defect: "precision losses here only
11938        // matter for tie-breaks well past 15 significant digits" was the
11939        // comment, and the measurement disagreed — f64 called
11940        // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
11941        // returned them in insertion order. Three of ten values came back
11942        // in the wrong place against PG18.4.
11943        Value::Numeric {
11944            scaled,
11945            scale,
11946            kind,
11947        } => {
11948            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11949                spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
11950            )));
11951        }
11952        Value::Float(x) => *x,
11953        // v7.37.16 — REAL sorts by its exact f64 widening (it had no
11954        // arm and fell through to the unsupported error).
11955        Value::Real(x) => f64::from(*x),
11956        Value::Bool(b) => {
11957            if *b {
11958                1.0
11959            } else {
11960                0.0
11961            }
11962        }
11963        Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
11964            return Err(EngineError::Unsupported(
11965                "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
11966            ));
11967        }
11968        // v7.37 — PG orders INTERVAL by its total time, treating a month as
11969        // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
11970        // f64 is exact for any interval under ~285 years, and only ORDER BY
11971        // tie-breaks past that magnitude lose precision. Matches the
11972        // min/max(interval) comparator in aggregate.rs.
11973        #[allow(clippy::cast_precision_loss)]
11974        Value::Interval {
11975            months,
11976            days,
11977            micros,
11978            kind,
11979        } => {
11980            let total = i128::from(*months) * 30 * 86_400_000_000
11981                + i128::from(*days) * 86_400_000_000
11982                + i128::from(*micros);
11983            total as f64
11984        }
11985        Value::Json(_) => {
11986            return Err(EngineError::Unsupported(
11987                "ORDER BY of a JSON value is not supported — cast the document to text first"
11988                    .into(),
11989            ));
11990        }
11991        // v7.5.0 — Value is #[non_exhaustive]; future variants need
11992        // an explicit ORDER BY mapping. Surface as Unsupported until
11993        // engine support is added.
11994        _ => {
11995            return Err(EngineError::Unsupported(
11996                "ORDER BY of this value type is not supported".into(),
11997            ));
11998        }
11999    };
12000    Ok(OrderKey::Num(num))
12001}
12002
12003/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
12004/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
12005/// `EngineError` so the projection-build path keeps `UnknownQualifier`
12006/// vs `ColumnNotFound` distinct.
12007/// PG's name for the physical row identity. It is reserved there — no table
12008/// can have a column called this — which is what lets `*` skip it by name.
12009pub(crate) const CTID_COLUMN: &str = "ctid";
12010
12011/// v7.39 (round 512) — PG's system columns, in the order they are appended.
12012/// All six are reserved names there, which is what lets `*` skip them and
12013/// lets a scan tell them from a user column without a flag.
12014pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
12015
12016/// Is this name one of them?
12017pub(crate) fn is_system_column(name: &str) -> bool {
12018    SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
12019}
12020
12021/// Where the scan's appended system columns begin, if this schema carries
12022/// them: the trailing six, named in order. A catalog view with a column of
12023/// its own called `xmin` does not match, which is the point.
12024fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
12025    let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
12026    cols[start..]
12027        .iter()
12028        .zip(SYSTEM_COLUMNS)
12029        .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
12030        .then_some(start)
12031}
12032
12033/// v7.39 (round 540) — which positions `*` must skip.
12034///
12035/// The rule stays round 512's — the synthetic columns are the trailing
12036/// six of a relation's block, matched by POSITION so a genuine `xmin`
12037/// column is not lost — but a JOINED schema names its columns
12038/// `alias.column` and lays the peers out end to end, so a peer's six sit
12039/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
12040/// "trailing six" test back on the block it was written for.
12041fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
12042    let mut skip = alloc::vec![false; cols.len()];
12043    fn qualifier(n: &str) -> Option<&str> {
12044        n.rsplit_once('.').map(|(q, _)| q)
12045    }
12046    fn bare(n: &str) -> &str {
12047        n.rsplit('.').next().unwrap_or(n)
12048    }
12049    let mut i = 0;
12050    while i < cols.len() {
12051        let q = qualifier(&cols[i].name);
12052        let mut end = i;
12053        while end < cols.len() && qualifier(&cols[end].name) == q {
12054            end += 1;
12055        }
12056        if let Some(start) = (end - i)
12057            .checked_sub(SYSTEM_COLUMNS.len())
12058            .map(|off| i + off)
12059            && cols[start..end]
12060                .iter()
12061                .zip(SYSTEM_COLUMNS)
12062                .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
12063        {
12064            for s in skip.iter_mut().take(end).skip(start) {
12065                *s = true;
12066            }
12067        }
12068        i = end;
12069    }
12070    skip
12071}
12072
12073/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
12074/// read? Only then is the column materialised.
12075pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
12076    let mut found = false;
12077    crate::expr_analysis::visit_expr_columns_and_subqueries(
12078        e,
12079        &mut |c| {
12080            if is_system_column(&c.name) {
12081                found = true;
12082            }
12083        },
12084        &mut |_| {},
12085    );
12086    found
12087}
12088
12089fn references_ctid(stmt: &SelectStatement) -> bool {
12090    let in_expr = expr_references_ctid;
12091    stmt.items.iter().any(|i| match i {
12092        SelectItem::Expr { expr, .. } => in_expr(expr),
12093        _ => false,
12094    }) || stmt.where_.as_ref().is_some_and(in_expr)
12095        || stmt.order_by.iter().any(|o| in_expr(&o.expr))
12096        || stmt
12097            .group_by
12098            .as_ref()
12099            .is_some_and(|g| g.iter().any(in_expr))
12100        || stmt.having.as_ref().is_some_and(in_expr)
12101}
12102
12103/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
12104/// is a name the projection has to TYPE before any row exists.
12105///
12106/// Evaluation has answered this since round T9 (`resolve_column` builds a
12107/// `Value::Composite` of every column), but the typing side below had no
12108/// such branch and raised `column "t" does not exist` first — so the
12109/// feature was unreachable through a projection. Measured against PG18.4:
12110/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
12111///
12112/// The type is `Jsonb` + a composite marker, which is exactly how a
12113/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
12114/// the value travels as a `Value::Composite` and renders in the canonical
12115/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
12116/// so the marker names the alias and no rehydration keys off it — the
12117/// value arrives already built.
12118fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
12119    let mut s = ColumnSchema::new(
12120        alloc::string::String::from(alias),
12121        spg_storage::DataType::Jsonb,
12122        true,
12123    );
12124    s.user_composite_type = Some(alloc::string::String::from(alias));
12125    s
12126}
12127
12128/// v7.39.3 — `mysql` makes the name comparison case-INSENSITIVE, which
12129/// is MySQL's rule for column names (measured on 9.7.2: `mycol`,
12130/// `MYCOL` and a backquoted `MyCol` all resolve the same column).
12131///
12132/// SPG compared byte for byte and its lexer folds an UNQUOTED
12133/// identifier, so a table restored from a `mysqldump` — where every
12134/// identifier is backquoted and keeps its case — had every mixed-case
12135/// column unreachable from ordinary unquoted SQL. Same "two spellings,
12136/// two things" defect v7.39.1 closed for relation names.
12137pub(crate) fn resolve_projection_column<'a>(
12138    c: &ColumnName,
12139    schema_cols: &'a [ColumnSchema],
12140    table_alias: &str,
12141    mysql: bool,
12142) -> Result<Cow<'a, ColumnSchema>, EngineError> {
12143    let same = |a: &str, b: &str| {
12144        if mysql {
12145            a.eq_ignore_ascii_case(b)
12146        } else {
12147            a == b
12148        }
12149    };
12150    if let Some(q) = &c.qualifier {
12151        let composite = alloc::format!("{q}.{name}", name = c.name);
12152        if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &composite)) {
12153            return Ok(Cow::Borrowed(s));
12154        }
12155        // Single-table case: the qualifier may equal the active alias —
12156        // then look for the bare column name.
12157        if same(q, table_alias)
12158            && let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name))
12159        {
12160            return Ok(Cow::Borrowed(s));
12161        }
12162        // For multi-table schemas the qualifier is unknown only if no
12163        // column bears the "<q>." prefix. For single-table, the alias
12164        // mismatch alone is enough.
12165        let prefix = alloc::format!("{q}.");
12166        let qualifier_known =
12167            same(q, table_alias) || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
12168        if !qualifier_known {
12169            return Err(EngineError::Eval(EvalError::UnknownQualifier {
12170                qualifier: q.clone(),
12171                column: c.name.clone(),
12172            }));
12173        }
12174        return Err(EngineError::Eval(EvalError::ColumnNotFound {
12175            name: c.name.clone(),
12176        }));
12177    }
12178    if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name)) {
12179        return Ok(Cow::Borrowed(s));
12180    }
12181    let suffix = alloc::format!(".{name}", name = c.name);
12182    let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
12183    let first = matches.next();
12184    let extra = matches.next();
12185    match (first, extra) {
12186        (Some(s), None) => Ok(Cow::Borrowed(s)),
12187        (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
12188            detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
12189        })),
12190        // The whole-row reference, checked LAST so a real column carrying
12191        // the alias's name still wins — the same precedence
12192        // `resolve_column` applies on the evaluation side.
12193        //
12194        // Two schema shapes reach here. A single-table (or subquery, or
12195        // CTE) scan carries its alias and bare column names, so the name
12196        // has to equal the alias. A JOIN's combined schema carries no
12197        // alias at all and qualifies every column `alias.col`, so the
12198        // alias is identified by the prefix instead — which is exactly
12199        // how `whole_row_composite` picks the fields out on the
12200        // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
12201        // answers `(7,z)` on PG18.4 and errored here until this arm
12202        // covered the joined shape too.
12203        _ if !table_alias.is_empty() && c.name == table_alias => {
12204            Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
12205        }
12206        _ if table_alias.is_empty() && {
12207            let prefix = alloc::format!("{name}.", name = c.name);
12208            schema_cols.iter().any(|s| s.name.starts_with(&prefix))
12209        } =>
12210        {
12211            Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
12212        }
12213        _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
12214            name: c.name.clone(),
12215        })),
12216    }
12217}
12218
12219/// v7.40.0 — a column the grouping-set rewrite injected purely to sort
12220/// on, and which must not reach the client. Two families: `__grp_ord_*`
12221/// carries a branch's `grouping()` mask (round 135), `__grp_key_*`
12222/// carries a key the rollup orders by that the query did not project —
12223/// without it `SELECT SUM(qty) … GROUP BY qty WITH ROLLUP` answered
12224/// `column "qty" does not exist`, because a UNION's ORDER BY can only
12225/// name output columns.
12226fn is_synthetic_group_col(name: &str) -> bool {
12227    name.starts_with("__grp_ord_") || name.starts_with("__grp_key_")
12228}
12229
12230/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
12231/// parser to carry per-branch GROUPING() masks into a grouping-set query's
12232/// ORDER BY. They must never reach the output. No-op unless such a column is
12233/// present, so the common path is untouched.
12234/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
12235///
12236/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
12237/// a `LIMIT 2` that should have answered two groups answered one.
12238fn apply_deferred_limit(
12239    rows: alloc::vec::Vec<Row<'static>>,
12240    deferred: &(
12241        Option<spg_sql::ast::LimitExpr>,
12242        Option<spg_sql::ast::LimitExpr>,
12243    ),
12244) -> alloc::vec::Vec<Row<'static>> {
12245    let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
12246        Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
12247        _ => None,
12248    };
12249    let mut rows = rows;
12250    if let Some(off) = count(&deferred.1) {
12251        rows = rows.split_off(off.min(rows.len()));
12252    }
12253    if let Some(lim) = count(&deferred.0) {
12254        rows.truncate(lim);
12255    }
12256    rows
12257}
12258
12259fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
12260    let QueryResult::Rows { columns, rows } = result else {
12261        return result;
12262    };
12263    if !columns.iter().any(|c| is_synthetic_group_col(&c.name)) {
12264        return QueryResult::Rows { columns, rows };
12265    }
12266    let keep: Vec<usize> = columns
12267        .iter()
12268        .enumerate()
12269        .filter(|(_, c)| !is_synthetic_group_col(&c.name))
12270        .map(|(i, _)| i)
12271        .collect();
12272    let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
12273    let new_rows: Vec<Row<'static>> = rows
12274        .into_iter()
12275        .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
12276        .collect();
12277    QueryResult::Rows {
12278        columns: new_cols,
12279        rows: new_rows,
12280    }
12281}
12282
12283/// v7.39 (round 487) — bind every projection item that is a bare column
12284/// reference to its position, once per query.
12285///
12286/// `#[inline(never)]` and out of line on purpose. Round 486 established
12287/// that adding code inside these scan bodies moves neighbouring hot
12288/// functions around under fat LTO: the first version of this had the loop
12289/// inline in `run_single_table_scan` and four aggregate shapes that never
12290/// touch that function — `full_agg`, `join_agg`, `group_500k`,
12291/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
12292/// the same machine. Keeping it out of line kept them still.
12293#[inline(never)]
12294fn bind_direct_columns(
12295    projection: &[ProjectedItem],
12296    ctx: &eval::EvalContext<'_>,
12297) -> Vec<Option<usize>> {
12298    projection
12299        .iter()
12300        .map(|p| match &p.expr {
12301            Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
12302                // Same exclusion `compile_into` makes: a composite column
12303                // has to be rehydrated from stored JSON, which is not a
12304                // cell read.
12305                ctx.columns
12306                    .get(*pos)
12307                    .is_none_or(|sc| sc.user_composite_type.is_none())
12308            }),
12309            _ => None,
12310        })
12311        .collect()
12312}
12313
12314/// v7.39 (round 505) — the name an un-aliased projected expression reports.
12315///
12316/// PG18 names a call for its function and everything else `?column?`;
12317/// measured with `\gdesc`. SPG used to print the parsed expression back
12318/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
12319/// name-keyed row access found nothing under `upper`.
12320///
12321/// The MySQL half is NOT this rule and is deliberately left alone here:
12322/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
12323/// which needs the parser to hand over spans the AST does not carry yet.
12324/// Until it does, a MySQL session keeps the printed form — closer to what
12325/// MariaDB answers than `?column?` would be.
12326pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
12327    if mysql {
12328        return expr.to_string();
12329    }
12330    spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
12331}
12332
12333pub(crate) fn build_projection(
12334    items: &[SelectItem],
12335    schema_cols: &[ColumnSchema],
12336    table_alias: &str,
12337    mysql: bool,
12338    cat: Option<&Catalog>,
12339) -> Result<Vec<ProjectedItem>, EngineError> {
12340    build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0, cat)
12341}
12342
12343/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
12344/// invisible to `*`.
12345///
12346/// The windowed-SELECT path appends a synthetic `__win_N` column per window
12347/// function so the rewritten projection can reference the computed values as
12348/// ordinary columns. `*` then expanded them too, and
12349/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
12350/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
12351/// silent one: the row simply had one more field than the client asked for.
12352///
12353/// Hidden by POSITION rather than by name, for the reason round 512 recorded
12354/// about the system columns: a name test looks safe until a real column
12355/// happens to carry the name. These are appended last, so the count is what
12356/// identifies them.
12357pub(crate) fn build_projection_hiding_tail(
12358    items: &[SelectItem],
12359    schema_cols: &[ColumnSchema],
12360    table_alias: &str,
12361    mysql: bool,
12362    hidden_tail: usize,
12363    // v7.38.19 — the catalog, so a user-defined function's DECLARED
12364    // return type reaches the projection. Without it `describe_expr`
12365    // cannot type `f_sql()` and the column falls back to text, which is
12366    // what psql reads to decide alignment: `SELECT 7::bigint, f_sql()`
12367    // right-aligned one cell and left-aligned the other while both held
12368    // a bigint. Reported by sentori against 7.38.18 (their §2.2), who
12369    // also established that the EXECUTOR was never confused -- CTAS off
12370    // the same expression gives a bigint column, and arithmetic on it
12371    // works. Only the type travelling in the RowDescription was wrong.
12372    cat: Option<&Catalog>,
12373) -> Result<Vec<ProjectedItem>, EngineError> {
12374    let visible = schema_cols.len().saturating_sub(hidden_tail);
12375    // v7.39 (round 462) — a join's combined schema qualifies every column
12376    // `alias.col` so the deferred-join cell lookups resolve by composite
12377    // name. That is an internal convention, and `*` was handing it to the
12378    // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
12379    // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
12380    // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
12381    // already learned this for `q.*`; plain `*` never got the same rule.
12382    //
12383    // The signal is the schema itself, not the call site: only a combined
12384    // join schema arrives with no table alias AND every column qualified.
12385    // A single-table schema carries its alias, an empty schema has nothing
12386    // to strip, and a synthetic schema's names carry no dot.
12387    let joined_schema = table_alias.is_empty()
12388        && !schema_cols.is_empty()
12389        && schema_cols.iter().all(|c| c.name.contains('.'));
12390    let bare_name = |name: &str| -> String {
12391        if !joined_schema {
12392            return name.to_string();
12393        }
12394        match name.split_once('.') {
12395            Some((_, rest)) if !rest.is_empty() => rest.to_string(),
12396            _ => name.to_string(),
12397        }
12398    };
12399    let mut out = Vec::new();
12400    for item in items {
12401        match item {
12402            SelectItem::Wildcard => {
12403                // v7.39 (round 511) — `*` never expands a system column, as
12404                // PG's does not. They join the schema only when the statement
12405                // asked for them, so this matters for the mixed shape
12406                // `SELECT *, ctid FROM t`.
12407                //
12408                // v7.39 (round 512) — by POSITION, not by name. Matching on
12409                // the name alone looked safe because PG reserves them, and it
12410                // is not: `pg_replication_slots` genuinely has a column called
12411                // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
12412                // Only the trailing six, in the order the scan appends them,
12413                // are the synthetic ones.
12414                let sys_skip = synthetic_system_positions(schema_cols);
12415                for (idx, col) in schema_cols.iter().enumerate() {
12416                    if sys_skip[idx] || idx >= visible {
12417                        continue;
12418                    }
12419                    out.push(ProjectedItem {
12420                        expr: Expr::Column(ColumnName {
12421                            qualifier: None,
12422                            name: col.name.clone(),
12423                        }),
12424                        output_name: bare_name(&col.name),
12425                        ty: col.ty,
12426                        nullable: col.nullable,
12427                        user_enum_type: col.user_enum_type.clone(),
12428                        mysql_fsp: col.mysql_fsp,
12429                        collation_name: col.collation_name.clone(),
12430                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12431                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
12432                    });
12433                }
12434            }
12435            // v7.39 (round 128) — `q.*` expands to every column belonging to
12436            // the qualifier `q`. Single-table schemas carry bare column names
12437            // reachable via `table_alias`; a join's combined schema carries
12438            // `alias.col` names, so a column belongs to `q` when its name has
12439            // the `q.` prefix. PG labels the expanded columns by their bare
12440            // name, so the `alias.` prefix is stripped from the output name.
12441            SelectItem::QualifiedWildcard(q) => {
12442                let prefix = alloc::format!("{q}.");
12443                let single_table = !table_alias.is_empty() && q == table_alias;
12444                let mut matched = 0usize;
12445                for col in &schema_cols[..visible] {
12446                    let belongs =
12447                        col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
12448                    if !belongs {
12449                        continue;
12450                    }
12451                    matched += 1;
12452                    let output_name = col
12453                        .name
12454                        .strip_prefix(&prefix)
12455                        .unwrap_or(&col.name)
12456                        .to_string();
12457                    out.push(ProjectedItem {
12458                        expr: Expr::Column(ColumnName {
12459                            qualifier: None,
12460                            name: col.name.clone(),
12461                        }),
12462                        output_name,
12463                        ty: col.ty,
12464                        nullable: col.nullable,
12465                        user_enum_type: col.user_enum_type.clone(),
12466                        mysql_fsp: col.mysql_fsp,
12467                        collation_name: col.collation_name.clone(),
12468                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12469                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
12470                    });
12471                }
12472                if matched == 0 {
12473                    // `q.*` names no column, so the reference IS the star.
12474                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
12475                        qualifier: q.clone(),
12476                        column: alloc::string::String::from("*"),
12477                    }));
12478                }
12479            }
12480            SelectItem::Expr { expr, alias } => {
12481                // Plain column ref keeps full schema info (real type +
12482                // nullability). For compound expressions try the
12483                // describe-side function-return-type table first
12484                // (e.g. `SELECT now()` → Timestamptz, `SELECT
12485                // concat(…)` → Text). Falls back to nullable Text
12486                // for shapes the describe path can't resolve.
12487                if let Expr::Column(c) = expr {
12488                    let sch = resolve_projection_column(c, schema_cols, table_alias, mysql)?;
12489                    let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
12490                    out.push(ProjectedItem {
12491                        expr: expr.clone(),
12492                        output_name,
12493                        ty: sch.ty,
12494                        nullable: sch.nullable,
12495                        // v7.39 (read01 round 54) — a bare enum column keeps
12496                        // its enum identity through the projection.
12497                        user_enum_type: sch.user_enum_type.clone(),
12498                        mysql_fsp: sch.mysql_fsp,
12499                        collation_name: sch.collation_name.clone(),
12500                        // v7.38.13 — and its byte-wise-ness. This is the
12501                        // site `SELECT DISTINCT t FROM t` arrives at.
12502                        fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
12503                        pads: crate::collate::pads_space(sch.collation_name.as_deref()),
12504                    });
12505                } else if let Some(shape) = describe::describe_expr_in(expr, schema_cols, cat) {
12506                    let output_name = alias
12507                        .clone()
12508                        .unwrap_or_else(|| default_output_name(expr, mysql));
12509                    out.push(ProjectedItem {
12510                        expr: expr.clone(),
12511                        // v7.38.18 — a projected EXPRESSION has no column collation
12512                        // to read, so it takes the session default, which is MySQL
12513                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12514                        pads: false,
12515                        output_name,
12516                        ty: shape.ty,
12517                        // v7.39 (round 258) — a projected EXPRESSION keeps its
12518                        // enum identity too, not just a bare column. `FROM
12519                        // (VALUES ('happy'::mood), …) t(m)` lowers to constant
12520                        // SELECTs, so the derived column arrived here as a cast
12521                        // and lost the enum — making the outer ORDER BY / min /
12522                        // max / array_agg sort by the label's TEXT.
12523                        nullable: shape.nullable,
12524                        user_enum_type: None,
12525                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12526                        // A bare column reference keeps its collation; any
12527                        // other expression produces a new value and has none.
12528                        collation_name: match expr {
12529                            Expr::Column(c) => schema_cols
12530                                .iter()
12531                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12532                                .and_then(|sc| sc.collation_name.clone()),
12533                            _ => None,
12534                        },
12535                        fold_exempt: match expr {
12536                            Expr::Column(c) => schema_cols
12537                                .iter()
12538                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12539                                .is_some_and(|sc| {
12540                                    matches!(sc.collation, spg_storage::Collation::Binary)
12541                                }),
12542                            // Not a column: no declared collation to honour,
12543                            // so the session default applies and it folds.
12544                            _ => false,
12545                        },
12546                    });
12547                } else {
12548                    let output_name = alias
12549                        .clone()
12550                        .unwrap_or_else(|| default_output_name(expr, mysql));
12551                    out.push(ProjectedItem {
12552                        expr: expr.clone(),
12553                        // v7.38.18 — a projected EXPRESSION has no column collation
12554                        // to read, so it takes the session default, which is MySQL
12555                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12556                        pads: false,
12557                        output_name,
12558                        // A user ENUM has no DataType of its own, so
12559                        // `describe_expr` cannot type `'ok'::mood` and the
12560                        // item lands HERE, defaulting to text — which is why
12561                        // pg_typeof answered `text` and a derived table sorted
12562                        // enum values by their label.
12563                        ty: DataType::Text,
12564                        nullable: true,
12565                        user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
12566                            .map(alloc::string::String::from),
12567                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12568                        collation_name: match expr {
12569                            Expr::Column(c) => schema_cols
12570                                .iter()
12571                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12572                                .and_then(|sc| sc.collation_name.clone()),
12573                            _ => None,
12574                        },
12575                        fold_exempt: match expr {
12576                            Expr::Column(c) => schema_cols
12577                                .iter()
12578                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12579                                .is_some_and(|sc| {
12580                                    matches!(sc.collation, spg_storage::Collation::Binary)
12581                                }),
12582                            // Not a column: no declared collation to honour,
12583                            // so the session default applies and it folds.
12584                            _ => false,
12585                        },
12586                    });
12587                }
12588            }
12589        }
12590    }
12591    Ok(out)
12592}
12593
12594// ---- v4.12 window-function helpers ----
12595// The (partition-key, order-key, original-index) tuple shape used
12596// across these helpers is intrinsic to the planner. Factoring it
12597// into a typedef adds indirection without making the code clearer,
12598// so several lints are allowed inline on the affected functions
12599// rather than module-wide.
12600
12601/// v4.22: pick more specific column types from observed rows when
12602/// the projection builder defaulted to Text (the v1.x behavior for
12603/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
12604/// land an Int column in the CTE storage table rather than failing
12605/// the insert with "expected TEXT, got INT".
12606pub(crate) fn infer_column_types(
12607    columns: &[ColumnSchema],
12608    rows: &[Row<'static>],
12609) -> Vec<ColumnSchema> {
12610    let mut out = columns.to_vec();
12611    for (col_idx, col) in out.iter_mut().enumerate() {
12612        if col.ty != DataType::Text {
12613            continue;
12614        }
12615        let mut inferred: Option<DataType> = None;
12616        let mut all_null = true;
12617        for row in rows {
12618            let Some(v) = row.values.get(col_idx) else {
12619                continue;
12620            };
12621            let ty = match v {
12622                Value::Null => continue,
12623                Value::SmallInt(_) => DataType::SmallInt,
12624                Value::Int(_) => DataType::Int,
12625                Value::BigInt(_) => DataType::BigInt,
12626                Value::Float(_) => DataType::Float,
12627                Value::Bool(_) => DataType::Bool,
12628                Value::Vector(_) => DataType::Vector {
12629                    dim: 0,
12630                    encoding: VecEncoding::F32,
12631                },
12632                // v7.38 (read01 U16) — carry array values through with an
12633                // array type so a recursive CTE that projects an array
12634                // (e.g. a SEARCH/CYCLE ord / path column) types the working
12635                // column as an array, not Text.
12636                Value::TextArray(_) => DataType::TextArray,
12637                Value::IntArray(_) => DataType::IntArray,
12638                Value::BigIntArray(_) => DataType::BigIntArray,
12639                Value::SmallIntArray(_) => DataType::SmallIntArray,
12640                Value::FloatArray(_) => DataType::FloatArray,
12641                Value::BoolArray(_) => DataType::BoolArray,
12642                // v7.39 (GUC knife 2) — an interval projection describes
12643                // as INTERVAL (typed drivers read the RowDescription OID).
12644                Value::Interval { .. } => DataType::Interval,
12645                _ => DataType::Text,
12646            };
12647            all_null = false;
12648            inferred = Some(match inferred {
12649                None => ty,
12650                Some(prev) if prev == ty => prev,
12651                Some(_) => DataType::Text,
12652            });
12653        }
12654        if let Some(t) = inferred {
12655            col.ty = t;
12656            col.nullable = true;
12657        } else if all_null {
12658            col.nullable = true;
12659        }
12660    }
12661    out
12662}
12663
12664/// Numeric widening rank for UNION type resolution (higher = wider).
12665fn numeric_rank(t: DataType) -> Option<u8> {
12666    match t {
12667        DataType::SmallInt => Some(1),
12668        DataType::Int => Some(2),
12669        DataType::BigInt => Some(3),
12670        DataType::Numeric { .. } => Some(4),
12671        DataType::Float => Some(5),
12672        _ => None,
12673    }
12674}
12675
12676/// Resolve the common result type for a UNION / VALUES column from the
12677/// set of concrete (non-NULL) branch types, following the safe subset
12678/// of PG's type resolution:
12679///   * all-numeric  → the widest numeric (int ∪ bigint → bigint, … ∪
12680///     numeric → numeric, … ∪ float → float);
12681///   * DATE ∪ TIMESTAMP → TIMESTAMP;
12682///   * exactly one concrete non-TEXT type mixed with TEXT literals →
12683///     that concrete type (the TEXT cells get parsed into it).
12684/// Returns `None` for anything ambiguous, so the caller leaves the
12685/// column untouched rather than risk a wrong or failing coercion.
12686fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
12687    // NB: types are collected from RUNTIME values, which are coarser
12688    // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
12689    // a single-concrete-type fast path must NOT overwrite the column
12690    // type — it would downgrade tstz to ts. NULL-only unification (PG:
12691    // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
12692    // row's pg_typeof) needs schema-level resolution — recorded, not
12693    // attempted here.
12694    if types.len() < 2 {
12695        return None;
12696    }
12697    if types.iter().all(|t| numeric_rank(*t).is_some()) {
12698        return types
12699            .iter()
12700            .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
12701            .copied();
12702    }
12703    let non_text: Vec<&DataType> = types
12704        .iter()
12705        .filter(|t| !matches!(t, DataType::Text))
12706        .collect();
12707    // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
12708    // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
12709    // if any is timestamp the result is timestamp (ts ∪ date). All values are
12710    // the same UTC-micros instant, so widening date/ts to tstz is lossless.
12711    if non_text.iter().all(|t| {
12712        matches!(
12713            t,
12714            DataType::Date | DataType::Timestamp | DataType::Timestamptz
12715        )
12716    }) && non_text
12717        .iter()
12718        .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
12719    {
12720        if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
12721            return Some(DataType::Timestamptz);
12722        }
12723        return Some(DataType::Timestamp);
12724    }
12725    // A single concrete non-TEXT type mixed with TEXT literals.
12726    if non_text.len() == 1 {
12727        return Some(*non_text[0]);
12728    }
12729    // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
12730    // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
12731    // text): resolve the concrete set first (PG treats the unknown-
12732    // typed string literals as castable to whatever the knowns
12733    // resolve to), then the TEXT cells parse into that target — the
12734    // caller's coercion dry-run still abandons the column if any
12735    // literal doesn't parse.
12736    if !non_text.is_empty() && non_text.len() < types.len() {
12737        let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
12738        return resolve_union_common_type(&concrete);
12739    }
12740    None
12741}
12742
12743/// Coerce every cell of a UNION / VALUES result column to one common
12744/// type (see [`resolve_union_common_type`]). Conservative: a column
12745/// whose branches already agree, or whose types don't resolve, or where
12746/// any cell fails to coerce, is left exactly as it was — this never
12747/// turns a previously-working query into an error.
12748fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
12749    for col_idx in 0..columns.len() {
12750        let mut seen: Vec<DataType> = Vec::new();
12751        for row in rows.iter() {
12752            if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
12753                if !seen.contains(&dt) {
12754                    seen.push(dt);
12755                }
12756            }
12757        }
12758        // v7.37.16 — a single concrete runtime type under a TEXT-typed
12759        // column means the column type came off a NULL (or unknown-text)
12760        // branch: NULL literals describe as TEXT (`L::Null → Text`), so
12761        // `VALUES (NULL),(1.5)` left the column "text" while every
12762        // non-NULL cell is numeric. Adopt the concrete type — schema
12763        // only, no cell changes. tstz-safe by construction: a real
12764        // timestamptz column's schema type is Timestamptz, not Text, so
12765        // the coarser runtime type (Value::Timestamp) can't downgrade it
12766        // through this arm; and a real text column's non-NULL cells are
12767        // Text, which keeps seen == [Text] and skips it.
12768        if seen.len() == 1
12769            && matches!(columns[col_idx].ty, DataType::Text)
12770            && !matches!(seen[0], DataType::Text)
12771        {
12772            columns[col_idx].ty = seen[0];
12773            continue;
12774        }
12775        let Some(target) = resolve_union_common_type(&seen) else {
12776            continue;
12777        };
12778        // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
12779        // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
12780        // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
12781        // existing numeric cell untouched and only promote integers (to scale 0)
12782        // rather than rescaling everything to the widest scale.
12783        let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
12784        // Dry-run the coercion; abandon the whole column if any fails.
12785        let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
12786        let mut ok = true;
12787        for row in rows.iter() {
12788            match row.values.get(col_idx) {
12789                Some(Value::Numeric { .. }) if scale_preserving_numeric => {
12790                    coerced.push(Some(row.values[col_idx].clone()));
12791                }
12792                Some(v) => {
12793                    let cell_target = if scale_preserving_numeric {
12794                        DataType::Numeric {
12795                            precision: 0,
12796                            scale: 0,
12797                        }
12798                    } else {
12799                        target
12800                    };
12801                    match crate::conversions::coerce_value(
12802                        v.clone(),
12803                        cell_target,
12804                        &columns[col_idx].name,
12805                        col_idx,
12806                    ) {
12807                        Ok(cv) => coerced.push(Some(cv)),
12808                        Err(_) => {
12809                            ok = false;
12810                            break;
12811                        }
12812                    }
12813                }
12814                None => coerced.push(None),
12815            }
12816        }
12817        if !ok {
12818            continue;
12819        }
12820        for (row, cv) in rows.iter_mut().zip(coerced) {
12821            if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
12822                *slot = nv;
12823            }
12824        }
12825        columns[col_idx].ty = target;
12826    }
12827}
12828
12829/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
12830/// dedup inside the recursive iteration. Crude but deterministic
12831/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
12832fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
12833    let mut out = Vec::new();
12834    for v in &row.values {
12835        // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
12836        // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
12837        // like PG (and like GROUP BY, which already normalizes). The old
12838        // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
12839        // the exact-decimal family through one scale-stripped canonical form.
12840        match v {
12841            Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12842            Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12843            Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12844            Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
12845            other => {
12846                let s = alloc::format!("{other:?}|");
12847                out.extend_from_slice(s.as_bytes());
12848            }
12849        }
12850    }
12851    out
12852}
12853
12854/// Append a scale-independent canonical key for an exact-decimal value: strip
12855/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
12856/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
12857fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
12858    while scale > 0 && scaled % 10 == 0 {
12859        scaled /= 10;
12860        scale -= 1;
12861    }
12862    let s = alloc::format!("\u{1}{scaled}e-{scale}|");
12863    out.extend_from_slice(s.as_bytes());
12864}
12865
12866/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
12867/// (uncorrelated; outer refs were substituted upstream), then zip
12868/// them in parallel, NULL-padding shorter arrays to the longest
12869/// (PG's ROWS FROM shorthand). Shared by the primary-position
12870/// executor and the join-position materialiser, which both detect
12871/// the parser's `__unnest_zip` marker call.
12872pub(crate) fn unnest_zip_rows(
12873    args: &[Expr],
12874) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
12875    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12876    let ctx = EvalContext::new(&empty_schema, None);
12877    let dummy_row = Row::new(alloc::vec::Vec::new());
12878    let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
12879    let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
12880        alloc::vec::Vec::with_capacity(args.len());
12881    for a in args {
12882        let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
12883        // v7.39.13 — the element menu the rest of the workspace already
12884        // has, not a third copy of a shortened one.
12885        //
12886        // This arm listed Text, Int and BigInt and refused everything
12887        // else, so `unnest(uuid[], text[])` raised while
12888        // `unnest(uuid[])` — a different path — did not. A shipped
12889        // endpoint of a customer's returned 500 on every call because
12890        // of it. `array_elements` and `array_element_type` are the two
12891        // halves of the menu that `array_element_at`'s own comment
12892        // describes: "previously only matched Text/Int/BigInt arrays
12893        // and errored on every other element type". Same sentence,
12894        // third arm.
12895        let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = if matches!(v, Value::Null) {
12896            (DataType::Text, alloc::vec::Vec::new())
12897        } else if let Some(items) = crate::eval::values::array_elements(&v) {
12898            let dt = v
12899                .data_type()
12900                .and_then(crate::describe::array_element_type)
12901                .unwrap_or(DataType::Text);
12902            (dt, items)
12903        } else {
12904            return Err(EngineError::Unsupported(alloc::format!(
12905                "unnest() expects array arguments, got {}",
12906                crate::conversions::pg_type_name_for_error_opt(v.data_type())
12907            )));
12908        };
12909        dtypes.push(dt);
12910        columns.push(items);
12911    }
12912    let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
12913    let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
12914    for i in 0..max_len {
12915        let vals: alloc::vec::Vec<Value<'static>> = columns
12916            .iter()
12917            .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
12918            .collect();
12919        rows.push(Row::new(vals));
12920    }
12921    Ok((dtypes, rows))
12922}
12923
12924/// Detect the parser's multi-arg unnest marker on an unnest_expr.
12925pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
12926    match expr {
12927        Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
12928        _ => None,
12929    }
12930}
12931
12932/// Evaluate generate_series arguments (uncorrelated — outer refs
12933/// were substituted upstream where applicable) and build the row
12934/// stream. Dispatches on the start value's shape and rejects
12935/// mixed-shape calls early (e.g. start = timestamp, stop =
12936/// integer) so the caller gets a clean error rather than a panic.
12937/// Shared by the primary-position executor and the join-position
12938/// materialiser.
12939pub(crate) fn generate_series_rows(
12940    args: &[Expr],
12941    cancel: &CancelToken<'_>,
12942) -> Result<(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 arg_values: alloc::vec::Vec<Value<'static>> =
12947        alloc::vec::Vec::with_capacity(args.len());
12948    for a in args {
12949        arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
12950    }
12951    generate_series_from_values(arg_values, args, cancel)
12952}
12953
12954/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
12955/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
12956/// full integer / numeric / timestamp overload set with the FROM-clause path.
12957/// Before this split the target-list arm reimplemented only the integer case,
12958/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
12959/// NULL for the timestamp column instead of the series. `arg_values` are the
12960/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
12961/// timestamp type resolution (it inspects the argument expressions' types).
12962pub(crate) fn generate_series_from_values(
12963    mut arg_values: alloc::vec::Vec<Value<'static>>,
12964    args: &[Expr],
12965    cancel: &CancelToken<'_>,
12966) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12967    // PG: a NULL bound or step yields zero rows (also keeps the
12968    // NULL-padded lateral probe alive — schema without data).
12969    if arg_values.iter().any(|v| matches!(v, Value::Null)) {
12970        return Ok((DataType::BigInt, alloc::vec::Vec::new()));
12971    }
12972    // PG resolves `generate_series(date, date, interval)` to the
12973    // timestamp/timestamptz overload by implicitly casting each date
12974    // bound up to a timestamp at midnight (verified vs live PG18.4:
12975    // date args yield rows anchored at 00:00:00). SPG's TZ-naive
12976    // timestamp model renders the same instants, so fold any Date
12977    // bound to its midnight Timestamp (canonical `days *
12978    // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
12979    // the shape match so the existing timestamp arm drives the walk.
12980    // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
12981    // `generate_series(date, date, interval)` has no date overload, and among
12982    // the two candidates PG prefers the timestamptz one (timestamptz is the
12983    // preferred type of the datetime category), so the column comes back
12984    // `timestamp with time zone` — the rows render with a `+00` offset. A
12985    // timestamptz bound obviously lands there too. Only genuinely
12986    // timestamp-typed bounds keep the TZ-naive result type.
12987    let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12988    let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
12989        || args.iter().any(|a| {
12990            crate::describe::describe_expr(a, &empty_cols)
12991                .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
12992        });
12993    for v in &mut arg_values {
12994        if let Value::Date(d) = *v {
12995            *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
12996        }
12997    }
12998    match arg_values.as_slice() {
12999        [Value::Timestamp(start), Value::Timestamp(stop), step] => {
13000            let interval_step = match step {
13001                Value::Interval { .. } => step.clone(),
13002                // v7.38 (read01) — PG resolves an unknown-type string step
13003                // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
13004                // a bare text step by parsing it the same way `::interval` does.
13005                Value::Text(s) => crate::conversions::coerce_value(
13006                    Value::text(s.as_ref()),
13007                    DataType::Interval,
13008                    "",
13009                    0,
13010                )
13011                .map_err(|_| {
13012                    EngineError::Unsupported(alloc::format!(
13013                        "generate_series(timestamp, timestamp, …): \
13014                         could not parse step {s:?} as INTERVAL"
13015                    ))
13016                })?,
13017                other => {
13018                    return Err(EngineError::Unsupported(alloc::format!(
13019                        "generate_series(timestamp, timestamp, …): \
13020                         step must be INTERVAL, got {}",
13021                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
13022                    )));
13023                }
13024            };
13025            let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
13026            Ok((
13027                if tz {
13028                    DataType::Timestamptz
13029                } else {
13030                    DataType::Timestamp
13031                },
13032                rows,
13033            ))
13034        }
13035        [start, stop, step]
13036            if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
13037        {
13038            let s = value_to_i64(start);
13039            let e = value_to_i64(stop);
13040            let st = value_to_i64(step);
13041            // PG types the series by the argument type: int4 args → int4
13042            // elements, int8 (bigint) args → int8. Any BigInt operand widens.
13043            let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
13044            let rows = generate_series_integers(s, e, st, wide, cancel)?;
13045            Ok((
13046                if wide {
13047                    DataType::BigInt
13048                } else {
13049                    DataType::Int
13050                },
13051                rows,
13052            ))
13053        }
13054        [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
13055            let s = value_to_i64(start);
13056            let e = value_to_i64(stop);
13057            let wide = value_is_bigint(start) || value_is_bigint(stop);
13058            let rows = generate_series_integers(s, e, 1, wide, cancel)?;
13059            Ok((
13060                if wide {
13061                    DataType::BigInt
13062                } else {
13063                    DataType::Int
13064                },
13065                rows,
13066            ))
13067        }
13068        // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
13069        // series in exact numeric arithmetic; NaN / infinity bounds and a
13070        // zero step get dedicated wordings, and a mixed int/numeric call
13071        // resolves here via the implicit int→numeric cast.
13072        [_, _] | [_, _, _]
13073            if arg_values
13074                .iter()
13075                .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
13076                && arg_values.iter().all(|v| {
13077                    matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
13078                }) =>
13079        {
13080            use spg_storage::NumericKind as K;
13081            let words: [(&str, &str); 3] = [
13082                (
13083                    "start value cannot be NaN",
13084                    "start value cannot be infinity",
13085                ),
13086                ("stop value cannot be NaN", "stop value cannot be infinity"),
13087                ("step size cannot be NaN", "step size cannot be infinity"),
13088            ];
13089            for (i, v) in arg_values.iter().enumerate() {
13090                if let Value::Numeric { kind, .. } = v {
13091                    if *kind != K::Finite {
13092                        let (nan_w, inf_w) = words[i];
13093                        return Err(EngineError::Unsupported(
13094                            if *kind == K::NaN { nan_w } else { inf_w }.into(),
13095                        ));
13096                    }
13097                }
13098            }
13099            let big =
13100                |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
13101            let start = big(&arg_values[0]);
13102            let stop = big(&arg_values[1]);
13103            let step = if arg_values.len() == 3 {
13104                big(&arg_values[2])
13105            } else {
13106                spg_storage::bignum::BigNumeric::from_i128(1, 0)
13107            };
13108            if step.is_zero() {
13109                return Err(EngineError::Unsupported(
13110                    "step size cannot equal zero".into(),
13111                ));
13112            }
13113            let descending = step.parts().0;
13114            let mut rows = alloc::vec::Vec::new();
13115            let mut cur = start;
13116            const MAX_ROWS: usize = 10_000_000;
13117            loop {
13118                cancel.check()?;
13119                let c = cur.cmp(&stop);
13120                if descending {
13121                    if c == core::cmp::Ordering::Less {
13122                        break;
13123                    }
13124                } else if c == core::cmp::Ordering::Greater {
13125                    break;
13126                }
13127                if rows.len() >= MAX_ROWS {
13128                    return Err(EngineError::Unsupported(alloc::format!(
13129                        "generate_series() result exceeds {MAX_ROWS} rows"
13130                    )));
13131                }
13132                rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
13133                    cur.clone()
13134                )]));
13135                cur = cur.add(&step);
13136            }
13137            Ok((
13138                DataType::Numeric {
13139                    precision: 0,
13140                    scale: 0,
13141                },
13142                rows,
13143            ))
13144        }
13145        _ => Err(EngineError::Unsupported(alloc::format!(
13146            "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
13147             argument shapes; got {}",
13148            arg_values
13149                .iter()
13150                .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
13151                .collect::<alloc::vec::Vec<_>>()
13152                .join(", ")
13153        ))),
13154    }
13155}
13156
13157/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
13158/// Step direction follows the sign: positive step iterates upward
13159/// (stops when current > stop); negative iterates downward; zero
13160/// errors. Caller-facing row stream is `BigInt`-typed so a single
13161/// projection schema covers SmallInt / Int / BigInt callers.
13162fn generate_series_integers(
13163    start: i64,
13164    stop: i64,
13165    step: i64,
13166    wide: bool,
13167    cancel: &CancelToken<'_>,
13168) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13169    if step == 0 {
13170        return Err(EngineError::Unsupported(
13171            "step size cannot equal zero".into(),
13172        ));
13173    }
13174    let mut out = alloc::vec::Vec::new();
13175    let mut cur = start;
13176    // Hard cap to keep a runaway call from eating all memory. PG
13177    // has no such cap but does honour query timeout; SPG's cancel
13178    // token will fire too — this is a defense-in-depth backstop.
13179    const MAX_ROWS: usize = 10_000_000;
13180    loop {
13181        cancel.check()?;
13182        if step > 0 && cur > stop {
13183            break;
13184        }
13185        if step < 0 && cur < stop {
13186            break;
13187        }
13188        out.push(Row::new(alloc::vec![if wide {
13189            Value::BigInt(cur)
13190        } else {
13191            Value::Int(cur as i32)
13192        }]));
13193        if out.len() > MAX_ROWS {
13194            return Err(EngineError::Unsupported(alloc::format!(
13195                "generate_series(): exceeded {MAX_ROWS} rows; \
13196                 narrow start/stop or use a larger step"
13197            )));
13198        }
13199        cur = match cur.checked_add(step) {
13200            Some(n) => n,
13201            None => break,
13202        };
13203    }
13204    Ok(out)
13205}
13206
13207/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
13208/// `Value::Interval { months, micros }` per the caller's guard;
13209/// each iteration adds the interval via `apply_binary_interval`
13210/// so month-shifting handles short-month rollover (PG semantics).
13211fn generate_series_timestamps(
13212    start: i64,
13213    stop: i64,
13214    step: Value,
13215    cancel: &CancelToken<'_>,
13216) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13217    let (months, days, micros) = match &step {
13218        Value::Interval {
13219            months,
13220            days,
13221            micros,
13222            kind,
13223        } => (*months, *days, *micros),
13224        _ => unreachable!("caller guards step.is_interval"),
13225    };
13226    if months == 0 && days == 0 && micros == 0 {
13227        return Err(EngineError::Unsupported(
13228            "generate_series(): INTERVAL step cannot be zero".into(),
13229        ));
13230    }
13231    let ascending = months > 0 || days > 0 || micros > 0;
13232    let mut out = alloc::vec::Vec::new();
13233    let mut cur = Value::Timestamp(start);
13234    const MAX_ROWS: usize = 10_000_000;
13235    loop {
13236        cancel.check()?;
13237        let cur_t = match cur {
13238            Value::Timestamp(t) => t,
13239            _ => unreachable!("loop invariant: cur is Timestamp"),
13240        };
13241        if ascending && cur_t > stop {
13242            break;
13243        }
13244        if !ascending && cur_t < stop {
13245            break;
13246        }
13247        out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
13248        if out.len() > MAX_ROWS {
13249            return Err(EngineError::Unsupported(alloc::format!(
13250                "generate_series(): exceeded {MAX_ROWS} rows; \
13251                 narrow start/stop or use a larger step"
13252            )));
13253        }
13254        let next = eval::apply_binary_interval(
13255            spg_sql::ast::BinOp::Add,
13256            &cur,
13257            &Value::Interval {
13258                months,
13259                days,
13260                micros,
13261                kind: spg_storage::IntervalKind::Finite,
13262            },
13263        )
13264        .map_err(EngineError::Eval)?;
13265        cur = match next {
13266            Some(v) => v,
13267            None => break,
13268        };
13269    }
13270    Ok(out)
13271}
13272
13273/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
13274/// WITH TIES` requires an `ORDER BY`. Without one, there's no
13275/// way to identify "ties" deterministically, so PG errors at
13276/// plan time. SPG mirrors that surface so the same DDL / app
13277/// behaviour holds on cutover.
13278fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
13279    if stmt.limit_with_ties && stmt.order_by.is_empty() {
13280        return Err(EngineError::Unsupported(alloc::string::String::from(
13281            "WITH TIES cannot be specified without ORDER BY clause",
13282        )));
13283    }
13284    Ok(())
13285}
13286
13287/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
13288/// (case-insensitive). Used by `exec_select_cancel`'s
13289/// projection loop to detect Set-Returning-Function rows that
13290/// need per-row expansion. Only the top-level call counts —
13291/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
13292/// projection's perspective; it would surface as an "unknown
13293/// function" mismatch downstream, which is what we want
13294/// (multi-SRF / nested SRF is documented carve-out for v7.19).
13295fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
13296    top_level_srf_kind(expr).is_some()
13297}
13298
13299/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
13300/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
13301/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
13302/// source row.
13303#[derive(Clone, Copy, PartialEq, Eq)]
13304pub(crate) enum SrfKind {
13305    Unnest,
13306    /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
13307    /// list. It used to be handled ONLY by the parser's lift into FROM, so a
13308    /// second one in the same list came back as "unknown function".
13309    GenerateSeries,
13310    GenerateSubscripts,
13311    /// `_text` variants unwrap scalars to their lexeme; the plain forms render
13312    /// every value as compact JSON text.
13313    ArrayElements {
13314        as_text: bool,
13315    },
13316    PathQuery,
13317    RegexpMatches,
13318    Each {
13319        as_text: bool,
13320    },
13321    ObjectKeys,
13322}
13323
13324/// Case-insensitive match against any of `names`.
13325fn name_is(name: &str, names: &[&str]) -> bool {
13326    names.iter().any(|n| name.eq_ignore_ascii_case(n))
13327}
13328
13329pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
13330    let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13331        return None;
13332    };
13333    let n = args.len();
13334    // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
13335    // SELECT list (it returned an array there before) and shares the unnest
13336    // expansion machinery.
13337    if n == 1 && name.eq_ignore_ascii_case("unnest") {
13338        return Some(SrfKind::Unnest);
13339    }
13340    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
13341        return Some(SrfKind::GenerateSeries);
13342    }
13343    if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
13344        return Some(SrfKind::GenerateSubscripts);
13345    }
13346    // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
13347    // per element / match in the SELECT list; they collapsed to a single row
13348    // (a TextArray, or an "unknown function" error for `each`) before.
13349    if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
13350        return Some(SrfKind::ArrayElements { as_text: false });
13351    }
13352    if n == 1
13353        && name_is(
13354            name,
13355            &["jsonb_array_elements_text", "json_array_elements_text"],
13356        )
13357    {
13358        return Some(SrfKind::ArrayElements { as_text: true });
13359    }
13360    // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
13361    if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
13362        return Some(SrfKind::PathQuery);
13363    }
13364    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
13365        return Some(SrfKind::RegexpMatches);
13366    }
13367    if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
13368        return Some(SrfKind::Each { as_text: false });
13369    }
13370    if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
13371        return Some(SrfKind::Each { as_text: true });
13372    }
13373    if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
13374        return Some(SrfKind::ObjectKeys);
13375    }
13376    None
13377}
13378
13379/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
13380/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
13381/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
13382/// rows, as in PG).
13383pub(crate) fn top_level_srf_output(
13384    expr: &spg_sql::ast::Expr,
13385    row: &Row<'static>,
13386    ctx: &EvalContext<'_>,
13387) -> Result<Vec<Value<'static>>, EngineError> {
13388    let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
13389        (top_level_srf_kind(expr), expr)
13390    else {
13391        return Err(EngineError::Unsupported(
13392            "expected a SELECT-list SRF call".into(),
13393        ));
13394    };
13395    match kind {
13396        SrfKind::Unnest => {
13397            // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
13398            // the elements DIRECTLY: the old path built the whole
13399            // Value::Array (one eval + a clone per element) only for
13400            // array_value_to_elements to clone every element back out.
13401            // Any other argument shape (a column, a function result)
13402            // keeps the build-then-split path.
13403            if let spg_sql::ast::Expr::Array(items) = &args[0] {
13404                return items
13405                    .iter()
13406                    .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
13407                    .collect();
13408            }
13409            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13410            array_value_to_elements(&arr)
13411        }
13412        SrfKind::GenerateSeries => {
13413            // v7.39 (read01 round 96) — evaluate the args against the actual
13414            // row, then hand off to the shared core so the numeric and
13415            // timestamp/timestamptz overloads work here too (this arm used to
13416            // handle only integers, silently NULLing a temporal/numeric series
13417            // when it shared a target list with another SRF).
13418            let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
13419            for a in args {
13420                arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13421            }
13422            let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
13423            Ok(rows
13424                .into_iter()
13425                .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
13426                .collect())
13427        }
13428        SrfKind::GenerateSubscripts => {
13429            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13430            let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13431            if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
13432                return Ok(Vec::new());
13433            }
13434            let len = array_value_to_elements(&arr)?.len();
13435            Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
13436        }
13437        // One Value per array element (`_text` → text / SQL NULL, plain → the
13438        // element's compact JSON text) — the element list the FROM-clause form
13439        // materialises.
13440        SrfKind::ArrayElements { as_text } => {
13441            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13442            if matches!(arg, Value::Null) {
13443                return Ok(Vec::new());
13444            }
13445            let items =
13446                crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13447            Ok(items
13448                .into_iter()
13449                .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13450                .collect())
13451        }
13452        // The scalar form already yields a TextArray of the keys (or errors on
13453        // a non-object, like PG); expand it into rows.
13454        SrfKind::ObjectKeys => {
13455            let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
13456            array_value_to_elements(&v)
13457        }
13458        // One row per match, each a text[] of the pattern's capture groups.
13459        SrfKind::RegexpMatches => {
13460            let vals: Vec<Value<'static>> = args
13461                .iter()
13462                .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
13463                .collect::<Result<_, _>>()?;
13464            crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
13465        }
13466        // One composite `(key, value)` row per object member (plain → jsonb
13467        // value, `_text` → text / SQL NULL).
13468        SrfKind::Each { as_text } => {
13469            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13470            if matches!(arg, Value::Null) {
13471                return Ok(Vec::new());
13472            }
13473            let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13474            Ok(pairs
13475                .into_iter()
13476                .map(|(k, v)| {
13477                    let val = if as_text {
13478                        v.map(Value::text).unwrap_or(Value::Null)
13479                    } else {
13480                        v.map(Value::json).unwrap_or(Value::Null)
13481                    };
13482                    Value::Composite(alloc::vec![
13483                        ("key".to_string(), Value::text(k)),
13484                        ("value".to_string(), val),
13485                    ])
13486                })
13487                .collect())
13488        }
13489        // One Value per matched JSON value.
13490        SrfKind::PathQuery => {
13491            let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13492            let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13493            // v7.39 — optional vars document (3rd arg).
13494            let vars = match args.get(2) {
13495                Some(a) => {
13496                    let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
13497                    crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
13498                }
13499                None => None,
13500            };
13501            match crate::json::path_query_vars(&doc, &path, vars.as_ref())
13502                .map_err(EngineError::Eval)?
13503            {
13504                Value::Null => Ok(Vec::new()),
13505                Value::TextArray(items) => Ok(items
13506                    .into_iter()
13507                    .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13508                    .collect()),
13509                other => Ok(alloc::vec![other]),
13510            }
13511        }
13512    }
13513}
13514
13515/// v7.19 P5 — turn an array-typed `Value` into the element list
13516/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
13517/// = (no rows)`). Non-array values fall through to a type-mismatch
13518/// error.
13519pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
13520    // v7.39 (round 236) — PG unnests a multidimensional array into its
13521    // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
13522    // rows). SPG stores 2-D arrays as their own variants, which fell
13523    // through to the type-mismatch arm below.
13524    if let Some(flat) = crate::eval::values::flatten_2d(v) {
13525        return array_value_to_elements(&flat);
13526    }
13527    // v7.39.11 — every array-family value, through the one element
13528    // menu. The arms below name int / bigint / text / json and stop, so
13529    // `SELECT unnest(ARRAY[1,2]::smallint[])` raised "expects an array
13530    // argument, got smallint[]" — the type it had just been given —
13531    // and so did every catalog vector. Found while closing sentori's
13532    // §4 against 7.39.10; the FROM-clause unnest has the same arm.
13533    if crate::eval::values::array_len(v).is_some() {
13534        if let Some(elems) = crate::eval::values::array_elements(v) {
13535            return Ok(elems);
13536        }
13537    }
13538    match v {
13539        Value::Null => Ok(Vec::new()),
13540        Value::TextArray(items) => Ok(items
13541            .iter()
13542            .map(|opt| {
13543                opt.as_ref()
13544                    .map(|s| Value::text(s.clone()))
13545                    .unwrap_or(Value::Null)
13546            })
13547            .collect()),
13548        Value::IntArray(items) => Ok(items
13549            .iter()
13550            .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
13551            .collect()),
13552        Value::BigIntArray(items) => Ok(items
13553            .iter()
13554            .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
13555            .collect()),
13556        // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
13557        // range per canonical span.
13558        Value::Multirange { kind, ranges } => Ok(ranges
13559            .iter()
13560            .map(|s| Value::Range {
13561                kind: *kind,
13562                lower: s.lower.clone(),
13563                upper: s.upper.clone(),
13564                lower_inc: s.lower_inc,
13565                upper_inc: s.upper_inc,
13566                empty: false,
13567            })
13568            .collect()),
13569        other => Err(EngineError::Eval(EvalError::TypeMismatch {
13570            detail: alloc::format!(
13571                "unnest() expects an array argument, got {}",
13572                crate::conversions::pg_type_name_for_error_opt(other.data_type())
13573            ),
13574        })),
13575    }
13576}
13577
13578impl Engine {
13579    /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
13580    /// the SELECT's FROM / JOIN graph, re-parse each view's body
13581    /// source, and prepend it as a synthetic CTE on the
13582    /// returned SelectStatement. Returns `None` when no view
13583    /// references are found (caller proceeds with the original
13584    /// statement); returns `Some(rewritten)` otherwise (caller
13585    /// re-runs exec_select_cancel on the rewritten form so the
13586    /// regular CTE materialiser handles it).
13587    fn expand_views_in_select(
13588        &self,
13589        stmt: &SelectStatement,
13590    ) -> Result<Option<SelectStatement>, EngineError> {
13591        let cat = self.active_catalog();
13592        let mut referenced: Vec<String> = Vec::new();
13593        if let Some(from) = &stmt.from {
13594            collect_view_refs(&from.primary, cat, &mut referenced);
13595            for j in &from.joins {
13596                collect_view_refs(&j.table, cat, &mut referenced);
13597            }
13598        }
13599        // Don't expand a view name that's already shadowed by a
13600        // CTE on the same SELECT — the CTE wins per PG.
13601        referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
13602        if referenced.is_empty() {
13603            return Ok(None);
13604        }
13605        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
13606        for name in &referenced {
13607            let view = cat.view(name).ok_or_else(|| {
13608                EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13609                    "view {name:?} disappeared mid-expansion"
13610                )))
13611            })?;
13612            let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
13613                EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
13614            })?;
13615            let Statement::Select(body) = parsed else {
13616                return Err(EngineError::Unsupported(alloc::format!(
13617                    "view {name:?} body is not a SELECT (catalog corruption)"
13618                )));
13619            };
13620            new_ctes.push(spg_sql::ast::Cte {
13621                name: name.clone(),
13622                body: spg_sql::ast::CteBody::Select(body),
13623                recursive: false,
13624                column_overrides: view.columns.clone(),
13625                search: None,
13626                cycle: None,
13627            });
13628        }
13629        let mut out = stmt.clone();
13630        // Prepend so view CTEs are visible to caller-supplied CTEs.
13631        new_ctes.extend(out.ctes);
13632        out.ctes = new_ctes;
13633        Ok(Some(out))
13634    }
13635
13636    /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
13637    /// any partition-parent table, rewrite the SELECT so each parent
13638    /// reference resolves to a CTE whose body is a `UNION ALL` over the
13639    /// children that pass the WHERE-derived partition-key range. Returns
13640    /// `None`(no rewrite needed)when no parent is referenced or all
13641    /// references are shadowed by a same-name CTE.
13642    ///
13643    /// Pruning vocabulary at v7.37.6-B:
13644    ///   * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
13645    ///     and `<key> BETWEEN literal AND literal`.
13646    ///   * Anything outside that(OR / nested IN / function call on the
13647    ///     key)defaults to "no pruning" — every child + DEFAULT lands
13648    ///     in the UNION. Correctness is preserved; only the plan size
13649    ///     widens.
13650    fn expand_partition_parents_in_select(
13651        &self,
13652        stmt: &SelectStatement,
13653    ) -> Result<Option<SelectStatement>, EngineError> {
13654        let cat = self.active_catalog();
13655        let Some(from) = &stmt.from else {
13656            return Ok(None);
13657        };
13658        let mut parent_refs: Vec<String> = Vec::new();
13659        collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
13660        for j in &from.joins {
13661            collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
13662        }
13663        // Drop names shadowed by a CTE on the same SELECT(PG semantics
13664        // — same as view expansion above).
13665        parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
13666        if parent_refs.is_empty() {
13667            return Ok(None);
13668        }
13669        // Synthesise a CTE name per parent so the existing
13670        // "CTE shadows a real table" guard doesn't fire (the parent
13671        // IS a real table in the catalog, unlike VIEW expansion's
13672        // case). The FROM-clause TableRef walker below rewrites
13673        // every parent reference to point at the synthetic CTE.
13674        let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
13675        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
13676        let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
13677        for parent_name in &parent_refs {
13678            // No children = no rewrite. The parent itself is a real
13679            // (empty-rows) table — the regular FROM-resolution path
13680            // will scan it and return 0 rows, matching the
13681            // "partition parent with no children" plan. Skipping the
13682            // CTE here also avoids `SELECT * FROM parent` re-entering
13683            // this rewrite on the synthetic body (infinite recursion).
13684            let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
13685                continue;
13686            };
13687            new_ctes.push(spg_sql::ast::Cte {
13688                name: synth_name(parent_name),
13689                body: spg_sql::ast::CteBody::Select(body),
13690                recursive: false,
13691                column_overrides: Vec::new(),
13692                search: None,
13693                cycle: None,
13694            });
13695            expanded_parents.push(parent_name.clone());
13696        }
13697        if expanded_parents.is_empty() {
13698            return Ok(None);
13699        }
13700        let mut out = stmt.clone();
13701        if let Some(from) = out.from.as_mut() {
13702            rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
13703            for j in &mut from.joins {
13704                rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
13705            }
13706        }
13707        new_ctes.extend(out.ctes);
13708        out.ctes = new_ctes;
13709        Ok(Some(out))
13710    }
13711
13712    /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
13713    /// Children include every overlap-hit `Range` plus(always)the
13714    /// `Default` child(if any). Returns `Ok(None)` when no children
13715    /// would survive — caller skips the CTE injection and lets the
13716    /// parent fall through to the regular(empty-rows)scan path,
13717    /// avoiding the infinite recursion that an empty-body CTE
13718    /// referencing the parent name would trigger.
13719    /// v7.37.16 (16.10) — public helper invoked from explain.rs to
13720    /// surface "which children survive the WHERE-clause prune" in
13721    /// EXPLAIN output. Returns `None` when `parent_name` isn't
13722    /// actually a partition parent; otherwise returns the list of
13723    /// children the planner would scan (same algorithm as
13724    /// [`Self::build_partition_parent_union_body`] but without the
13725    /// SQL re-parse).
13726    /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
13727    /// expression (the PG-shaped EXPLAIN's scan builder has no full
13728    /// SelectStatement in hand). Wraps the original by synthesising a
13729    /// minimal statement carrying just the predicate.
13730    pub(crate) fn explain_partition_kept_children_by_where(
13731        &self,
13732        parent_name: &str,
13733        where_: Option<&spg_sql::ast::Expr>,
13734    ) -> Option<Vec<alloc::string::String>> {
13735        let mut synth = SelectStatement::default();
13736        synth.where_ = where_.cloned();
13737        self.explain_partition_kept_children(parent_name, &synth)
13738    }
13739
13740    pub(crate) fn explain_partition_kept_children(
13741        &self,
13742        parent_name: &str,
13743        outer: &SelectStatement,
13744    ) -> Option<Vec<alloc::string::String>> {
13745        use spg_storage::PartitionRole;
13746        let cat = self.active_catalog();
13747        let parent = cat.get(parent_name)?;
13748        let (key_position, parent_kind) = match &parent.schema().partition_role {
13749            Some(PartitionRole::Parent {
13750                key_column_positions,
13751                kind,
13752                ..
13753            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13754            _ => return None,
13755        };
13756        let key_col_name = parent.schema().columns[key_position].name.clone();
13757        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13758            Some(expr) => extract_key_range(expr, &key_col_name),
13759            None => (None, None),
13760        };
13761        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13762            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13763            None => None,
13764        };
13765        let children = crate::partition::children_of_parent(cat, parent_name);
13766        let mut kept: Vec<alloc::string::String> = Vec::new();
13767        let mut default_child: Option<alloc::string::String> = None;
13768        for child_name in &children {
13769            let Some(child) = cat.get(child_name) else {
13770                continue;
13771            };
13772            match &child.schema().partition_role {
13773                Some(PartitionRole::Range { lower, upper, .. }) => {
13774                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13775                        kept.push(child_name.clone());
13776                    }
13777                }
13778                Some(PartitionRole::List { values, .. }) => match &eq_value {
13779                    Some(v) => {
13780                        if values.iter().any(|b| b.equals_value(v)) {
13781                            kept.push(child_name.clone());
13782                        }
13783                    }
13784                    None => kept.push(child_name.clone()),
13785                },
13786                Some(PartitionRole::Hash {
13787                    modulus, remainder, ..
13788                }) => match &eq_value {
13789                    Some(v) => {
13790                        let h = crate::partition::pg_compatible_hash(v);
13791                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13792                            kept.push(child_name.clone());
13793                        }
13794                    }
13795                    None => kept.push(child_name.clone()),
13796                },
13797                Some(PartitionRole::Default { .. }) => {
13798                    default_child = Some(child_name.clone());
13799                }
13800                _ => {}
13801            }
13802        }
13803        let _ = parent_kind;
13804        if let Some(d) = default_child {
13805            if kept.is_empty() || eq_value.is_none() {
13806                kept.push(d);
13807            }
13808        }
13809        Some(kept)
13810    }
13811
13812    fn build_partition_parent_union_body(
13813        &self,
13814        parent_name: &str,
13815        outer: &SelectStatement,
13816    ) -> Result<Option<SelectStatement>, EngineError> {
13817        use spg_storage::PartitionRole;
13818        let cat = self.active_catalog();
13819        let parent = cat.get(parent_name).ok_or_else(|| {
13820            EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13821                "partition parent {parent_name:?} disappeared mid-expansion"
13822            )))
13823        })?;
13824        let (key_position, parent_kind) = match &parent.schema().partition_role {
13825            Some(PartitionRole::Parent {
13826                key_column_positions,
13827                kind,
13828                ..
13829            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13830            // v7.39 (round 645) — an INHERITANCE parent, which has no
13831            // role of its own: the relationship is recorded only in the
13832            // children. Three things differ from a partition parent and
13833            // all three are in this body.
13834            //
13835            //   * The parent HOLDS ROWS, so it is a term of the union —
13836            //     `FROM ONLY`, or expanding it would recurse.
13837            //   * There is no partition key, so there is nothing to
13838            //     prune: every child is a term.
13839            //   * A child may declare columns of its own, so the terms
13840            //     name the PARENT's columns rather than `*`. PG's
13841            //     `SELECT * FROM parent` returns the parent's shape.
13842            //
13843            // Answered from this match rather than a branch before it —
13844            // round 644 measured what an extra early return beside an
13845            // existing test costs in this file.
13846            _ if crate::partition::has_inheritance_children(cat, parent_name) => {
13847                let cols = parent
13848                    .schema()
13849                    .columns
13850                    .iter()
13851                    .map(|c| quote_ident_for_sql(&c.name))
13852                    .collect::<Vec<_>>()
13853                    .join(", ");
13854                let carry_sys = references_ctid(outer);
13855                let sys = if carry_sys {
13856                    let mut t = alloc::string::String::new();
13857                    for s in SYSTEM_COLUMNS {
13858                        t.push_str(", ");
13859                        t.push_str(s);
13860                    }
13861                    t
13862                } else {
13863                    alloc::string::String::new()
13864                };
13865                let mut body = alloc::format!(
13866                    "SELECT {cols}{sys} FROM ONLY {}",
13867                    quote_ident_for_sql(parent_name)
13868                );
13869                for child in crate::partition::children_of_parent(cat, parent_name) {
13870                    body.push_str(&alloc::format!(
13871                        " UNION ALL SELECT {cols}{sys} FROM {}",
13872                        quote_ident_for_sql(&child)
13873                    ));
13874                }
13875                return parse_select_or_corrupt(&body).map(Some);
13876            }
13877            _ => {
13878                return Err(EngineError::Unsupported(alloc::format!(
13879                    "partition expansion: {parent_name:?} is not a parent"
13880                )));
13881            }
13882        };
13883        let key_col_name = parent.schema().columns[key_position].name.clone();
13884        // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
13885        // off the WHERE; for LIST / HASH we extract a single `=`
13886        // literal (and the rest of the planner falls back to "keep
13887        // every child" — same conservative path as 16.1/16.2).
13888        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13889            Some(expr) => extract_key_range(expr, &key_col_name),
13890            None => (None, None),
13891        };
13892        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13893            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13894            None => None,
13895        };
13896        let children = crate::partition::children_of_parent(cat, parent_name);
13897        let mut kept: Vec<String> = Vec::new();
13898        let mut default_child: Option<String> = None;
13899        // First pass — apply per-strategy gates, defer DEFAULT until
13900        // we know whether some non-DEFAULT child matched.
13901        for child_name in &children {
13902            let Some(child) = cat.get(child_name) else {
13903                continue;
13904            };
13905            match &child.schema().partition_role {
13906                Some(PartitionRole::Range { lower, upper, .. }) => {
13907                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13908                        kept.push(child_name.clone());
13909                    }
13910                }
13911                // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
13912                // = <lit>`, only the child whose values contain that
13913                // literal survives. Otherwise (no equality predicate
13914                // or planner couldn't extract one) keep the child
13915                // conservatively.
13916                Some(PartitionRole::List { values, .. }) => match &eq_value {
13917                    Some(v) => {
13918                        if values.iter().any(|b| b.equals_value(v)) {
13919                            kept.push(child_name.clone());
13920                        }
13921                    }
13922                    None => kept.push(child_name.clone()),
13923                },
13924                // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
13925                // we know the residue class deterministically, so
13926                // only the matching REMAINDER child survives.
13927                Some(PartitionRole::Hash {
13928                    modulus, remainder, ..
13929                }) => match &eq_value {
13930                    Some(v) => {
13931                        let h = crate::partition::pg_compatible_hash(v);
13932                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13933                            kept.push(child_name.clone());
13934                        }
13935                    }
13936                    None => kept.push(child_name.clone()),
13937                },
13938                Some(PartitionRole::Default { .. }) => {
13939                    default_child = Some(child_name.clone());
13940                }
13941                _ => {}
13942            }
13943        }
13944        // PG-style DEFAULT semantics: the DEFAULT child must be
13945        // scanned iff some row could fall outside every concrete
13946        // child's bound predicate. We approximate that as "no
13947        // concrete child matched" (== full prune) — strictly
13948        // conservative for LIST / HASH (DEFAULT also catches rows
13949        // outside the union of value-sets / residues), and matches
13950        // PG for the equality case where we *do* know the routing
13951        // outcome.
13952        let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
13953        if let Some(d) = default_child {
13954            if kept.is_empty() {
13955                kept.push(d);
13956            } else if eq_value.is_none() {
13957                // Without an equality literal, the DEFAULT child may
13958                // still hold matching rows (e.g. LIKE on TEXT keys
13959                // for which a LIST partition exists). Keep it.
13960                kept.push(d);
13961            }
13962        }
13963        // Build the UNION ALL body text and re-parse — keeps the
13964        // rewrite expressible in surface SQL so the engine's existing
13965        // parser path handles the AST shape uniformly.
13966        if kept.is_empty() {
13967            // No children survive — caller falls back to scanning the
13968            // (empty) parent table. Returning None here is what
13969            // prevents the synthetic CTE from referring back to the
13970            // parent name and re-entering this rewrite pass.
13971            let _ = parent_name;
13972            return Ok(None);
13973        }
13974        // v7.39 (round 622, S05a) — the system columns of the CHILD the row
13975        // actually lives in.
13976        //
13977        // The parent is read through a synthetic CTE, so a `tableoid` on it
13978        // resolved against that CTE: every row of every child reported
13979        // `__spg_partition_pm`, an internal name no user ever typed, where
13980        // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
13981        // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
13982        // one asks "which partition is this row in", answering 0 rows where
13983        // PG answers 1. `ctid` had the same shape: it numbered the CTE's
13984        // output, so rows in different children got distinct ctids instead
13985        // of each child's own physical position.
13986        //
13987        // Naming them in the term is what carries them: the child scan
13988        // materialises its own six because the statement now references
13989        // them, and they land in SYSTEM_COLUMNS order right after the user
13990        // columns — the exact layout the positional `*` skip already
13991        // expects. Only done when the outer statement asks for one, so a
13992        // plain `SELECT * FROM parent` scans exactly what it scanned.
13993        let carry_sys = references_ctid(outer);
13994        let mut body = alloc::string::String::new();
13995        for (i, child_name) in kept.iter().enumerate() {
13996            if i > 0 {
13997                body.push_str(" UNION ALL ");
13998            }
13999            body.push_str("SELECT *");
14000            if carry_sys {
14001                for sys in SYSTEM_COLUMNS {
14002                    body.push_str(", ");
14003                    body.push_str(sys);
14004                }
14005            }
14006            body.push_str(" FROM ");
14007            body.push_str(&quote_ident_for_sql(child_name));
14008        }
14009        parse_select_or_corrupt(&body).map(Some)
14010    }
14011}
14012
14013/// Rewrite a `TableRef` pointing at a partition parent so it
14014/// references the synthetic CTE created by the expansion. If the
14015/// original ref had no alias, preserve the parent name as an alias
14016/// so column references like `events_partitioned.received_at`
14017/// keep resolving.
14018fn rewrite_partition_parent_table_ref(
14019    t: &mut spg_sql::ast::TableRef,
14020    parents: &[alloc::string::String],
14021    synth_name: &impl Fn(&str) -> alloc::string::String,
14022) {
14023    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
14024        return;
14025    }
14026    // v7.39 (round 644) — an ONLY reference stays pointed at the parent
14027    // itself. The rewrite is keyed on the NAME, so in
14028    // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
14029    // parent list and this then rewrote BOTH — including the one that
14030    // asked not to descend. PG answers 0 for that join; SPG answered 2.
14031    // Folded into the existing test — see the note in
14032    // `collect_partition_parent_refs` for what a separate one cost.
14033    if t.only || !parents.iter().any(|p| p == &t.name) {
14034        return;
14035    }
14036    if t.alias.is_none() {
14037        t.alias = Some(t.name.clone());
14038    }
14039    t.name = synth_name(&t.name);
14040}
14041
14042/// Walk a `TableRef` and push its `name` if it resolves to a partition
14043/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
14044/// `generate_series_args` references — those aren't catalog tables.
14045fn collect_partition_parent_refs(
14046    t: &spg_sql::ast::TableRef,
14047    cat: &spg_storage::Catalog,
14048    out: &mut Vec<alloc::string::String>,
14049) {
14050    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
14051        return;
14052    }
14053    // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
14054    // The keyword used to be absorbed at parse time, so this fanned out
14055    // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
14056    // answered 2 where PG answers 0.
14057    //
14058    // Folded into the existing test rather than given an early return of
14059    // its own: as two extra lines in this function's body it cost
14060    // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
14061    // outside the panel. Rounds 641 and 643 met the same wall from the
14062    // other two directions — adding to a hot function and taking away
14063    // from a cold one. What goes in a body near the row loop is a
14064    // codegen decision whatever its shape.
14065    if !t.only && crate::partition::has_children(cat, &t.name) {
14066        out.push(t.name.clone());
14067    }
14068}
14069
14070/// v7.37.6-B partition-key range derived from a WHERE expression.
14071/// `i64` microseconds since epoch with the same sign convention as
14072/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
14073/// / `=`),`false` ⇒ exclusive(`>` / `<`).
14074#[derive(Debug, Clone, Copy)]
14075pub(crate) struct PartitionFilterBound {
14076    pub micros: i64,
14077    pub inclusive: bool,
14078}
14079
14080/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
14081/// shapes; tighten the running lo / hi as we go. Anything outside that
14082/// (OR / nested calls / non-key columns)is ignored — caller treats
14083/// `None` as "no constraint on that side."
14084fn extract_key_range(
14085    expr: &spg_sql::ast::Expr,
14086    key_col: &str,
14087) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
14088    let mut lo: Option<PartitionFilterBound> = None;
14089    let mut hi: Option<PartitionFilterBound> = None;
14090    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14091    while let Some(e) = stack.pop() {
14092        match e {
14093            spg_sql::ast::Expr::Binary {
14094                lhs,
14095                op: spg_sql::ast::BinOp::And,
14096                rhs,
14097            } => {
14098                stack.push(lhs);
14099                stack.push(rhs);
14100            }
14101            // BETWEEN is desugared at parse time into `lhs >= low AND
14102            // lhs <= high`, so it lands here as two regular Binary
14103            // arms via the AND walker above.
14104            spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
14105                let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
14106                    (Some(lhs.as_ref()), rhs.as_ref(), false)
14107                } else if is_column_ref(rhs, key_col) {
14108                    (Some(rhs.as_ref()), lhs.as_ref(), true)
14109                } else {
14110                    (None, lhs.as_ref(), false)
14111                };
14112                if col_ref.is_none() {
14113                    continue;
14114                }
14115                let Some(lit) = literal_to_micros(lit_side) else {
14116                    continue;
14117                };
14118                use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
14119                let effective_op = if swapped {
14120                    match op {
14121                        Lt => Gt,
14122                        LtEq => GtEq,
14123                        Gt => Lt,
14124                        GtEq => LtEq,
14125                        other => *other,
14126                    }
14127                } else {
14128                    *op
14129                };
14130                match effective_op {
14131                    Eq => {
14132                        tighten_lo(
14133                            &mut lo,
14134                            PartitionFilterBound {
14135                                micros: lit,
14136                                inclusive: true,
14137                            },
14138                        );
14139                        tighten_hi(
14140                            &mut hi,
14141                            PartitionFilterBound {
14142                                micros: lit,
14143                                inclusive: true,
14144                            },
14145                        );
14146                    }
14147                    GtEq => {
14148                        tighten_lo(
14149                            &mut lo,
14150                            PartitionFilterBound {
14151                                micros: lit,
14152                                inclusive: true,
14153                            },
14154                        );
14155                    }
14156                    Gt => {
14157                        tighten_lo(
14158                            &mut lo,
14159                            PartitionFilterBound {
14160                                micros: lit,
14161                                inclusive: false,
14162                            },
14163                        );
14164                    }
14165                    LtEq => {
14166                        tighten_hi(
14167                            &mut hi,
14168                            PartitionFilterBound {
14169                                micros: lit,
14170                                inclusive: true,
14171                            },
14172                        );
14173                    }
14174                    Lt => {
14175                        tighten_hi(
14176                            &mut hi,
14177                            PartitionFilterBound {
14178                                micros: lit,
14179                                inclusive: false,
14180                            },
14181                        );
14182                    }
14183                    _ => {}
14184                }
14185            }
14186            _ => {}
14187        }
14188    }
14189    (lo, hi)
14190}
14191
14192fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14193    match slot {
14194        None => *slot = Some(new),
14195        Some(cur) => {
14196            if new.micros > cur.micros
14197                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14198            {
14199                *slot = Some(new);
14200            }
14201        }
14202    }
14203}
14204
14205fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14206    match slot {
14207        None => *slot = Some(new),
14208        Some(cur) => {
14209            if new.micros < cur.micros
14210                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14211            {
14212                *slot = Some(new);
14213            }
14214        }
14215    }
14216}
14217
14218fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
14219    if let spg_sql::ast::Expr::Column(c) = e {
14220        c.name.eq_ignore_ascii_case(key_col)
14221    } else {
14222        false
14223    }
14224}
14225
14226/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
14227/// `key_col = <literal>` predicate out for LIST/HASH partition
14228/// pruning. Returns `None` when no equality literal can be lifted
14229/// (planner then keeps every child — correctness preserved). The
14230/// returned `Value<'static>` is an owned coercion so the caller can
14231/// outlive any AST node it was extracted from.
14232pub(crate) fn extract_key_eq_value(
14233    expr: &spg_sql::ast::Expr,
14234    key_col: &str,
14235) -> Option<spg_storage::Value<'static>> {
14236    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14237    while let Some(e) = stack.pop() {
14238        match e {
14239            spg_sql::ast::Expr::Binary {
14240                lhs,
14241                op: spg_sql::ast::BinOp::And,
14242                rhs,
14243            } => {
14244                stack.push(lhs);
14245                stack.push(rhs);
14246            }
14247            spg_sql::ast::Expr::Binary {
14248                lhs,
14249                op: spg_sql::ast::BinOp::Eq,
14250                rhs,
14251            } => {
14252                let lit_side = if is_column_ref(lhs, key_col) {
14253                    rhs.as_ref()
14254                } else if is_column_ref(rhs, key_col) {
14255                    lhs.as_ref()
14256                } else {
14257                    continue;
14258                };
14259                let cloned = lit_side.clone();
14260                let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
14261                    continue;
14262                };
14263                // Coerce to an owned Value<'static> so the caller
14264                // can hold it past the WHERE expression's lifetime.
14265                let owned: spg_storage::Value<'static> = match v {
14266                    spg_storage::Value::Text(s) => {
14267                        spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
14268                    }
14269                    spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
14270                    spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
14271                    spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
14272                    spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
14273                    spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
14274                    spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
14275                    spg_storage::Value::Null => spg_storage::Value::Null,
14276                    // Anything else (Vector / Json / Bytes / Numeric /
14277                    // arrays / interval / …) isn't a current partition
14278                    // key type; skip without pruning.
14279                    _ => continue,
14280                };
14281                return Some(owned);
14282            }
14283            _ => {}
14284        }
14285    }
14286    None
14287}
14288
14289/// Coerce a literal Expr(after the parser folded sequence calls etc.)
14290/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
14291/// pruning and routing agree on the literal vocabulary. Returns
14292/// `None` when the literal isn't recognised(planner then skips
14293/// pruning on that branch — correctness preserved).
14294fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
14295    let cloned = e.clone();
14296    let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
14297    match value {
14298        spg_storage::Value::Timestamp(m) => Some(m),
14299        spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
14300        spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
14301        _ => None,
14302    }
14303}
14304
14305/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
14306/// satisfying the WHERE-derived filter range. PG-style half-open:
14307/// child upper exclusive. Filter inclusivity is honoured per-bound.
14308fn range_satisfies_filter(
14309    range_lo: &spg_storage::PartitionBound,
14310    range_hi: &spg_storage::PartitionBound,
14311    filter_lo: Option<&PartitionFilterBound>,
14312    filter_hi: Option<&PartitionFilterBound>,
14313) -> bool {
14314    use spg_storage::PartitionBound;
14315    // For each filter side, reject children that can't host any row
14316    // matching the predicate.
14317    if let Some(lo) = filter_lo {
14318        // child upper bound vs filter lower:
14319        //   if filter is x >= L, child rejects iff child.hi <= L
14320        //   if filter is x  > L, child rejects iff child.hi <= L
14321        //   (child.hi exclusive, so equality with L still rejects)
14322        match range_hi {
14323            PartitionBound::MinValue => return false,
14324            PartitionBound::MaxValue => {}
14325            PartitionBound::TimestampTz(hi) => {
14326                if *hi <= lo.micros {
14327                    return false;
14328                }
14329            }
14330            // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
14331            // matched against TIMESTAMPTZ filters here; keep child
14332            // (conservative: don't prune).
14333            PartitionBound::BigInt(_)
14334            | PartitionBound::Int(_)
14335            | PartitionBound::SmallInt(_)
14336            | PartitionBound::Date(_)
14337            | PartitionBound::Text(_) => {}
14338        }
14339    }
14340    if let Some(hi) = filter_hi {
14341        // child lower bound vs filter upper:
14342        //   if filter is x <= U, child rejects iff child.lo > U
14343        //   if filter is x  < U, child rejects iff child.lo >= U
14344        match range_lo {
14345            PartitionBound::MaxValue => return false,
14346            PartitionBound::MinValue => {}
14347            PartitionBound::TimestampTz(lo) => {
14348                let rejects = if hi.inclusive {
14349                    *lo > hi.micros
14350                } else {
14351                    *lo >= hi.micros
14352                };
14353                if rejects {
14354                    return false;
14355                }
14356            }
14357            PartitionBound::BigInt(_)
14358            | PartitionBound::Int(_)
14359            | PartitionBound::SmallInt(_)
14360            | PartitionBound::Date(_)
14361            | PartitionBound::Text(_) => {}
14362        }
14363    }
14364    true
14365}
14366
14367fn quote_ident_for_sql(name: &str) -> alloc::string::String {
14368    // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
14369    // identifier, otherwise quoted). Conservative: always quote so
14370    // children with reserved names round-trip safely through the
14371    // CTE-body parse.
14372    let mut out = alloc::string::String::with_capacity(name.len() + 2);
14373    out.push('"');
14374    for c in name.chars() {
14375        if c == '"' {
14376            out.push('"');
14377        }
14378        out.push(c);
14379    }
14380    out.push('"');
14381    out
14382}
14383
14384fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
14385    let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
14386        EngineError::Unsupported(alloc::format!(
14387            "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
14388        ))
14389    })?;
14390    let Statement::Select(body) = parsed else {
14391        return Err(EngineError::Unsupported(alloc::format!(
14392            "partition expansion: generated SQL {sql:?} is not a SELECT"
14393        )));
14394    };
14395    Ok(body)
14396}
14397
14398/// v7.39 (read01 round 65/66) — the column shape a set-returning function
14399/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
14400/// yields ONE column named after the call's alias when there is one (`FROM
14401/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
14402/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
14403fn setof_column_shape_from(
14404    declared: &str,
14405    name: &str,
14406    alias: Option<&str>,
14407    got: &[ColumnSchema],
14408) -> alloc::vec::Vec<ColumnSchema> {
14409    let upper = declared.to_ascii_uppercase();
14410    if upper.starts_with("TABLE(") {
14411        let raw = &declared["TABLE(".len()..declared.len() - 1];
14412        return raw
14413            .split(',')
14414            .zip(got.iter())
14415            .map(|(decl, g)| {
14416                let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
14417                ColumnSchema::new(cname.to_string(), g.ty, true)
14418            })
14419            .collect();
14420    }
14421    let cname = alias.unwrap_or(name);
14422    got.first()
14423        .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
14424        .unwrap_or_default()
14425}
14426
14427/// The plpgsql twin: the interpreter hands back raw value rows, so the types
14428/// come off the first row.
14429fn setof_column_shape(
14430    declared: &str,
14431    name: &str,
14432    alias: Option<&str>,
14433    first_row: Option<&alloc::vec::Vec<Value<'static>>>,
14434) -> alloc::vec::Vec<ColumnSchema> {
14435    let got: alloc::vec::Vec<ColumnSchema> = first_row
14436        .map(|r| {
14437            r.iter()
14438                .enumerate()
14439                .map(|(i, v)| {
14440                    ColumnSchema::new(
14441                        alloc::format!("col{i}"),
14442                        v.data_type().unwrap_or(DataType::Text),
14443                        true,
14444                    )
14445                })
14446                .collect()
14447        })
14448        .unwrap_or_default();
14449    setof_column_shape_from(declared, name, alias, &got)
14450}
14451
14452/// v7.39 (read01 round 67) — expand every set-returning call in a target list
14453/// for ONE input row, PG's ProjectSet semantics.
14454///
14455/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
14456/// output has as many rows as the LONGEST of them, and a shorter one is padded
14457/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
14458/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
14459/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
14460/// is zero rows, not one NULL row.
14461///
14462/// Non-SRF items repeat, evaluated once per output row from the same input row.
14463/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
14464/// used to reach the scalar function dispatcher, which reported the aggregate as
14465/// an *unknown function* — the same "symptom two layers above the cause" shape
14466/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
14467/// sees a call, not the clause it came from. The statement knows.
14468/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
14469/// clause may appear.
14470///
14471/// PG rejects `FOR UPDATE` on exactly the shapes that have no
14472/// identifiable base row to lock, each with its own wording. SPG
14473/// accepted all of them and locked nothing, so a query that PG refuses
14474/// outright came back looking like it had taken locks.
14475///
14476/// Every wording read off live PG 18.4.
14477impl crate::Engine {
14478    /// v7.39.2 — a column name in WHERE / ORDER BY / GROUP BY / HAVING
14479    /// that names nothing is refused before the scan, not when a row
14480    /// reaches it.
14481    ///
14482    /// The projection resolves its names eagerly; a predicate only meets
14483    /// them per row. So on an EMPTY table `SELECT a FROM t WHERE nosuch
14484    /// = 1` answered zero rows and no error, and the same statement over
14485    /// a table with one row raised. Measured on PostgreSQL 18.6 and
14486    /// MySQL 9.7.2: both refuse it whatever the row count. A typo in a
14487    /// predicate therefore passed a test written against an empty
14488    /// fixture and failed in production — or, worse, ran nightly over an
14489    /// empty window and reported nothing.
14490    ///
14491    /// Deliberately narrow: ONE plain base table, nothing else. A join,
14492    /// a CTE, a set operation, a lateral or function source, or a
14493    /// subquery in the clause all bring a second scope into which a name
14494    /// may legitimately resolve, and refusing one of those would be a
14495    /// worse defect than the one this closes. Those shapes keep the
14496    /// old behaviour; the walk below does not descend into a subquery
14497    /// for the same reason.
14498    /// v7.39.2 — refuse a call whose argument count no overload accepts,
14499    /// BEFORE the scan rather than per row.
14500    ///
14501    /// `SELECT lower(t, n) FROM t` answered zero rows and no error over
14502    /// an EMPTY table and raised the moment the table had one row in it,
14503    /// because the arity check lives inside the row-time dispatch. It is
14504    /// the same shape as the unknown-column-in-a-predicate defect closed
14505    /// earlier in this release, and it hides in the same place: a query
14506    /// written against an empty fixture passes its test.
14507    ///
14508    /// The accepted counts come from `eval::arity::REFUSED_ARITIES`,
14509    /// which is derived by asking the dispatch itself offline and can
14510    /// only ever UNDER-refuse — see that file for why the two other
14511    /// candidate oracles were refuted.
14512    /// v7.39.3 — MySQL's column names are case-insensitive; PostgreSQL's
14513    /// quoted ones are not. See `EvalContext::col_eq`.
14514    fn col_name_eq(&self, a: &str, b: &str) -> bool {
14515        if self.speaks_mysql {
14516            a.eq_ignore_ascii_case(b)
14517        } else {
14518            a == b
14519        }
14520    }
14521
14522    pub(crate) fn validate_function_arity(
14523        &self,
14524        stmt: &SelectStatement,
14525    ) -> Result<(), EngineError> {
14526        let mut calls: Vec<(alloc::string::String, Vec<Expr>)> = Vec::new();
14527        for it in &stmt.items {
14528            if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14529                collect_function_calls(expr, &mut calls);
14530            }
14531        }
14532        if let Some(w) = &stmt.where_ {
14533            collect_function_calls(w, &mut calls);
14534        }
14535        for o in &stmt.order_by {
14536            collect_function_calls(&o.expr, &mut calls);
14537        }
14538        // The columns a name in this statement could resolve to. Only
14539        // plain base tables; anything else and the types are not
14540        // statically knowable, so nothing is refused early.
14541        let cat = self.active_catalog();
14542        let mut cols: Vec<ColumnSchema> = Vec::new();
14543        if let Some(from) = &stmt.from {
14544            for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14545                if let Some(table) = cat.get(&t.name) {
14546                    cols.extend(table.schema().columns.iter().cloned());
14547                }
14548            }
14549        }
14550        for (name, args) in calls {
14551            let Ok(i) = crate::eval::arity::REFUSED_ARITIES
14552                .binary_search_by(|(n, _)| (*n).cmp(name.as_str()))
14553            else {
14554                continue;
14555            };
14556            if !crate::eval::arity::REFUSED_ARITIES[i]
14557                .1
14558                .contains(&args.len())
14559            {
14560                continue;
14561            }
14562            // v7.39.2 — PostgreSQL names the SIGNATURE it could not
14563            // match, and before the scan there are no values to read a
14564            // type from. Where every argument's type is knowable
14565            // statically — a column of a source table, or a literal —
14566            // the sentence is PostgreSQL's exactly; where one is not,
14567            // this leaves the call to the row-time raise, which has the
14568            // values. Refusing early with a WORSE message would trade
14569            // one defect for another.
14570            let mut types: Vec<alloc::string::String> = Vec::new();
14571            for a in &args {
14572                let Some(t) = static_arg_type(a, &cols) else {
14573                    types.clear();
14574                    break;
14575                };
14576                types.push(t);
14577            }
14578            if types.len() != args.len() {
14579                continue;
14580            }
14581            return Err(EngineError::Eval(EvalError::WrongArity {
14582                name,
14583                types: types.join(", "),
14584            }));
14585        }
14586        Ok(())
14587    }
14588
14589    pub(crate) fn validate_clause_columns(
14590        &self,
14591        stmt: &SelectStatement,
14592    ) -> Result<(), EngineError> {
14593        let Some(from) = &stmt.from else {
14594            return Ok(());
14595        };
14596        if !stmt.ctes.is_empty() {
14597            return Ok(());
14598        }
14599        // v7.39.2 — every source, not just the first. A join is checkable
14600        // for the same reason one table is: with no CTE and no
14601        // subquery-shaped source, a bare name has to come from one of
14602        // them. Refusing the check for joins left `SELECT … FROM a JOIN b
14603        // … WHERE nosuch = 1` labelled `'field list'` where MySQL 9.7.2
14604        // says `'where clause'`.
14605        let plain = |t: &spg_sql::ast::TableRef| -> bool {
14606            t.unnest_expr.is_none()
14607                && t.generate_series_args.is_none()
14608                && t.lateral_subquery.is_none()
14609                && t.jsonb_each_text_arg.is_none()
14610                && t.table_fn_call.is_none()
14611                && t.rows_from.is_none()
14612                && t.json_table.is_none()
14613                && !t.scalar_fn_item
14614        };
14615        let cat = self.active_catalog();
14616        let mut sources: Vec<(String, &spg_storage::Table)> = Vec::new();
14617        for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14618            if !plain(t) {
14619                return Ok(());
14620            }
14621            let Some(table) = cat.get(&t.name) else {
14622                return Ok(());
14623            };
14624            sources.push((t.alias.clone().unwrap_or_else(|| t.name.clone()), table));
14625        }
14626        let known = |c: &spg_sql::ast::ColumnName| -> bool {
14627            // A system column is not in a table's list and is a perfectly
14628            // good predicate: `WHERE ctid = '(0,4)'::tid` and `WHERE
14629            // tableoid::regclass::text = 'pm_a'` are both real, and the
14630            // first draft of this check refused them. The e2e suite said
14631            // so immediately, which is what it is for.
14632            if is_system_column(&c.name) {
14633                return true;
14634            }
14635            if let Some(q) = &c.qualifier {
14636                // A qualifier must name one of this statement's sources,
14637                // and that source must carry the column. An alias
14638                // REPLACES the written name, which is PostgreSQL's rule
14639                // and MySQL's: `FROM pg_cast c WHERE pg_cast.oid <> 0`
14640                // is an error on both.
14641                return match sources.iter().find(|(a, _)| a == q) {
14642                    Some((_, t)) => t
14643                        .schema()
14644                        .columns
14645                        .iter()
14646                        .any(|sc| self.col_name_eq(&sc.name, &c.name)),
14647                    None => false,
14648                };
14649            }
14650            sources
14651                .iter()
14652                .any(|(_, t)| {
14653                    t.schema()
14654                        .columns
14655                        .iter()
14656                        .any(|sc| self.col_name_eq(&sc.name, &c.name))
14657                })
14658                // An output name the statement itself defines: ORDER BY,
14659                // GROUP BY and HAVING may all name one.
14660                || stmt.items.iter().any(|it| match it {
14661                    SelectItem::Expr { expr, alias } => {
14662                        alias.as_deref() == Some(c.name.as_str())
14663                            || matches!(expr, Expr::Column(pc) if pc.name == c.name)
14664                    }
14665                    _ => false,
14666                })
14667        };
14668        // v7.39.2 — the CLAUSE travels with the reference, because MySQL
14669        // names it: `Unknown column 'x' in 'where clause'`, `'order
14670        // clause'`, `'group statement'`, `'having clause'`. Measured on
14671        // 9.7.2, and a driver's error handling reads the sentence as well
14672        // as the number. PostgreSQL says only `column "x" does not
14673        // exist`, with no clause, so its wording is unchanged.
14674        //
14675        // This walk is the only place the clause is still known: by the
14676        // time a row-time resolver meets the name, the expression has
14677        // been detached from the statement that held it.
14678        let mut refs: Vec<(spg_sql::ast::ColumnName, &'static str)> = Vec::new();
14679        let mut push = |e: &Expr, ctx: &'static str, out: &mut Vec<_>| {
14680            let mut here: Vec<spg_sql::ast::ColumnName> = Vec::new();
14681            collect_plain_column_refs(e, &mut here);
14682            out.extend(here.into_iter().map(|c| (c, ctx)));
14683        };
14684        if let Some(w) = &stmt.where_ {
14685            push(w, "where clause", &mut refs);
14686        }
14687        if let Some(g) = &stmt.group_by {
14688            for e in g {
14689                push(e, "group statement", &mut refs);
14690            }
14691        }
14692        if let Some(h) = &stmt.having {
14693            push(h, "having clause", &mut refs);
14694        }
14695        for o in &stmt.order_by {
14696            push(&o.expr, "order clause", &mut refs);
14697        }
14698        // v7.39.2 — and the join predicates, which MySQL calls the `on
14699        // clause`. Measured on 9.7.2: `Unknown column 'j1.nosuch' in 'on
14700        // clause'`, qualifier and all.
14701        for j in &from.joins {
14702            if let Some(on) = &j.on {
14703                push(on, "on clause", &mut refs);
14704            }
14705        }
14706        for (c, ctx) in &refs {
14707            if !known(c) {
14708                if self.speaks_mysql {
14709                    // The QUALIFIER travels with it: MySQL 9.7.2 answers
14710                    // `Unknown column 'j1.nosuch' in 'on clause'`, not the
14711                    // bare name. Measured.
14712                    let shown = match &c.qualifier {
14713                        Some(q) => alloc::format!("{q}.{}", c.name),
14714                        None => c.name.clone(),
14715                    };
14716                    return Err(EngineError::Eval(EvalError::TypeMismatch {
14717                        detail: alloc::format!("Unknown column '{shown}' in '{ctx}'"),
14718                    }));
14719                }
14720                // PostgreSQL 18.6 names the missing TABLE when the
14721                // qualifier is the part that resolves to nothing
14722                // (`missing FROM-clause entry for table "pg_cast"`) and
14723                // the COLUMN otherwise. Raising the column error for both
14724                // dropped the table name a caller matches on.
14725                if let Some(q) = &c.qualifier
14726                    && !sources.iter().any(|(a, _)| a == q)
14727                {
14728                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
14729                        qualifier: q.clone(),
14730                        column: c.name.clone(),
14731                    }));
14732                }
14733                // v7.39.2 — and a qualified reference whose qualifier
14734                // DOES resolve prints the whole thing, unquoted:
14735                // `column ea.no_such does not exist` (measured on PG
14736                // 18.6). The bare `column "no_such" does not exist` drops
14737                // the alias a caller matches on, which is what the
14738                // sqlx round-20 pin says.
14739                if let Some(q) = &c.qualifier {
14740                    return Err(EngineError::Eval(EvalError::QualifiedColumnNotFound {
14741                        qualifier: q.clone(),
14742                        column: c.name.clone(),
14743                    }));
14744                }
14745                return Err(EngineError::Eval(EvalError::ColumnNotFound {
14746                    name: c.name.clone(),
14747                }));
14748            }
14749        }
14750        Ok(())
14751    }
14752}
14753
14754/// v7.39.2 — the column references of an expression, NOT descending into
14755/// a subquery.
14756///
14757/// A correlated subquery resolves its names against an outer scope this
14758/// walk cannot see, so descending would refuse valid queries. Missing a
14759/// typo inside one is the safe direction; refusing a good query is not.
14760/// v7.39.2 — the type PostgreSQL would name for an argument, when it
14761/// can be known without a row: a column of a source table, or a
14762/// literal. `None` for anything else, which is what keeps the pre-scan
14763/// refusal from printing a worse sentence than the row-time one.
14764pub(crate) fn static_arg_type(e: &Expr, cols: &[ColumnSchema]) -> Option<alloc::string::String> {
14765    use spg_sql::ast::Literal as L;
14766    match e {
14767        Expr::Column(c) => cols
14768            .iter()
14769            .find(|s| s.name.eq_ignore_ascii_case(&c.name))
14770            .map(|s| crate::conversions::pg_type_name_for_error(s.ty)),
14771        // A bare literal has no type yet on PostgreSQL — it names it
14772        // `unknown` in this very sentence — except where the lexeme
14773        // fixes one.
14774        Expr::Literal(L::String(_)) | Expr::Literal(L::Null) => {
14775            Some(alloc::string::String::from("unknown"))
14776        }
14777        Expr::Literal(L::Integer(_)) => Some(alloc::string::String::from("integer")),
14778        Expr::Literal(L::Bool(_)) => Some(alloc::string::String::from("boolean")),
14779        _ => None,
14780    }
14781}
14782
14783/// v7.39.2 — the function calls of an expression, name and argument
14784/// count, NOT descending into a subquery (its scope is its own).
14785fn collect_function_calls(e: &Expr, out: &mut Vec<(alloc::string::String, Vec<Expr>)>) {
14786    match e {
14787        Expr::FunctionCall { name, args } => {
14788            out.push((name.to_ascii_lowercase(), args.clone()));
14789            for a in args {
14790                collect_function_calls(a, out);
14791            }
14792        }
14793        Expr::Binary { lhs, rhs, .. } => {
14794            collect_function_calls(lhs, out);
14795            collect_function_calls(rhs, out);
14796        }
14797        Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14798            collect_function_calls(expr, out);
14799        }
14800        _ => {}
14801    }
14802}
14803
14804fn collect_plain_column_refs(e: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
14805    match e {
14806        Expr::Column(c) => out.push(c.clone()),
14807        Expr::Binary { lhs, rhs, .. } => {
14808            collect_plain_column_refs(lhs, out);
14809            collect_plain_column_refs(rhs, out);
14810        }
14811        Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14812            collect_plain_column_refs(expr, out);
14813        }
14814        Expr::FunctionCall { args, .. } => {
14815            for a in args {
14816                collect_plain_column_refs(a, out);
14817            }
14818        }
14819        _ => {}
14820    }
14821}
14822
14823fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
14824    let Some(lock) = &stmt.locking else {
14825        return Ok(());
14826    };
14827    let verb = lock_clause_verb(lock.strength);
14828    let refuse = |what: &str| {
14829        Err(EngineError::Unsupported(alloc::format!(
14830            "{verb} is not allowed with {what}"
14831        )))
14832    };
14833    if !stmt.unions.is_empty() {
14834        return refuse("UNION/INTERSECT/EXCEPT");
14835    }
14836    if stmt.distinct || !stmt.distinct_on.is_empty() {
14837        return refuse("DISTINCT clause");
14838    }
14839    if stmt.group_by.is_some() || stmt.group_by_all {
14840        return refuse("GROUP BY clause");
14841    }
14842    let has_agg = stmt.items.iter().any(|it| match it {
14843        spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
14844        _ => false,
14845    });
14846    if has_agg {
14847        return refuse("aggregate functions");
14848    }
14849    // `FOR UPDATE OF t` must name a relation that is actually in FROM.
14850    for want in &lock.of_tables {
14851        if !locking_from_names(stmt)
14852            .iter()
14853            .any(|n| n.eq_ignore_ascii_case(want))
14854        {
14855            return Err(EngineError::Unsupported(alloc::format!(
14856                "relation \"{want}\" in {verb} clause not found in FROM clause"
14857            )));
14858        }
14859    }
14860    Ok(())
14861}
14862
14863/// How PG names the clause in its diagnostics.
14864const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
14865    use spg_sql::ast::LockStrength as LS;
14866    match s {
14867        LS::Update => "FOR UPDATE",
14868        LS::NoKeyUpdate => "FOR NO KEY UPDATE",
14869        LS::Share => "FOR SHARE",
14870        LS::KeyShare => "FOR KEY SHARE",
14871    }
14872}
14873
14874/// Every relation name (or alias) the FROM clause exposes.
14875fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
14876    let mut out = alloc::vec::Vec::new();
14877    if let Some(f) = &stmt.from {
14878        let mut push = |t: &spg_sql::ast::TableRef| {
14879            if let Some(a) = &t.alias {
14880                out.push(a.clone());
14881            }
14882            out.push(t.name.clone());
14883        };
14884        push(&f.primary);
14885        for j in &f.joins {
14886            push(&j.table);
14887        }
14888    }
14889    out
14890}
14891
14892fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
14893    use spg_sql::ast::Expr;
14894    if let Some(w) = &stmt.where_
14895        && aggregate::contains_aggregate(w)
14896    {
14897        return Err(EngineError::Unsupported(
14898            "aggregate functions are not allowed in WHERE".into(),
14899        ));
14900    }
14901    let mut nested = false;
14902    let mut check = |e: &Expr| {
14903        let mut probe = e.clone();
14904        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14905            let args = match n {
14906                Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
14907                _ => return false,
14908            };
14909            if args.iter().any(aggregate::contains_aggregate) {
14910                nested = true;
14911            }
14912            false
14913        });
14914    };
14915    for it in &stmt.items {
14916        if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14917            check(expr);
14918        }
14919    }
14920    if let Some(h) = &stmt.having {
14921        check(h);
14922    }
14923    for o in &stmt.order_by {
14924        check(&o.expr);
14925    }
14926    if nested {
14927        return Err(EngineError::Unsupported(
14928            "aggregate function calls cannot be nested".into(),
14929        ));
14930    }
14931    Ok(())
14932}
14933
14934/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
14935/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
14936/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
14937/// to a set and then applies the enclosing expression once per element. SPG only
14938/// ever recognised an SRF that WAS the item, so everything above died on
14939/// "unknown function unnest" — the set-returning call, wrapped in anything at
14940/// all, fell through to the scalar function dispatcher which has no such name.
14941///
14942/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
14943/// rewritten to read that column, and the rewritten expression is evaluated once
14944/// per output row against the input row extended with the lifted values. The
14945/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
14946/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
14947/// executors (the single-table scan, the synthetic-table pipeline, and the
14948/// unnest FROM path) each evaluated the key as an ordinary expression, where the
14949/// literal `n` is just the constant n — the same sort key for every row. The
14950/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
14951/// back in input order, not in a wrong order. Statement prep resolves the common
14952/// case, but only when the SELECT item is an expression — a `*` is not one, and
14953/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
14954/// spelling landed on exactly the shape prep could not resolve.
14955///
14956/// A set-returning item is left alone: copying it into ORDER BY would make the
14957/// key "the whole set", evaluated once per INPUT row.
14958fn resolve_positional_order_by(
14959    order_by: &[spg_sql::ast::OrderBy],
14960    projection: &[ProjectedItem],
14961) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
14962    order_by
14963        .iter()
14964        .filter_map(|o| {
14965            let mut o = o.clone();
14966            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
14967                && *n >= 1
14968                && let Ok(idx) = usize::try_from(*n - 1)
14969                && let Some(item) = projection.get(idx)
14970                && !expr_contains_builtin_srf(&item.expr)
14971            {
14972                // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
14973                // item is itself an integer LITERAL must not be
14974                // substituted textually: the literal would read as an
14975                // ordinal again downstream, and `SELECT 10 … ORDER BY
14976                // 1` died with "position 10 is not in select list"
14977                // where PG happily returns the rows. Ordering by a
14978                // constant orders nothing, so the key drops.
14979                if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
14980                    return None;
14981                }
14982                o.expr = item.expr.clone();
14983            }
14984            Some(o)
14985        })
14986        .collect()
14987}
14988
14989/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
14990/// this expression? Statement preparation (`resolve_order_by_position`) runs
14991/// before any catalog is in hand, and it only needs to know "is this item's value
14992/// a set", which the builtin SRFs answer syntactically.
14993pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
14994    let mut found = false;
14995    let mut probe = e.clone();
14996    crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14997        if is_top_level_unnest(n) {
14998            found = true;
14999            return true;
15000        }
15001        false
15002    });
15003    found
15004}
15005
15006/// v7.39 (round 599) — everything about a target-list SRF that does not
15007/// depend on the row.
15008///
15009/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
15010/// each SRF-bearing projection expression, walked and rewrote the tree,
15011/// formatted a `__srf_N` name per node, and copied the whole column schema.
15012/// A counting allocator put the path at 24 allocations per input row for a
15013/// single-element `unnest`, against 0 for the same scan without one — 211 MB
15014/// where the plain scan took 4.3 — and the shape held whatever the array
15015/// contained, which is what invariant work looks like.
15016struct SrfPlan {
15017    /// The lifted SRF calls, in slot order.
15018    nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
15019    /// Per projection position, the expression with its SRF calls replaced
15020    /// by `__srf_N` column references. `None` means the item has none.
15021    rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
15022    /// The input schema followed by one column per slot. Only the slots'
15023    /// TYPES vary per row, and they are patched in place.
15024    ext_cols: alloc::vec::Vec<ColumnSchema>,
15025    /// v7.39 (round 743) — the rewritten projection COMPILED against the
15026    /// extended schema, once per plan. The per-output-row evaluation ran
15027    /// the interpreter (~560 ns/row on the unnest panel cell); the Step
15028    /// VM reads the `__srf_N` slots as plain columns. `None` = that item
15029    /// is not fully compilable and keeps the interpreter.
15030    compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
15031    base_cols: usize,
15032}
15033
15034fn build_srf_plan(
15035    engine: &Engine,
15036    projection: &[ProjectedItem],
15037    srf_idxs: &[usize],
15038    ctx: &EvalContext<'_>,
15039) -> Result<SrfPlan, EngineError> {
15040    // Lift every SRF node out of every item that contains one.
15041    let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
15042    let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
15043    let mut reject: Option<EngineError> = None;
15044    for &i in srf_idxs {
15045        let mut e = projection[i].expr.clone();
15046        crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
15047            if reject.is_some() {
15048                return true;
15049            }
15050            // PG refuses a set-returning function inside a conditional: the set
15051            // would have to be produced before anyone knows whether the branch
15052            // is even taken.
15053            let conditional = match n {
15054                spg_sql::ast::Expr::Case { .. } => Some("CASE"),
15055                spg_sql::ast::Expr::FunctionCall { name, .. }
15056                    if name.eq_ignore_ascii_case("coalesce") =>
15057                {
15058                    Some("COALESCE")
15059                }
15060                _ => None,
15061            };
15062            if let Some(kind) = conditional
15063                && engine.expr_contains_srf(n)
15064            {
15065                reject = Some(EngineError::Unsupported(alloc::format!(
15066                    "set-returning functions are not allowed in {kind}"
15067                )));
15068                return true;
15069            }
15070            if !engine.is_srf_node(n) {
15071                return false;
15072            }
15073            let slot = nodes.len();
15074            nodes.push(n.clone());
15075            *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
15076                qualifier: None,
15077                name: alloc::format!("__srf_{slot}"),
15078            });
15079            true
15080        });
15081        rewritten[i] = Some(e);
15082    }
15083    if let Some(err) = reject {
15084        return Err(err);
15085    }
15086    let base_cols = ctx.columns.len();
15087    let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
15088    for slot in 0..nodes.len() {
15089        ext_cols.push(ColumnSchema::new(
15090            alloc::format!("__srf_{slot}"),
15091            DataType::Text,
15092            true,
15093        ));
15094    }
15095    // v7.39 (round 743) — compile the rewritten items against the
15096    // EXTENDED schema. The slot columns' declared type is a per-row
15097    // patched detail the compiled column read does not consult.
15098    let compiled: Vec<Option<eval::CompiledExpr>> = {
15099        let mut ext_ctx = ctx.clone();
15100        ext_ctx.columns = &ext_cols;
15101        projection
15102            .iter()
15103            .enumerate()
15104            .map(|(i, p)| {
15105                let e = rewritten[i].as_ref().unwrap_or(&p.expr);
15106                if eval::fully_compilable(e) {
15107                    Some(eval::compile_expr(e, &ext_ctx))
15108                } else {
15109                    None
15110                }
15111            })
15112            .collect()
15113    };
15114    Ok(SrfPlan {
15115        nodes,
15116        rewritten,
15117        ext_cols,
15118        compiled,
15119        base_cols,
15120    })
15121}
15122
15123/// One input row expanded through a plan built once for the whole scan.
15124/// v7.39 (round 621) — expand a projection whose target list contains
15125/// set-returning items, remembering which INPUT row each output row came from.
15126///
15127/// The three materialised-source tails — `FROM unnest(…)`, `FROM
15128/// generate_series(…)`, and the one that serves VALUES / a derived table /
15129/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
15130/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
15131/// v(x)` answered `function unnest(integer[]) does not exist` on all the
15132/// others, for a query PG answers. Sharing the expansion is the point: a
15133/// fourth copy would have been the fourth place to forget.
15134fn expand_projection_srfs(
15135    engine: &Engine,
15136    projection: &[ProjectedItem],
15137    srf_idxs: &[usize],
15138    filtered: &[Row<'static>],
15139    ctx: &EvalContext<'_>,
15140) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
15141    let mut out = alloc::vec::Vec::with_capacity(filtered.len());
15142    let mut src = alloc::vec::Vec::with_capacity(filtered.len());
15143    // v7.39 (round 726) — ONE plan for the whole scan. The per-row
15144    // spelling rebuilt it for every input row: a full clone of the
15145    // rewritten projection trees and the extended schema, 50k times on
15146    // the panel's unnest cell.
15147    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15148    // v7.39 (round 733) — shard the expansion. Each shard clones the
15149    // plan (its ext_cols slot types are per-row mutable) and builds a
15150    // MINIMAL context — EvalContext is not Sync — which is sound only
15151    // when every expression involved is pure: the whole projection and
15152    // every SRF argument must be fully_compilable, or the row loop
15153    // stays serial with the full session context.
15154    // The projection is judged in its REWRITTEN form — the SRF call
15155    // itself is never compilable, but after the lift it is a plain
15156    // `__srf_N` column reference.
15157    let all_pure = projection
15158        .iter()
15159        .enumerate()
15160        .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
15161        && plan.nodes.iter().all(|n| match n {
15162            Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
15163            other => eval::fully_compilable(other),
15164        });
15165    if all_pure
15166        && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
15167        && let Some(r) = engine.parallel_runner.0.as_deref()
15168    {
15169        let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
15170        let chunk = filtered.len().div_ceil(n_shards);
15171        type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
15172        let schema_cols = ctx.columns;
15173        let alias = ctx.table_alias;
15174        let mysql = ctx.mysql_dialect;
15175        let style = ctx.render_style;
15176        let plan_ref = &plan;
15177        let results = r.run_shards(n_shards, &|si| {
15178            let lo = si * chunk;
15179            let hi = ((si + 1) * chunk).min(filtered.len());
15180            let mut sctx = eval::EvalContext::new(schema_cols, alias);
15181            sctx.mysql_dialect = mysql;
15182            sctx.render_style = style;
15183            // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
15184            // compiled programs); each shard rebuilds it, which also
15185            // recompiles against the shard's own context. Build errors
15186            // were already surfaced by the outer build above.
15187            let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
15188                Ok(p) => p,
15189                Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
15190            };
15191            let mut run = || -> ShardOut {
15192                let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
15193                let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
15194                for (i, row) in filtered[lo..hi].iter().enumerate() {
15195                    let expanded =
15196                        expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
15197                    sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
15198                    o.extend(expanded);
15199                }
15200                Ok((o, sidx))
15201            };
15202            alloc::boxed::Box::new(run())
15203        });
15204        for boxed in results {
15205            let shard = boxed
15206                .downcast::<ShardOut>()
15207                .expect("runner echoes the closure's box");
15208            let (o, sidx) = (*shard)?;
15209            out.extend(o);
15210            src.extend(sidx);
15211        }
15212        return Ok((out, src));
15213    }
15214    for (i, row) in filtered.iter().enumerate() {
15215        let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
15216        src.extend(core::iter::repeat_n(i, expanded.len()));
15217        out.extend(expanded);
15218    }
15219    Ok((out, src))
15220}
15221
15222/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
15223///
15224/// A key that names a select-list item reads it out of the EXPANDED row,
15225/// because PG sorts after the expansion. A key that names a source column the
15226/// query does not project is evaluated against the input row that output row
15227/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
15228fn srf_order_key(
15229    ob: &spg_sql::ast::OrderBy,
15230    out_col: Option<usize>,
15231    out: &Row<'static>,
15232    src: &Row<'static>,
15233    ctx: &EvalContext<'_>,
15234) -> Result<Value<'static>, EngineError> {
15235    match out_col {
15236        Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
15237        None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
15238    }
15239}
15240
15241fn expand_srf_row_with(
15242    engine: &Engine,
15243    plan: &mut SrfPlan,
15244    projection: &[ProjectedItem],
15245    row: &Row<'static>,
15246    ctx: &EvalContext<'_>,
15247) -> Result<Vec<Row<'static>>, EngineError> {
15248    let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
15249    for n in &plan.nodes {
15250        lists.push(engine.srf_values(n, row, ctx)?);
15251    }
15252    let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
15253    // Only the slots' element types depend on the row; the names and the
15254    // input schema around them do not.
15255    for (slot, list) in lists.iter().enumerate() {
15256        plan.ext_cols[plan.base_cols + slot].ty = list
15257            .iter()
15258            .find_map(|v| v.data_type())
15259            .unwrap_or(DataType::Text);
15260    }
15261    let mut ext_ctx = ctx.clone();
15262    ext_ctx.columns = &plan.ext_cols;
15263    let mut out = Vec::with_capacity(n_rows);
15264    // v7.39 (round 726) — the base columns are the SAME for every
15265    // expanded row; clone them once and rewrite only the SRF slots per
15266    // k. The old form cloned the whole input row per OUTPUT row — for
15267    // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
15268    // TEXT column the projection never reads.
15269    let base_len = row.values.len();
15270    let mut ext_vals = row.values.clone();
15271    ext_vals.resize(base_len + lists.len(), Value::Null);
15272    let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15273    for k in 0..n_rows {
15274        for (slot, list) in lists.iter().enumerate() {
15275            // Past the end of THIS srf's rows → NULL (PG pads).
15276            ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
15277        }
15278        let ext_row = Row::new(core::mem::take(&mut ext_vals));
15279        let mut vals = Vec::with_capacity(projection.len());
15280        for (i, p) in projection.iter().enumerate() {
15281            // v7.39 (round 743) — compiled when possible; the
15282            // interpreter for the rest, with its exact wording.
15283            vals.push(match &plan.compiled[i] {
15284                Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
15285                    .map_err(EngineError::Eval)?,
15286                None => {
15287                    let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
15288                    eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
15289                }
15290            });
15291        }
15292        ext_vals = ext_row.values;
15293        out.push(Row::new(vals));
15294    }
15295    Ok(out)
15296}
15297
15298/// The one-shot spelling, for the callers that expand a single row.
15299/// v7.39 (round 600) — which output column each ORDER BY key names, for a
15300/// query whose target list contains a set-returning function.
15301///
15302/// The keys used to be built from the INPUT row, before the SRF expanded, so
15303/// anything that named the SRF's own output was evaluated as a scalar call:
15304/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
15305/// "function unnest(integer[]) does not exist", and so did the spellings that
15306/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
15307/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
15308/// back in input order. PG sorts AFTER the expansion, so a key that names a
15309/// select-list item reads that item's value out of the expanded row.
15310///
15311/// `None` keeps the key on the input row, which is where an ORDER BY naming
15312/// a column the query does not project has to be evaluated.
15313/// v7.38.19 — the output column an ORDER BY term reads, when reading it
15314/// is provably the same as building a key from the input row.
15315///
15316/// A sort key is a COPY of the sort column, made because the source row
15317/// is gone by the time the sort runs — only the projection survives. On
15318/// `SELECT s_long FROM t ORDER BY s_long` that copy is of data the
15319/// projected row already holds, and on 400,000 rows of 192-character
15320/// text it is 400,000 allocations, 400,000 frees and 77 MB of copying.
15321/// A profile of that cell put the allocator at 2,025 leaf samples of the
15322/// working set, second only to the comparison chain.
15323///
15324/// The condition is narrow on purpose. `srf_order_output_cols` resolves
15325/// an ORDER BY term the way SQL does — a positional ordinal, or a name
15326/// matching the select list — and SQL resolves against the select list
15327/// BEFORE the input columns. The key path resolves against the INPUT
15328/// columns. For `SELECT g AS id … ORDER BY id` on a table that also has
15329/// an `id`, those are different columns, and swapping one for the other
15330/// would change answers rather than timings.
15331///
15332/// So this takes only the case where the two cannot disagree: a bare
15333/// unqualified column name, matching exactly one output item, whose own
15334/// expression is that same column. The projected cell then IS the input
15335/// cell, and the key would have been its copy.
15336/// True when comparing two of this column's VALUES gives the same order
15337/// as comparing the sort KEYS built from them.
15338///
15339/// It does not hold widely. A user ENUM stores its label as text but
15340/// orders by DECLARATION position; an array orders element-wise; a
15341/// domain or composite carries its own rules. For those the two paths
15342/// answer differently, and a sort that skipped the key would silently
15343/// reorder the result. This is the short list where they agree.
15344fn value_order_is_key_order(col: &ColumnSchema) -> bool {
15345    use spg_storage::DataType as T;
15346    col.user_enum_type.is_none()
15347        && col.user_domain_type.is_none()
15348        && col.user_composite_type.is_none()
15349        && col.collation_name.is_none()
15350        && col.collation == spg_storage::Collation::Binary
15351        && matches!(
15352            col.ty,
15353            T::SmallInt | T::Int | T::BigInt | T::Text | T::Varchar(_) | T::Bool | T::Uuid
15354        )
15355}
15356
15357/// The full ORDER BY comparison between two rows, named by index.
15358///
15359/// v7.38.19 — what a permutation sort falls back to when its key ties.
15360fn row_cmp_by_index(
15361    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15362    terms: &[(usize, bool, Option<bool>)],
15363    colls: &[Option<crate::collate::Collated>],
15364    mysql: bool,
15365    ia: u32,
15366    ib: u32,
15367) -> core::cmp::Ordering {
15368    let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
15369    for (i, (col, desc, nf)) in terms.iter().enumerate() {
15370        let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col)) else {
15371            continue;
15372        };
15373        let ord = match (va, vb) {
15374            (Value::Text(x), Value::Text(y)) => match colls.get(i).and_then(Option::as_ref) {
15375                Some(c) => {
15376                    let o = c.compare(x, y);
15377                    if *desc { o.reverse() } else { o }
15378                }
15379                None if !mysql => {
15380                    let o = crate::orderby::str_cmp_prefix_first(x, y);
15381                    if *desc { o.reverse() } else { o }
15382                }
15383                None => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15384            },
15385            _ => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15386        };
15387        if ord != core::cmp::Ordering::Equal {
15388            return ord;
15389        }
15390    }
15391    core::cmp::Ordering::Equal
15392}
15393
15394/// Whether ordering these rows by BYTES is what the collation in force
15395/// would have answered anyway.
15396///
15397/// v7.38.19 — a collated sort used to be shut out of the keyed path
15398/// entirely, and the cost of that showed up the moment the byte path
15399/// got fast: on the same fixture, the same binary took 92 ms under `C`
15400/// and 371 ms under `en_US`, so declaring a collation had become a
15401/// four-fold tax on a query that sorts md5 hex.
15402///
15403/// It need not be. For several locales `[0-9a-z]` orders exactly as
15404/// bytes do -- `collate::ascii_byte_order` carries that fact, and the
15405/// test beside it re-derives the whole allowlist by sorting a corpus
15406/// twice rather than asserting it. So when the collation is one of
15407/// those AND every value in every sort column is drawn from that
15408/// alphabet, the byte answer IS the collated answer.
15409///
15410/// Both halves are required. A collation outside the list can put `z`
15411/// between `s` and `t`; a value outside the alphabet can be `Ápple`,
15412/// which no locale in the list orders by its bytes. Either one and this
15413/// returns false, and the sort takes the collator's own path.
15414fn byte_order_answers_the_collation(
15415    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15416    terms: &[(usize, bool, Option<bool>)],
15417    colls: &[Option<crate::collate::Collated>],
15418) -> bool {
15419    if colls.iter().all(Option::is_none) {
15420        return true;
15421    }
15422    if !colls
15423        .iter()
15424        .flatten()
15425        .all(crate::collate::Collated::ascii_byte_order)
15426    {
15427        return false;
15428    }
15429    tagged.iter().all(|(_, row)| {
15430        terms.iter().all(|(col, _, _)| match row.values.get(*col) {
15431            // Only TEXT is collation-sensitive; a number or a NULL
15432            // orders the same under every collation there is.
15433            Some(Value::Text(t)) => crate::collate::is_ascii_alnum_lower(t),
15434            _ => true,
15435        })
15436    })
15437}
15438
15439/// An eight-byte key for each row's sort column, paired with the row's
15440/// index — or `None` when the column cannot give one on every row.
15441///
15442/// v7.38.19 — the pair is what the sort array holds instead of the row.
15443/// Two kinds of column can supply it:
15444///
15445///   * an INTEGER, whose whole value fits. Flipping the sign bit maps
15446///     the signed order onto the unsigned one, so the key is EXACT and
15447///     a comparison never has to look at the row at all.
15448///   * TEXT, as the first eight bytes big-endian, zero-padded. That
15449///     orders the same as the string — two that differ inside those
15450///     bytes differ at the same index either way, and one shorter than
15451///     eight pads with zeros exactly where `[u8]`'s own comparison runs
15452///     out — but it is a PREFIX, so equal keys must still ask the full
15453///     comparator.
15454///
15455/// The `None` is the safety of it: a NULL or any other type has no
15456/// faithful eight-byte key, so such a column takes the ordinary path
15457/// rather than being given a made-up one.
15458/// The prefix keys for a sort, at the width the DATA asks for.
15459///
15460/// v7.40.1 — the width used to be eight bytes for every text column, and
15461/// the panel's two text cells priced both halves of that choice against
15462/// PostgreSQL 18.6, in memory on both legs, 400,000 rows:
15463///
15464/// ```text
15465///   short text (9 bytes, shared prefix)    SPG 96.6   PG 71.0   1.36x behind
15466///   long text (192 bytes, byte 0 decides)  SPG 70.9   PG 72.4   parity
15467/// ```
15468///
15469/// `'k' || lpad(n, 8, '0')` is nine bytes, so an eight-byte prefix drops
15470/// the last digit: ten rows share every key, forty thousand tie-runs
15471/// each fall back to the full comparator, and each of those reads at
15472/// random into a 400,000-element array. The md5 column decides on byte
15473/// zero and never ties, which is why only one of the two cells lost.
15474///
15475/// Widened to sixteen bytes and measured -- same window, two binaries
15476/// named by md5, order digests identical:
15477///
15478/// ```text
15479///   short text   104.9 -> 56.9 ms   1.84x faster (and 0.80x of PG)
15480///   long text     74.9 -> 90.7 ms   1.21x SLOWER
15481/// ```
15482///
15483/// So a fixed width is the wrong shape either way: `(u128, u32)` is 32
15484/// bytes against `(u64, u32)`'s 16, and a column that already decided on
15485/// byte zero pays double the sort's memory traffic for eight bytes it
15486/// never reads. That is the tax a shared hot path levies on the workload
15487/// it does not help.
15488///
15489/// The width comes from the longest value instead, which is exact and
15490/// free -- it is one pass the loop below already makes. Every value at
15491/// sixteen bytes or under makes the wide key the WHOLE key, so `exact`
15492/// is true and the tie fallback with its random reads disappears
15493/// altogether; anything longer keeps the narrow key and pays nothing.
15494enum PrefixKeys {
15495    Narrow(Vec<(u64, u32)>, bool),
15496    Wide(Vec<(u128, u32)>, bool),
15497}
15498
15499fn sort_keys_of(
15500    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15501    col: usize,
15502) -> Option<PrefixKeys> {
15503    let n = u32::try_from(tagged.len()).ok()?;
15504    let is_text = match tagged.first()?.1.values.get(col)? {
15505        Value::Text(_) => true,
15506        Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => false,
15507        _ => return None,
15508    };
15509    if !is_text {
15510        let mut out: Vec<(u64, u32)> = Vec::with_capacity(tagged.len());
15511        for (i, row) in (0..n).zip(tagged.iter()) {
15512            let key = match row.1.values.get(col) {
15513                Some(Value::SmallInt(v)) => (i64::from(*v) as u64) ^ (1 << 63),
15514                Some(Value::Int(v)) => (i64::from(*v) as u64) ^ (1 << 63),
15515                Some(Value::BigInt(v)) => (*v as u64) ^ (1 << 63),
15516                _ => return None,
15517            };
15518            out.push((key, i));
15519        }
15520        return Some(PrefixKeys::Narrow(out, true));
15521    }
15522    // One pass, and it answers both questions: the bytes of every key,
15523    // and whether the longest of them fits the wide one.
15524    let mut wide: Vec<(u128, u32)> = Vec::with_capacity(tagged.len());
15525    let mut longest = 0usize;
15526    for (i, row) in (0..n).zip(tagged.iter()) {
15527        let Some(Value::Text(t)) = row.1.values.get(col) else {
15528            return None;
15529        };
15530        let bytes = t.as_bytes();
15531        longest = longest.max(bytes.len());
15532        let mut k = [0u8; 16];
15533        let take = bytes.len().min(16);
15534        k[..take].copy_from_slice(&bytes[..take]);
15535        wide.push((u128::from_be_bytes(k), i));
15536    }
15537    if longest <= 16 {
15538        return Some(PrefixKeys::Wide(wide, true));
15539    }
15540    // Longer than the wide key: the narrow one costs half the memory
15541    // traffic and decides exactly as much, since neither is the whole
15542    // value. Built from the wide keys rather than reading the rows again.
15543    let narrow = wide
15544        .into_iter()
15545        .map(|(k, i)| ((k >> 64) as u64, i))
15546        .collect();
15547    Some(PrefixKeys::Narrow(narrow, false))
15548}
15549
15550/// Sort a prefix-key permutation, whatever the key's width.
15551///
15552/// v7.40.1 -- extracted so the two widths share one body. `low_card`
15553/// keeps the run-at-a-time shortcut and `exact` keeps the "a tie means
15554/// the values are equal" one; both are the caller's to decide.
15555struct PrefixSort {
15556    /// The first ORDER BY term is descending.
15557    first_desc: bool,
15558    /// The key does not discriminate, so sort it and settle each run of
15559    /// equal keys in one pass instead of n log n comparisons.
15560    low_card: bool,
15561    /// The key IS the value, so a tie means the values are equal.
15562    exact: bool,
15563    /// One ORDER BY term, so nothing else can speak after a tie.
15564    single_term: bool,
15565}
15566
15567fn sort_prefix_permutation<K: Copy + Ord>(
15568    mut order: Vec<(K, u32)>,
15569    how: &PrefixSort,
15570    row_cmp: &dyn Fn(u32, u32) -> core::cmp::Ordering,
15571    same_value: &dyn Fn(u32, u32) -> bool,
15572) -> Vec<u32> {
15573    let PrefixSort {
15574        first_desc,
15575        low_card,
15576        exact,
15577        single_term,
15578    } = *how;
15579    if low_card {
15580        // Integer sort first, then one pass per run.
15581        order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
15582            let c = pa.cmp(&pb);
15583            let c = if first_desc { c.reverse() } else { c };
15584            c.then_with(|| ia.cmp(&ib))
15585        });
15586        let mut lo = 0;
15587        while lo < order.len() {
15588            let mut hi = lo + 1;
15589            while hi < order.len() && order[hi].0 == order[lo].0 {
15590                hi += 1;
15591            }
15592            if hi - lo > 1 {
15593                let head = order[lo].1;
15594                let uniform = order[lo + 1..hi].iter().all(|&(_, i)| same_value(head, i));
15595                if !uniform {
15596                    order[lo..hi]
15597                        .sort_by(|&(_, ia), &(_, ib)| row_cmp(ia, ib).then_with(|| ia.cmp(&ib)));
15598                }
15599                // A uniform run is already in index order, which IS the
15600                // stable answer.
15601            }
15602            lo = hi;
15603        }
15604    } else {
15605        order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
15606            let c = pa.cmp(&pb);
15607            let c = if first_desc { c.reverse() } else { c };
15608            if c != core::cmp::Ordering::Equal {
15609                return c;
15610            }
15611            // An EXACT key that ties means the values are equal, so only
15612            // the remaining terms can speak. A prefix that ties has
15613            // decided nothing yet and the first term must be asked again,
15614            // which `row_cmp` does by walking every term from the start.
15615            if exact && single_term {
15616                return ia.cmp(&ib);
15617            }
15618            row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
15619        });
15620    }
15621    order.into_iter().map(|(_, i)| i).collect()
15622}
15623
15624/// Whether a PREFIX key is worth sorting a permutation on.
15625///
15626/// v7.38.19 — it is not always, and the panel says so in one cell. The
15627/// `text (26 values)` fixture is two hundred identical characters drawn
15628/// from twenty-six letters, so every eight-byte prefix inside a letter
15629/// is the same and 15,000 rows tie on it. Each tie then pays the prefix
15630/// compare, a two-hundred-byte comparison, AND a random read into a
15631/// 400,000-element array — while sorting the rows in place keeps the
15632/// partition contiguous. Measured: 160 ms sorting rows, 247 ms sorting
15633/// the permutation, on the very fixture built to be degenerate.
15634///
15635/// So the permutation is taken when the key DECIDES, and a sample says
15636/// whether it does. An exact key always decides; a prefix has to earn
15637/// it.
15638fn key_discriminates<K: Copy + Ord>(keys: &[(K, u32)]) -> bool {
15639    const SAMPLE: usize = 1024;
15640    let step = (keys.len() / SAMPLE).max(1);
15641    let mut seen: Vec<K> = keys
15642        .iter()
15643        .step_by(step)
15644        .take(SAMPLE)
15645        .map(|&(k, _)| k)
15646        .collect();
15647    let taken = seen.len();
15648    if taken < 8 {
15649        return true;
15650    }
15651    seen.sort_unstable();
15652    seen.dedup();
15653    seen.len() * 2 >= taken
15654}
15655
15656fn order_by_output_cols_if_identical(
15657    order_by: &[spg_sql::ast::OrderBy],
15658    projection: &[ProjectedItem],
15659    schema_cols: &[ColumnSchema],
15660) -> Option<Vec<usize>> {
15661    if order_by.is_empty() {
15662        return None;
15663    }
15664    let mut out = Vec::with_capacity(order_by.len());
15665    for ob in order_by {
15666        let Expr::Column(c) = &ob.expr else {
15667            return None;
15668        };
15669        if c.qualifier.is_some() {
15670            return None;
15671        }
15672        let mut hit = None;
15673        for (i, p) in projection.iter().enumerate() {
15674            if !p.output_name.eq_ignore_ascii_case(&c.name) {
15675                continue;
15676            }
15677            if hit.is_some() {
15678                return None; // ambiguous — SQL would reject it too
15679            }
15680            // The item must BE that column, not merely be named for it.
15681            let Expr::Column(pc) = &p.expr else {
15682                return None;
15683            };
15684            if !pc.name.eq_ignore_ascii_case(&c.name) {
15685                return None;
15686            }
15687            let sc = schema_cols
15688                .iter()
15689                .find(|s| s.name.eq_ignore_ascii_case(&c.name))?;
15690            if !value_order_is_key_order(sc) {
15691                return None;
15692            }
15693            hit = Some(i);
15694        }
15695        out.push(hit?);
15696    }
15697    Some(out)
15698}
15699
15700fn srf_order_output_cols(
15701    order_by: &[spg_sql::ast::OrderBy],
15702    projection: &[ProjectedItem],
15703) -> Vec<Option<usize>> {
15704    order_by
15705        .iter()
15706        .map(|ob| {
15707            // A positive ordinal is the Nth output column, directly.
15708            // `resolve_positional_order_by` deliberately leaves an ordinal
15709            // pointing at a set-returning item alone — copying the call into
15710            // ORDER BY would have made the key "the whole set" back when keys
15711            // came from the input row. Reading the expanded row's column is
15712            // what it should have meant, and is what this does.
15713            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
15714                && *n >= 1
15715                && let Ok(idx) = usize::try_from(*n - 1)
15716                && idx < projection.len()
15717            {
15718                return Some(idx);
15719            }
15720            // An unqualified name matching exactly one output name. SQL
15721            // resolves ORDER BY against the select list first, so this wins
15722            // over an input column of the same name — which is the whole
15723            // point of `SELECT g AS id … ORDER BY id`.
15724            if let Expr::Column(c) = &ob.expr
15725                && c.qualifier.is_none()
15726            {
15727                let mut hit = None;
15728                for (i, p) in projection.iter().enumerate() {
15729                    if p.output_name.eq_ignore_ascii_case(&c.name) {
15730                        if hit.is_some() {
15731                            hit = None;
15732                            break;
15733                        }
15734                        hit = Some(i);
15735                    }
15736                }
15737                if hit.is_some() {
15738                    return hit;
15739                }
15740            }
15741            // Or the same expression as a select-list item — which is what
15742            // `ORDER BY 1` becomes once `resolve_positional_order_by` has
15743            // run, and what a repeated `ORDER BY unnest(…)` is.
15744            projection.iter().position(|p| p.expr == ob.expr)
15745        })
15746        .collect()
15747}
15748
15749fn expand_srf_row(
15750    engine: &Engine,
15751    projection: &[ProjectedItem],
15752    srf_idxs: &[usize],
15753    row: &Row<'static>,
15754    ctx: &EvalContext<'_>,
15755) -> Result<Vec<Row<'static>>, EngineError> {
15756    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15757    expand_srf_row_with(engine, &mut plan, projection, row, ctx)
15758}
15759
15760impl Engine {
15761    /// The rows one target-list SRF yields for an input row. `None` from
15762    /// `srf_target_idxs` means the expression is not set-returning at all.
15763    fn srf_values(
15764        &self,
15765        expr: &spg_sql::ast::Expr,
15766        row: &Row<'static>,
15767        ctx: &EvalContext<'_>,
15768    ) -> Result<Vec<Value<'static>>, EngineError> {
15769        if top_level_srf_kind(expr).is_some() {
15770            return top_level_srf_output(expr, row, ctx);
15771        }
15772        // A user set-returning function. Its body runs through the real
15773        // executor, like every function body since round 63.
15774        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
15775            return Err(EngineError::Unsupported(
15776                "expected a SELECT-list SRF call".into(),
15777            ));
15778        };
15779        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15780        for a in args {
15781            vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
15782        }
15783        let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
15784        // v7.39 (read01 round 68) — in a target list a multi-column function is
15785        // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
15786        // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
15787        // what it is for. A single-column function contributes its bare value.
15788        Ok(rows
15789            .into_iter()
15790            .map(|r| {
15791                if r.values.len() == 1 {
15792                    r.values.into_iter().next().unwrap_or(Value::Null)
15793                } else {
15794                    Value::Composite(
15795                        cols.iter()
15796                            .map(|c| c.name.clone())
15797                            .zip(r.values)
15798                            .collect::<alloc::vec::Vec<_>>(),
15799                    )
15800                }
15801            })
15802            .collect())
15803    }
15804
15805    /// Is THIS node a set-returning call: one of the builtin kinds, or a user
15806    /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
15807    fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
15808        if is_top_level_unnest(e) {
15809            return true;
15810        }
15811        let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
15812            return false;
15813        };
15814        self.active_catalog().functions_named(name).iter().any(|f| {
15815            let r = f.returns.trim().to_ascii_uppercase();
15816            r.starts_with("SETOF") || r.starts_with("TABLE(")
15817        })
15818    }
15819
15820    /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
15821    fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
15822        let mut found = false;
15823        let mut probe = e.clone();
15824        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
15825            if self.is_srf_node(n) {
15826                found = true;
15827                return true;
15828            }
15829            false
15830        });
15831        found
15832    }
15833
15834    /// Which projection items CONTAIN a set-returning call. Before round 78 this
15835    /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
15836    /// ordinary scalar call all the way down to the function dispatcher, which
15837    /// then reported `unnest` as an unknown function.
15838    fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
15839        projection
15840            .iter()
15841            .enumerate()
15842            .filter(|(_, p)| self.expr_contains_srf(&p.expr))
15843            .map(|(i, _)| i)
15844            .collect()
15845    }
15846}
15847
15848impl Engine {
15849    /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
15850    /// no `(f(args)).*` item.
15851    fn lower_record_expansion(
15852        &self,
15853        stmt: &SelectStatement,
15854    ) -> Result<Option<SelectStatement>, EngineError> {
15855        use spg_sql::ast::{Expr, SelectItem};
15856        let is_marker = |it: &SelectItem| {
15857            matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
15858                if name == "__record_expand")
15859        };
15860        if !stmt.items.iter().any(is_marker) {
15861            return Ok(None);
15862        }
15863        let mut out = stmt.clone();
15864        let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
15865        let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
15866        for (n, item) in stmt.items.iter().enumerate() {
15867            if !is_marker(item) {
15868                items.push(item.clone());
15869                continue;
15870            }
15871            let SelectItem::Expr {
15872                expr: Expr::FunctionCall { args, .. },
15873                ..
15874            } = item
15875            else {
15876                unreachable!("checked by is_marker");
15877            };
15878            let Some(Expr::FunctionCall {
15879                name: fname,
15880                args: fargs,
15881            }) = args.first()
15882            else {
15883                return Err(EngineError::Unsupported(
15884                    "(<expr>).* expands a function's record — it needs a function call".into(),
15885                ));
15886            };
15887            let cols = self.setof_declared_columns(fname)?;
15888            let alias = alloc::format!("__rec{n}");
15889            let mut tref = bare_table_ref_named(&alias);
15890            tref.table_fn_call = Some(alloc::boxed::Box::new((
15891                fname.to_ascii_lowercase(),
15892                fargs.clone(),
15893            )));
15894            tref.alias = Some(alias.clone());
15895            lateral_refs.push(tref);
15896            for c in cols {
15897                items.push(SelectItem::Expr {
15898                    expr: Expr::Column(spg_sql::ast::ColumnName {
15899                        qualifier: Some(alias.clone()),
15900                        name: c,
15901                    }),
15902                    alias: None,
15903                });
15904            }
15905        }
15906        out.items = items;
15907        // The function joins the FROM. With no FROM it BECOMES the FROM; with one
15908        // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
15909        // (the arguments may reference the outer row — the round-69 correlation).
15910        for tref in lateral_refs {
15911            match &mut out.from {
15912                None => {
15913                    out.from = Some(spg_sql::ast::FromClause {
15914                        primary: tref,
15915                        joins: alloc::vec::Vec::new(),
15916                    });
15917                }
15918                Some(from) => from.joins.push(spg_sql::ast::FromJoin {
15919                    kind: spg_sql::ast::JoinKind::Cross,
15920                    table: tref,
15921                    on: None,
15922                    using_cols: None,
15923                    natural: false,
15924                }),
15925            }
15926        }
15927        Ok(Some(out))
15928    }
15929
15930    /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
15931    /// v text)` names them; a `SETOF <scalar>` is one column named after the
15932    /// function.
15933    fn setof_declared_columns(
15934        &self,
15935        name: &str,
15936    ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
15937        let cat = self.active_catalog();
15938        let overloads = cat.functions_named(name);
15939        let def = overloads.first().ok_or_else(|| {
15940            EngineError::Unsupported(alloc::format!("function {name} does not exist"))
15941        })?;
15942        let declared = def.returns.trim();
15943        let upper = declared.to_ascii_uppercase();
15944        if upper.starts_with("TABLE(") {
15945            let raw = &declared["TABLE(".len()..declared.len() - 1];
15946            return Ok(raw
15947                .split(',')
15948                .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
15949                .collect());
15950        }
15951        Ok(alloc::vec![name.to_string()])
15952    }
15953}
15954
15955/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
15956/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
15957/// COLUMNS list (data-independent), NESTED children inlined in
15958/// declaration order (PG's flattened output shape).
15959/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
15960/// correlated JSON_TABLE's static schema without evaluating its doc.
15961pub(crate) fn json_table_schema_pub(
15962    cols: &[spg_sql::ast::JsonTableColumn],
15963) -> alloc::vec::Vec<ColumnSchema> {
15964    json_table_schema(cols)
15965}
15966
15967fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
15968    use spg_sql::ast::JsonTableColumn as C;
15969    let mut out = alloc::vec::Vec::new();
15970    for c in cols {
15971        match c {
15972            C::Ordinality { name } => {
15973                out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
15974            }
15975            C::Regular {
15976                name, ty, exists, ..
15977            } => {
15978                let dt = if *exists {
15979                    DataType::Bool
15980                } else {
15981                    crate::conversions::column_type_to_data_type(*ty)
15982                };
15983                out.push(ColumnSchema::new(name.clone(), dt, true));
15984            }
15985            C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
15986        }
15987    }
15988    out
15989}
15990
15991/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
15992/// JSON_TABLE column's declared type (the DEFAULT expr may be a
15993/// string literal like `'none'` that must land as the column type).
15994fn coerce_json_table_default(
15995    v: Value<'static>,
15996    ty: spg_sql::ast::ColumnTypeName,
15997    name: &str,
15998) -> Result<Value<'static>, EngineError> {
15999    if v.is_null() {
16000        return Ok(Value::Null);
16001    }
16002    let dt = crate::conversions::column_type_to_data_type(ty);
16003    crate::conversions::coerce_value(v, dt, name, 0)
16004}
16005
16006/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
16007fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
16008    use crate::json::JsonValue as J;
16009    match v {
16010        Value::Null => J::Null,
16011        Value::Bool(b) => J::Bool(*b),
16012        Value::SmallInt(n) => J::Number(f64::from(*n)),
16013        Value::Int(n) => J::Number(f64::from(*n)),
16014        Value::BigInt(n) => J::Number(*n as f64),
16015        Value::Float(x) => J::Number(*x),
16016        Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
16017        other => J::String(crate::eval::value_to_text(other)),
16018    }
16019}
16020
16021fn bare_table_ref_named(name: &str) -> TableRef {
16022    TableRef {
16023        name: name.to_string(),
16024        alias: None,
16025        only: false,
16026        as_of_segment: None,
16027        unnest_expr: None,
16028        unnest_column_aliases: alloc::vec::Vec::new(),
16029        with_ordinality: false,
16030        generate_series_args: None,
16031        lateral_subquery: None,
16032        jsonb_each_text_arg: None,
16033        table_fn_call: None,
16034        rows_from: None,
16035        json_table: None,
16036        scalar_fn_item: false,
16037    }
16038}
16039
16040impl Engine {
16041    /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
16042    /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
16043    /// entries are the array-able SRFs, already lowered by the parser into their
16044    /// scalar array form.
16045    fn rows_from_rows(
16046        &self,
16047        primary: &TableRef,
16048    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
16049        let entries = primary
16050            .rows_from
16051            .as_ref()
16052            .expect("caller guards rows_from.is_some()");
16053        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
16054        let ctx = self.ev_ctx(&empty, None);
16055        let dummy = Row::new(alloc::vec::Vec::new());
16056        let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
16057        let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
16058        for (name, args) in entries {
16059            let (vals, colname) = if name == "__array" {
16060                // The parser lowered this one to `<array expr>`; its rows are the
16061                // array's elements.
16062                let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
16063                (
16064                    array_value_to_elements(&arr)?,
16065                    alloc::string::String::from("unnest"),
16066                )
16067            } else {
16068                let call = spg_sql::ast::Expr::FunctionCall {
16069                    name: name.clone(),
16070                    args: args.clone(),
16071                };
16072                (self.srf_values(&call, &dummy, &ctx)?, name.clone())
16073            };
16074            let ty = vals
16075                .first()
16076                .and_then(spg_storage::Value::data_type)
16077                .unwrap_or(DataType::Text);
16078            cols.push(ColumnSchema::new(colname, ty, true));
16079            lists.push(vals);
16080        }
16081        let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
16082        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
16083        for k in 0..n {
16084            let mut vals: alloc::vec::Vec<Value<'static>> =
16085                alloc::vec::Vec::with_capacity(lists.len() + 1);
16086            for l in &lists {
16087                vals.push(l.get(k).cloned().unwrap_or(Value::Null));
16088            }
16089            rows.push(Row::new(vals));
16090        }
16091        if primary.with_ordinality {
16092            cols.push(ColumnSchema::new(
16093                "ordinality".to_string(),
16094                DataType::BigInt,
16095                false,
16096            ));
16097            rows = rows
16098                .into_iter()
16099                .enumerate()
16100                .map(|(i, r)| {
16101                    let mut v = r.values;
16102                    v.push(Value::BigInt(i as i64 + 1));
16103                    Row::new(v)
16104                })
16105                .collect();
16106        }
16107        Ok((rows, cols))
16108    }
16109}
16110
16111/// v7.39 (round 232) — PG names the offending set operation in its
16112/// arity / type-mismatch messages ("each UNION query must have the same
16113/// number of columns"). `UNION ALL` is still spelled UNION there.
16114fn set_op_name(kind: UnionKind) -> &'static str {
16115    match kind {
16116        UnionKind::All | UnionKind::Distinct => "UNION",
16117        UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
16118        UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
16119    }
16120}
16121
16122/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
16123/// type: a bare string or NULL literal that no context has typed yet. SPG
16124/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
16125/// be the syntax. A wildcard or a non-literal expression is never unknown.
16126/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
16127/// LABELS as text (the wire render) but the value is an oid-carrying
16128/// dual, so a UNION with a numeric column must not be refused on the
16129/// label (pg_dump: `SELECT classid … UNION ALL SELECT
16130/// 'pg_opfamily'::regclass …`).
16131fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
16132    fn is_regcast(e: &Expr) -> bool {
16133        matches!(
16134            e,
16135            Expr::Cast {
16136                target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
16137                ..
16138            }
16139        )
16140    }
16141    stmt.items
16142        .iter()
16143        .map(|item| match item {
16144            SelectItem::Expr { expr, .. } => is_regcast(expr),
16145            _ => false,
16146        })
16147        .collect()
16148}
16149
16150fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
16151    stmt.items
16152        .iter()
16153        .map(|item| match item {
16154            SelectItem::Expr { expr, .. } => matches!(
16155                expr,
16156                Expr::Literal(spg_sql::ast::Literal::String(_))
16157                    | Expr::Literal(spg_sql::ast::Literal::Null)
16158            ),
16159            _ => false,
16160        })
16161        .collect()
16162}
16163
16164/// v7.39 (round 233) — retype one branch column's cells, reporting the
16165/// conversion failure the way PG does rather than leaving the column
16166/// half-converted. Used when the other branch typed an untyped literal.
16167fn coerce_branch_column(
16168    rows: &mut [Row<'static>],
16169    col_idx: usize,
16170    target: DataType,
16171    col_name: &str,
16172) -> Result<(), EngineError> {
16173    for row in rows.iter_mut() {
16174        let Some(slot) = row.values.get_mut(col_idx) else {
16175            continue;
16176        };
16177        if matches!(slot, Value::Null) {
16178            continue;
16179        }
16180        *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
16181    }
16182    Ok(())
16183}
16184
16185/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
16186/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
16187/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
16188/// reference to q's output columns substituted by the underlying column.
16189///
16190/// Admission is deliberately narrow — anything that changes cardinality,
16191/// order, or scope stays on the materialising path:
16192/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
16193///   FROM with no ordinality or positional column aliases, and no
16194///   subquery anywhere its expressions (an inner scope could reference
16195///   q too — descending is a later knife);
16196/// * inner: one stored table, bare-column projection only, no
16197///   CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
16198/// * every outer column reference must resolve inside q's output list —
16199///   a name that does not is an ERROR today, and flattening would
16200///   silently legalise it against the base table.
16201fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16202    use spg_sql::ast::SelectItem;
16203    let inner = primary.lateral_subquery.as_deref()?;
16204    // Outer shape.
16205    if !stmt.ctes.is_empty()
16206        || !stmt.unions.is_empty()
16207        || stmt.distinct
16208        || !stmt.distinct_on.is_empty()
16209        || !stmt.window_check_exprs.is_empty()
16210        || stmt.locking.is_some()
16211        || primary.with_ordinality
16212        || !primary.unnest_column_aliases.is_empty()
16213    {
16214        return None;
16215    }
16216    // Inner shape.
16217    if !inner.ctes.is_empty()
16218        || !inner.unions.is_empty()
16219        || inner.distinct
16220        || !inner.distinct_on.is_empty()
16221        || inner.group_by.is_some()
16222        || inner.group_by_all
16223        || inner.having.is_some()
16224        || !inner.order_by.is_empty()
16225        || inner.limit.is_some()
16226        || inner.offset.is_some()
16227        || !inner.window_check_exprs.is_empty()
16228        || inner.locking.is_some()
16229    {
16230        return None;
16231    }
16232    let ifrom = inner.from.as_ref()?;
16233    let it = &ifrom.primary;
16234    if !ifrom.joins.is_empty()
16235        || it.name.is_empty()
16236        || it.lateral_subquery.is_some()
16237        || it.unnest_expr.is_some()
16238        || it.generate_series_args.is_some()
16239        || it.as_of_segment.is_some()
16240        || it.jsonb_each_text_arg.is_some()
16241        || it.table_fn_call.is_some()
16242        || it.rows_from.is_some()
16243        || it.json_table.is_some()
16244        || it.with_ordinality
16245        || !it.unnest_column_aliases.is_empty()
16246    {
16247        return None;
16248    }
16249    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16250        return None;
16251    }
16252    // The output map: q's visible name -> the underlying column.
16253    let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
16254    let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
16255        alloc::collections::BTreeMap::new();
16256    for item in &inner.items {
16257        let SelectItem::Expr { expr, alias } = item else {
16258            return None;
16259        };
16260        let Expr::Column(c) = expr else {
16261            return None;
16262        };
16263        if let Some(q) = c.qualifier.as_deref()
16264            && !q.eq_ignore_ascii_case(&inner_alias)
16265        {
16266            return None;
16267        }
16268        let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
16269        // A duplicated output name would make substitution ambiguous.
16270        if map
16271            .insert(out_name.to_ascii_lowercase(), c.clone())
16272            .is_some()
16273        {
16274            return None;
16275        }
16276    }
16277    if map.is_empty() {
16278        return None;
16279    }
16280    let derived_alias = primary
16281        .alias
16282        .clone()
16283        .unwrap_or_else(|| primary.name.clone())
16284        .to_ascii_lowercase();
16285    // Substitute in a clone; bail (None) on the first reference the map
16286    // cannot answer.
16287    let mut out = stmt.clone();
16288    let ok = core::cell::Cell::new(true);
16289    let mut subst = |e: &mut Expr| -> bool {
16290        match e {
16291            Expr::Column(c) => {
16292                match c.qualifier.as_deref() {
16293                    Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
16294                    None => {}
16295                    Some(_) => {
16296                        ok.set(false);
16297                        return true;
16298                    }
16299                }
16300                match map.get(&c.name.to_ascii_lowercase()) {
16301                    Some(target) => *c = target.clone(),
16302                    None => ok.set(false),
16303                }
16304                true
16305            }
16306            // Any subquery could reference q from its own scope;
16307            // descending is a later knife — bail for now.
16308            Expr::ScalarSubquery(_)
16309            | Expr::Exists { .. }
16310            | Expr::InSubquery { .. }
16311            | Expr::RowInSubquery { .. }
16312            | Expr::RowCmpSubquery { .. } => {
16313                ok.set(false);
16314                true
16315            }
16316            _ => false,
16317        }
16318    };
16319    for item in &mut out.items {
16320        match item {
16321            SelectItem::Expr { expr, .. } => {
16322                crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
16323            }
16324            // `SELECT * FROM (…) q` means q's columns, in q's order.
16325            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
16326        }
16327    }
16328    if let Some(w) = &mut out.where_ {
16329        crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
16330    }
16331    if let Some(gs) = &mut out.group_by {
16332        for g in gs {
16333            crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
16334        }
16335    }
16336    if let Some(h) = &mut out.having {
16337        crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
16338    }
16339    for o in &mut out.order_by {
16340        crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
16341    }
16342    for d in &mut out.distinct_on {
16343        crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
16344    }
16345    if !ok.get() {
16346        return None;
16347    }
16348    // FROM becomes the stored table; the filters conjoin.
16349    out.from = Some(spg_sql::ast::FromClause {
16350        primary: it.clone(),
16351        joins: Vec::new(),
16352    });
16353    out.where_ = match (inner.where_.clone(), out.where_.take()) {
16354        (Some(a), Some(b)) => Some(Expr::Binary {
16355            lhs: alloc::boxed::Box::new(a),
16356            op: spg_sql::ast::BinOp::And,
16357            rhs: alloc::boxed::Box::new(b),
16358        }),
16359        (Some(a), None) => Some(a),
16360        (None, b) => b,
16361    };
16362    Some(out)
16363}
16364
16365/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
16366/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
16367/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
16368/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
16369/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
16370/// DISTINCT, an SRF, or an unprovable inner shape stays put.
16371fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16372    use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
16373    let inner = primary.lateral_subquery.as_deref()?;
16374    // Outer: exactly `SELECT count(*)`, nothing else.
16375    if !stmt.ctes.is_empty()
16376        || !stmt.unions.is_empty()
16377        || stmt.distinct
16378        || !stmt.distinct_on.is_empty()
16379        || stmt.where_.is_some()
16380        || stmt.group_by.is_some()
16381        || stmt.having.is_some()
16382        || !stmt.order_by.is_empty()
16383        || stmt.limit.is_some()
16384        || stmt.offset.is_some()
16385        || stmt.items.len() != 1
16386    {
16387        return None;
16388    }
16389    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16390        return None;
16391    };
16392    let E::FunctionCall { name, args } = expr else {
16393        return None;
16394    };
16395    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16396        return None;
16397    }
16398    // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
16399    let Some(LimitExpr::Literal(k)) = &inner.offset else {
16400        return None;
16401    };
16402    let k = i64::from(*k);
16403    if inner.limit.is_some() || inner.order_by.is_empty() {
16404        return None;
16405    }
16406    let mut counted = inner.clone();
16407    counted.order_by = Vec::new();
16408    counted.offset = None;
16409    // The stripped inner must now be a provable simple shape (its
16410    // items become irrelevant — count(*) reads none of them — but an
16411    // SRF item would change the row count, so the flatten predicate's
16412    // scrutiny still applies).
16413    let base = matview_flatten_probe(&counted)?;
16414    let mut out = stmt.clone();
16415    out.items = alloc::vec![SelectItem::Expr {
16416        expr: E::FunctionCall {
16417            name: String::from("greatest"),
16418            args: alloc::vec![
16419                E::Binary {
16420                    lhs: alloc::boxed::Box::new(E::FunctionCall {
16421                        name: String::from("count_star"),
16422                        args: alloc::vec![],
16423                    }),
16424                    op: spg_sql::ast::BinOp::Sub,
16425                    rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16426                },
16427                E::Literal(spg_sql::ast::Literal::Integer(0)),
16428            ],
16429        },
16430        alias: Some(String::from("count")),
16431    }];
16432    out.from = Some(spg_sql::ast::FromClause {
16433        primary: base,
16434        joins: Vec::new(),
16435    });
16436    out.where_ = counted.where_.clone();
16437    Some(out)
16438}
16439
16440/// The inner-shape probe `try_count_over_offset` shares with the
16441/// flatten: single stored table, no modifiers, no subqueries, no SRF
16442/// items. Returns the base TableRef.
16443fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
16444    use spg_sql::ast::SelectItem;
16445    if !inner.ctes.is_empty()
16446        || !inner.unions.is_empty()
16447        || inner.distinct
16448        || !inner.distinct_on.is_empty()
16449        || inner.group_by.is_some()
16450        || inner.group_by_all
16451        || inner.having.is_some()
16452        || !inner.order_by.is_empty()
16453        || inner.limit.is_some()
16454        || inner.offset.is_some()
16455        || !inner.window_check_exprs.is_empty()
16456        || inner.locking.is_some()
16457    {
16458        return None;
16459    }
16460    let ifrom = inner.from.as_ref()?;
16461    let it = &ifrom.primary;
16462    if !ifrom.joins.is_empty()
16463        || it.name.is_empty()
16464        || it.lateral_subquery.is_some()
16465        || it.unnest_expr.is_some()
16466        || it.generate_series_args.is_some()
16467        || it.as_of_segment.is_some()
16468        || it.jsonb_each_text_arg.is_some()
16469        || it.table_fn_call.is_some()
16470        || it.rows_from.is_some()
16471        || it.json_table.is_some()
16472        || it.with_ordinality
16473    {
16474        return None;
16475    }
16476    for item in &inner.items {
16477        match item {
16478            SelectItem::Expr { expr, .. } => {
16479                if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
16480                    return None;
16481                }
16482            }
16483            SelectItem::Wildcard => {}
16484            SelectItem::QualifiedWildcard(_) => return None,
16485        }
16486    }
16487    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16488        return None;
16489    }
16490    Some(it.clone())
16491}
16492
16493/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
16494/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
16495/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
16496/// constant-LENGTH array literal unnests to exactly k rows per input
16497/// row (NULL elements are rows too). One SRF item only, elements
16498/// subquery-free, and the stripped inner must pass the same probe the
16499/// count-over-offset rewrite uses.
16500fn try_count_over_const_unnest(
16501    stmt: &SelectStatement,
16502    primary: &TableRef,
16503) -> Option<SelectStatement> {
16504    use spg_sql::ast::{Expr as E, SelectItem};
16505    let inner = primary.lateral_subquery.as_deref()?;
16506    if !stmt.ctes.is_empty()
16507        || !stmt.unions.is_empty()
16508        || stmt.distinct
16509        || !stmt.distinct_on.is_empty()
16510        || stmt.where_.is_some()
16511        || stmt.group_by.is_some()
16512        || stmt.having.is_some()
16513        || !stmt.order_by.is_empty()
16514        || stmt.limit.is_some()
16515        || stmt.offset.is_some()
16516        || stmt.items.len() != 1
16517    {
16518        return None;
16519    }
16520    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16521        return None;
16522    };
16523    let E::FunctionCall { name, args } = expr else {
16524        return None;
16525    };
16526    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16527        return None;
16528    }
16529    // Inner: exactly one item, and it is unnest(ARRAY[...]).
16530    if inner.items.len() != 1
16531        || !inner.order_by.is_empty()
16532        || inner.limit.is_some()
16533        || inner.offset.is_some()
16534    {
16535        return None;
16536    }
16537    let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
16538        return None;
16539    };
16540    let E::FunctionCall {
16541        name: fname,
16542        args: fargs,
16543    } = item
16544    else {
16545        return None;
16546    };
16547    if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
16548        return None;
16549    }
16550    let E::Array(elems) = &fargs[0] else {
16551        return None;
16552    };
16553    if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
16554        return None;
16555    }
16556    let k = elems.len() as i64;
16557    // The stripped inner (the SRF item replaced by a plain constant)
16558    // must be the provable simple shape.
16559    let mut counted = inner.clone();
16560    counted.items = alloc::vec![SelectItem::Expr {
16561        expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
16562        alias: None,
16563    }];
16564    let base = matview_flatten_probe(&counted)?;
16565    let mut out = stmt.clone();
16566    out.items = alloc::vec![SelectItem::Expr {
16567        expr: E::Binary {
16568            lhs: alloc::boxed::Box::new(E::FunctionCall {
16569                name: String::from("count_star"),
16570                args: alloc::vec![],
16571            }),
16572            op: spg_sql::ast::BinOp::Mul,
16573            rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16574        },
16575        alias: Some(String::from("count")),
16576    }];
16577    out.from = Some(spg_sql::ast::FromClause {
16578        primary: base,
16579        joins: Vec::new(),
16580    });
16581    out.where_ = counted.where_.clone();
16582    Some(out)
16583}