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                        );
1496                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1497                }
1498                // v7.37.17 — information_schema.constraint_column_usage.
1499                "__spg_info_constraint_column_usage" => {
1500                    let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1501                        self.active_catalog(),
1502                    );
1503                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1504                }
1505                // v7.37.17 — information_schema.triggers.
1506                "__spg_info_triggers" => {
1507                    let (schema, rows) =
1508                        crate::system_catalog::synth_info_triggers(self.active_catalog());
1509                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1510                }
1511                // v7.37.17 — information_schema.check_constraints.
1512                "__spg_info_check_constraints" => {
1513                    let (schema, rows) =
1514                        crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1515                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1516                }
1517                // v7.37.17 — information_schema.sequences.
1518                "__spg_info_sequences" => {
1519                    let (schema, rows) =
1520                        crate::system_catalog::synth_info_sequences(self.active_catalog());
1521                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1522                }
1523                // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1524                "__spg_mysql_user" => {
1525                    let (schema, rows) = synth_mysql_user(self);
1526                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1527                }
1528                "__spg_mysql_db" => {
1529                    let (schema, rows) = synth_mysql_db();
1530                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1531                }
1532                // v7.39 (round 541) — the catalogs PG has that SPG is
1533                // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1534                other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1535                    let (schema, rows) =
1536                        crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1537                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1538                }
1539                _ => {
1540                    return Err(EngineError::Unsupported(alloc::format!(
1541                        "meta view {view:?} is not yet materialisable; \
1542                         v7.16.2 covers information_schema.columns / .tables \
1543                         and pg_catalog.pg_class / pg_attribute; \
1544                         v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1545                         pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1546                         pg_user / pg_views / pg_matviews / pg_settings"
1547                    )));
1548                }
1549            }
1550        }
1551        Ok(catalog)
1552    }
1553
1554    pub(crate) fn exec_with_ctes(
1555        &self,
1556        stmt: &SelectStatement,
1557        cancel: CancelToken<'_>,
1558    ) -> Result<QueryResult, EngineError> {
1559        cancel.check()?;
1560        // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1561        // bodies are supported here. Writable CTEs on a SELECT
1562        // outer require `&mut self` and route through the
1563        // top-level `exec_select_cancel_mut` entry; sentori
1564        // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1565        // INSERT, not a SELECT, so this restriction is harmless
1566        // in practice.
1567        if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1568            // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1569            // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1570            // of a statement, not nested inside a subquery; this path is
1571            // reached exactly when one is nested. The old text described SPG's
1572            // own executor plumbing ("the top-level mutable entry"), which
1573            // means nothing to a client.
1574            return Err(EngineError::Unsupported(
1575                "WITH clause containing a data-modifying statement must be at the top level".into(),
1576            ));
1577        }
1578        let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1579        // Strip CTEs from the body before running on the temp engine
1580        // so we don't recurse forever.
1581        let mut body = stmt.clone();
1582        body.ctes = Vec::new();
1583        let mut temp = Engine::restore(catalog);
1584        if let Some(c) = self.clock {
1585            temp = temp.with_clock(c);
1586        }
1587        if let Some(f) = self.salt_fn {
1588            temp = temp.with_salt_fn(f);
1589        }
1590        temp.exec_select_cancel(&body, cancel)
1591    }
1592
1593    /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1594    /// `&self` SELECT path. Caller guarantees no modifying CTE
1595    /// bodies are present.
1596    pub(crate) fn materialise_ctes_readonly(
1597        &self,
1598        ctes: &[spg_sql::ast::Cte],
1599        cancel: CancelToken<'_>,
1600    ) -> Result<crate::Catalog, EngineError> {
1601        cancel.check()?;
1602        let mut catalog = self.active_catalog().clone();
1603        for cte in ctes {
1604            let body_select = cte.body.as_select().ok_or_else(|| {
1605                EngineError::Unsupported(alloc::format!(
1606                    "data-modifying CTE not supported on this SELECT entry"
1607                ))
1608            })?;
1609            // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1610            // (PG scoping: the WITH name wins for the outer query and later
1611            // CTEs, while THIS body still sees the real table — a
1612            // non-recursive body's self-name is the table, probe P2). This
1613            // materialiser works on a CLONE, so the shadow is simply: run
1614            // the body against the untouched clone, then drop the real
1615            // table from the clone before installing the CTE's temp. A
1616            // RECURSIVE self-reference is the CTE itself (P6), so there the
1617            // drop happens before the iterating materialiser runs.
1618            let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1619                let synthetic = spg_sql::ast::Cte {
1620                    name: cte.name.clone(),
1621                    body: spg_sql::ast::CteBody::Select(body_select.clone()),
1622                    recursive: true,
1623                    column_overrides: cte.column_overrides.clone(),
1624                    search: None,
1625                    cycle: None,
1626                };
1627                if catalog.get(&cte.name).is_some() {
1628                    let _ = catalog.drop_table(&cte.name);
1629                }
1630                self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1631            } else {
1632                let mut cte_engine = Engine::restore(catalog.clone());
1633                if let Some(c) = self.clock {
1634                    cte_engine = cte_engine.with_clock(c);
1635                }
1636                if let Some(f) = self.salt_fn {
1637                    cte_engine = cte_engine.with_salt_fn(f);
1638                }
1639                let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1640                let QueryResult::Rows { columns, rows } = body_result else {
1641                    return Err(EngineError::Unsupported(alloc::format!(
1642                        "CTE {:?} body did not return rows",
1643                        cte.name
1644                    )));
1645                };
1646                (columns, rows)
1647            };
1648            let inferred = infer_column_types(&columns, &rows);
1649            let mut columns = inferred;
1650            if !cte.column_overrides.is_empty() {
1651                if cte.column_overrides.len() != columns.len() {
1652                    return Err(EngineError::Unsupported(alloc::format!(
1653                        "CTE {:?} column list has {} names but body returns {} columns",
1654                        cte.name,
1655                        cte.column_overrides.len(),
1656                        columns.len()
1657                    )));
1658                }
1659                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1660                    col.name.clone_from(name);
1661                }
1662            }
1663            let schema = TableSchema::new(cte.name.clone(), columns);
1664            // v7.39 (round 156) — the body ran against the untouched clone;
1665            // from here on the CTE name resolves to the temp (PG scoping).
1666            if catalog.get(&cte.name).is_some() {
1667                let _ = catalog.drop_table(&cte.name);
1668            }
1669            catalog.create_table(schema).map_err(EngineError::Storage)?;
1670            let table = catalog
1671                .get_mut(&cte.name)
1672                .expect("just-created CTE table must exist");
1673            for row in rows {
1674                table.insert(row).map_err(EngineError::Storage)?;
1675            }
1676        }
1677        Ok(catalog)
1678    }
1679
1680    /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1681    /// Retained for non-DML callers; the DML path (writable CTE on
1682    /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1683    /// `dml.rs` which installs the CTE temps directly on the
1684    /// active catalog so the outer statement's writes hit real
1685    /// tables.
1686    #[allow(dead_code)]
1687    pub(crate) fn materialise_ctes(
1688        &mut self,
1689        ctes: &[spg_sql::ast::Cte],
1690        cancel: CancelToken<'_>,
1691    ) -> Result<crate::Catalog, EngineError> {
1692        cancel.check()?;
1693        // v7.37.43-T4.4 — modifying CTEs need to write through the
1694        // SAME catalog as the outer statement, not a clone (PG's
1695        // writable CTE puts all modifications in one transaction).
1696        // For the read-only case the original logic cloned, but
1697        // since the outer statement also goes through the cloned
1698        // engine and ALL writes must converge, we now drive the
1699        // accumulator off `self.active_catalog().clone()` and
1700        // commit the modifying writes directly to `self`'s active
1701        // catalog so the surface is consistent.
1702        let mut catalog = self.active_catalog().clone();
1703        // v7.39 (round 149) — a modifying CTE body's target must be a
1704        // real relation, never a sibling CTE (PG: relation does not
1705        // exist); checked before any alias lands in the accumulator.
1706        for cte in ctes {
1707            let body_target = match &cte.body {
1708                spg_sql::ast::CteBody::Select(_) => None,
1709                spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1710                spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1711                spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1712                spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1713            };
1714            if let Some(t) = body_target
1715                && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1716                && catalog.get(t).is_none()
1717            {
1718                return Err(EngineError::Storage(
1719                    spg_storage::StorageError::TableNotFound { name: t.into() },
1720                ));
1721            }
1722        }
1723        for cte in ctes {
1724            if catalog.get(&cte.name).is_some() {
1725                return Err(EngineError::Unsupported(alloc::format!(
1726                    "CTE name {:?} shadows an existing table; rename the CTE",
1727                    cte.name
1728                )));
1729            }
1730            let (columns, rows) = match &cte.body {
1731                // v7.39 (round 145) — see the sibling site: only a body that
1732                // truly self-references takes the iterating materialiser.
1733                spg_sql::ast::CteBody::Select(body)
1734                    if cte.recursive && select_refers_to(body, &cte.name) =>
1735                {
1736                    // Recursive CTE — the existing helper takes a
1737                    // SELECT body and the snapshot catalog.
1738                    let synthetic = spg_sql::ast::Cte {
1739                        name: cte.name.clone(),
1740                        body: spg_sql::ast::CteBody::Select(body.clone()),
1741                        recursive: true,
1742                        column_overrides: cte.column_overrides.clone(),
1743                        search: None,
1744                        cycle: None,
1745                    };
1746                    self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1747                }
1748                spg_sql::ast::CteBody::Select(body) => {
1749                    // v7.25 (round-17) — run against the accumulated
1750                    // catalog so later CTEs can reference earlier
1751                    // ones in the same WITH clause.
1752                    let mut cte_engine = Engine::restore(catalog.clone());
1753                    if let Some(c) = self.clock {
1754                        cte_engine = cte_engine.with_clock(c);
1755                    }
1756                    if let Some(f) = self.salt_fn {
1757                        cte_engine = cte_engine.with_salt_fn(f);
1758                    }
1759                    let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1760                    let QueryResult::Rows { columns, rows } = body_result else {
1761                        return Err(EngineError::Unsupported(alloc::format!(
1762                            "CTE {:?} body did not return rows",
1763                            cte.name
1764                        )));
1765                    };
1766                    (columns, rows)
1767                }
1768                spg_sql::ast::CteBody::Insert(body) => {
1769                    self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1770                }
1771                spg_sql::ast::CteBody::Update(body) => {
1772                    self.exec_modifying_cte_update(&cte.name, body, cancel)?
1773                }
1774                spg_sql::ast::CteBody::Delete(body) => {
1775                    self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1776                }
1777                spg_sql::ast::CteBody::Merge(body) => {
1778                    self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1779                }
1780            };
1781            // v4.22: the projection builder labels any non-column
1782            // expression as Text — including literal SELECT 1.
1783            // Promote each column's type to whatever the rows
1784            // actually carry so the CTE storage table accepts them.
1785            let inferred = infer_column_types(&columns, &rows);
1786            let mut columns = inferred;
1787            if !cte.column_overrides.is_empty() {
1788                if cte.column_overrides.len() != columns.len() {
1789                    return Err(EngineError::Unsupported(alloc::format!(
1790                        "CTE {:?} column list has {} names but body returns {} columns",
1791                        cte.name,
1792                        cte.column_overrides.len(),
1793                        columns.len()
1794                    )));
1795                }
1796                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1797                    col.name.clone_from(name);
1798                }
1799            }
1800            let schema = TableSchema::new(cte.name.clone(), columns);
1801            catalog.create_table(schema).map_err(EngineError::Storage)?;
1802            let table = catalog
1803                .get_mut(&cte.name)
1804                .expect("just-created CTE table must exist");
1805            for row in rows {
1806                table.insert(row).map_err(EngineError::Storage)?;
1807            }
1808        }
1809        Ok(catalog)
1810    }
1811
1812    /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1813    /// against `self` (so the mutation lands in the active catalog
1814    /// inside the current transaction) and captures the RETURNING
1815    /// projection — column schema + rows — to materialise as the
1816    /// CTE alias's table. An INSERT without RETURNING produces a
1817    /// 0-row table with a synthetic single-column placeholder
1818    /// (matches PG: the CTE alias is still defined, but referencing
1819    /// it from the outer query without RETURNING raises a
1820    /// column-resolution error at scan time).
1821    fn exec_modifying_cte_insert(
1822        &mut self,
1823        cte_name: &str,
1824        body: &spg_sql::ast::InsertStatement,
1825        _cancel: CancelToken<'_>,
1826    ) -> Result<
1827        (
1828            Vec<spg_storage::ColumnSchema>,
1829            Vec<spg_storage::Row<'static>>,
1830        ),
1831        EngineError,
1832    > {
1833        // round 151 — a WITH-headed body keeps its own ctes; the body
1834        // statement routes through its writable-CTE entry (outer CTEs
1835        // are never copied into bodies, so no recursion risk).
1836        let body = body.clone();
1837        let result = self.exec_insert(body)?;
1838        match result {
1839            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1840            QueryResult::CommandOk { .. } => {
1841                // No RETURNING — emit a sentinel single-column
1842                // schema with zero rows so the alias is defined.
1843                let placeholder = spg_storage::ColumnSchema::new(
1844                    alloc::format!("{cte_name}_returning_absent"),
1845                    spg_storage::DataType::Text,
1846                    true,
1847                );
1848                Ok((alloc::vec![placeholder], Vec::new()))
1849            }
1850        }
1851    }
1852
1853    /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1854    /// as INSERT above.
1855    fn exec_modifying_cte_update(
1856        &mut self,
1857        cte_name: &str,
1858        body: &spg_sql::ast::UpdateStatement,
1859        cancel: CancelToken<'_>,
1860    ) -> Result<
1861        (
1862            Vec<spg_storage::ColumnSchema>,
1863            Vec<spg_storage::Row<'static>>,
1864        ),
1865        EngineError,
1866    > {
1867        let body = body.clone();
1868        let result = self.exec_update_cancel(&body, cancel)?;
1869        match result {
1870            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1871            QueryResult::CommandOk { .. } => {
1872                let placeholder = spg_storage::ColumnSchema::new(
1873                    alloc::format!("{cte_name}_returning_absent"),
1874                    spg_storage::DataType::Text,
1875                    true,
1876                );
1877                Ok((alloc::vec![placeholder], Vec::new()))
1878            }
1879        }
1880    }
1881
1882    /// v7.37.43-T4.4 — execute a DELETE CTE body.
1883    fn exec_modifying_cte_delete(
1884        &mut self,
1885        cte_name: &str,
1886        body: &spg_sql::ast::DeleteStatement,
1887        cancel: CancelToken<'_>,
1888    ) -> Result<
1889        (
1890            Vec<spg_storage::ColumnSchema>,
1891            Vec<spg_storage::Row<'static>>,
1892        ),
1893        EngineError,
1894    > {
1895        let body = body.clone();
1896        let result = self.exec_delete_cancel(&body, cancel)?;
1897        match result {
1898            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1899            QueryResult::CommandOk { .. } => {
1900                let placeholder = spg_storage::ColumnSchema::new(
1901                    alloc::format!("{cte_name}_returning_absent"),
1902                    spg_storage::DataType::Text,
1903                    true,
1904                );
1905                Ok((alloc::vec![placeholder], Vec::new()))
1906            }
1907        }
1908    }
1909
1910    /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1911    fn exec_modifying_cte_merge(
1912        &mut self,
1913        cte_name: &str,
1914        body: &spg_sql::ast::MergeStatement,
1915        cancel: CancelToken<'_>,
1916    ) -> Result<
1917        (
1918            Vec<spg_storage::ColumnSchema>,
1919            Vec<spg_storage::Row<'static>>,
1920        ),
1921        EngineError,
1922    > {
1923        let body = body.clone();
1924        let result = self.exec_merge_cancel(&body, cancel)?;
1925        match result {
1926            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1927            QueryResult::CommandOk { .. } => {
1928                let placeholder = spg_storage::ColumnSchema::new(
1929                    alloc::format!("{cte_name}_returning_absent"),
1930                    spg_storage::DataType::Text,
1931                    true,
1932                );
1933                Ok((alloc::vec![placeholder], Vec::new()))
1934            }
1935        }
1936    }
1937
1938    /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1939    /// UNION (or UNION ALL) of an anchor that does not reference
1940    /// the CTE name, and one or more recursive terms that do. The
1941    /// anchor runs first; each subsequent iteration runs the
1942    /// recursive term against a temp catalog where the CTE name is
1943    /// bound to the *previous* iteration's output. Iteration stops
1944    /// when the recursive term yields no rows; UNION (DISTINCT)
1945    /// deduplicates against the accumulated result, UNION ALL does
1946    /// not. A hard cap on total rows prevents runaway queries.
1947    #[allow(clippy::too_many_lines)]
1948    pub(crate) fn materialise_recursive_cte(
1949        &self,
1950        cte: &spg_sql::ast::Cte,
1951        base_catalog: &Catalog,
1952        cancel: CancelToken<'_>,
1953    ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1954        const MAX_TOTAL_ROWS: usize = 1_000_000;
1955        const MAX_ITERATIONS: usize = 100_000;
1956        cancel.check()?;
1957        // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1958        // a modifying recursive CTE is parser-rejectable but we
1959        // guard here defensively.
1960        let body_select = cte.body.as_select().ok_or_else(|| {
1961            EngineError::Unsupported(alloc::format!(
1962                "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1963                cte.name
1964            ))
1965        })?;
1966        if body_select.unions.is_empty() {
1967            return Err(EngineError::Unsupported(alloc::format!(
1968                "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1969                cte.name
1970            )));
1971        }
1972        // Anchor: the body's leading SELECT, with unions stripped.
1973        let mut anchor = body_select.clone();
1974        let all_union_terms = core::mem::take(&mut anchor.unions);
1975        anchor.ctes = Vec::new();
1976        // v7.37 D.42 — split the UNION members: those that do NOT reference the
1977        // CTE are additional ANCHOR terms, only the ones that do recurse. A
1978        // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1979        // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1980        // treating the non-recursive `SELECT r2` as a recursive term made it
1981        // re-emit its constant row every iteration → runaway loop.
1982        let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1983            .into_iter()
1984            .partition(|(_, t)| !select_refers_to(t, &cte.name));
1985        let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1986        let QueryResult::Rows {
1987            columns: anchor_cols,
1988            rows: mut anchor_rows,
1989        } = anchor_result
1990        else {
1991            return Err(EngineError::Unsupported(alloc::format!(
1992                "WITH RECURSIVE {:?}: anchor did not return rows",
1993                cte.name
1994            )));
1995        };
1996        // Append every non-recursive UNION member's rows to the anchor set.
1997        for (_, term) in &anchor_terms {
1998            let mut term = term.clone();
1999            term.ctes = Vec::new();
2000            if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
2001                anchor_rows.extend(rows);
2002            }
2003        }
2004        // The projection builder labels non-column expressions Text;
2005        // refine column types from the anchor's actual values so the
2006        // intermediate iter-catalog tables accept them.
2007        let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
2008        if !cte.column_overrides.is_empty() {
2009            if cte.column_overrides.len() != columns.len() {
2010                return Err(EngineError::Unsupported(alloc::format!(
2011                    "CTE {:?} column list has {} names but anchor returns {} columns",
2012                    cte.name,
2013                    cte.column_overrides.len(),
2014                    columns.len()
2015                )));
2016            }
2017            for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
2018                col.name.clone_from(name);
2019            }
2020        }
2021        let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
2022        let mut working_set: Vec<Row<'static>> = anchor_rows;
2023        let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
2024        // Track at least one "all UNION ALL" flag — if every union
2025        // kind is ALL we skip the dedup step (faster + matches PG).
2026        let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
2027        if !all_union_all {
2028            for r in &all_rows {
2029                seen.insert(encode_row_key(r));
2030            }
2031        }
2032        // v7.39 (round 598) — the engine and its catalog are built ONCE.
2033        // Each iteration used to clone the catalog, create the CTE table,
2034        // and construct a whole `Engine` — which initialises 82 fields — to
2035        // hold that round's working set. A counting allocator put the loop
2036        // at 63 allocations and 104 kB per iteration, or 1 GB for a
2037        // 10,000-row recursive CTE, and none of it varied with how much
2038        // else was in the catalog: the per-round rebuild WAS the cost. The
2039        // table is emptied and refilled instead.
2040        let mut iter_catalog = base_catalog.clone();
2041        let schema = TableSchema::new(cte.name.clone(), columns.clone());
2042        iter_catalog
2043            .create_table(schema)
2044            .map_err(EngineError::Storage)?;
2045        let mut iter_engine = Engine::restore(iter_catalog);
2046        if let Some(c) = self.clock {
2047            iter_engine = iter_engine.with_clock(c);
2048        }
2049        if let Some(f) = self.salt_fn {
2050            iter_engine = iter_engine.with_salt_fn(f);
2051        }
2052        // The recursive terms are cloned once too — the clone stripped the
2053        // CTE list off each of them, per term per iteration.
2054        let recursive_terms: Vec<SelectStatement> = union_terms
2055            .iter()
2056            .map(|(_, t)| {
2057                let mut t = t.clone();
2058                t.ctes = Vec::new();
2059                t
2060            })
2061            .collect();
2062        // v7.39 (round 618) — plan every recursive term once. Taken only if
2063        // ALL of them plan, so a query never runs half on each path.
2064        let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2065            .iter()
2066            .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2067            .collect();
2068        let fast_ctx = term_plans.as_ref().map(|plans| {
2069            let alias = plans[0].alias.clone();
2070            (alias, ())
2071        });
2072        for iter in 0..MAX_ITERATIONS {
2073            cancel.check()?;
2074            if working_set.is_empty() {
2075                break;
2076            }
2077            if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2078                // The worktable IS the working set: no table to empty and
2079                // refill, and no query execution per round.
2080                let mut next_set: Vec<Row<'static>> = Vec::new();
2081                for plan in plans {
2082                    let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2083                    for row in &working_set {
2084                        cancel.check()?;
2085                        if let Some(w) = plan.where_ {
2086                            let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2087                            if !matches!(v, Value::Bool(true)) {
2088                                continue;
2089                            }
2090                        }
2091                        let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2092                        for it in &plan.items {
2093                            vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2094                        }
2095                        let out = Row::new(vals);
2096                        if !all_union_all {
2097                            let key = encode_row_key(&out);
2098                            if !seen.insert(key) {
2099                                continue;
2100                            }
2101                        }
2102                        next_set.push(out);
2103                    }
2104                }
2105                if next_set.is_empty() {
2106                    break;
2107                }
2108                all_rows.extend(next_set.iter().cloned());
2109                working_set = next_set;
2110                if all_rows.len() > MAX_TOTAL_ROWS {
2111                    return Err(EngineError::Unsupported(alloc::format!(
2112                        "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2113                        cte.name
2114                    )));
2115                }
2116                if iter + 1 == MAX_ITERATIONS {
2117                    return Err(EngineError::Unsupported(alloc::format!(
2118                        "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2119                        cte.name
2120                    )));
2121                }
2122                continue;
2123            }
2124            {
2125                // Truncated rather than dropped and recreated: the table's
2126                // own structure is what dropping it throws away, and it is
2127                // identical every round.
2128                let cat = iter_engine.base_catalog_mut();
2129                let table = cat.get_mut(&cte.name).expect("created above");
2130                table.truncate();
2131                for row in &working_set {
2132                    table.insert(row.clone()).map_err(EngineError::Storage)?;
2133                }
2134            }
2135            // Run each recursive term in sequence and collect new rows.
2136            let mut next_set: Vec<Row<'static>> = Vec::new();
2137            for term in &recursive_terms {
2138                let r = iter_engine.exec_select_cancel(term, cancel)?;
2139                let QueryResult::Rows {
2140                    columns: rc,
2141                    rows: rs,
2142                } = r
2143                else {
2144                    return Err(EngineError::Unsupported(alloc::format!(
2145                        "WITH RECURSIVE {:?}: recursive term did not return rows",
2146                        cte.name
2147                    )));
2148                };
2149                if rc.len() != columns.len() {
2150                    return Err(EngineError::Unsupported(alloc::format!(
2151                        "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2152                        cte.name,
2153                        rc.len(),
2154                        columns.len()
2155                    )));
2156                }
2157                for row in rs {
2158                    if !all_union_all {
2159                        let key = encode_row_key(&row);
2160                        if !seen.insert(key) {
2161                            continue;
2162                        }
2163                    }
2164                    next_set.push(row);
2165                }
2166            }
2167            if next_set.is_empty() {
2168                break;
2169            }
2170            all_rows.extend(next_set.iter().cloned());
2171            working_set = next_set;
2172            if all_rows.len() > MAX_TOTAL_ROWS {
2173                return Err(EngineError::Unsupported(alloc::format!(
2174                    "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2175                    cte.name
2176                )));
2177            }
2178            if iter + 1 == MAX_ITERATIONS {
2179                return Err(EngineError::Unsupported(alloc::format!(
2180                    "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2181                    cte.name
2182                )));
2183            }
2184        }
2185        Ok((columns, all_rows))
2186    }
2187
2188    pub(crate) fn resolve_select_subqueries(
2189        &self,
2190        stmt: &mut SelectStatement,
2191        cancel: CancelToken<'_>,
2192    ) -> Result<(), EngineError> {
2193        for item in &mut stmt.items {
2194            if let SelectItem::Expr { expr, alias } = item {
2195                // An UNCORRELATED subquery is replaced by its value right
2196                // here, and the shape the column was named for goes with
2197                // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2198                // boolean literal, so SPG answered `?column?` where PG18
2199                // answers `exists`. Only a subquery at the TOP of the item
2200                // loses its name this way — one nested inside a call still
2201                // reports the call.
2202                if alias.is_none()
2203                    && matches!(
2204                        expr,
2205                        Expr::ScalarSubquery(_)
2206                            | Expr::Exists { .. }
2207                            | Expr::InSubquery { .. }
2208                            | Expr::RowInSubquery { .. }
2209                            | Expr::RowCmpSubquery { .. }
2210                    )
2211                {
2212                    *alias = Some(default_output_name(expr, self.speaks_mysql));
2213                }
2214                self.resolve_expr_subqueries(expr, cancel)?;
2215            }
2216        }
2217        if let Some(w) = &mut stmt.where_ {
2218            self.resolve_expr_subqueries(w, cancel)?;
2219        }
2220        // v7.24.1 — JOIN ON conditions can carry subqueries too;
2221        // they were never walked, so even an UNCORRELATED subquery
2222        // in ON hit "subquery reached row eval".
2223        if let Some(from) = &mut stmt.from {
2224            for j in &mut from.joins {
2225                if let Some(on) = &mut j.on {
2226                    self.resolve_expr_subqueries(on, cancel)?;
2227                }
2228            }
2229        }
2230        if let Some(gs) = &mut stmt.group_by {
2231            for g in gs {
2232                self.resolve_expr_subqueries(g, cancel)?;
2233            }
2234        }
2235        if let Some(h) = &mut stmt.having {
2236            self.resolve_expr_subqueries(h, cancel)?;
2237        }
2238        for o in &mut stmt.order_by {
2239            self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2240        }
2241        for (_, peer) in &mut stmt.unions {
2242            self.resolve_select_subqueries(peer, cancel)?;
2243        }
2244        Ok(())
2245    }
2246
2247    #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2248    pub(crate) fn resolve_expr_subqueries(
2249        &self,
2250        e: &mut Expr,
2251        cancel: CancelToken<'_>,
2252    ) -> Result<(), EngineError> {
2253        // Replace-on-this-node cases first.
2254        if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2255            *e = replacement;
2256            return Ok(());
2257        }
2258        match e {
2259            Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } => {
2260                self.resolve_expr_subqueries(expr, cancel)?
2261            }
2262            Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2263            Expr::AggregateOrdered { call, order_by, .. } => {
2264                self.resolve_expr_subqueries(call, cancel)?;
2265                for o in order_by.iter_mut() {
2266                    self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2267                }
2268            }
2269            Expr::Binary { lhs, rhs, .. } => {
2270                self.resolve_expr_subqueries(lhs, cancel)?;
2271                self.resolve_expr_subqueries(rhs, cancel)?;
2272            }
2273            Expr::Unary { expr, .. }
2274            | Expr::Cast { expr, .. }
2275            | Expr::IsNull { expr, .. }
2276            | Expr::BoolTest { expr, .. }
2277            | Expr::FieldAccess { base: expr, .. } => {
2278                self.resolve_expr_subqueries(expr, cancel)?;
2279            }
2280            Expr::FunctionCall { args, .. } => {
2281                for a in args {
2282                    self.resolve_expr_subqueries(a, cancel)?;
2283                }
2284            }
2285            Expr::Like { expr, pattern, .. } => {
2286                self.resolve_expr_subqueries(expr, cancel)?;
2287                self.resolve_expr_subqueries(pattern, cancel)?;
2288            }
2289            Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2290            // v4.12 window functions — recurse into args + ORDER BY
2291            // + PARTITION BY in case they carry inner subqueries.
2292            Expr::WindowFunction {
2293                args,
2294                partition_by,
2295                order_by,
2296                ..
2297            } => {
2298                for a in args {
2299                    self.resolve_expr_subqueries(a, cancel)?;
2300                }
2301                for p in partition_by {
2302                    self.resolve_expr_subqueries(p, cancel)?;
2303                }
2304                for (e, _, _) in order_by {
2305                    self.resolve_expr_subqueries(e, cancel)?;
2306                }
2307            }
2308            // Subquery nodes are handled in subquery_replacement
2309            // (which returned None — defensive no-op); Literal /
2310            // Column are leaves.
2311            Expr::ScalarSubquery(_)
2312            | Expr::Exists { .. }
2313            | Expr::InSubquery { .. }
2314            | Expr::RowInSubquery { .. }
2315            | Expr::RowCmpSubquery { .. }
2316            | Expr::Literal(_)
2317            | Expr::Placeholder(_)
2318            | Expr::Column(_) => {}
2319            // v7.30.2 — list elements can carry scalar subqueries
2320            // (`x IN (1, (SELECT …))`).
2321            Expr::InList { expr, list, .. } => {
2322                self.resolve_expr_subqueries(expr, cancel)?;
2323                for item in list {
2324                    self.resolve_expr_subqueries(item, cancel)?;
2325                }
2326            }
2327            // v7.10.10 — recurse children.
2328            Expr::Array(items) => {
2329                for elem in items {
2330                    self.resolve_expr_subqueries(elem, cancel)?;
2331                }
2332            }
2333            Expr::ArraySubscript { target, index } => {
2334                self.resolve_expr_subqueries(target, cancel)?;
2335                self.resolve_expr_subqueries(index, cancel)?;
2336            }
2337            Expr::ArraySlice { target, lo, hi } => {
2338                self.resolve_expr_subqueries(target, cancel)?;
2339                if let Some(l) = lo {
2340                    self.resolve_expr_subqueries(l, cancel)?;
2341                }
2342                if let Some(h) = hi {
2343                    self.resolve_expr_subqueries(h, cancel)?;
2344                }
2345            }
2346            Expr::AnyAll { expr, array, .. } => {
2347                self.resolve_expr_subqueries(expr, cancel)?;
2348                // Quantified subquery — an uncorrelated one
2349                // materialises up front; a correlated one stays for
2350                // the per-row resolver.
2351                if let Expr::ScalarSubquery(inner) = array.as_mut() {
2352                    if !crate::subquery::select_is_correlated(inner) {
2353                        let s = (**inner).clone();
2354                        **array = self.materialize_quantified_rows(&s, cancel)?;
2355                    }
2356                } else {
2357                    self.resolve_expr_subqueries(array, cancel)?;
2358                }
2359            }
2360            Expr::Case {
2361                operand,
2362                branches,
2363                else_branch,
2364            } => {
2365                if let Some(o) = operand {
2366                    self.resolve_expr_subqueries(o, cancel)?;
2367                }
2368                for (w, t) in branches {
2369                    self.resolve_expr_subqueries(w, cancel)?;
2370                    self.resolve_expr_subqueries(t, cancel)?;
2371                }
2372                if let Some(e) = else_branch {
2373                    self.resolve_expr_subqueries(e, cancel)?;
2374                }
2375            }
2376        }
2377        Ok(())
2378    }
2379}
2380
2381impl Engine {
2382    /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2383    /// `SelectItem::Wildcard` to all schema columns and
2384    /// `SelectItem::Expr` via the regular eval path.
2385    pub(crate) fn project_row_simple(
2386        &self,
2387        row: &Row<'static>,
2388        items: &[SelectItem],
2389        schema_cols: &[ColumnSchema],
2390        alias: &str,
2391    ) -> Result<Row<'static>, EngineError> {
2392        let ctx = self.ev_ctx(schema_cols, Some(alias));
2393        let cancel = CancelToken::none();
2394        let mut out_vals = Vec::new();
2395        for item in items {
2396            match item {
2397                // In a single-table projection (AS OF SEGMENT / RETURNING) a
2398                // qualified `t.*` covers exactly the same columns as a bare `*`.
2399                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2400                    out_vals.extend(row.values.iter().cloned());
2401                }
2402                SelectItem::Expr { expr, .. } => {
2403                    let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2404                    out_vals.push(v);
2405                }
2406            }
2407        }
2408        Ok(Row::new(out_vals))
2409    }
2410
2411    /// v6.10.2 — derive the output `ColumnSchema` list for an
2412    /// AS OF SEGMENT projection. Wildcards take the full schema;
2413    /// expressions take the alias if present or a synthetic
2414    /// `?column?` (PG convention) otherwise.
2415    pub(crate) fn derive_output_columns(
2416        &self,
2417        items: &[SelectItem],
2418        schema_cols: &[ColumnSchema],
2419        table_alias: &str,
2420    ) -> Vec<ColumnSchema> {
2421        let mut out = Vec::new();
2422        for item in items {
2423            match item {
2424                // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2425                // a single-table projection.
2426                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2427                    out.extend(schema_cols.iter().cloned());
2428                }
2429                SelectItem::Expr { expr, alias } => {
2430                    // Bare column references inherit the schema
2431                    // column's name + type — PG names `RETURNING id`
2432                    // "id" and types it BIGINT, and the sqlx embed
2433                    // path type-checks RowDescription against the
2434                    // Rust target (mailrs embed round-12).
2435                    if let Expr::Column(col) = expr
2436                        && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2437                    {
2438                        let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2439                        let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2440                        // v7.39 (read01 round 54) — carry the enum identity:
2441                        // it lives outside the DataType lattice, so a derived
2442                        // table built from this schema otherwise forgets it and
2443                        // the OUTER `ORDER BY <enum col>` silently sorts by the
2444                        // label's TEXT instead of member order.
2445                        c.user_enum_type = sc.user_enum_type.clone();
2446                        out.push(c);
2447                        continue;
2448                    }
2449                    let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2450                    // v7.30.4 (mailrs round-27, P0) — type the
2451                    // expression with the same inference the SELECT
2452                    // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2453                    // The old Text default broke every typed decode
2454                    // of `RETURNING uidnext - 1 AS uid`: four days
2455                    // of inbound mail indexed nowhere. Inference
2456                    // failure keeps the old Text fallback rather
2457                    // than inventing new error paths here.
2458                    // v7.39 (round 258) — take the enum identity from the
2459                    // same projection build, not just the type: a constant
2460                    // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2461                    // VALUES row lowers to) is an EXPRESSION, so it landed
2462                    // here and the derived table forgot the enum.
2463                    let (ty, nullable) = build_projection(
2464                        core::slice::from_ref(item),
2465                        schema_cols,
2466                        table_alias,
2467                        self.speaks_mysql,
2468                        Some(self.active_catalog()),
2469                    )
2470                    .ok()
2471                    .and_then(|p| p.into_iter().next())
2472                    .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2473                    out.push(ColumnSchema::new(name, ty, nullable));
2474                }
2475            }
2476        }
2477        out
2478    }
2479
2480    /// v4.5: SELECT with cooperative cancellation. The token is
2481    /// honoured between UNION peers and inside the bare-SELECT row
2482    /// loop; HNSW kNN graph walks and the aggregate executor don't
2483    /// honour it yet (deferred — those paths bound their work
2484    /// internally by `LIMIT k` and `GROUP BY` cardinality).
2485    /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2486    /// its (lowercased) name, or None if the name isn't a virtual view.
2487    /// Callers decide whether to return it directly (`SELECT *`) or stage
2488    /// it as a temp table for the full query pipeline.
2489    fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2490        Some(match name {
2491            "spg_statistic" => self.exec_spg_statistic(),
2492            "spg_stat_replication" => self.exec_spg_stat_replication(),
2493            "spg_stat_segment" => self.exec_spg_stat_segment(),
2494            "spg_memory_stats" => self.exec_spg_memory_stats(),
2495            "spg_stat_query" => self.exec_spg_stat_query(),
2496            "pg_stat_statements" => self.exec_pg_stat_statements(),
2497            "spg_stat_activity" => self.exec_spg_stat_activity(),
2498            "pg_stat_activity" => self.exec_pg_stat_activity(),
2499            "pg_locks" => self.exec_pg_locks(),
2500            "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2501            "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2502            "spg_partition_health" => self.exec_spg_partition_health(),
2503            "spg_audit_chain" => self.exec_spg_audit_chain(),
2504            "spg_audit_verify" => self.exec_spg_audit_verify(),
2505            "spg_table_ddl" => self.exec_spg_table_ddl(),
2506            "spg_role_ddl" => self.exec_spg_role_ddl(),
2507            "spg_database_ddl" => self.exec_spg_database_ddl(),
2508            _ => return None,
2509        })
2510    }
2511
2512    /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2513    /// describes against: this engine's catalog with the view staged as a
2514    /// table, exactly as `exec_select_cancel_as` stages it for a
2515    /// non-bare query.
2516    ///
2517    /// These views never reach the catalog — each is a fixed row set built
2518    /// inside its own `exec_*` — so Describe reported no columns for all
2519    /// seventeen of them. Rows are deliberately not inserted: Describe
2520    /// only needs the shape, and `infer_column_types` reads the rows we
2521    /// already have in hand.
2522    pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2523        let from = stmt.from.as_ref()?;
2524        if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2525            return None;
2526        }
2527        let lower = from.primary.name.to_ascii_lowercase();
2528        let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2529            return None;
2530        };
2531        let mut catalog = self.active_catalog().clone();
2532        let cols = infer_column_types(&columns, &rows);
2533        catalog
2534            .create_table(TableSchema::new(from.primary.name.clone(), cols))
2535            .ok()?;
2536        Some(catalog)
2537    }
2538
2539    pub(crate) fn exec_select_cancel(
2540        &self,
2541        stmt: &SelectStatement,
2542        cancel: CancelToken<'_>,
2543    ) -> Result<QueryResult, EngineError> {
2544        self.exec_select_cancel_as(stmt, cancel, None)
2545    }
2546
2547    /// v7.39 (round 334, V55) — the same read core, authorised as
2548    /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2549    /// function's OWNER: that is the entire point of the form, and without
2550    /// it every definer function failed with "permission denied" on the
2551    /// very table it exists to expose.
2552    /// v7.39 (round 559) — see the call site. `None` for anything but
2553    /// the bare shape, so every other query keeps its old path.
2554    fn try_bare_count_star(
2555        &self,
2556        stmt: &SelectStatement,
2557        as_role: Option<&str>,
2558    ) -> Result<Option<QueryResult>, EngineError> {
2559        use spg_sql::ast::SelectItem;
2560        if as_role.is_some()
2561            || !stmt.ctes.is_empty()
2562            || !stmt.unions.is_empty()
2563            || stmt.where_.is_some()
2564            || stmt.group_by.is_some()
2565            || stmt.having.is_some()
2566            || stmt.distinct
2567            || !stmt.order_by.is_empty()
2568            || stmt.limit.is_some()
2569            || stmt.offset.is_some()
2570            || stmt.items.len() != 1
2571        {
2572            return Ok(None);
2573        }
2574        let Some(from) = &stmt.from else {
2575            return Ok(None);
2576        };
2577        if !from.joins.is_empty()
2578            || stmt.locking.is_some()
2579            || from.primary.lateral_subquery.is_some()
2580            || from.primary.unnest_expr.is_some()
2581            || from.primary.generate_series_args.is_some()
2582            || from.primary.name.is_empty()
2583            || from.primary.name.starts_with("__spg_")
2584        {
2585            return Ok(None);
2586        }
2587        // A partition PARENT holds no rows of its own — they live in the
2588        // children — so its header count is 0 and the ordinary path has
2589        // to fan out. Caught by the partition conformance cases.
2590        //
2591        // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2592        // of them, which is worse: its header count is a real number,
2593        // just not the answer. `SELECT count(*) FROM par` returned 1
2594        // where PG returns 2, because this shortcut fired before the
2595        // fan-out could. The question is "does anything descend from
2596        // this", not "was it declared a partition parent".
2597        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2598            return Ok(None);
2599        }
2600        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2601            return Ok(None);
2602        };
2603        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2604            return Ok(None);
2605        };
2606        if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2607            return Ok(None);
2608        }
2609        // A row-security policy filters rows, so the header count is not
2610        // the answer; the ordinary path applies the policy.
2611        let Some(table) = self.active_catalog().get(&from.primary.name) else {
2612            return Ok(None);
2613        };
2614        if table.schema().row_security {
2615            return Ok(None);
2616        }
2617        // Rows frozen to the cold tier are not in `headers`, so the
2618        // header count would miss them. Caught by the cold-tier e2e.
2619        if table.has_cold_rows_fast() {
2620            return Ok(None);
2621        }
2622        let n = table.count_visible(&self.current_snapshot());
2623        let col = alias.clone().unwrap_or_else(|| String::from("count"));
2624        Ok(Some(QueryResult::Rows {
2625            columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2626            rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2627                i64::try_from(n).unwrap_or(i64::MAX)
2628            )])],
2629        }))
2630    }
2631
2632    /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2633    /// that col>` served from the index, never reading a row.
2634    ///
2635    /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2636    /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2637    /// count (2x at 1k). PG needs its visibility map for this — a heap
2638    /// tuple carries its own visibility, so an index entry alone cannot
2639    /// say whether the row is live, and PG reads the heap for any page
2640    /// the map does not mark all-visible. SPG keeps a header array
2641    /// beside the rows, so the locator answers it directly and there is
2642    /// no map to be stale.
2643    /// v7.39 (round 564) — the shape test, once, for both the
2644    /// materialising scan and the streaming one.
2645    ///
2646    /// Two callers asking the same question in two places is how a fact
2647    /// starts drifting; the answer here is the single copy. Returns the
2648    /// table, the alias the predicate is written against, the projected
2649    /// column's position, and the name the single output column takes.
2650    pub(crate) fn index_only_shape<'s>(
2651        &'s self,
2652        stmt: &'s SelectStatement,
2653    ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2654        use spg_sql::ast::SelectItem;
2655        if !stmt.ctes.is_empty()
2656            || !stmt.unions.is_empty()
2657            || stmt.group_by.is_some()
2658            || stmt.having.is_some()
2659            || stmt.distinct
2660            || stmt.locking.is_some()
2661            || !stmt.order_by.is_empty()
2662            || stmt.limit.is_some()
2663            || stmt.offset.is_some()
2664            || stmt.items.len() != 1
2665        {
2666            return None;
2667        }
2668        let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2669            return None;
2670        };
2671        if !from.joins.is_empty()
2672            || from.primary.lateral_subquery.is_some()
2673            || from.primary.unnest_expr.is_some()
2674            || from.primary.generate_series_args.is_some()
2675            || from.primary.name.is_empty()
2676            || from.primary.name.starts_with("__spg_")
2677        {
2678            return None;
2679        }
2680        // v7.39 (round 645) — see the note on the sibling shortcut above:
2681        // an inheritance parent's own header count is not the answer.
2682        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2683            return None;
2684        }
2685        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2686            return None;
2687        };
2688        let spg_sql::ast::Expr::Column(c) = expr else {
2689            return None;
2690        };
2691        let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2692        if let Some(q) = c.qualifier.as_deref()
2693            && !q.eq_ignore_ascii_case(alias_name)
2694        {
2695            return None;
2696        }
2697        let table = self.active_catalog().get(&from.primary.name)?;
2698        if table.schema().row_security {
2699            return None;
2700        }
2701        let cols = &table.schema().columns;
2702        let pos = cols
2703            .iter()
2704            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2705        let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2706        Some((table, alias_name, pos, out))
2707    }
2708
2709    /// v7.39 (round 565) — would this statement be answered out of the
2710    /// index alone?
2711    ///
2712    /// EXPLAIN has to name the node the executor will actually run, and
2713    /// the only honest way to know is to ask the same two questions the
2714    /// executor asks: the statement's shape, and everything decidable
2715    /// about the scan before it walks. Neither is re-stated here.
2716    pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2717        let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2718            return false;
2719        };
2720        let Some(where_) = stmt.where_.as_ref() else {
2721            return false;
2722        };
2723        crate::index_access::index_only_precheck(
2724            where_,
2725            &table.schema().columns,
2726            table,
2727            alias_name,
2728            pos,
2729            self.speaks_mysql,
2730        )
2731        .is_some()
2732    }
2733
2734    fn try_index_only_scan(
2735        &self,
2736        stmt: &SelectStatement,
2737    ) -> Result<Option<QueryResult>, EngineError> {
2738        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2739            return Ok(None);
2740        };
2741        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2742        // are not materialised here, and a partition parent's own
2743        // heap/indexes are empty (its rows live in the children).
2744        if !stmt.ctes.is_empty() {
2745            return Ok(None);
2746        }
2747        if let Some(from) = &stmt.from
2748            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2749        {
2750            return Ok(None);
2751        }
2752        let where_ = stmt.where_.as_ref().expect("shape checked it");
2753        let cols = &table.schema().columns;
2754        let Some(values) = crate::index_access::try_index_only_range(
2755            where_,
2756            cols,
2757            table,
2758            alias_name,
2759            &self.current_snapshot(),
2760            pos,
2761            self.speaks_mysql,
2762        ) else {
2763            return Ok(None);
2764        };
2765        let schema = alloc::vec![ColumnSchema::new(
2766            out_name,
2767            cols[pos].ty,
2768            cols[pos].nullable
2769        )];
2770        Ok(Some(QueryResult::Rows {
2771            columns: schema,
2772            rows: values
2773                .into_iter()
2774                .map(|v| Row::new(alloc::vec![v]))
2775                .collect(),
2776        }))
2777    }
2778
2779    /// v7.39 (round 564) — the same scan, emitting each value instead of
2780    /// building a `Vec<Row>` for the encoder to walk once and drop.
2781    ///
2782    /// A profile of the server serving a 50k-row range put 10.2% of the
2783    /// connection thread's CPU on BUILDING that vector and another 9.7%
2784    /// on dropping it — a fifth of the query, spent allocating and
2785    /// freeing one single-element `Vec` per output row so that the wire
2786    /// encoder could borrow each value for a few nanoseconds. The
2787    /// streaming interface it then hands them to takes `&[Value]`
2788    /// already.
2789    ///
2790    /// Returns `None` when the shape does not apply, so the caller falls
2791    /// back before anything has been emitted.
2792    pub(crate) fn try_index_only_stream<F>(
2793        &self,
2794        stmt: &SelectStatement,
2795        emit: &mut F,
2796    ) -> Result<Option<usize>, EngineError>
2797    where
2798        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2799    {
2800        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2801            return Ok(None);
2802        };
2803        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2804        // are not materialised here, and a partition parent's own
2805        // heap/indexes are empty (its rows live in the children).
2806        if !stmt.ctes.is_empty() {
2807            return Ok(None);
2808        }
2809        if let Some(from) = &stmt.from
2810            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2811        {
2812            return Ok(None);
2813        }
2814        let where_ = stmt.where_.as_ref().expect("shape checked it");
2815        let cols = &table.schema().columns;
2816        let schema = alloc::vec![ColumnSchema::new(
2817            out_name,
2818            cols[pos].ty,
2819            cols[pos].nullable
2820        )];
2821        let snapshot = self.current_snapshot();
2822        // The header goes out only once the walk has agreed to run — a
2823        // shape rejection after it would leave the client with a
2824        // RowDescription for a result that never comes.
2825        let mut wrote_header = false;
2826        let counted = crate::index_access::index_only_range_each(
2827            where_,
2828            cols,
2829            table,
2830            alias_name,
2831            &snapshot,
2832            pos,
2833            self.speaks_mysql,
2834            &mut |v: spg_storage::Value<'_>| {
2835                if !wrote_header {
2836                    emit(crate::StreamItem::Header(&schema))?;
2837                    wrote_header = true;
2838                }
2839                emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2840            },
2841        );
2842        match counted {
2843            None => Ok(None),
2844            Some(Err(e)) => Err(e),
2845            Some(Ok(n)) => {
2846                if !wrote_header {
2847                    emit(crate::StreamItem::Header(&schema))?;
2848                }
2849                Ok(Some(n))
2850            }
2851        }
2852    }
2853
2854    /// `DISTINCT ON`'s de-duplication, which runs after the inner
2855    /// SELECT has produced its rows.
2856    ///
2857    /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2858    /// reason round 848 established: a debug build gives every branch's
2859    /// locals a slot in the frame whichever branch runs, and this one is
2860    /// eighty lines of hashing, key slicing and survivor sorting that a
2861    /// statement without `DISTINCT ON` never touches. Round 867
2862    /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2863    /// reaches none of it — the segment that had been blamed on
2864    /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2865    #[inline(never)]
2866    fn apply_distinct_on(
2867        &self,
2868        result: QueryResult,
2869        don_hidden: usize,
2870        don_limit: &(
2871            Option<spg_sql::ast::LimitExpr>,
2872            Option<spg_sql::ast::LimitExpr>,
2873        ),
2874        don_top1: usize,
2875        orig_order_by: &[spg_sql::ast::OrderBy],
2876    ) -> Result<QueryResult, EngineError> {
2877        let QueryResult::Rows { columns, rows } = result else {
2878            return Ok(result);
2879        };
2880        // The keys are the hidden trailing columns appended above.
2881        // v7.39 (round 729) — top-1 mode: the trailing columns are the
2882        // DON keys plus the ORDER tail; keep each group's best in one
2883        // hash pass, then sort the SURVIVORS with the original spec.
2884        let mut kept: alloc::vec::Vec<Row<'static>>;
2885        let key_start;
2886        if don_top1 > 0 {
2887            let tail = don_top1 - 1;
2888            key_start = columns.len().saturating_sub(don_hidden + tail);
2889            let ord_start = key_start + don_hidden;
2890            let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2891                .iter()
2892                .map(|o| (o.desc, o.nulls_first))
2893                .collect();
2894            let mysql = self.speaks_mysql;
2895            let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2896                for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2897                    let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2898                    let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2899                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2900                        core::cmp::Ordering::Less => return true,
2901                        core::cmp::Ordering::Greater => return false,
2902                        core::cmp::Ordering::Equal => {}
2903                    }
2904                }
2905                false
2906            };
2907            let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2908            let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2909            let mut keybuf = String::new();
2910            for row in rows {
2911                keybuf.clear();
2912                for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2913                    aggregate::push_canonical_key(&mut keybuf, v);
2914                }
2915                match slot.get(keybuf.as_str()) {
2916                    Some(&i) => {
2917                        if better(&row, &best[i]) {
2918                            best[i] = row;
2919                        }
2920                    }
2921                    None => {
2922                        slot.insert(keybuf.clone(), best.len());
2923                        best.push(row);
2924                    }
2925                }
2926            }
2927            // Survivors sort with the FULL original spec (keys are still
2928            // aboard as hidden columns).
2929            let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2930                .iter()
2931                .map(|o| (o.desc, o.nulls_first))
2932                .collect();
2933            best.sort_by(|a, b| {
2934                for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2935                    let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2936                    let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2937                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2938                        core::cmp::Ordering::Equal => {}
2939                        o => return o,
2940                    }
2941                }
2942                core::cmp::Ordering::Equal
2943            });
2944            for r in &mut best {
2945                r.values.truncate(key_start);
2946            }
2947            kept = best;
2948        } else {
2949            key_start = columns.len().saturating_sub(don_hidden);
2950            let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2951            kept = alloc::vec::Vec::new();
2952            for mut row in rows {
2953                let key: alloc::vec::Vec<Value<'static>> =
2954                    row.values.get(key_start..).unwrap_or(&[]).to_vec();
2955                if seen.iter().any(|k| k == &key) {
2956                    continue;
2957                }
2958                seen.push(key);
2959                row.values.truncate(key_start);
2960                kept.push(row);
2961            }
2962        }
2963        let mut columns = columns;
2964        columns.truncate(key_start);
2965        // PG limits what DISTINCT ON left, not what fed it.
2966        let kept = apply_deferred_limit(kept, don_limit);
2967        Ok(QueryResult::Rows {
2968            columns,
2969            rows: kept,
2970        })
2971    }
2972
2973    pub(crate) fn exec_select_cancel_as(
2974        &self,
2975        stmt: &SelectStatement,
2976        cancel: CancelToken<'_>,
2977        as_role: Option<&str>,
2978    ) -> Result<QueryResult, EngineError> {
2979        // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2980        // <all columns>` is legal PG (the wildcard expands to grouped
2981        // columns); SPG refused the whole shape. Expand the wildcard
2982        // into explicit column refs up front — the aggregate layer's
2983        // existing "must appear in the GROUP BY clause" validation
2984        // then answers PG's sentence for any non-grouped column.
2985        if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2986            return self.exec_select_cancel_as(&expanded, cancel, as_role);
2987        }
2988        // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2989        // a row.
2990        //
2991        // The aggregate layer already short-circuits this to
2992        // `rows.len()`, so the O(1) part was never the problem — the
2993        // cost is UPSTREAM, materialising every visible row so that
2994        // layer can take its length. Measured over pgwire on 500k rows:
2995        // PG18 8.2 ms with two parallel workers, 10.3 ms with
2996        // parallelism off, SPG 16.5 ms — 1.6x slower than a
2997        // single-threaded PG on the commonest aggregate there is, and no
2998        // ledger entry recorded it.
2999        //
3000        // Counting visible HEADERS needs no row at all. PG cannot do
3001        // this: its visibility lives in the heap tuples themselves, so
3002        // it has to read them (that is why its own count(*) is a full
3003        // scan, parallel or not).
3004        // v7.39 (read01 round 57) — the table-privilege gate on the common
3005        // read core. A superuser session returns from it immediately.
3006        // v7.39 (round 529) — resolve an ORDER BY that names an output
3007        // ALIAS. The statement-level pass never reached a SELECT nested in
3008        // a FROM clause, a CTE or a scalar subquery, so the same query
3009        // worked on its own and failed the moment anything wrapped it —
3010        // which is what generated SQL does constantly.
3011        let aliased;
3012        let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
3013            let mut s = stmt.clone();
3014            crate::orderby::resolve_order_by_position(&mut s);
3015            aliased = s;
3016            &aliased
3017        } else {
3018            stmt
3019        };
3020        // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
3021        //
3022        // Its keys were evaluated against the PROJECTED row, so a key that
3023        // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
3024        // ORDER BY g, v DESC`, the canonical "latest row per group" — could
3025        // not be read at all and the query failed. PG evaluates them on the
3026        // input. They are projected as hidden columns here and stripped
3027        // again below, the same way the grouping-set ordering columns
3028        // already travel.
3029        //
3030        // And the dedup ran AFTER the inner statement's LIMIT, so
3031        // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
3032        // PG answers two: the limit had already taken two rows of the same
3033        // group before anything deduplicated them. A paginated DISTINCT ON
3034        // returned short pages, with no error. The limit is deferred to
3035        // after the dedup, which is PG's order.
3036        let don_stmt;
3037        // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
3038        // order spec (the rewritten stmt's is emptied).
3039        let orig_order_by = stmt.order_by.clone();
3040        let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
3041            (stmt, 0, (None, None), 0usize)
3042        } else {
3043            let mut s = stmt.clone();
3044            let hidden = s.distinct_on.len();
3045            for (i, e) in stmt.distinct_on.iter().enumerate() {
3046                s.items.push(SelectItem::Expr {
3047                    expr: e.clone(),
3048                    alias: Some(alloc::format!("__distinct_on_{i}")),
3049                });
3050            }
3051            // v7.39 (round 729) — group-top-1 short circuit. When the
3052            // DISTINCT ON keys are exactly the ORDER BY's leading keys,
3053            // the answer is "per group, the row that wins the remaining
3054            // order" — a single O(n) hash pass. The old path sorted the
3055            // ENTIRE input first (500k rows, ~180 ms on the panel cell)
3056            // to keep 100. The inner query runs UNSORTED with every
3057            // order key appended as a hidden column; the dedup below
3058            // keeps each group's best, then sorts the SURVIVORS.
3059            // Declared-collation order keys stay on the sorting path
3060            // (the value comparator here is collation-blind).
3061            let prefix_matches = s.order_by.len() >= hidden
3062                && stmt
3063                    .distinct_on
3064                    .iter()
3065                    .zip(s.order_by.iter())
3066                    .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3067            let colls_plain =
3068                crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3069                    .map(|cs| cs.iter().all(Option::is_none))
3070                    .unwrap_or(false);
3071            let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3072                let tail = s.order_by.len() - hidden;
3073                for (j, o) in s.order_by[hidden..].iter().enumerate() {
3074                    s.items.push(SelectItem::Expr {
3075                        expr: o.expr.clone(),
3076                        alias: Some(alloc::format!("__don_ord_{j}")),
3077                    });
3078                }
3079                // Carry the tail's direction flags through the aliases'
3080                // ORDER; the survivors re-sort below with the full spec.
3081                s.order_by = Vec::new();
3082                tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3083            } else {
3084                0
3085            };
3086            // Only a folded literal is deferred; a placeholder or an
3087            // expression keeps the path it has today rather than being
3088            // resolved a second way here.
3089            let deferrable = matches!(
3090                (&s.limit, &s.offset),
3091                (
3092                    None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3093                    None | Some(spg_sql::ast::LimitExpr::Literal(_))
3094                )
3095            );
3096            let deferred = if deferrable {
3097                (s.limit.take(), s.offset.take())
3098            } else {
3099                (None, None)
3100            };
3101            don_stmt = s;
3102            (&don_stmt, hidden, deferred, top1_tail)
3103        };
3104        self.acl_check_select_as(stmt, as_role)?;
3105        validate_aggregate_placement(stmt)?;
3106        // BEFORE the fast paths below, not after: a name that resolves to
3107        // nothing is not a question the count fast path or the index-only
3108        // scan should get to answer first. Placed after them at first,
3109        // and the two of them swallowed `WHERE` and `ORDER BY` while
3110        // `GROUP BY` and `HAVING`, which cannot take those routes, raised
3111        // — the same statement answering two ways depending on the plan.
3112        self.validate_clause_columns(stmt)?;
3113        self.validate_function_arity(stmt)?;
3114        // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3115        // privilege gate above. Placed before it at first, and the
3116        // security-definer e2e caught it immediately: a SECURITY INVOKER
3117        // function whose body is `SELECT count(*) FROM t` answered
3118        // instead of being refused, because the fast path never reached
3119        // the check.
3120        if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3121            return Ok(r);
3122        }
3123        // v7.39 (round 560) — an index-only range scan. Same placement
3124        // reasoning as the count above: after the privilege gate.
3125        if let Some(r) = self.try_index_only_scan(stmt)? {
3126            return Ok(r);
3127        }
3128        validate_locking_clause(stmt)?;
3129        let result = self.exec_select_cancel_inner(stmt, cancel)?;
3130        // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3131        // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3132        // They carry the per-branch mask through the UNION-ALL sort and must not
3133        // appear in the output. Stripped per SELECT level (grouping-set queries
3134        // are often wrapped in a derived subquery), before DISTINCT ON.
3135        let result = strip_synthetic_order_cols(result);
3136        // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3137        // rows arrive here already ORDER BY'd; keep the FIRST row of
3138        // each group the expressions define (PG semantics). The
3139        // expressions evaluate against the projected schema — an
3140        // expression that isn't in the select list errors honestly.
3141        if stmt.distinct_on.is_empty() {
3142            return Ok(result);
3143        }
3144        self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3145    }
3146
3147    /// The UNION chain: execute the head as a bare block, then fold each
3148    /// peer in with left-associative dedup.
3149    ///
3150    /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3151    /// reason round 848 established. A statement with no unions returns
3152    /// one line above the call — and every nested subquery on a deep
3153    /// path is such a statement, so each level of the recursion carried
3154    /// 170 lines of locals it could not reach. Round 867 measured that
3155    /// frame at 34,800 bytes, the largest single one on the descent,
3156    /// after two earlier attributions had blamed its caller and then its
3157    /// callee: the gap between two marks is the frame of everything
3158    /// BETWEEN them, and this function had no mark of its own.
3159    #[inline(never)]
3160    fn exec_union_chain(
3161        &self,
3162        stmt_ref: &SelectStatement,
3163        stmt: &SelectStatement,
3164        cancel: CancelToken<'_>,
3165    ) -> Result<QueryResult, EngineError> {
3166        // UNION path: clone-strip the head into a bare block (its own
3167        // DISTINCT and any inner ORDER BY are dropped by parser rule —
3168        // the wrapper SelectStatement carries them), execute, then chain
3169        // peers with left-associative dedup semantics.
3170        // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3171        // output columns; a position past their count is PG's 42P10.
3172        crate::orderby::check_order_by_positions(stmt_ref)?;
3173        let mut head_unknown = branch_unknown_mask(stmt_ref);
3174        let head_regcast = branch_regcast_mask(stmt_ref);
3175        let mut head = stmt_ref.clone();
3176        head.unions = Vec::new();
3177        head.order_by = Vec::new();
3178        head.limit = None;
3179        let QueryResult::Rows {
3180            mut columns,
3181            mut rows,
3182        } = self.exec_bare_select_cancel(&head, cancel)?
3183        else {
3184            unreachable!("bare SELECT cannot return CommandOk")
3185        };
3186        for (kind, peer) in &stmt_ref.unions {
3187            // v7.37.17 (17.6 siblings) — a peer carrying its own
3188            // unions is a nested INTERSECT group (the parser's
3189            // precedence regrouping); recurse through the
3190            // union-aware wrapper for it.
3191            let peer_result = if peer.unions.is_empty() {
3192                self.exec_bare_select_cancel(peer, cancel)?
3193            } else {
3194                self.exec_select_cancel(peer, cancel)?
3195            };
3196            let QueryResult::Rows {
3197                columns: peer_cols,
3198                rows: mut peer_rows,
3199            } = peer_result
3200            else {
3201                unreachable!("bare SELECT cannot return CommandOk")
3202            };
3203            if peer_cols.len() != columns.len() {
3204                // v7.39 (round 232) — PG's wording, which clients match on.
3205                return Err(EngineError::Unsupported(alloc::format!(
3206                    "each {} query must have the same number of columns",
3207                    set_op_name(*kind)
3208                )));
3209            }
3210            // v7.39 (round 232+233) — PG resolves each result column to one
3211            // type before it merges anything, and refuses the query when the
3212            // two branches have no common type. SPG's unifier
3213            // (`unify_union_columns`) is value-driven and deliberately
3214            // conservative — "a column where any cell fails to coerce is left
3215            // exactly as it was" — so a mismatch produced a column holding
3216            // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3217            // back with integers and text interleaved) instead of an error.
3218            //
3219            // The check has to read the branch ASTs, not just their schemas:
3220            // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3221            // as TEXT and is indistinguishable from a real text column by
3222            // schema alone — yet PG treats the two completely differently
3223            // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3224            // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3225            let peer_unknown = branch_unknown_mask(peer);
3226            let peer_regcast = branch_regcast_mask(peer);
3227            for i in 0..columns.len() {
3228                let hu = head_unknown.get(i).copied().unwrap_or(false);
3229                let pu = peer_unknown.get(i).copied().unwrap_or(false);
3230                let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3231                let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3232                    || head_regcast.get(i).copied().unwrap_or(false);
3233                match (hu, pu) {
3234                    // Both sides carry a real type: they must share a category.
3235                    (false, false) => {
3236                        if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3237                            return Err(EngineError::Unsupported(alloc::format!(
3238                                "{} types {} and {} cannot be matched",
3239                                set_op_name(*kind),
3240                                crate::conversions::pg_type_name_for_error(ht),
3241                                crate::conversions::pg_type_name_for_error(pt),
3242                            )));
3243                        }
3244                    }
3245                    // One side is an untyped literal: it takes the other's
3246                    // type, and failing to convert is the error PG reports.
3247                    (true, false) => {
3248                        coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3249                        columns[i].ty = pt;
3250                        head_unknown[i] = false;
3251                    }
3252                    (false, true) => {
3253                        coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3254                    }
3255                    // Both untyped — nothing to resolve against yet.
3256                    (true, true) => {}
3257                }
3258            }
3259            // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3260            // nullable (PG semantics). Previously the result kept only the head's
3261            // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3262            // non-null `1`) wrongly reported the column NOT NULL, which let
3263            // `count(col)`'s NOT-NULL fast-path count the NULL row.
3264            for (i, pc) in peer_cols.iter().enumerate() {
3265                if pc.nullable {
3266                    columns[i].nullable = true;
3267                }
3268            }
3269            // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3270            // text by the session collation (CI + accent + PAD SPACE), like
3271            // GROUP BY. PG stays byte-exact.
3272            let mysql = self.speaks_mysql;
3273            // v7.38.14 — the mask, which 7.38.13 recorded as impossible here
3274            // and was wrong about. `columns` and `peer_cols` are both in
3275            // scope; what was actually missing is that the branches' output
3276            // schemas did not CARRY the collation, so a mask built from them
3277            // would have marked every column byte-wise. Unifying the
3278            // projection-to-schema conversion fixed the supply side, and the
3279            // mask is now buildable from what was always there.
3280            //
3281            // Either side byte-wise keeps the position byte-wise, mirroring
3282            // `eval::resolve::mysql_text_fold_applies`: a set operation
3283            // between a folding column and a declared-binary one must not
3284            // quietly fold the binary one's values away.
3285            let set_mask: alloc::vec::Vec<bool> = columns
3286                .iter()
3287                .zip(peer_cols.iter())
3288                .map(|(l, r)| {
3289                    matches!(l.collation, spg_storage::Collation::Binary)
3290                        || matches!(r.collation, spg_storage::Collation::Binary)
3291                })
3292                .collect();
3293            let fold = FoldSpec::of(mysql, &set_mask);
3294            match kind {
3295                UnionKind::All => rows.extend(peer_rows),
3296                UnionKind::Distinct => {
3297                    rows.extend(peer_rows);
3298                    rows = dedup_rows(rows, fold);
3299                }
3300                // v7.37.17 (17.6 siblings) — PG set semantics.
3301                // v7.39 (round 591) — all four ask the same question of the
3302                // right side, and all four used to answer it by scanning it
3303                // once per left row. `PeerIndex` buckets it by the hash
3304                // DISTINCT already uses, so the answer is a lookup.
3305                // INTERSECT: distinct rows present on both sides.
3306                UnionKind::Intersect => {
3307                    let idx = PeerIndex::build(&peer_rows, fold);
3308                    rows = dedup_rows(rows, fold)
3309                        .into_iter()
3310                        .filter(|r| idx.contains(r))
3311                        .collect();
3312                }
3313                // INTERSECT ALL: multiset intersection — each row
3314                // keeps min(left count, right count) occurrences.
3315                UnionKind::IntersectAll => {
3316                    let mut idx = PeerIndex::build(&peer_rows, fold);
3317                    let mut kept: Vec<Row<'static>> = Vec::new();
3318                    for r in rows {
3319                        if idx.take_one(&r) {
3320                            kept.push(r);
3321                        }
3322                    }
3323                    rows = kept;
3324                }
3325                // EXCEPT: distinct left rows absent from the right.
3326                UnionKind::Except => {
3327                    let idx = PeerIndex::build(&peer_rows, fold);
3328                    rows = dedup_rows(rows, fold)
3329                        .into_iter()
3330                        .filter(|r| !idx.contains(r))
3331                        .collect();
3332                }
3333                // EXCEPT ALL: multiset subtraction — each right
3334                // occurrence cancels one left occurrence.
3335                UnionKind::ExceptAll => {
3336                    let mut idx = PeerIndex::build(&peer_rows, fold);
3337                    let mut kept: Vec<Row<'static>> = Vec::new();
3338                    for r in rows {
3339                        if !idx.take_one(&r) {
3340                            kept.push(r);
3341                        }
3342                    }
3343                    rows = kept;
3344                }
3345            }
3346        }
3347        // PG resolves a UNION / VALUES result column to one common type
3348        // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3349        // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3350        // built each branch independently, leaving mixed-type columns
3351        // that broke ORDER BY, comparisons, and value-based window
3352        // frames. Unify + coerce before the combined ORDER BY sees them.
3353        unify_union_columns(&mut columns, &mut rows);
3354        // ORDER BY at the top of a UNION applies to the combined result.
3355        // Eval against the projected schema (NOT the source table).
3356        if !stmt.order_by.is_empty() {
3357            // v7.39 (read01 round 54) — the combined-result ctx must carry the
3358            // catalog, and the projected columns must keep their enum identity
3359            // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3360            // by TEXT instead of member order — silently wrong rows, not an
3361            // error. (Same shape as the enum-order knife's GROUP BY fix.)
3362            let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3363            // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3364            // survive to here when the head projects a Wildcard (the
3365            // group-tail wrapper shape): map them onto the Nth
3366            // projected column so the combined sort works.
3367            let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3368                .order_by
3369                .iter()
3370                .map(|o| {
3371                    let mut o = o.clone();
3372                    if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3373                        && *n >= 1
3374                        && let Ok(idx) = usize::try_from(*n - 1)
3375                        && idx < columns.len()
3376                    {
3377                        o.expr = Expr::Column(spg_sql::ast::ColumnName {
3378                            qualifier: None,
3379                            name: columns[idx].name.clone(),
3380                        });
3381                    }
3382                    o
3383                })
3384                .collect();
3385            let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3386            let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3387            for r in rows {
3388                // v7.39.12 — a correlated subquery in ORDER BY is resolved
3389                // for this row before the key is built; see
3390                // `Engine::order_by_resolved_for_row`.
3391                let per_row =
3392                    self.order_by_resolved_for_row(&resolved_order, &r, &synth_ctx, cancel)?;
3393                let keys = build_order_keys(
3394                    per_row.as_deref().unwrap_or(&resolved_order),
3395                    &r,
3396                    &synth_ctx,
3397                )?;
3398                tagged.push((keys, r));
3399            }
3400            sort_by_keys(&mut tagged, &descs);
3401            rows = tagged.into_iter().map(|(_, r)| r).collect();
3402        }
3403        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3404        Ok(QueryResult::Rows { columns, rows })
3405    }
3406
3407    fn exec_select_cancel_inner(
3408        &self,
3409        stmt: &SelectStatement,
3410        cancel: CancelToken<'_>,
3411    ) -> Result<QueryResult, EngineError> {
3412        cancel.check()?;
3413        // v7.38 P0 元机制 A — first observable point inside the
3414        // planner / executor. Tests use this to inject a delay or
3415        // a cancellation race before any row is produced. Release
3416        // build expands to `let _ = (...);` — zero cost.
3417        crate::injection_point!("planner_first_row_fetch", &stmt.from);
3418        // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3419        // PG analyses every definition, referenced or not, so `SELECT i FROM
3420        // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3421        // succeeded here (the parser used to drop the unreferenced defs
3422        // whole). The check is the CREATE VIEW check's shape (round 700): a
3423        // LIMIT-0 run of the same FROM with the definitions' key
3424        // expressions as the projection — it cannot disagree with what a
3425        // referencing window would have done, because it resolves the same
3426        // names the same way. Zero cost for the ordinary statement: the
3427        // list is empty unless a WINDOW clause left unreferenced defs.
3428        if !stmt.window_check_exprs.is_empty() {
3429            let mut probe = stmt.clone();
3430            probe.items = stmt
3431                .window_check_exprs
3432                .iter()
3433                .map(|e| spg_sql::ast::SelectItem::Expr {
3434                    expr: e.clone(),
3435                    alias: None,
3436                })
3437                .collect();
3438            probe.window_check_exprs = Vec::new();
3439            probe.distinct = false;
3440            probe.distinct_on = Vec::new();
3441            probe.group_by = None;
3442            probe.group_by_all = false;
3443            probe.having = None;
3444            probe.unions = Vec::new();
3445            probe.order_by = Vec::new();
3446            probe.locking = None;
3447            probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3448            probe.offset = None;
3449            probe.limit_with_ties = false;
3450            self.exec_select_cancel_inner(&probe, cancel)?;
3451        }
3452        // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3453        // takes the catalog, so the parser leaves a marker and the rewrite lands
3454        // here: the call moves into a LATERAL FROM item and the item becomes one
3455        // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3456        // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3457        // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3458        // second one.
3459        if let Some(lowered) = self.lower_record_expansion(stmt)? {
3460            return self.exec_select_cancel_inner(&lowered, cancel);
3461        }
3462        // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3463        // FROM / JOIN graph references any catalogued view name,
3464        // re-parse the view body and prepend it as a synthetic
3465        // CTE. Recurses on views-in-views via the regular CTE
3466        // dispatch below. Fast-path: skip the walker entirely when
3467        // the catalog has no views (the typical OLTP load).
3468        if !self.active_catalog().views_all().is_empty() {
3469            if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3470                return self.exec_select_cancel(&rewritten, cancel);
3471            }
3472        }
3473        // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3474        // gets rewritten to a UNION-ALL over the children that overlap
3475        // the WHERE-derived key range. Uses the same CTE-injection
3476        // trick as VIEW expansion above so downstream resolution
3477        // doesn't need a partition-aware code path.
3478        if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3479            return self.exec_select_cancel(&rewritten, cancel);
3480        }
3481        // v7.16.2 — information_schema / pg_catalog virtual
3482        // views (mailrs round-10 A.3). If the SELECT touches a
3483        // synthetic meta-table name (`__spg_info_*` /
3484        // `__spg_pg_*` — produced by the parser for
3485        // `information_schema.X` / `pg_catalog.X`), clone the
3486        // catalog, materialise the requested view as a real
3487        // temporary table, and re-execute against an enriched
3488        // engine. Same pattern as `exec_with_ctes` for CTEs.
3489        if !self.meta_views_materialised && select_references_meta_view(stmt) {
3490            return self.exec_select_with_meta_views(stmt, cancel);
3491        }
3492        // v6.10.2 — cold-tier time-travel short-circuit. When the
3493        // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3494        // dedicated cold-segment scan instead of the regular
3495        // hot+index path. The scope is intentionally narrow for
3496        // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3497        // optionally with a single-column-equality WHERE. JOINs /
3498        // aggregates / ORDER BY / subqueries on top of a time-
3499        // travelled scan are STABILITY § "Out of v6.10".
3500        if let Some(from) = &stmt.from
3501            && let Some(seg_id) = from.primary.as_of_segment
3502        {
3503            return self.exec_select_as_of_segment(stmt, from, seg_id);
3504        }
3505        // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3506        // pre-CTE because they don't read from the catalog and
3507        // shouldn't participate in regular FROM resolution.
3508        // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3509        // short-circuits. A meta-view FROM materialises to a fixed row
3510        // set. For a bare `SELECT *` we return it directly; otherwise we
3511        // stage it as a temp table and run the normal pipeline, so
3512        // projection / WHERE / ORDER BY / aggregates work over these views
3513        // (they were `SELECT *`-only before). A real table shadowing the
3514        // name wins (checked first), which also stops the staged re-run
3515        // from recursing back into meta-view detection.
3516        if let Some(from) = &stmt.from
3517            && from.joins.is_empty()
3518            && self.active_catalog().get(&from.primary.name).is_none()
3519        {
3520            let lower = from.primary.name.to_ascii_lowercase();
3521            if let Some(result) = self.meta_view_result(&lower) {
3522                let bare = stmt.where_.is_none()
3523                    && stmt.group_by.is_none()
3524                    && stmt.having.is_none()
3525                    && stmt.unions.is_empty()
3526                    && stmt.order_by.is_empty()
3527                    && stmt.limit.is_none()
3528                    && stmt.offset.is_none()
3529                    && !stmt.distinct
3530                    && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3531                if bare {
3532                    return Ok(result);
3533                }
3534                if let QueryResult::Rows { columns, rows } = result {
3535                    let mut catalog = self.active_catalog().clone();
3536                    let cols = infer_column_types(&columns, &rows);
3537                    let schema = TableSchema::new(from.primary.name.clone(), cols);
3538                    catalog.create_table(schema).map_err(EngineError::Storage)?;
3539                    let t = catalog
3540                        .get_mut(&from.primary.name)
3541                        .expect("just-created meta-view table must exist");
3542                    for row in rows {
3543                        t.insert(row).map_err(EngineError::Storage)?;
3544                    }
3545                    let mut eng = Engine::restore(catalog);
3546                    if let Some(c) = self.clock {
3547                        eng = eng.with_clock(c);
3548                    }
3549                    if let Some(f) = self.salt_fn {
3550                        eng = eng.with_salt_fn(f);
3551                    }
3552                    // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3553                    // connection identity so `WHERE pid = pg_backend_pid()`
3554                    // matches inside the staged meta-view run.
3555                    if let Some(f) = self.backend_pid_fn {
3556                        eng.set_backend_pid_fn(f);
3557                    }
3558                    return eng.exec_select_cancel(stmt, cancel);
3559                }
3560                return Ok(result);
3561            }
3562        }
3563        // v4.11: CTEs materialise into a temporary enriched catalog
3564        // *before* anything else — the body SELECT can then refer
3565        // to CTE names via the regular FROM-clause resolution.
3566        // Uncorrelated only: each CTE body runs once against the
3567        // current catalog, not against later CTEs' results (left-
3568        // to-right materialisation would relax this, but we keep
3569        // it simple for v4.11 MVP).
3570        if !stmt.ctes.is_empty() {
3571            return self.exec_with_ctes(stmt, cancel);
3572        }
3573        // v4.10: subqueries (uncorrelated) are resolved here, before
3574        // the executor sees the row loop. We clone the statement so
3575        // we can mutate without disturbing the caller's AST — most
3576        // queries pass through with no subquery nodes and the clone
3577        // is cheap; with subqueries the materialisation cost
3578        // dominates anyway.
3579        let mut stmt_owned;
3580        let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3581            stmt_owned = stmt.clone();
3582            // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3583            // aggregate-wrapped correlated scalar subquery whose
3584            // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3585            // executor streams one join instead of splicing a per-row
3586            // subplan. Runs before the per-row/batch resolver, which then
3587            // only sees the subqueries the pull-up left behind.
3588            self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3589            // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3590            // the "per-key latest" scalar subquery shape (inbox / feed
3591            // / timeline applications) becomes a CTE + LEFT JOIN
3592            // against a GROUP BY pre-aggregation that reuses the v7.33
3593            // first_ordered argmax executor. Runs AFTER unique-key
3594            // pull-up (so the unique-key fast path still wins for
3595            // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3596            // Phase 1 (this commit) is skeleton only — no-op pass.
3597            self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3598            // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3599            // sublink pull-up to semi/anti-join, before the resolver gets
3600            // a chance to walk per-row.
3601            self.pull_up_exists_sublinks(&mut stmt_owned);
3602            // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3603            // exec_with_ctes so they materialise once before the body
3604            // SELECT runs. exec_with_ctes strips ctes from the body
3605            // clone, then re-enters select.
3606            if !stmt_owned.ctes.is_empty() {
3607                return self.exec_with_ctes(&stmt_owned, cancel);
3608            }
3609            // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3610            //   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3611            // BEFORE `resolve_select_subqueries` materialises the inner
3612            // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3613            // INSUBQ benchmark). Run the inner once, collect the result
3614            // values into a `HashSet<i64>` directly, then probe A.pk per
3615            // value and tally. Returns `Some` when the shape matches.
3616            if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3617                return Ok(out);
3618            }
3619            self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3620            &stmt_owned
3621        } else {
3622            stmt
3623        };
3624        if stmt_ref.unions.is_empty() {
3625            return self.exec_bare_select_cancel(stmt_ref, cancel);
3626        }
3627        self.exec_union_chain(stmt_ref, stmt, cancel)
3628    }
3629
3630    #[allow(clippy::too_many_lines)]
3631    #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3632    /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3633    /// Synthesises a single-column virtual table whose column type
3634    /// is TEXT and whose rows are the array elements. Routes
3635    /// through the regular projection / WHERE / ORDER BY / LIMIT
3636    /// machinery so set-returning UNNEST composes naturally with
3637    /// the rest of the SELECT surface.
3638    fn exec_select_unnest(
3639        &self,
3640        stmt: &SelectStatement,
3641        primary: &TableRef,
3642        cancel: CancelToken<'_>,
3643    ) -> Result<QueryResult, EngineError> {
3644        let expr = primary
3645            .unnest_expr
3646            .as_deref()
3647            .expect("caller guards unnest_expr.is_some()");
3648        // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3649        // N value columns instead of one; the shared builder does
3650        // the work and the tail below (WHERE / agg / projection)
3651        // runs against the wider schema.
3652        let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3653            match unnest_zip_args(expr) {
3654                Some(args) => Some(unnest_zip_rows(args)?),
3655                None => None,
3656            };
3657        // Evaluate the array expression once. Empty schema / empty
3658        // row — uncorrelated UNNEST cannot reference outer columns.
3659        // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3660        // introspection family (enum_range / enum_first / enum_last) resolves
3661        // its labels from the argument's STATIC enum type against the
3662        // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3663        // fell through to the generic arm, got NULL, and expanded to zero rows
3664        // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3665        // carry the catalog) worked.
3666        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3667        let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3668        let dummy_row = Row::new(alloc::vec::Vec::new());
3669        // v7.11.13 — unnest dispatches per array element type so
3670        // INT[] / BIGINT[] surface their PG types in projection.
3671        // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3672        // columns (PG: lexeme | positions | weights); everything else
3673        // keeps the alias / "unnest" defaults below.
3674        let mut composite_names: Option<&[&str]> = None;
3675        let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3676            if let Some(m) = multi {
3677                m
3678            } else {
3679                // v7.39 (round 236) — flatten a multidimensional array into
3680                // its row-major elements (PG) before the 1-D-only match.
3681                let unnest_src = {
3682                    let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3683                    crate::eval::values::flatten_2d(&v).unwrap_or(v)
3684                };
3685                let mut return_multi: Option<(
3686                    alloc::vec::Vec<DataType>,
3687                    alloc::vec::Vec<Row<'static>>,
3688                )> = None;
3689                let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3690                {
3691                    Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3692                    Value::TextArray(items) => {
3693                        let rows = items
3694                            .into_iter()
3695                            .map(|item| {
3696                                Row::new(alloc::vec![match item {
3697                                    Some(s) => Value::text(s),
3698                                    None => Value::Null,
3699                                }])
3700                            })
3701                            .collect();
3702                        (DataType::Text, rows)
3703                    }
3704                    Value::IntArray(items) => {
3705                        let rows = items
3706                            .into_iter()
3707                            .map(|item| {
3708                                Row::new(alloc::vec![match item {
3709                                    Some(n) => Value::Int(n),
3710                                    None => Value::Null,
3711                                }])
3712                            })
3713                            .collect();
3714                        (DataType::Int, rows)
3715                    }
3716                    Value::BigIntArray(items) => {
3717                        let rows = items
3718                            .into_iter()
3719                            .map(|item| {
3720                                Row::new(alloc::vec![match item {
3721                                    Some(n) => Value::BigInt(n),
3722                                    None => Value::Null,
3723                                }])
3724                            })
3725                            .collect();
3726                        (DataType::BigInt, rows)
3727                    }
3728                    Value::Multirange { kind, ranges } => {
3729                        let rows = ranges
3730                            .iter()
3731                            .map(|sp| {
3732                                Row::new(alloc::vec![Value::Range {
3733                                    kind,
3734                                    lower: sp.lower.clone(),
3735                                    upper: sp.upper.clone(),
3736                                    lower_inc: sp.lower_inc,
3737                                    upper_inc: sp.upper_inc,
3738                                    empty: false,
3739                                }])
3740                            })
3741                            .collect();
3742                        (DataType::Range(kind), rows)
3743                    }
3744                    // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3745                    // one row per lexeme, PG18-measured columns
3746                    // lexeme | positions | weights (`a | {1,3} |
3747                    // {D,D}`); a position-less lexeme (a stripped
3748                    // vector) reads NULL in both array columns.
3749                    Value::TsVector(lexemes) => {
3750                        composite_names = Some(&["lexeme", "positions", "weights"]);
3751                        let rows = lexemes
3752                            .iter()
3753                            .map(|l| {
3754                                let (pos, wts) = if l.positions.is_empty() {
3755                                    (Value::Null, Value::Null)
3756                                } else {
3757                                    let letter = match l.weight {
3758                                        3 => "A",
3759                                        2 => "B",
3760                                        1 => "C",
3761                                        _ => "D",
3762                                    };
3763                                    (
3764                                        Value::SmallIntArray(
3765                                            l.positions
3766                                                .iter()
3767                                                .map(|p| {
3768                                                    Some(i16::try_from(*p).unwrap_or(i16::MAX))
3769                                                })
3770                                                .collect(),
3771                                        ),
3772                                        Value::TextArray(
3773                                            l.positions
3774                                                .iter()
3775                                                .map(|_| Some(letter.into()))
3776                                                .collect(),
3777                                        ),
3778                                    )
3779                                };
3780                                Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3781                            })
3782                            .collect();
3783                        return_multi = Some((
3784                            alloc::vec![
3785                                DataType::Text,
3786                                DataType::SmallIntArray,
3787                                DataType::TextArray
3788                            ],
3789                            rows,
3790                        ));
3791                        (DataType::Text, alloc::vec::Vec::new())
3792                    }
3793                    // v7.39.11 — every remaining array-family value,
3794                    // through the one element menu, so a type does not
3795                    // have to be written out here a second time to be
3796                    // unnestable. `unnest(ARRAY[1,2]::smallint[])`
3797                    // raised "expects an array argument, got
3798                    // smallint[]" until this arm — the arms above name
3799                    // int / bigint / text / json and stop — and so did
3800                    // every catalog vector. Found while closing
3801                    // sentori's §4 against 7.39.10.
3802                    ref v if crate::eval::values::array_len(v).is_some() => {
3803                        let elems = crate::eval::values::array_elements(v).unwrap_or_default();
3804                        let dt = elems
3805                            .iter()
3806                            .find_map(spg_storage::Value::data_type)
3807                            .unwrap_or(DataType::Text);
3808                        let rows = elems
3809                            .into_iter()
3810                            .map(|e| Row::new(alloc::vec![e]))
3811                            .collect();
3812                        (dt, rows)
3813                    }
3814                    other => {
3815                        // v7.39 (round 622, S05a) — see table_access.rs:
3816                        // the same sentence, and it is a type mismatch.
3817                        return Err(EngineError::Eval(EvalError::TypeMismatch {
3818                            detail: alloc::format!(
3819                                "unnest() expects an array argument, got {}",
3820                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3821                            ),
3822                        }));
3823                    }
3824                };
3825                if let Some(m) = return_multi {
3826                    m
3827                } else {
3828                    (alloc::vec![elem_dtype], rows)
3829                }
3830            };
3831        let alias = primary
3832            .alias
3833            .clone()
3834            .unwrap_or_else(|| "unnest".to_string());
3835        // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3836        // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3837        // entries map positionally over the value columns. Without
3838        // the column list, a single column falls back to the table
3839        // alias (pre-v7.13.2 behaviour); multi-arg columns default
3840        // to PG's `unnest`.
3841        let n_vals = dtypes.len();
3842        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3843            .iter()
3844            .enumerate()
3845            .map(|(i, dt)| {
3846                let name = primary
3847                    .unnest_column_aliases
3848                    .get(i)
3849                    .cloned()
3850                    .unwrap_or_else(|| {
3851                        if let Some(names) = composite_names {
3852                            names
3853                                .get(i)
3854                                .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3855                        } else if n_vals == 1 {
3856                            alias.clone()
3857                        } else {
3858                            "unnest".to_string()
3859                        }
3860                    });
3861                ColumnSchema::new(name, *dt, true)
3862            })
3863            .collect();
3864        // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3865        // parser desugared a base-type-returning function here (see
3866        // TableRef::scalar_fn_item); the marker rides the column so it survives
3867        // every EvalContext an inner stage rebuilds.
3868        if primary.scalar_fn_item && schema_cols.len() == 1 {
3869            schema_cols[0].scalar_row_source = true;
3870        }
3871        // WITH ORDINALITY — trailing BIGINT counting rows from 1
3872        // in element order. The alias entry after the value
3873        // columns renames it (PG default: `ordinality`).
3874        let rows = if primary.with_ordinality {
3875            let ord_name = primary
3876                .unnest_column_aliases
3877                .get(n_vals)
3878                .cloned()
3879                .unwrap_or_else(|| "ordinality".to_string());
3880            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3881            rows.into_iter()
3882                .enumerate()
3883                .map(|(i, row)| {
3884                    let mut vals = row.values.clone();
3885                    vals.push(Value::BigInt(i as i64 + 1));
3886                    Row::new(vals)
3887                })
3888                .collect()
3889        } else {
3890            rows
3891        };
3892        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3893        // `EvalContext::new` drops it and every catalog-dependent cast
3894        // (regclass / enum / composite / domain) silently degrades.
3895        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3896        // Apply WHERE.
3897        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3898            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3899            for row in rows {
3900                cancel.check()?;
3901                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3902                if matches!(v, Value::Bool(true)) {
3903                    out.push(row);
3904                }
3905            }
3906            out
3907        } else {
3908            rows
3909        };
3910        // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3911        // unnest source. Same routing the relational scan path
3912        // already takes — without it `SELECT COUNT(*) FROM
3913        // unnest(ARRAY[…])` either errored at projection time or
3914        // returned the wrong shape.
3915        if aggregate::uses_aggregate(stmt) {
3916            // v7.29 — a per-query memo so correlated scalar
3917            // subqueries batch-evaluate once (group map) instead of
3918            // executing per group.
3919            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3920            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3921                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3922                    .map_err(|err| match err {
3923                        EngineError::Eval(ev) => ev,
3924                        other => eval::EvalError::TypeMismatch {
3925                            detail: alloc::format!("{other}"),
3926                        },
3927                    })
3928            };
3929            // v7.39 (round 656) — hand the rows over as they are rather than
3930            // collecting a second vector of `RowRef` wrappers. Note this is
3931            // a set-returning-function path, NOT the relational scan: the
3932            // measured O(rows) cost lived in `run_single_table_aggregate`,
3933            // and converting these four first was a miss that cost a full
3934            // round — every test stayed green and the number did not move.
3935            let agg = aggregate::run(
3936                stmt,
3937                crate::join::AggRows::Owned(&filtered),
3938                &schema_cols,
3939                Some(&alias),
3940                Some(&agg_correlated),
3941                self.parallel_runner.0.as_deref(),
3942                Some(self.active_catalog()),
3943                Some(self),
3944            )?;
3945            return self.finish_agg_result(agg, stmt, cancel);
3946        }
3947        // Projection.
3948        let projection = build_projection(
3949            &stmt.items,
3950            &schema_cols,
3951            &alias,
3952            self.speaks_mysql,
3953            Some(self.active_catalog()),
3954        )?;
3955        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3956            alloc::vec::Vec::with_capacity(filtered.len());
3957        // v7.19 P5 — Set-Returning-Function in projection
3958        // position (PG `SELECT unnest(arr) FROM t` shape). When a
3959        // SELECT item evaluates to a top-level unnest(arr) call,
3960        // expand it: for each input row, evaluate the array, emit
3961        // one output row per element, broadcasting non-SRF
3962        // projections from the same input row. Multi-SRF + LCM
3963        // padding stays a documented carve-out; mailrs uses
3964        // single-SRF for redirect_uris.
3965        // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3966        // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3967        let srf_idxs = self.srf_target_idxs(&projection);
3968        // v7.39 (round 621) — which input row each output row came from. An
3969        // SRF turns one input row into many, and the ORDER BY below used to
3970        // index the EXPANDED rows by the INPUT row's position: the result was
3971        // silently truncated to the input row count and left unsorted, so
3972        // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3973        // answered three of its six rows, in no order. Without the ORDER BY
3974        // the same query was already right.
3975        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3976        if !srf_idxs.is_empty() {
3977            let (rows, src) =
3978                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3979            projected_rows = rows;
3980            src_of_row = src;
3981        } else {
3982            // v7.24 (round-16 B) — select-list subqueries resolve
3983            // per row (correlated-aware; plain exprs take the fast
3984            // path inside).
3985            let mut proj_memo = memoize::MemoizeCache::default();
3986            for row in &filtered {
3987                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3988                for p in &projection {
3989                    vals.push(self.eval_expr_with_correlated(
3990                        &p.expr,
3991                        row,
3992                        &scan_ctx,
3993                        cancel,
3994                        Some(&mut proj_memo),
3995                    )?);
3996                }
3997                projected_rows.push(Row::new(vals));
3998            }
3999        }
4000        // ORDER BY / LIMIT — apply on the projected rows (cheap;
4001        // unnest result sets are small by design).
4002        let columns: alloc::vec::Vec<ColumnSchema> = projection
4003            .iter()
4004            // v7.39 (read01 round 54) — keep the column's enum identity through
4005            // the projection (it lives outside the DataType lattice), or a
4006            // derived table / UNION / windowed result forgets it and any outer
4007            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4008            .map(|p| p.to_column_schema())
4009            .collect();
4010        // Re-evaluate ORDER BY against the source schema (pre-projection
4011        // so col refs by name still resolve through `scan_ctx`).
4012        // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
4013        // column. Evaluated as an expression it is just the constant N: the same
4014        // key for every row, so the sort ran and changed nothing.
4015        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4016        if !order_by.is_empty() {
4017            // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
4018            // A key that names a select-list item reads it out of the expanded
4019            // row (PG sorts AFTER the expansion); one that names a source
4020            // column the query does not project is evaluated on the input row
4021            // it came from, which is what `srf_order_output_cols` decides.
4022            let out_cols = if srf_idxs.is_empty() {
4023                alloc::vec![None; order_by.len()]
4024            } else {
4025                srf_order_output_cols(&order_by, &projection)
4026            };
4027            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4028                .iter()
4029                .enumerate()
4030                .map(|(k, out)| -> Result<_, EngineError> {
4031                    let src = src_of_row.get(k).copied().unwrap_or(k);
4032                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4033                        .iter()
4034                        .zip(out_cols.iter())
4035                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
4036                        .collect();
4037                    Ok((k, keys?))
4038                })
4039                .collect::<Result<_, _>>()?;
4040            indexed.sort_by(|a, b| {
4041                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4042                    let o = &order_by[idx];
4043                    let cmp = order_by_value_cmp_in(
4044                        o.desc,
4045                        o.nulls_first,
4046                        ka,
4047                        kb,
4048                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4049                    );
4050                    if cmp != core::cmp::Ordering::Equal {
4051                        return cmp;
4052                    }
4053                }
4054                core::cmp::Ordering::Equal
4055            });
4056            projected_rows = indexed
4057                .into_iter()
4058                .map(|(i, _)| projected_rows[i].clone())
4059                .collect();
4060        }
4061        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4062        if stmt.distinct {
4063            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4064            // spec folds EVERY text position, so a column declared
4065            // `COLLATE utf8mb4_bin` had its values merged here exactly the
4066            // way 3b494b6e fixed on the main scan path. The projection is
4067            // already in scope at each of these sites, so the mask needs no
4068            // new plumbing -- it was simply never asked for.
4069            projected_rows = dedup_rows(
4070                projected_rows,
4071                FoldSpec::of_masks(
4072                    scan_ctx.mysql_dialect,
4073                    &fold_mask(&projection),
4074                    &pad_mask(&projection),
4075                ),
4076            );
4077        }
4078        // LIMIT / OFFSET — apply at the tail.
4079        if let Some(offset) = stmt.offset_literal() {
4080            let off = (offset as usize).min(projected_rows.len());
4081            projected_rows.drain(..off);
4082        }
4083        if let Some(limit) = stmt.limit_literal() {
4084            projected_rows.truncate(limit as usize);
4085        }
4086        Ok(QueryResult::Rows {
4087            columns,
4088            rows: projected_rows,
4089        })
4090    }
4091
4092    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
4093    /// step])` set-returning source. Mirrors `exec_select_unnest`'s
4094    /// shape: evaluate the arg list once against an empty row,
4095    /// materialise the row stream by stepping start → stop, then
4096    /// route through the standard WHERE / projection / ORDER BY /
4097    /// LIMIT pipeline. Two arg-type combos in v7.17:
4098    ///   * integer / integer [/ integer] — SmallInt, Int, BigInt
4099    ///     (widened to BigInt internally; step defaults to 1)
4100    ///   * timestamp / timestamp / interval — date-range
4101    ///     iteration (mailrs's daily-report pattern)
4102    fn exec_select_generate_series(
4103        &self,
4104        stmt: &SelectStatement,
4105        primary: &TableRef,
4106        cancel: CancelToken<'_>,
4107    ) -> Result<QueryResult, EngineError> {
4108        let args = primary
4109            .generate_series_args
4110            .as_ref()
4111            .expect("caller guards generate_series_args.is_some()");
4112        let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
4113        let alias = primary
4114            .alias
4115            .clone()
4116            .unwrap_or_else(|| "generate_series".to_string());
4117        // `AS t(n)` — the first column-alias entry renames the
4118        // series column (PG semantics); bare alias keeps the
4119        // pre-existing behaviour of naming the column after it.
4120        let col_name = primary
4121            .unnest_column_aliases
4122            .first()
4123            .cloned()
4124            .unwrap_or_else(|| alias.clone());
4125        let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
4126        let mut schema_cols = alloc::vec![col_schema.clone()];
4127        // WITH ORDINALITY — trailing BIGINT counting rows from 1;
4128        // the second column-alias entry renames it.
4129        let rows = if primary.with_ordinality {
4130            let ord_name = primary
4131                .unnest_column_aliases
4132                .get(1)
4133                .cloned()
4134                .unwrap_or_else(|| "ordinality".to_string());
4135            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4136            rows.into_iter()
4137                .enumerate()
4138                .map(|(i, row)| {
4139                    let mut vals = row.values.clone();
4140                    vals.push(Value::BigInt(i as i64 + 1));
4141                    Row::new(vals)
4142                })
4143                .collect()
4144        } else {
4145            rows
4146        };
4147        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4148        // `EvalContext::new` drops it and every catalog-dependent cast
4149        // (regclass / enum / composite / domain) silently degrades.
4150        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4151        // WHERE.
4152        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4153            let mut out = alloc::vec::Vec::with_capacity(rows.len());
4154            for row in rows {
4155                cancel.check()?;
4156                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4157                if matches!(v, Value::Bool(true)) {
4158                    out.push(row);
4159                }
4160            }
4161            out
4162        } else {
4163            rows
4164        };
4165        // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4166        // returning sources. When the SELECT projection contains
4167        // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4168        // …) we route the filtered row stream through the same
4169        // aggregate executor the relational scan path uses, so
4170        // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4171        // a single 100 row instead of erroring at projection
4172        // time. GROUP BY / HAVING / ORDER BY over the aggregate
4173        // output all ride through `aggregate::run`.
4174        if aggregate::uses_aggregate(stmt) {
4175            // v7.29 — a per-query memo so correlated scalar
4176            // subqueries batch-evaluate once (group map) instead of
4177            // executing per group.
4178            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4179            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4180                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4181                    .map_err(|err| match err {
4182                        EngineError::Eval(ev) => ev,
4183                        other => eval::EvalError::TypeMismatch {
4184                            detail: alloc::format!("{other}"),
4185                        },
4186                    })
4187            };
4188            // v7.39 (round 656) — hand the rows over as they are rather than
4189            // collecting a second vector of `RowRef` wrappers. Note this is
4190            // a set-returning-function path, NOT the relational scan: the
4191            // measured O(rows) cost lived in `run_single_table_aggregate`,
4192            // and converting these four first was a miss that cost a full
4193            // round — every test stayed green and the number did not move.
4194            let agg = aggregate::run(
4195                stmt,
4196                crate::join::AggRows::Owned(&filtered),
4197                &schema_cols,
4198                Some(&alias),
4199                Some(&agg_correlated),
4200                self.parallel_runner.0.as_deref(),
4201                Some(self.active_catalog()),
4202                Some(self),
4203            )?;
4204            return self.finish_agg_result(agg, stmt, cancel);
4205        }
4206        // Projection.
4207        let projection = build_projection(
4208            &stmt.items,
4209            &schema_cols,
4210            &alias,
4211            self.speaks_mysql,
4212            Some(self.active_catalog()),
4213        )?;
4214        // v7.39 (round 621) — and here, for the same reason.
4215        let srf_idxs = self.srf_target_idxs(&projection);
4216        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4217        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4218            alloc::vec::Vec::with_capacity(filtered.len());
4219        let mut proj_memo = memoize::MemoizeCache::default();
4220        if !srf_idxs.is_empty() {
4221            let (rows, src) =
4222                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4223            projected_rows = rows;
4224            src_of_row = src;
4225        } else {
4226            for row in &filtered {
4227                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4228                for p in &projection {
4229                    // v7.24 (round-16 B) — correlated-aware.
4230                    vals.push(self.eval_expr_with_correlated(
4231                        &p.expr,
4232                        row,
4233                        &scan_ctx,
4234                        cancel,
4235                        Some(&mut proj_memo),
4236                    )?);
4237                }
4238                projected_rows.push(Row::new(vals));
4239            }
4240        }
4241        let columns: alloc::vec::Vec<ColumnSchema> = projection
4242            .iter()
4243            // v7.39 (read01 round 54) — keep the column's enum identity through
4244            // the projection (it lives outside the DataType lattice), or a
4245            // derived table / UNION / windowed result forgets it and any outer
4246            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4247            .map(|p| p.to_column_schema())
4248            .collect();
4249        // ORDER BY against the source schema.
4250        // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4251        // more of them than there were inputs), and a positional key means the
4252        // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4253        // and what the other two synthetic-source tails already did.
4254        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4255        if !order_by.is_empty() {
4256            let out_cols = if srf_idxs.is_empty() {
4257                alloc::vec![None; order_by.len()]
4258            } else {
4259                srf_order_output_cols(&order_by, &projection)
4260            };
4261            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4262                .iter()
4263                .enumerate()
4264                .map(|(k, out)| -> Result<_, EngineError> {
4265                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4266                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4267                        .iter()
4268                        .zip(out_cols.iter())
4269                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4270                        .collect();
4271                    Ok((k, keys?))
4272                })
4273                .collect::<Result<_, _>>()?;
4274            indexed.sort_by(|a, b| {
4275                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4276                    let o = &stmt.order_by[idx];
4277                    let cmp = order_by_value_cmp_in(
4278                        o.desc,
4279                        o.nulls_first,
4280                        ka,
4281                        kb,
4282                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4283                    );
4284                    if cmp != core::cmp::Ordering::Equal {
4285                        return cmp;
4286                    }
4287                }
4288                core::cmp::Ordering::Equal
4289            });
4290            projected_rows = indexed
4291                .into_iter()
4292                .map(|(i, _)| projected_rows[i].clone())
4293                .collect();
4294        }
4295        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4296        if stmt.distinct {
4297            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4298            // spec folds EVERY text position, so a column declared
4299            // `COLLATE utf8mb4_bin` had its values merged here exactly the
4300            // way 3b494b6e fixed on the main scan path. The projection is
4301            // already in scope at each of these sites, so the mask needs no
4302            // new plumbing -- it was simply never asked for.
4303            projected_rows = dedup_rows(
4304                projected_rows,
4305                FoldSpec::of_masks(
4306                    scan_ctx.mysql_dialect,
4307                    &fold_mask(&projection),
4308                    &pad_mask(&projection),
4309                ),
4310            );
4311        }
4312        if let Some(offset) = stmt.offset_literal() {
4313            let off = (offset as usize).min(projected_rows.len());
4314            projected_rows.drain(..off);
4315        }
4316        if let Some(limit) = stmt.limit_literal() {
4317            projected_rows.truncate(limit as usize);
4318        }
4319        Ok(QueryResult::Rows {
4320            columns,
4321            rows: projected_rows,
4322        })
4323    }
4324
4325    /// The FROM shapes that are not an ordinary table scan — joins, the
4326    /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4327    ///
4328    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4329    /// reason round 848 established in the parser: a debug build gives
4330    /// EVERY branch's locals a slot in the frame, whichever branch runs.
4331    /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4332    /// stacks several of them; a plain scan reaches none of these
4333    /// branches. Moving them out took the frame to 52,336.
4334    ///
4335    /// `Ok(None)` means "not one of these shapes, carry on".
4336    #[inline(never)]
4337    fn try_from_shape_paths(
4338        &self,
4339        stmt: &SelectStatement,
4340        from: &spg_sql::ast::FromClause,
4341        cancel: CancelToken<'_>,
4342    ) -> Result<Option<QueryResult>, EngineError> {
4343        if !from.joins.is_empty() {
4344            // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4345            // elimination: when a LEFT JOIN's right side is referenced
4346            // ONLY in the ON equality and the right-side join key is
4347            // UNIQUE/PK, the join preserves outer cardinality exactly
4348            // and contributes no values used downstream. Drop the
4349            // entire join. PG does this on the
4350            // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4351            // — A's row count is what survives, B never has to be
4352            // touched.
4353            if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4354                return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4355            }
4356            // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4357            // the v7.32 joinfold rewrite that turns inner JOINs into a
4358            // single-table scan when the catalogue can prove key-only
4359            // dependency. Tests use this to assert "without joinfold,
4360            // the join still executes correctly" (joinfold is a
4361            // semantically-equivalent rewrite, not a correctness fix).
4362            if !self.env_cfg().disable_joinfold {
4363                if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4364                    return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4365                }
4366            }
4367            return self.exec_joined_select(stmt, from, cancel).map(Some);
4368        }
4369        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4370        // single-column table at SELECT entry by evaluating the
4371        // expression once against the empty row (UNNEST is
4372        // uncorrelated in v7.11; correlated / LATERAL unnest is a
4373        // v7.12 carve-out). Build a virtual `Table` in a heap-only
4374        // catalog, then route to the regular scan path.
4375        if from.primary.unnest_expr.is_some() {
4376            return self
4377                .exec_select_unnest(stmt, &from.primary, cancel)
4378                .map(Some);
4379        }
4380        // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4381        // returning function. Same dispatch shape as unnest but
4382        // emits a two-column (key TEXT, value TEXT) row stream.
4383        if from.primary.jsonb_each_text_arg.is_some() {
4384            return self
4385                .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4386                .map(Some);
4387        }
4388        // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4389        // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4390        // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4391        // array form. Each function runs; the results zip in LOCKSTEP with the
4392        // shorter padded to NULL — the SAME rule the target-list SRFs follow
4393        // (round 67), which is why `srf_values` is what evaluates each entry.
4394        if from.primary.rows_from.is_some() {
4395            let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4396            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4397                if let Some(col) = schema_cols.get_mut(i) {
4398                    col.name = new_name.clone();
4399                }
4400            }
4401            let alias = from
4402                .primary
4403                .alias
4404                .clone()
4405                .unwrap_or_else(|| from.primary.name.clone());
4406            return self
4407                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4408                .map(Some);
4409        }
4410        // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4411        // COLUMNS (...))`. Materialise the row stream + schema by
4412        // walking the row path, then run the regular pipeline over it.
4413        if let Some(jt) = &from.primary.json_table {
4414            let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4415            let alias = from
4416                .primary
4417                .alias
4418                .clone()
4419                .unwrap_or_else(|| from.primary.name.clone());
4420            return self
4421                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4422                .map(Some);
4423        }
4424        if from.primary.table_fn_call.is_some() {
4425            let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4426            // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4427            // (from 1, in output order) AFTER the function's own columns. The
4428            // alias list names it like any other, which is why it is appended
4429            // BEFORE the renaming pass below.
4430            let rows = if from.primary.with_ordinality {
4431                schema_cols.push(ColumnSchema::new(
4432                    "ordinality".to_string(),
4433                    DataType::BigInt,
4434                    false,
4435                ));
4436                rows.into_iter()
4437                    .enumerate()
4438                    .map(|(i, r)| {
4439                        let mut vals = r.values;
4440                        vals.push(Value::BigInt(i as i64 + 1));
4441                        Row::new(vals)
4442                    })
4443                    .collect()
4444            } else {
4445                rows
4446            };
4447            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4448                if let Some(col) = schema_cols.get_mut(i) {
4449                    col.name = new_name.clone();
4450                }
4451            }
4452            let alias = from
4453                .primary
4454                .alias
4455                .clone()
4456                .unwrap_or_else(|| from.primary.name.clone());
4457            return self
4458                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4459                .map(Some);
4460        }
4461        // v7.37.17 (17.6 siblings) — plain derived table in primary
4462        // position: `FROM ( SELECT … ) alias` (no joins). The inner
4463        // SELECT materialises once (it is uncorrelated by
4464        // construction), then the outer projection / WHERE /
4465        // aggregate / ORDER BY pipeline runs over the synthetic
4466        // table. Joined derived tables keep riding the LATERAL
4467        // machinery in join.rs.
4468        if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4469            // v7.39 (round 727) — flatten first. A simple derived table
4470            // (bare-column projection over one stored table, nothing that
4471            // changes cardinality or order) used to force the inner
4472            // SELECT through the SERIAL row-at-a-time projection pipeline
4473            // just to materialise a synthetic table the outer query then
4474            // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4475            // measured 18.6 ms against PG's 5 — and bare count over the
4476            // same filter WITHOUT the wrapper is 2 ms here, because it
4477            // rides the fused parallel lane. Rewriting to the unwrapped
4478            // form is PG's subquery pull-up; the whole tree gets the
4479            // fast lanes back.
4480            if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4481                return self.exec_select_cancel(&flat, cancel).map(Some);
4482            }
4483            // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4484            // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4485            // ORDER BY never changes the row count, and OFFSET drops
4486            // exactly k. The materialising path sorted 500k rows to
4487            // count 10k (57 ms); PG runs its parallel sort anyway
4488            // (28 ms). The rewrite skips the sort entirely on both
4489            // counts — a plan PG itself does not have.
4490            if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4491                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4492            }
4493            // v7.39 (round 743) — `count(*) OVER a derived whose only
4494            // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4495            // a constant-length array unnests to exactly k rows per
4496            // input row, NULL elements included. PG expands the set to
4497            // count it (6.6 ms on the panel cell); the identity doesn't.
4498            if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4499                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4500            }
4501            return self
4502                .exec_select_derived(stmt, &from.primary, cancel)
4503                .map(Some);
4504        }
4505        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4506        // [, step])` set-returning source. Dispatch mirrors UNNEST:
4507        // materialise the row stream from a single eval pass, then
4508        // run the regular projection / WHERE / ORDER BY / LIMIT
4509        // pipeline over the synthetic single-column table.
4510        if from.primary.generate_series_args.is_some() {
4511            return self
4512                .exec_select_generate_series(stmt, &from.primary, cancel)
4513                .map(Some);
4514        }
4515        Ok(None)
4516    }
4517
4518    /// Pick an index seek for this WHERE, if any of the four apply:
4519    /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4520    ///
4521    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4522    /// frame reason on `try_from_shape_paths`: in a debug build a
4523    /// closure's locals belong to the enclosing frame, and this one is
4524    /// four seek attempts wide on a function that nests.
4525    #[inline(never)]
4526    fn pick_indexed_rows<'r>(
4527        &'r self,
4528        stmt: &SelectStatement,
4529        table: &'r spg_storage::Table,
4530        schema_cols: &[spg_storage::ColumnSchema],
4531        alias: &str,
4532        ctx: &crate::eval::EvalContext<'_>,
4533        seek_snapshot: &crate::Snapshot,
4534    ) -> Option<crate::index_access::Seeked<'r>> {
4535        stmt.where_.as_ref().and_then(|w| {
4536            // BTree / col=literal seek first — covers the v7.11.3 multi-
4537            // column AND case and the leading-column equality lookup.
4538            try_index_seek(
4539                w,
4540                schema_cols,
4541                self.active_catalog(),
4542                table,
4543                alias,
4544                seek_snapshot,
4545                ctx.mysql_dialect,
4546            )
4547            .or_else(|| {
4548                // v7.12.3 — GIN-accelerated `WHERE col @@
4549                // tsquery` when the column has a `USING gin`
4550                // index. Returns an over-approximate candidate
4551                // set; the WHERE re-eval loop below verifies
4552                // the full `@@` predicate per row.
4553                try_gin_seek(
4554                    w,
4555                    schema_cols,
4556                    self.active_catalog(),
4557                    table,
4558                    alias,
4559                    ctx,
4560                    seek_snapshot,
4561                )
4562                .map(crate::index_access::Seeked::over_approximate)
4563            })
4564            .or_else(|| {
4565                // v7.15.0 — trigram-GIN-accelerated
4566                // `WHERE col LIKE / ILIKE '<pat>'` when the
4567                // column has a `gin_trgm_ops` GIN index.
4568                // Over-approximate candidate set; the WHERE
4569                // re-eval verifies the LIKE per row.
4570                try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4571                    .map(crate::index_access::Seeked::over_approximate)
4572            })
4573            .or_else(|| {
4574                // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4575                // accelerated `WHERE col @> <jsonb_literal>`
4576                // when the column has a `USING gin` index. The
4577                // posting-list intersection returns an over-
4578                // approximate candidate set; the WHERE re-eval
4579                // verifies the full `@>` predicate per row.
4580                try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4581                    .map(crate::index_access::Seeked::over_approximate)
4582            })
4583        })
4584    }
4585
4586    /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4587    /// the two `count(*)` short-circuits. Out-of-line for the frame
4588    /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4589    /// of them, and in a debug build their locals sit in the frame
4590    /// regardless.
4591    #[inline(never)]
4592    fn try_seek_fast_paths(
4593        &self,
4594        stmt: &SelectStatement,
4595        table: &spg_storage::Table,
4596        schema_cols: &[spg_storage::ColumnSchema],
4597        alias: &str,
4598        seek_snapshot: &crate::Snapshot,
4599        cancel: CancelToken<'_>,
4600    ) -> Result<Option<QueryResult>, EngineError> {
4601        if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4602            // NSW kNN dispatches against the hot-tier vector index only
4603            // (vector cells aren't promoted to cold segments), so wrap
4604            // the returned row indices as `Cow::Borrowed` for the
4605            // unified `materialise_in_order` shape.
4606            let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4607                .into_iter()
4608                .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4609                .collect();
4610            return materialise_in_order(stmt, schema_cols, alias, &ordered, self.speaks_mysql)
4611                .map(Some);
4612        }
4613
4614        // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4615        // the scan via the BTree iterator in the requested direction
4616        // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4617        // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4618        // the load-bearing consumer; this skips the materialise-every-
4619        // row + partial-sort tail entirely. Walker output is already
4620        // in ORDER BY order so `materialise_in_order` (no extra sort)
4621        // is the natural sink.
4622        if let Some(walked) = try_pk_walk_top_n(
4623            stmt,
4624            self.active_catalog(),
4625            table,
4626            schema_cols,
4627            alias,
4628            self,
4629            cancel,
4630            self.speaks_mysql,
4631        ) {
4632            return materialise_in_order(stmt, schema_cols, alias, &walked, self.speaks_mysql)
4633                .map(Some);
4634        }
4635
4636        // Index seek: if WHERE is `col = literal` (or commuted) and the
4637        // referenced column has an index, dispatch each locator through
4638        // the catalog (hot tier → borrow, cold tier → page-read +
4639        // decode) and iterate just those rows. Otherwise fall back to a
4640        // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4641        // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4642        // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4643        // we don't pay the row materialisation cost twice. Returns
4644        // a bare `Rows{count}` if the shape matches.
4645        if aggregate::uses_aggregate(stmt)
4646            && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4647        {
4648            return Ok(Some(out));
4649        }
4650        // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4651        // locators directly, skipping row materialisation + WHERE re-eval.
4652        if aggregate::uses_aggregate(stmt)
4653            && let Some(out) = self.try_count_star_indexed_range_fast(
4654                stmt,
4655                table,
4656                schema_cols,
4657                alias,
4658                seek_snapshot,
4659            )
4660        {
4661            return Ok(Some(out));
4662        }
4663        Ok(None)
4664    }
4665
4666    /// The two rewrites that must happen before the FROM clause is even
4667    /// looked at: a meta-view reference needs the catalog views
4668    /// materialised, and a windowed projection belongs to the window
4669    /// executor. Out-of-line for the frame reason on
4670    /// `try_from_shape_paths`.
4671    #[inline(never)]
4672    fn try_pre_from_paths(
4673        &self,
4674        stmt: &SelectStatement,
4675        cancel: CancelToken<'_>,
4676    ) -> Result<Option<QueryResult>, EngineError> {
4677        if !self.meta_views_materialised && select_references_meta_view(stmt) {
4678            return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4679        }
4680        // v4.12: window-function path. When the projection contains
4681        // any `name(args) OVER (...)` we route to the dedicated
4682        // executor — partition + sort + per-row window value before
4683        // the regular projection.
4684        if select_has_window(stmt) {
4685            // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4686            // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4687            // needs the aggregation done first, then windows over the grouped
4688            // rows. Rewrite to an aggregate derived subquery + outer window query
4689            // (which the window-over-derived path, D.13, executes). Only fires on
4690            // the currently-erroring agg+window+GROUP BY shape, so it can't
4691            // regress working window-only or aggregate-only queries.
4692            if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4693                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4694            }
4695            return self.exec_select_with_window(stmt, cancel).map(Some);
4696        }
4697        Ok(None)
4698    }
4699
4700    /// A projection naming `ctid` or another system column: the schema
4701    /// has to be widened with them before the scan. Out-of-line for the
4702    /// frame reason on `try_from_shape_paths`.
4703    #[inline(never)]
4704    fn try_ctid_projection(
4705        &self,
4706        stmt: &SelectStatement,
4707        primary: &spg_sql::ast::TableRef,
4708        table: &spg_storage::Table,
4709        schema_cols: &[spg_storage::ColumnSchema],
4710        alias: &str,
4711        cancel: CancelToken<'_>,
4712    ) -> Result<Option<QueryResult>, EngineError> {
4713        if references_ctid(stmt) {
4714            let snapshot = self.current_snapshot();
4715            let mut ext_cols = schema_cols.to_vec();
4716            for name in SYSTEM_COLUMNS {
4717                ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4718            }
4719            let table_oid =
4720                crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4721                    .unwrap_or(0);
4722            let headers = table.headers();
4723            let rows: Vec<Row<'static>> = table
4724                .scan_visible(&snapshot)
4725                .map(|(i, r)| {
4726                    let mut vals = r.values.clone();
4727                    // One block, offsets from 1, as PG numbers them.
4728                    vals.push(Value::Tid(0, i as u32 + 1));
4729                    let h = headers.get(i);
4730                    vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4731                    vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4732                    // SPG keeps no per-statement command ids; PG shows 0 for
4733                    // every row a reader can see, which is every row here.
4734                    vals.push(Value::Cid(0));
4735                    vals.push(Value::Cid(0));
4736                    vals.push(Value::BigInt(table_oid));
4737                    Row::new(vals)
4738                })
4739                .collect();
4740            return self
4741                .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4742                .map(Some);
4743        }
4744        Ok(None)
4745    }
4746
4747    /// A sequence read as a one-row relation (`SELECT last_value FROM
4748    /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4749    /// the frame reason on `try_from_shape_paths`.
4750    #[inline(never)]
4751    fn try_sequence_relation(
4752        &self,
4753        stmt: &SelectStatement,
4754        primary: &spg_sql::ast::TableRef,
4755        cancel: CancelToken<'_>,
4756    ) -> Result<Option<QueryResult>, EngineError> {
4757        if self.active_catalog().get(&primary.name).is_none()
4758            && let Some(seq) = self.active_catalog().sequence(&primary.name)
4759        {
4760            let rows = alloc::vec![Row::new(alloc::vec![
4761                Value::BigInt(seq.last_value),
4762                Value::BigInt(0),
4763                Value::Bool(seq.is_called),
4764            ])];
4765            let schema_cols = alloc::vec![
4766                ColumnSchema::new("last_value", DataType::BigInt, false),
4767                ColumnSchema::new("log_cnt", DataType::BigInt, false),
4768                ColumnSchema::new("is_called", DataType::Bool, false),
4769            ];
4770            let alias = primary
4771                .alias
4772                .clone()
4773                .unwrap_or_else(|| primary.name.clone());
4774            return self
4775                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4776                .map(Some);
4777        }
4778        Ok(None)
4779    }
4780
4781    pub(crate) fn exec_bare_select_cancel(
4782        &self,
4783        stmt: &SelectStatement,
4784        cancel: CancelToken<'_>,
4785    ) -> Result<QueryResult, EngineError> {
4786        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4787        // is meaningless without an ORDER BY; PG raises a hard
4788        // error and SPG mirrors the surface so the same DDL/app
4789        // path behaves identically on cutover.
4790        check_with_ties_requires_order_by(stmt)?;
4791        // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4792        // PG rejects window calls there outright. Checked here rather than
4793        // on the window path: `HAVING row_number() OVER () = 1` has no
4794        // window in its projection at all.
4795        crate::window::reject_window_in_row_clauses(stmt)?;
4796        // v7.39 (round 232) — the ORDER BY legality rules (positional
4797        // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4798        // check: before anything scans.
4799        crate::orderby::check_order_by_legality(stmt)?;
4800        // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4801        // equivalent statement the regular executor handles (merged join
4802        // columns collapse to a single unqualified output column; NATURAL
4803        // gets its common-column ON synthesised). The rewrite clears the
4804        // flags, so this re-entrant call is a no-op on the second pass.
4805        if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4806            return self.exec_bare_select_cancel(&rewritten, cancel);
4807        }
4808        // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4809        // exactly the group keys, IS a DISTINCT and was paying for the
4810        // aggregate executor to find that out. Same placement and shape
4811        // as the desugar above; the rewrite clears `group_by`, so the
4812        // re-entry is a no-op on the second pass. See `baregroup` for
4813        // what the gate rules out.
4814        if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4815            return self.exec_bare_select_cancel(&rewritten, cancel);
4816        }
4817        // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4818        // operand in a security-barrier subquery, then re-enter (the wrapped
4819        // operands are no longer bare RLS tables, so this is a no-op on the
4820        // second pass).
4821        if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4822            return self.exec_bare_select_cancel(&rewritten, cancel);
4823        }
4824        // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4825        // AND the RLS USING predicate into a single-table SELECT's WHERE.
4826        // Superuser sessions and non-RLS tables get `None` (no clone, no
4827        // change). Applied inline (shadowing `stmt`) rather than via re-entry
4828        // so it can't re-inject on a recursive pass.
4829        let rls_stmt;
4830        let stmt = match self.rls_select_predicate(stmt)? {
4831            Some(pred) => {
4832                let mut s = stmt.clone();
4833                s.where_ = Some(match s.where_.take() {
4834                    Some(existing) => spg_sql::ast::Expr::Binary {
4835                        lhs: alloc::boxed::Box::new(existing),
4836                        op: spg_sql::ast::BinOp::And,
4837                        rhs: alloc::boxed::Box::new(pred),
4838                    },
4839                    None => pred,
4840                });
4841                rls_stmt = s;
4842                &rls_stmt
4843            }
4844            None => stmt,
4845        };
4846        // v7.16.2 — same meta-view dispatch as
4847        // `exec_select_cancel`, applied here too because
4848        // `subquery_replacement` enters this function directly
4849        // for Exists / ScalarSubquery / InSubquery resolution
4850        // (bypassing the top-level entry to avoid double
4851        // subquery walking). Without this dispatch the subquery
4852        // hits `__spg_info_columns` and reports TableNotFound.
4853        if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4854            return Ok(done);
4855        }
4856        // Constant SELECT (no FROM) — evaluate each item once against an
4857        // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4858        // `SELECT '7'::INT`. Column references will surface as
4859        // ColumnNotFound on eval since the schema is empty.
4860        let Some(from) = &stmt.from else {
4861            return self.exec_constant_select(stmt);
4862        };
4863        // Multi-table FROM (one or more joined peers) goes through the
4864        // nested-loop join executor. Single-table FROM stays on the
4865        // existing scan + index-seek path.
4866        if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4867            return Ok(done);
4868        }
4869        // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4870        // tested — eight ORDER BY shapes byte-identical spilled against
4871        // in-memory, with 103 runs opened to prove the spill ran — and it
4872        // loses on wall clock, which is a hard stop whatever the memory
4873        // buys. Measured round 865, same psql client both sides, same
4874        // machine, row counts verified, and both sides confirmed to be
4875        // doing an external merge rather than an indexed walk:
4876        //
4877        //   PG18        178.7 - 187.0 ms   Sort Method: external merge, 85 MB
4878        //   SPG spilled 269.7 - 299.6 ms   33 spill files at peak
4879        //
4880        // Non-overlapping, about 1.55x. Re-enable by restoring the call
4881        // below once that closes; nothing else has to change, which is
4882        // the point of it being a separate path.
4883        //
4884        //   if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4885        //       return Ok(done);
4886        //   }
4887        //
4888        // v7.37 (round 882) — this walk stays unhooked, but its streaming
4889        // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4890        // bail in `try_exec_joined_streaming`. Collecting the answer was
4891        // most of what this one cost: handing rows over as the merge
4892        // produces them holds peak to the budget plus one row, and the
4893        // wall clock lands inside PG18's range rather than 1.55x outside
4894        // it. Numbers in `extsort.rs`'s header.
4895        let primary = &from.primary;
4896        // v7.39 (round 244) — a sequence is selectable as a one-row relation
4897        // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4898        // read it). Synthesize PG's three columns.
4899        if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4900            return Ok(done);
4901        }
4902        let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4903            StorageError::TableNotFound {
4904                name: primary.name.clone(),
4905            }
4906        })?;
4907        let schema_cols = &table.schema().columns;
4908        // The qualifier accepted on column refs is the alias (if any) else the
4909        // bare table name.
4910        let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4911        // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4912        // system columns at all: `SELECT ctid FROM t` answered "column
4913        // \"ctid\" does not exist", which takes out the dedup idiom every
4914        // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4915        // GROUP BY key)`.
4916        //
4917        // The value comes from the row's position, which the scan already
4918        // yields; the column is appended to the schema and the rows only
4919        // when the statement asks for it, so nothing else pays for it. That
4920        // also routes the query down the general path, past the index fast
4921        // paths below — they hand back rows without positions, and a ctid
4922        // that was sometimes right would be worse than none.
4923        if let Some(done) =
4924            self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4925        {
4926            return Ok(done);
4927        }
4928        let ctx = self.ev_ctx(schema_cols, Some(alias));
4929
4930        // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4931        // WHERE and an NSW index on `col` skips the full scan. The
4932        // walk returns rows already in ascending-distance order, so
4933        // ORDER BY / LIMIT are honoured implicitly.
4934        // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4935        // and thread it into every index-seek fast path below. No-op
4936        // today (every hot header is committed-alive).
4937        let seek_snapshot = self.current_snapshot();
4938        if let Some(done) =
4939            self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4940        {
4941            return Ok(done);
4942        }
4943        // full scan over the hot tier (cold-tier rows are only reached
4944        // via index seek in v5.1 — full table scans against cold-tier
4945        // data ship in v5.2 with the freezer's per-segment scan API).
4946        let indexed_rows =
4947            self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4948
4949        // Aggregate path: filter rows first, then hand off to the
4950        // aggregate executor which does its own projection + ORDER BY.
4951        if aggregate::uses_aggregate(stmt) {
4952            return self.run_single_table_aggregate(
4953                stmt,
4954                table,
4955                schema_cols,
4956                alias,
4957                indexed_rows,
4958                cancel,
4959            );
4960        }
4961        self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4962    }
4963
4964    /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4965    /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4966    /// uncorrelated FROM-primary case is the simpler shape, used by
4967    /// e2e pins. Materialises the (key, value) pair stream into a
4968    /// synthetic two-column TEXT table, then routes through the
4969    /// regular projection / WHERE / ORDER BY pipeline.
4970    /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4971    /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4972    /// item into (rows, schema). `outer_doc` is `Some` only when this
4973    /// is a NESTED level being expanded against a parent row item's
4974    /// already-parsed sub-document; the top-level call parses the doc
4975    /// expr itself. Row/column paths reuse the existing jsonpath
4976    /// evaluator (`json::json_table_path`); coercion reuses
4977    /// `coerce_value` on the JSON scalar text, so a json string
4978    /// coerces to DATE by its content, matching PG.
4979    #[allow(clippy::type_complexity)]
4980    pub(crate) fn json_table_rows(
4981        &self,
4982        jt: &spg_sql::ast::JsonTable,
4983        outer_doc: Option<&crate::json::JsonValue>,
4984    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4985        // Column schema is static (independent of data): flatten the
4986        // COLUMNS tree in declaration order (NESTED contributes its
4987        // children inline, the PG output shape).
4988        let schema = json_table_schema(&jt.columns);
4989
4990        // PASSING variables → a single JsonValue object the jsonpath
4991        // engine reads `$name` from.
4992        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4993        let ctx = EvalContext::new(&empty_schema, None);
4994        let dummy = Row::new(alloc::vec::Vec::new());
4995        let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4996            None
4997        } else {
4998            let mut entries = alloc::vec::Vec::new();
4999            for (name, e) in &jt.passing {
5000                let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
5001                entries.push((name.clone(), value_to_json_value(&v)));
5002            }
5003            Some(crate::json::JsonValue::Object(entries))
5004        };
5005
5006        // The document root: a NESTED level gets it from the parent;
5007        // the top level parses its doc expr.
5008        let root_owned;
5009        let root: &crate::json::JsonValue = match outer_doc {
5010            Some(d) => d,
5011            None => {
5012                let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
5013                let src = match &doc_val {
5014                    Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
5015                    Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
5016                    other => {
5017                        return Err(EngineError::Unsupported(alloc::format!(
5018                            "JSON_TABLE document must be json/text, got {}",
5019                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
5020                        )));
5021                    }
5022                };
5023                root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
5024                &root_owned
5025            }
5026        };
5027
5028        let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
5029            .map_err(EngineError::Eval)?;
5030        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5031        for (idx, item) in items.iter().enumerate() {
5032            self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
5033        }
5034        Ok((rows, schema))
5035    }
5036
5037    /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
5038    /// Regular columns produce one value each; a NESTED column expands
5039    /// as an outer join (each nested match → one row sharing the
5040    /// parent cells; no nested match → one row with the nested cells
5041    /// NULL). Sibling NESTED at one level cross by concatenation of
5042    /// their independent expansions (PG's UNION-of-outer shape).
5043    fn json_table_emit_item(
5044        &self,
5045        jt: &spg_sql::ast::JsonTable,
5046        item: &crate::json::JsonValue,
5047        ordinality: usize,
5048        vars: Option<&crate::json::JsonValue>,
5049        out: &mut alloc::vec::Vec<Row<'static>>,
5050    ) -> Result<(), EngineError> {
5051        use spg_sql::ast::JsonTableColumn as C;
5052        // Parent cells (regular + ordinality), left-to-right; NESTED
5053        // columns contribute a run of child cells appended after.
5054        let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5055        let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
5056            alloc::vec::Vec::new();
5057        let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5058        for col in &jt.columns {
5059            match col {
5060                C::Ordinality { .. } => {
5061                    parent_cells.push(Value::BigInt(ordinality as i64 + 1));
5062                }
5063                C::Regular { .. } => {
5064                    parent_cells.push(self.json_table_column_value(col, item, vars)?);
5065                }
5066                C::Nested { path, columns } => {
5067                    // Recurse: a nested JSON_TABLE over `item` filtered
5068                    // by `path`, with the same PASSING vars.
5069                    let sub = spg_sql::ast::JsonTable {
5070                        doc: jt.doc.clone(), // unused (outer_doc provided)
5071                        row_path: path.clone(),
5072                        columns: columns.clone(),
5073                        passing: alloc::vec::Vec::new(),
5074                    };
5075                    let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
5076                    nested_widths.push(nschema.len());
5077                    nested_runs.push(nrows);
5078                }
5079            }
5080        }
5081        if nested_runs.is_empty() {
5082            out.push(Row::new(parent_cells));
5083            return Ok(());
5084        }
5085        // PG sibling-NESTED semantics: each sibling expands
5086        // INDEPENDENTLY and the results CONCATENATE — a row from
5087        // sibling s fills only s's cells, every other sibling's cells
5088        // NULL. An empty sibling contributes ZERO rows (not a NULL
5089        // row). Only when EVERY sibling is empty does the parent still
5090        // emit one all-NULL row (the outer-join guarantee that a parent
5091        // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
5092        // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
5093        let before = out.len();
5094        for (s_idx, run) in nested_runs.iter().enumerate() {
5095            for nrow in run {
5096                let mut cells = parent_cells.clone();
5097                for (o_idx, w) in nested_widths.iter().enumerate() {
5098                    if o_idx == s_idx {
5099                        cells.extend(nrow.values.iter().cloned());
5100                    } else {
5101                        for _ in 0..*w {
5102                            cells.push(Value::Null);
5103                        }
5104                    }
5105                }
5106                out.push(Row::new(cells));
5107            }
5108        }
5109        if out.len() == before {
5110            // Every sibling empty → one all-NULL nested row.
5111            let mut cells = parent_cells.clone();
5112            for w in &nested_widths {
5113                for _ in 0..*w {
5114                    cells.push(Value::Null);
5115                }
5116            }
5117            out.push(Row::new(cells));
5118        }
5119        Ok(())
5120    }
5121
5122    /// v7.39 (round 205) — evaluate one Regular column against a row
5123    /// item: EXISTS → bool; else path → at most one value, coerced to
5124    /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
5125    fn json_table_column_value(
5126        &self,
5127        col: &spg_sql::ast::JsonTableColumn,
5128        item: &crate::json::JsonValue,
5129        vars: Option<&crate::json::JsonValue>,
5130    ) -> Result<Value<'static>, EngineError> {
5131        use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
5132        let C::Regular {
5133            name,
5134            ty,
5135            path,
5136            exists,
5137            format_json,
5138            wrapper,
5139            on_empty,
5140            on_error,
5141        } = col
5142        else {
5143            unreachable!("caller guards Regular");
5144        };
5145        let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5146        if *exists {
5147            return Ok(Value::Bool(!matches.is_empty()));
5148        }
5149        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5150        let ctx = EvalContext::new(&empty_schema, None);
5151        let dummy = Row::new(alloc::vec::Vec::new());
5152        let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5153            match b {
5154                B::Null => Ok(Some(Value::Null)),
5155                B::Error => Ok(None),
5156                B::Default(e) => Ok(Some(
5157                    eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5158                )),
5159            }
5160        };
5161        // Empty match set → ON EMPTY.
5162        if matches.is_empty() {
5163            return match default_of(on_empty)? {
5164                Some(v) => coerce_json_table_default(v, *ty, name),
5165                None => Err(EngineError::Unsupported(alloc::format!(
5166                    "no SQL/JSON item found for JSON_TABLE column {name:?}"
5167                ))),
5168            };
5169        }
5170        let first = &matches[0];
5171        // FORMAT JSON: return the PG-canonical json representation.
5172        // WITH WRAPPER wraps the whole match SET in an array (even a
5173        // single scalar → `[5]`); without it, the single match's json.
5174        if *format_json {
5175            let text = if *wrapper {
5176                crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5177            } else {
5178                first.canonical_json_text()
5179            };
5180            return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5181        }
5182        if first.is_json_null() {
5183            return Ok(Value::Null);
5184        }
5185        // Coerce the scalar text to the declared type; on failure → ON
5186        // ERROR (default NULL, DEFAULT expr, or raise).
5187        let dt = crate::conversions::column_type_to_data_type(*ty);
5188        let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5189        match crate::conversions::coerce_value(scalar, dt, name, 0) {
5190            Ok(v) => Ok(v),
5191            Err(e) => match default_of(on_error)? {
5192                Some(v) => coerce_json_table_default(v, *ty, name),
5193                None => Err(e),
5194            },
5195        }
5196    }
5197
5198    /// table function into (rows, default schema). Dispatch by name.
5199    pub(crate) fn table_fn_rows(
5200        &self,
5201        primary: &TableRef,
5202    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5203        let (fn_name, args) = primary
5204            .table_fn_call
5205            .as_deref()
5206            .expect("caller guards table_fn_call.is_some()");
5207        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5208        let ctx = EvalContext::new(&empty_schema, None);
5209        let dummy_row = Row::new(alloc::vec::Vec::new());
5210        let arg0: Option<Value<'static>> = match args.first() {
5211            Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5212            None => None,
5213        };
5214        match fn_name.as_str() {
5215            // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5216            // `…_recordset` (+ json_ variants). The row shape is the BASE
5217            // argument's declared type — a table's or a composite type's
5218            // column list — which only the catalog knows, so the parser hands
5219            // the raw arguments here rather than desugaring blind.
5220            "jsonb_populate_record"
5221            | "json_populate_record"
5222            | "jsonb_populate_recordset"
5223            | "json_populate_recordset" => {
5224                let type_name = match args.first() {
5225                    Some(Expr::Cast {
5226                        target: spg_sql::ast::CastTarget::Named(n),
5227                        ..
5228                    }) => n.clone(),
5229                    _ => {
5230                        return Err(EngineError::Unsupported(alloc::format!(
5231                            "{fn_name}(): first argument must name a row type, \
5232                             e.g. NULL::mytable"
5233                        )));
5234                    }
5235                };
5236                let cat = self.active_catalog();
5237                let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5238                    t.schema().columns.clone()
5239                } else if let Some(c) = cat.composite_types().get(&type_name) {
5240                    c.fields
5241                        .iter()
5242                        .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5243                        .collect()
5244                } else {
5245                    return Err(EngineError::Unsupported(alloc::format!(
5246                        "type \"{type_name}\" does not exist"
5247                    )));
5248                };
5249                let json_arg = match args.get(1) {
5250                    Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5251                    None => Value::Null,
5252                };
5253                // The set form iterates the JSON array; the scalar form is
5254                // the one-element case of the same walk.
5255                let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5256                    crate::json::array_element_rows(&json_arg, false, fn_name)
5257                        .map_err(EngineError::Eval)?
5258                        .into_iter()
5259                        .map(|s| s.map_or(Value::Null, Value::json))
5260                        .collect()
5261                } else if matches!(json_arg, Value::Null) {
5262                    alloc::vec::Vec::new()
5263                } else {
5264                    alloc::vec![json_arg]
5265                };
5266                let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5267                for doc in &docs {
5268                    let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5269                    for c in &cols {
5270                        // `->>` semantics: a missing key is NULL, present keys
5271                        // arrive as text and cast to the declared column type.
5272                        let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5273                            .map_err(EngineError::Eval)?;
5274                        let v = if matches!(raw, Value::Null) {
5275                            Value::Null
5276                        } else {
5277                            crate::conversions::coerce_value(raw, c.ty, "", 0)
5278                                .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5279                        };
5280                        vals.push(v);
5281                    }
5282                    rows.push(Row::new(vals));
5283                }
5284                Ok((rows, cols))
5285            }
5286            // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5287            // a text[] of 'name=value' reloptions/fdw options → one
5288            // (option_name, option_value) row per element. NULL or an
5289            // empty array yields zero rows (PG); an element without
5290            // '=' carries a NULL option_value, matching PG's split.
5291            "pg_options_to_table" => {
5292                let schema = alloc::vec![
5293                    ColumnSchema::new("option_name", DataType::Text, true),
5294                    ColumnSchema::new("option_value", DataType::Text, true),
5295                ];
5296                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5297                if let Some(Value::TextArray(items)) = arg0 {
5298                    for item in items.into_iter().flatten() {
5299                        let (name, value) = match item.split_once('=') {
5300                            Some((n, v)) => (Value::text(n), Value::text(v)),
5301                            None => (Value::text(item.as_str()), Value::Null),
5302                        };
5303                        rows.push(Row::new(alloc::vec![name, value]));
5304                    }
5305                }
5306                Ok((rows, schema))
5307            }
5308            // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5309            // PG18's per-sequence state SRF, (last_value, is_called).
5310            // pg_dump reads it joined to pg_sequence for every dumped
5311            // sequence's setval line. The oid resolves through the
5312            // same relation_oid mapping seqrelid publishes.
5313            "pg_get_sequence_data" => {
5314                let schema = alloc::vec![
5315                    ColumnSchema::new("last_value", DataType::BigInt, false),
5316                    ColumnSchema::new("is_called", DataType::Bool, false),
5317                ];
5318                let want = match arg0 {
5319                    Some(Value::Int(n)) => i64::from(n),
5320                    Some(Value::BigInt(n)) => n,
5321                    _ => {
5322                        return Err(EngineError::Unsupported(
5323                            "pg_get_sequence_data(): argument must be a sequence oid".into(),
5324                        ));
5325                    }
5326                };
5327                let cat = self.active_catalog();
5328                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5329                for (name, def) in cat.sequences_all() {
5330                    if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5331                        rows.push(Row::new(alloc::vec![
5332                            Value::BigInt(def.last_value),
5333                            Value::Bool(def.is_called),
5334                        ]));
5335                        break;
5336                    }
5337                }
5338                Ok((rows, schema))
5339            }
5340            "pg_partition_tree" => {
5341                let cols = alloc::vec![
5342                    ColumnSchema::new("relid".to_string(), DataType::Text, true),
5343                    ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5344                    ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5345                    ColumnSchema::new("level".to_string(), DataType::Int, true),
5346                ];
5347                let Some(Value::Text(name)) = &arg0 else {
5348                    // NULL (or missing) argument → zero rows (PG).
5349                    return Ok((alloc::vec::Vec::new(), cols));
5350                };
5351                let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5352                if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5353                    return Err(EngineError::Unsupported(alloc::format!(
5354                        "relation \"{name}\" does not exist"
5355                    )));
5356                }
5357                let rows = entries
5358                    .into_iter()
5359                    .map(|(relid, parent, isleaf, level)| {
5360                        Row::new(alloc::vec![
5361                            Value::text(relid),
5362                            parent.map_or(Value::Null, Value::text),
5363                            Value::Bool(isleaf),
5364                            #[allow(clippy::cast_possible_truncation)]
5365                            Value::Int(level as i32),
5366                        ])
5367                    })
5368                    .collect();
5369                Ok((rows, cols))
5370            }
5371            "pg_partition_ancestors" => {
5372                let cols =
5373                    alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5374                let Some(Value::Text(name)) = &arg0 else {
5375                    return Ok((alloc::vec::Vec::new(), cols));
5376                };
5377                let cat = self.active_catalog();
5378                if cat.get(name.as_ref()).is_none() {
5379                    return Err(EngineError::Unsupported(alloc::format!(
5380                        "relation \"{name}\" does not exist"
5381                    )));
5382                }
5383                // A relation outside any partition tree yields no rows (PG).
5384                let in_tree = cat
5385                    .get(name.as_ref())
5386                    .is_some_and(|t| t.schema().partition_role.is_some());
5387                let rows = if in_tree {
5388                    crate::partition_walks::ancestors_of(cat, name.as_ref())
5389                        .into_iter()
5390                        .map(|n| Row::new(alloc::vec![Value::text(n)]))
5391                        .collect()
5392                } else {
5393                    alloc::vec::Vec::new()
5394                };
5395                Ok((rows, cols))
5396            }
5397            // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5398            // saw, what each token was called, which dictionary took it
5399            // and what came out. It is a projection of the same tokenizer
5400            // and the same map the indexer uses, so it cannot describe a
5401            // pipeline other than the one that runs.
5402            "ts_debug" => {
5403                use crate::fts::{TokenType, TsDict};
5404                let cols = alloc::vec![
5405                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5406                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5407                    ColumnSchema::new("token".to_string(), DataType::Text, false),
5408                    ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5409                    ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5410                    ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5411                ];
5412                // PG's one-arg form uses the session configuration; the
5413                // two-arg form names one.
5414                let (cfg_name, text) = match (&arg0, args.get(1)) {
5415                    (Some(Value::Text(c)), Some(t)) => {
5416                        let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5417                        (c.to_string(), crate::eval::value_to_text(&v))
5418                    }
5419                    (Some(v), None) => (
5420                        alloc::string::String::from("english"),
5421                        crate::eval::value_to_text(v),
5422                    ),
5423                    _ => return Ok((alloc::vec::Vec::new(), cols)),
5424                };
5425                let english = match cfg_name
5426                    .trim()
5427                    .trim_start_matches("pg_catalog.")
5428                    .to_ascii_lowercase()
5429                    .as_str()
5430                {
5431                    "english" => true,
5432                    "simple" => false,
5433                    other => {
5434                        return Err(EngineError::Unsupported(alloc::format!(
5435                            "text search configuration \"{other}\" does not exist"
5436                        )));
5437                    }
5438                };
5439                let rows = crate::fts::tokenize_typed(&text)
5440                    .into_iter()
5441                    .map(|tok| {
5442                        let dict = tok.ty.dictionary(english);
5443                        let dname = dict.map(|d| match d {
5444                            TsDict::Simple => "simple",
5445                            TsDict::EnglishStem => "english_stem",
5446                        });
5447                        let folded = tok.text.to_lowercase();
5448                        let lexemes = dict.map(|d| match d {
5449                            TsDict::Simple => alloc::vec![Some(folded.clone())],
5450                            TsDict::EnglishStem => {
5451                                if crate::fts::is_english_stopword(&folded) {
5452                                    alloc::vec::Vec::new()
5453                                } else {
5454                                    alloc::vec![Some(crate::fts::porter_stem(&folded))]
5455                                }
5456                            }
5457                        });
5458                        Row::new(alloc::vec![
5459                            Value::text(tok.ty.alias()),
5460                            Value::text(tok.ty.description()),
5461                            Value::text(tok.text),
5462                            Value::TextArray(
5463                                dname
5464                                    .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5465                                    .unwrap_or_default(),
5466                            ),
5467                            dname.map_or(Value::Null, Value::text),
5468                            lexemes.map_or(Value::Null, Value::TextArray),
5469                        ])
5470                    })
5471                    .collect();
5472                let _ = TokenType::AsciiWord;
5473                Ok((rows, cols))
5474            }
5475            // v7.39 (round 651) — `ts_token_type('default')`, the list the
5476            // parser actually produces. It is a projection of the
5477            // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5478            // read, so the three cannot disagree about what a token is.
5479            "ts_token_type" => {
5480                use crate::fts::TokenType as T;
5481                let cols = alloc::vec![
5482                    ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5483                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5484                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5485                ];
5486                // PG takes the parser by name or oid; SPG has the one.
5487                if let Some(Value::Text(p)) = &arg0
5488                    && !p.eq_ignore_ascii_case("default")
5489                    && !p.eq_ignore_ascii_case("pg_catalog.default")
5490                {
5491                    return Err(EngineError::Unsupported(alloc::format!(
5492                        "text search parser \"{p}\" does not exist"
5493                    )));
5494                }
5495                const TYPES: &[T] = &[
5496                    T::AsciiWord,
5497                    T::Word,
5498                    T::NumWord,
5499                    T::Email,
5500                    T::Url,
5501                    T::Host,
5502                    T::SFloat,
5503                    T::Version,
5504                    T::HwordNumPart,
5505                    T::HwordPart,
5506                    T::HwordAsciiPart,
5507                    T::Blank,
5508                    T::Tag,
5509                    T::Protocol,
5510                    T::NumHword,
5511                    T::AsciiHword,
5512                    T::Hword,
5513                    T::UrlPath,
5514                    T::File,
5515                    T::Float,
5516                    T::Int,
5517                    T::Uint,
5518                    T::Entity,
5519                ];
5520                let rows = TYPES
5521                    .iter()
5522                    .map(|t| {
5523                        Row::new(alloc::vec![
5524                            Value::Int(*t as i32),
5525                            Value::text(t.alias()),
5526                            Value::text(t.description()),
5527                        ])
5528                    })
5529                    .collect();
5530                Ok((rows, cols))
5531            }
5532            // v7.39 (read01 round 65) — a set-returning USER function in FROM
5533            // (`FROM rows_of(2)`). Its body runs through the real executor, like
5534            // every other function body since round 63.
5535            other => {
5536                if !self.active_catalog().functions_named(other).is_empty() {
5537                    return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5538                }
5539                Err(EngineError::Unsupported(alloc::format!(
5540                    "table function {other}() is not supported in FROM"
5541                )))
5542            }
5543        }
5544    }
5545
5546    /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5547    /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5548    /// are bound into it as literals and it goes through the read path, so the
5549    /// rows it yields are exactly the rows a hand-written query would see.
5550    ///
5551    /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5552    /// text)` names them, and a `SETOF <scalar>` yields a single column named
5553    /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5554    /// shows.
5555    fn exec_setof_user_function(
5556        &self,
5557        name: &str,
5558        args: &[spg_sql::ast::Expr],
5559        // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5560        // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5561        alias: Option<&str>,
5562    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5563        // The call's arguments belong to the ENCLOSING query, so they are
5564        // evaluated here and the body sees values.
5565        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5566        let arg_ctx = self.ev_ctx(&empty, None);
5567        let dummy = Row::new(alloc::vec::Vec::new());
5568        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5569        for a in args {
5570            vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5571        }
5572        self.setof_rows_of(name, &vals, alias)
5573    }
5574
5575    /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5576    /// arguments. Shared by the FROM position and the target-list expansion, so
5577    /// a function cannot behave differently depending on where it is called.
5578    pub(crate) fn setof_rows_of(
5579        &self,
5580        name: &str,
5581        arg_values: &[Value<'static>],
5582        alias: Option<&str>,
5583    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5584        let cat = self.active_catalog();
5585        let overloads = cat.functions_named(name);
5586        let def = overloads
5587            .iter()
5588            .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5589            .ok_or_else(|| {
5590                EngineError::Unsupported(alloc::format!(
5591                    "function {name} does not exist with {} argument(s)",
5592                    arg_values.len()
5593                ))
5594            })?;
5595        let declared = def.returns.trim().to_string();
5596        let upper = declared.to_ascii_uppercase();
5597        if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5598            return Err(EngineError::Unsupported(alloc::format!(
5599                "function {name}() does not return a set — it cannot be used in FROM"
5600            )));
5601        }
5602
5603        let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5604        // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5605        // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5606        if def.language.eq_ignore_ascii_case("plpgsql") {
5607            let out_rows = self
5608                .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5609                .map_err(EngineError::Eval)?;
5610            let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5611            let rows = out_rows.into_iter().map(Row::new).collect();
5612            return Ok((rows, cols));
5613        }
5614        let body = def.body.trim().trim_end_matches(';');
5615        let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5616            EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5617        })?;
5618        let spg_sql::ast::Statement::Select(body_select) = stmt else {
5619            return Err(EngineError::Unsupported(alloc::format!(
5620                "function {name}(): a set-returning body must be a SELECT"
5621            )));
5622        };
5623        let arg_names = spg_storage::function_arg_names(&def.args_repr);
5624        let bound = crate::eval::bind_user_fn_args(
5625            self.active_catalog(),
5626            &body_select,
5627            &arg_names,
5628            arg_values,
5629        )
5630        .map_err(EngineError::Eval)?;
5631        let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5632        let QueryResult::Rows { columns, rows } = out else {
5633            return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5634        };
5635        // Name the columns from the DECLARED shape — the same rule the plpgsql
5636        // path above uses, so a body's language cannot change the row shape.
5637        let cols = setof_column_shape_from(&declared, name, alias, &columns);
5638        Ok((rows, cols))
5639    }
5640
5641    fn exec_select_jsonb_each_text(
5642        &self,
5643        stmt: &SelectStatement,
5644        primary: &TableRef,
5645        cancel: CancelToken<'_>,
5646    ) -> Result<QueryResult, EngineError> {
5647        let (each_fn, arg_expr) = primary
5648            .jsonb_each_text_arg
5649            .as_ref()
5650            .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5651            .expect("caller guards jsonb_each_text_arg.is_some()");
5652        // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5653        // forms keep JSON rendering in the value column (JSON null
5654        // stays jsonb 'null', strings keep their quotes).
5655        let as_text = each_fn.ends_with("_text");
5656        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5657        let ctx = EvalContext::new(&empty_schema, None);
5658        let dummy_row = Row::new(alloc::vec::Vec::new());
5659        let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5660        let pairs =
5661            crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5662        let rows: alloc::vec::Vec<Row<'static>> = pairs
5663            .into_iter()
5664            .map(|(k, v)| {
5665                let key_val = Value::text(k);
5666                let value_val = match v {
5667                    Some(s) if as_text => Value::text(s),
5668                    Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5669                    None => Value::Null,
5670                };
5671                Row::new(alloc::vec![key_val, value_val])
5672            })
5673            .collect();
5674        let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5675        let value_dtype = if as_text {
5676            spg_storage::DataType::Text
5677        } else {
5678            spg_storage::DataType::Json
5679        };
5680        let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5681        let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5682        let mut schema_cols = alloc::vec![key_col, value_col];
5683        // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5684        // LATERAL-position form of the same call already honours it.
5685        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5686            if let Some(col) = schema_cols.get_mut(i) {
5687                col.name = new_name.clone();
5688            }
5689        }
5690        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5691        // `EvalContext::new` drops it and every catalog-dependent cast
5692        // (regclass / enum / composite / domain) silently degrades.
5693        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5694        // WHERE.
5695        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5696            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5697            for row in rows {
5698                cancel.check()?;
5699                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5700                if matches!(v, Value::Bool(true)) {
5701                    out.push(row);
5702                }
5703            }
5704            out
5705        } else {
5706            rows
5707        };
5708        // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5709        if aggregate::uses_aggregate(stmt) {
5710            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5711            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5712                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5713                    .map_err(|err| match err {
5714                        EngineError::Eval(ev) => ev,
5715                        other => eval::EvalError::TypeMismatch {
5716                            detail: alloc::format!("{other}"),
5717                        },
5718                    })
5719            };
5720            // v7.39 (round 656) — hand the rows over as they are rather than
5721            // collecting a second vector of `RowRef` wrappers. Note this is
5722            // a set-returning-function path, NOT the relational scan: the
5723            // measured O(rows) cost lived in `run_single_table_aggregate`,
5724            // and converting these four first was a miss that cost a full
5725            // round — every test stayed green and the number did not move.
5726            let agg = aggregate::run(
5727                stmt,
5728                crate::join::AggRows::Owned(&filtered),
5729                &schema_cols,
5730                Some(&alias),
5731                Some(&agg_correlated),
5732                self.parallel_runner.0.as_deref(),
5733                Some(self.active_catalog()),
5734                Some(self),
5735            )?;
5736            return self.finish_agg_result(agg, stmt, cancel);
5737        }
5738        // Projection.
5739        let projection = build_projection(
5740            &stmt.items,
5741            &schema_cols,
5742            &alias,
5743            self.speaks_mysql,
5744            Some(self.active_catalog()),
5745        )?;
5746        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5747            alloc::vec::Vec::with_capacity(filtered.len());
5748        for row in &filtered {
5749            let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5750            for p in &projection {
5751                let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5752                vals.push(v);
5753            }
5754            projected_rows.push(Row::new(vals));
5755        }
5756        let columns: alloc::vec::Vec<ColumnSchema> = projection
5757            .iter()
5758            // v7.39 (read01 round 54) — keep the column's enum identity through
5759            // the projection (it lives outside the DataType lattice), or a
5760            // derived table / UNION / windowed result forgets it and any outer
5761            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5762            .map(|p| p.to_column_schema())
5763            .collect();
5764        // ORDER BY.
5765        if !stmt.order_by.is_empty() {
5766            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5767                .iter()
5768                .enumerate()
5769                .map(|(i, r)| -> Result<_, EngineError> {
5770                    let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5771                        .order_by
5772                        .iter()
5773                        .map(|ob| {
5774                            eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5775                        })
5776                        .collect();
5777                    Ok((i, keys?))
5778                })
5779                .collect::<Result<_, _>>()?;
5780            indexed.sort_by(|a, b| {
5781                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5782                    let o = &stmt.order_by[idx];
5783                    let cmp = order_by_value_cmp_in(
5784                        o.desc,
5785                        o.nulls_first,
5786                        ka,
5787                        kb,
5788                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5789                    );
5790                    if cmp != core::cmp::Ordering::Equal {
5791                        return cmp;
5792                    }
5793                }
5794                core::cmp::Ordering::Equal
5795            });
5796            projected_rows = indexed
5797                .into_iter()
5798                .map(|(i, _)| projected_rows[i].clone())
5799                .collect();
5800        }
5801        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5802        if stmt.distinct {
5803            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
5804            // spec folds EVERY text position, so a column declared
5805            // `COLLATE utf8mb4_bin` had its values merged here exactly the
5806            // way 3b494b6e fixed on the main scan path. The projection is
5807            // already in scope at each of these sites, so the mask needs no
5808            // new plumbing -- it was simply never asked for.
5809            projected_rows = dedup_rows(
5810                projected_rows,
5811                FoldSpec::of_masks(
5812                    scan_ctx.mysql_dialect,
5813                    &fold_mask(&projection),
5814                    &pad_mask(&projection),
5815                ),
5816            );
5817        }
5818        if let Some(offset) = stmt.offset_literal() {
5819            let off = (offset as usize).min(projected_rows.len());
5820            projected_rows.drain(..off);
5821        }
5822        if let Some(limit) = stmt.limit_literal() {
5823            projected_rows.truncate(limit as usize);
5824        }
5825        Ok(QueryResult::Rows {
5826            columns,
5827            rows: projected_rows,
5828        })
5829    }
5830
5831    /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5832    /// ( SELECT … ) alias` in primary position. The inner SELECT
5833    /// materialises once through the regular bare-select executor
5834    /// (UNION tails included), then the outer WHERE / aggregate /
5835    /// projection / ORDER BY / LIMIT pipeline runs over the
5836    /// synthetic table — the same post-materialisation shape as
5837    /// exec_select_jsonb_each_text, generalised to N columns.
5838    fn exec_select_derived(
5839        &self,
5840        stmt: &SelectStatement,
5841        primary: &TableRef,
5842        cancel: CancelToken<'_>,
5843    ) -> Result<QueryResult, EngineError> {
5844        let inner = primary
5845            .lateral_subquery
5846            .as_deref()
5847            .expect("caller guards lateral_subquery.is_some()");
5848        // exec_select_cancel is the union-aware wrapper — the inner
5849        // SELECT may carry UNION tails on stmt.unions.
5850        let QueryResult::Rows {
5851            columns: inner_cols,
5852            rows,
5853        } = self.exec_select_cancel(inner, cancel)?
5854        else {
5855            return Err(EngineError::Unsupported(
5856                "derived table subquery must return rows".into(),
5857            ));
5858        };
5859        let alias = primary
5860            .alias
5861            .clone()
5862            .unwrap_or_else(|| primary.name.clone());
5863        // `AS t(a, b)` renames the materialised columns positionally
5864        // (extra inner columns keep their own names, PG behaviour).
5865        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5866        // v7.39 (read01 round 78) — a column-alias list longer than the item is
5867        // the error PG reports; SPG used to let the extra names through and then
5868        // fail two layers downstream with "column not found: <the extra name>".
5869        let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5870        if primary.unnest_column_aliases.len() > n_out {
5871            return Err(EngineError::Unsupported(alloc::format!(
5872                "table \"{alias}\" has {n_out} columns available but {} columns specified",
5873                primary.unnest_column_aliases.len()
5874            )));
5875        }
5876        if primary.scalar_fn_item && schema_cols.len() == 1 {
5877            schema_cols[0].scalar_row_source = true;
5878        }
5879        // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5880        // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5881        // The column-alias list, if given, names it like any other column.
5882        let mut rows = rows;
5883        if primary.with_ordinality {
5884            schema_cols.push(ColumnSchema::new(
5885                "ordinality".to_string(),
5886                DataType::BigInt,
5887                false,
5888            ));
5889            rows = rows
5890                .into_iter()
5891                .enumerate()
5892                .map(|(i, r)| {
5893                    let mut v = r.values;
5894                    #[allow(clippy::cast_possible_wrap)]
5895                    v.push(Value::BigInt(i as i64 + 1));
5896                    Row::new(v)
5897                })
5898                .collect();
5899        }
5900        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5901            if let Some(col) = schema_cols.get_mut(i) {
5902                col.name = new_name.clone();
5903            }
5904        }
5905        self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5906    }
5907
5908    /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5909    /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5910    /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5911    /// derived-table executor and the FROM-position table functions.
5912    fn exec_select_over_rows(
5913        &self,
5914        stmt: &SelectStatement,
5915        rows: alloc::vec::Vec<Row<'static>>,
5916        schema_cols: alloc::vec::Vec<ColumnSchema>,
5917        alias: &str,
5918        cancel: CancelToken<'_>,
5919    ) -> Result<QueryResult, EngineError> {
5920        let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5921        // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5922        // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5923        // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5924        // (the same path the aggregate branch uses); the old plain eval_expr let
5925        // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5926        let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5927        // WHERE.
5928        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5929            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5930            for row in rows {
5931                cancel.check()?;
5932                let v = self.eval_expr_with_correlated(
5933                    w,
5934                    &row,
5935                    &scan_ctx,
5936                    cancel,
5937                    Some(&mut corr_memo.borrow_mut()),
5938                )?;
5939                if matches!(v, Value::Bool(true)) {
5940                    out.push(row);
5941                }
5942            }
5943            out
5944        } else {
5945            rows
5946        };
5947        // Aggregate dispatch.
5948        if aggregate::uses_aggregate(stmt) {
5949            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5950            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5951                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5952                    .map_err(|err| match err {
5953                        EngineError::Eval(ev) => ev,
5954                        other => eval::EvalError::TypeMismatch {
5955                            detail: alloc::format!("{other}"),
5956                        },
5957                    })
5958            };
5959            // v7.39 (round 656) — hand the rows over as they are rather than
5960            // collecting a second vector of `RowRef` wrappers. Note this is
5961            // a set-returning-function path, NOT the relational scan: the
5962            // measured O(rows) cost lived in `run_single_table_aggregate`,
5963            // and converting these four first was a miss that cost a full
5964            // round — every test stayed green and the number did not move.
5965            let agg = aggregate::run(
5966                stmt,
5967                crate::join::AggRows::Owned(&filtered),
5968                &schema_cols,
5969                Some(alias),
5970                Some(&agg_correlated),
5971                self.parallel_runner.0.as_deref(),
5972                Some(self.active_catalog()),
5973                Some(self),
5974            )?;
5975            return self.finish_agg_result(agg, stmt, cancel);
5976        }
5977        // Projection.
5978        let projection = build_projection(
5979            &stmt.items,
5980            &schema_cols,
5981            alias,
5982            self.speaks_mysql,
5983            Some(self.active_catalog()),
5984        )?;
5985        // v7.39 (round 621) — a target-list SRF expands here too. This tail
5986        // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5987        // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5988        // answered `function unnest(integer[]) does not exist` for a query PG
5989        // answers.
5990        let srf_idxs = self.srf_target_idxs(&projection);
5991        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5992        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5993            alloc::vec::Vec::with_capacity(filtered.len());
5994        if !srf_idxs.is_empty() {
5995            let (rows, src) =
5996                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5997            projected_rows = rows;
5998            src_of_row = src;
5999        } else {
6000            for row in &filtered {
6001                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
6002                for p in &projection {
6003                    let v = self.eval_expr_with_correlated(
6004                        &p.expr,
6005                        row,
6006                        &scan_ctx,
6007                        cancel,
6008                        Some(&mut corr_memo.borrow_mut()),
6009                    )?;
6010                    vals.push(v);
6011                }
6012                projected_rows.push(Row::new(vals));
6013            }
6014        }
6015        let columns: alloc::vec::Vec<ColumnSchema> = projection
6016            .iter()
6017            // v7.39 (read01 round 54) — keep the column's enum identity through
6018            // the projection (it lives outside the DataType lattice), or a
6019            // derived table / UNION / windowed result forgets it and any outer
6020            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
6021            .map(|p| p.to_column_schema())
6022            .collect();
6023        // ORDER BY over the source rows (same shape as the other
6024        // synthetic-table executors).
6025        // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
6026        // OUTPUT column. Evaluated as an expression, as it was here, the literal
6027        // `1` is just the constant 1: the same sort key for every row, so the
6028        // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
6029        // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
6030        // landing on this executor) came back in input order.
6031        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6032        if !order_by.is_empty() {
6033            // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
6034            // SRF makes more of them than there were inputs.
6035            let out_cols = if srf_idxs.is_empty() {
6036                alloc::vec![None; order_by.len()]
6037            } else {
6038                srf_order_output_cols(&order_by, &projection)
6039            };
6040            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
6041                .iter()
6042                .enumerate()
6043                .map(|(k, out)| -> Result<_, EngineError> {
6044                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
6045                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
6046                        .iter()
6047                        .zip(out_cols.iter())
6048                        .map(|(ob, oc)| {
6049                            // v7.39 (read01 round 54) — this path builds its
6050                            // sort keys itself instead of going through
6051                            // `build_order_keys`, so it skipped the enum-ordinal
6052                            // substitution: an OUTER `ORDER BY <enum col>` over
6053                            // a DERIVED TABLE sorted by the label TEXT, not by
6054                            // member order. Silently wrong rows, not an error.
6055                            let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
6056                            Ok(
6057                                match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
6058                                    Some(ord) => Value::Float(ord),
6059                                    None => v,
6060                                },
6061                            )
6062                        })
6063                        .collect();
6064                    Ok((k, keys?))
6065                })
6066                .collect::<Result<_, _>>()?;
6067            indexed.sort_by(|a, b| {
6068                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
6069                    let o = &stmt.order_by[idx];
6070                    let cmp = order_by_value_cmp_in(
6071                        o.desc,
6072                        o.nulls_first,
6073                        ka,
6074                        kb,
6075                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
6076                    );
6077                    if cmp != core::cmp::Ordering::Equal {
6078                        return cmp;
6079                    }
6080                }
6081                core::cmp::Ordering::Equal
6082            });
6083            projected_rows = indexed
6084                .into_iter()
6085                .map(|(i, _)| projected_rows[i].clone())
6086                .collect();
6087        }
6088        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
6089        if stmt.distinct {
6090            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
6091            // spec folds EVERY text position, so a column declared
6092            // `COLLATE utf8mb4_bin` had its values merged here exactly the
6093            // way 3b494b6e fixed on the main scan path. The projection is
6094            // already in scope at each of these sites, so the mask needs no
6095            // new plumbing -- it was simply never asked for.
6096            projected_rows = dedup_rows(
6097                projected_rows,
6098                FoldSpec::of_masks(
6099                    scan_ctx.mysql_dialect,
6100                    &fold_mask(&projection),
6101                    &pad_mask(&projection),
6102                ),
6103            );
6104        }
6105        if let Some(offset) = stmt.offset_literal() {
6106            let off = (offset as usize).min(projected_rows.len());
6107            projected_rows.drain(..off);
6108        }
6109        if let Some(limit) = stmt.limit_literal() {
6110            projected_rows.truncate(limit as usize);
6111        }
6112        Ok(QueryResult::Rows {
6113            columns,
6114            rows: projected_rows,
6115        })
6116    }
6117
6118    /// Constant `SELECT` with no FROM: evaluate each projection item
6119    /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
6120    fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
6121        let empty_schema: Vec<ColumnSchema> = Vec::new();
6122        let ctx = self.ev_ctx(&empty_schema, None);
6123        // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
6124        // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
6125        // `SELECT string_agg('x',',')` → x). Before this it fell through to the
6126        // scalar projection, where the aggregate name looked like an unknown
6127        // function. The WHERE filters that one row, so `… WHERE false` leaves
6128        // the aggregate zero input rows (`count(*)` → 0).
6129        if aggregate::uses_aggregate(stmt) {
6130            let dummy = Row::new(Vec::new());
6131            let passes = match &stmt.where_ {
6132                Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
6133                None => true,
6134            };
6135            let rows: Vec<RowRef<'_>> = if passes {
6136                alloc::vec![RowRef::Owned(&dummy)]
6137            } else {
6138                Vec::new()
6139            };
6140            let agg = aggregate::run(
6141                stmt,
6142                crate::join::AggRows::Refs(&rows),
6143                &empty_schema,
6144                None,
6145                None,
6146                self.parallel_runner.0.as_deref(),
6147                Some(self.active_catalog()),
6148                Some(self),
6149            )?;
6150            return self.finish_agg_result(agg, stmt, CancelToken::none());
6151        }
6152        let projection = build_projection(
6153            &stmt.items,
6154            &empty_schema,
6155            "",
6156            self.speaks_mysql,
6157            Some(self.active_catalog()),
6158        )?;
6159        // `SELECT … WHERE cond` with no FROM — the one conceptual
6160        // row survives only when the condition is true (previously
6161        // the WHERE was silently ignored: `SELECT 1 WHERE false`
6162        // returned a row).
6163        let dummy_row = Row::new(Vec::new());
6164        if let Some(w) = &stmt.where_ {
6165            let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
6166            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6167                let columns: Vec<ColumnSchema> = projection
6168                    .into_iter()
6169                    .map(|p| p.to_column_schema())
6170                    .collect();
6171                return Ok(QueryResult::Rows {
6172                    columns,
6173                    rows: Vec::new(),
6174                });
6175            }
6176        }
6177        // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6178        // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6179        // desugar to unnest) expands here: one output row per SRF row, sibling
6180        // scalar columns repeated. unnest / array_elements / path_query reach a
6181        // real FROM via the parser rewrite and never land here.
6182        // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6183        let srf_idxs = self.srf_target_idxs(&projection);
6184        if !srf_idxs.is_empty() {
6185            let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6186            let columns: Vec<ColumnSchema> = projection
6187                .into_iter()
6188                .map(|p| p.to_column_schema())
6189                .collect();
6190            // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6191            // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6192            // to. This returned straight out of the expansion, so
6193            // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6194            // input order — the sort was not wrong, it never ran. (There is
6195            // exactly one conceptual input row here, which is why the ordinary
6196            // scan pipeline is not on this path at all.)
6197            if !stmt.order_by.is_empty() {
6198                let synth_ctx =
6199                    EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6200                let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6201                    .order_by
6202                    .iter()
6203                    .map(|o| {
6204                        let mut o = o.clone();
6205                        if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6206                            && *n >= 1
6207                            && let Ok(idx) = usize::try_from(*n - 1)
6208                            && idx < columns.len()
6209                        {
6210                            o.expr = Expr::Column(spg_sql::ast::ColumnName {
6211                                qualifier: None,
6212                                name: columns[idx].name.clone(),
6213                            });
6214                        }
6215                        o
6216                    })
6217                    .collect();
6218                let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6219                let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6220                for r in rows {
6221                    // v7.39.12 — a correlated subquery in ORDER BY is resolved
6222                    // for this row before the key is built; see
6223                    // `Engine::order_by_resolved_for_row`.
6224                    let per_row = self.order_by_resolved_for_row(
6225                        &resolved,
6226                        &r,
6227                        &synth_ctx,
6228                        CancelToken::none(),
6229                    )?;
6230                    let keys =
6231                        build_order_keys(per_row.as_deref().unwrap_or(&resolved), &r, &synth_ctx)?;
6232                    tagged.push((keys, r));
6233                }
6234                sort_by_keys(&mut tagged, &descs);
6235                rows = tagged.into_iter().map(|(_, r)| r).collect();
6236            }
6237            apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6238            return Ok(QueryResult::Rows { columns, rows });
6239        }
6240        let mut values = Vec::with_capacity(projection.len());
6241        for p in &projection {
6242            values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6243        }
6244        let columns: Vec<ColumnSchema> = projection
6245            .into_iter()
6246            .map(|p| p.to_column_schema())
6247            .collect();
6248        // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6249        // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6250        // returns none. (The SRF and aggregate arms above already applied
6251        // them; this tail was the one that didn't.)
6252        let mut rows = alloc::vec![Row::new(values)];
6253        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6254        Ok(QueryResult::Rows { columns, rows })
6255    }
6256
6257    /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6258    /// circuit. Catches
6259    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6260    /// BEFORE `resolve_select_subqueries` materialises the inner result
6261    /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6262    /// values into a `HashSet<i64>` directly, then probes A.pk per
6263    /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6264    /// (~150 µs / query at INSUBQ benchmark scale).
6265    pub(crate) fn try_count_star_pk_in_subquery_fast(
6266        &self,
6267        stmt: &SelectStatement,
6268        cancel: CancelToken<'_>,
6269    ) -> Result<Option<QueryResult>, EngineError> {
6270        use spg_sql::ast::SelectItem;
6271        if stmt.distinct
6272            || stmt.limit_with_ties
6273            || stmt.group_by.is_some()
6274            || stmt.having.is_some()
6275            || !stmt.unions.is_empty()
6276            || !stmt.order_by.is_empty()
6277            || stmt.limit.is_some()
6278            || stmt.offset.is_some()
6279            || stmt.items.len() != 1
6280        {
6281            return Ok(None);
6282        }
6283        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6284            return Ok(None);
6285        };
6286        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6287            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6288        if !is_count_star {
6289            return Ok(None);
6290        }
6291        let Some(from) = stmt.from.as_ref() else {
6292            return Ok(None);
6293        };
6294        if !from.joins.is_empty()
6295            || from.primary.lateral_subquery.is_some()
6296            || from.primary.unnest_expr.is_some()
6297            || from.primary.generate_series_args.is_some()
6298            || from.primary.table_fn_call.is_some()
6299            || from.primary.as_of_segment.is_some()
6300        {
6301            return Ok(None);
6302        }
6303        let Some(where_expr) = stmt.where_.as_ref() else {
6304            return Ok(None);
6305        };
6306        // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6307        // negated=false; no other predicates.
6308        let Expr::InSubquery {
6309            expr: col_expr,
6310            subquery,
6311            negated: false,
6312        } = where_expr
6313        else {
6314            return Ok(None);
6315        };
6316        let Expr::Column(c) = col_expr.as_ref() else {
6317            return Ok(None);
6318        };
6319        let outer_alias = from
6320            .primary
6321            .alias
6322            .as_deref()
6323            .unwrap_or(from.primary.name.as_str());
6324        if let Some(q) = c.qualifier.as_deref()
6325            && !q.eq_ignore_ascii_case(outer_alias)
6326        {
6327            return Ok(None);
6328        }
6329        // Outer column must be a single-column PK on integer family.
6330        let catalog = self.active_catalog();
6331        let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6332            return Ok(None);
6333        };
6334        let outer_schema = outer_table.schema();
6335        let Some(outer_pos) = outer_schema
6336            .columns
6337            .iter()
6338            .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6339        else {
6340            return Ok(None);
6341        };
6342        if !matches!(
6343            outer_schema.columns[outer_pos].ty,
6344            spg_storage::DataType::BigInt
6345                | spg_storage::DataType::Int
6346                | spg_storage::DataType::SmallInt
6347        ) {
6348            return Ok(None);
6349        }
6350        if !outer_schema
6351            .uniqueness_constraints
6352            .iter()
6353            .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6354        {
6355            return Ok(None);
6356        }
6357        let Some(idx) = outer_table.index_on(outer_pos) else {
6358            return Ok(None);
6359        };
6360        // Inner must be uncorrelated. The cheap-correlation pre-check
6361        // exists upstream; here we just attempt the bare exec.
6362        if crate::subquery::select_is_correlated(subquery) {
6363            return Ok(None);
6364        }
6365        let mut inner = (**subquery).clone();
6366        self.resolve_select_subqueries(&mut inner, cancel)?;
6367        let r = match self.exec_bare_select_cancel(&inner, cancel) {
6368            Ok(r) => r,
6369            Err(_) => return Ok(None),
6370        };
6371        let QueryResult::Rows { columns, rows, .. } = r else {
6372            return Ok(None);
6373        };
6374        if columns.len() != 1 {
6375            return Ok(None);
6376        }
6377        // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6378        // subquery projects a column known to be UNIQUE/PK on its table
6379        // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6380        // in `tbl.uniqueness_constraints`), survivor values are
6381        // guaranteed distinct and the per-survivor `HashSet::insert`
6382        // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6383        //
6384        // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6385        // projection that is a bare Column ref, table-column lookup in
6386        // catalog confirms the column appears as a unique constraint's
6387        // sole member. UNIQUE NOT NULL is required — a nullable unique
6388        // column may have multiple NULLs, but NULLs are already skipped
6389        // above (`Value::Null => continue`), so a UNIQUE-only column is
6390        // still safe to dedup-skip.
6391        let inner_unique = (|| -> bool {
6392            if inner.distinct
6393                || inner.group_by.is_some()
6394                || !inner.unions.is_empty()
6395                || inner.having.is_some()
6396                || inner.items.len() != 1
6397            {
6398                return false;
6399            }
6400            let Some(inner_from) = inner.from.as_ref() else {
6401                return false;
6402            };
6403            if !inner_from.joins.is_empty()
6404                || inner_from.primary.lateral_subquery.is_some()
6405                || inner_from.primary.unnest_expr.is_some()
6406                || inner_from.primary.generate_series_args.is_some()
6407                || inner_from.primary.table_fn_call.is_some()
6408            {
6409                return false;
6410            }
6411            let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6412                return false;
6413            };
6414            let Expr::Column(pc) = proj else {
6415                return false;
6416            };
6417            let inner_alias = inner_from
6418                .primary
6419                .alias
6420                .as_deref()
6421                .unwrap_or(inner_from.primary.name.as_str());
6422            if let Some(q) = pc.qualifier.as_deref()
6423                && !q.eq_ignore_ascii_case(inner_alias)
6424            {
6425                return false;
6426            }
6427            let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6428                return false;
6429            };
6430            let isch = inner_table.schema();
6431            let Some(ipos) = isch
6432                .columns
6433                .iter()
6434                .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6435            else {
6436                return false;
6437            };
6438            isch.uniqueness_constraints
6439                .iter()
6440                .any(|u| u.columns.as_slice() == [ipos])
6441        })();
6442        // Collect inner i64 values directly into a HashSet, then probe.
6443        let mut count: i64 = 0;
6444        let mut probed = if inner_unique {
6445            hashbrown::HashSet::<i64>::new()
6446        } else {
6447            hashbrown::HashSet::<i64>::with_capacity(rows.len())
6448        };
6449        for row in &rows {
6450            let v = row.values.first().cloned().unwrap_or(Value::Null);
6451            let n = match v {
6452                Value::BigInt(n) => n,
6453                Value::Int(n) => i64::from(n),
6454                Value::SmallInt(n) => i64::from(n),
6455                Value::Null => continue,
6456                _ => return Ok(None),
6457            };
6458            // De-duplicate inner key set so a duplicate inner value
6459            // doesn't double-count the same outer row. Skipped when
6460            // the inner projection is statically unique.
6461            if !inner_unique && !probed.insert(n) {
6462                continue;
6463            }
6464            // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6465            // the `IndexKey::from_value` enum-dispatch and the per-call
6466            // `IndexKey` wrapper construction. The outer column is
6467            // already gated to integer-family above, so an i64 key
6468            // always corresponds to a valid PK lookup.
6469            if !idx.lookup_eq_i64(n).is_empty() {
6470                count += 1;
6471            }
6472        }
6473        let columns_out = alloc::vec![ColumnSchema::new(
6474            "count".to_string(),
6475            spg_storage::DataType::BigInt,
6476            false,
6477        )];
6478        let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6479        Ok(Some(QueryResult::Rows {
6480            columns: columns_out,
6481            rows: rows_out,
6482        }))
6483    }
6484
6485    /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6486    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6487    /// (the post-subquery-replacement shape of the INSUBQ probe
6488    /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6489    /// The general aggregate path materialises every seeked row into
6490    /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6491    /// For COUNT(*) we only care how many keys hit; iterate the list
6492    /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6493    /// row materialisation, the aggregate state machine, and the per-
6494    /// row WHERE re-eval (the seek already filtered by the same list).
6495    /// Returns `None` when the shape doesn't match.
6496    fn try_count_star_pk_in_list_fast(
6497        &self,
6498        stmt: &SelectStatement,
6499        table: &spg_storage::Table,
6500        schema_cols: &[ColumnSchema],
6501        alias: &str,
6502    ) -> Option<QueryResult> {
6503        use spg_sql::ast::{ColumnName, SelectItem};
6504        // Gates on the SELECT shape.
6505        if stmt.distinct
6506            || stmt.limit_with_ties
6507            || stmt.group_by.is_some()
6508            || stmt.having.is_some()
6509            || !stmt.unions.is_empty()
6510            || !stmt.order_by.is_empty()
6511            || stmt.limit.is_some()
6512            || stmt.offset.is_some()
6513            || stmt.items.len() != 1
6514        {
6515            return None;
6516        }
6517        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6518            return None;
6519        };
6520        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6521            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6522        if !is_count_star {
6523            return None;
6524        }
6525        // WHERE must be `<col> IN (literal list)` with no other
6526        // conjuncts (the seek result is a true subset of the row
6527        // population for this predicate).
6528        let where_expr = stmt.where_.as_ref()?;
6529        let Expr::InList {
6530            expr: col_expr,
6531            list,
6532            negated: false,
6533        } = where_expr
6534        else {
6535            return None;
6536        };
6537        let Expr::Column(c) = col_expr.as_ref() else {
6538            return None;
6539        };
6540        if let Some(q) = c.qualifier.as_deref()
6541            && !q.eq_ignore_ascii_case(alias)
6542        {
6543            return None;
6544        }
6545        let col_pos = schema_cols
6546            .iter()
6547            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6548        // The column must be a single-column PK on an integer family
6549        // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6550        // so the antiset stays collision-free under `HashSet<i64>`.
6551        let schema = table.schema();
6552        if !matches!(
6553            schema.columns[col_pos].ty,
6554            spg_storage::DataType::BigInt
6555                | spg_storage::DataType::Int
6556                | spg_storage::DataType::SmallInt
6557        ) {
6558            return None;
6559        }
6560        if !schema
6561            .uniqueness_constraints
6562            .iter()
6563            .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6564        {
6565            return None;
6566        }
6567        let idx = table.index_on(col_pos)?;
6568        // Tally non-empty seek results across all literal values.
6569        let mut count: i64 = 0;
6570        for lit in list {
6571            let Expr::Literal(l) = lit else {
6572                return None;
6573            };
6574            // r1039 — through the shared resolver, so a literal spelled
6575            // in another type ('5' against an integer PK) is read as the
6576            // column's before it becomes a key. This tally answers from
6577            // the index alone, so a key in the wrong space would return a
6578            // COUNT of zero rather than fall back to a scan.
6579            let col = schema.columns.get(col_pos)?;
6580            let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6581            let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6582            if !idx.lookup_eq(&key).is_empty() {
6583                count += 1;
6584            }
6585        }
6586        let columns = alloc::vec![ColumnSchema::new(
6587            "count".to_string(),
6588            spg_storage::DataType::BigInt,
6589            false,
6590        )];
6591        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6592        let _ = ColumnName {
6593            qualifier: None,
6594            name: String::new(),
6595        };
6596        Some(QueryResult::Rows { columns, rows })
6597    }
6598
6599    /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6600    /// BETWEEN a AND b` on an indexed column. The index range walk yields
6601    /// exactly the matching (visible) rows, so we count locators directly —
6602    /// skipping the row materialisation, the aggregate state machine, and the
6603    /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6604    /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6605    /// when the shape doesn't match.
6606    fn try_count_star_indexed_range_fast(
6607        &self,
6608        stmt: &SelectStatement,
6609        table: &spg_storage::Table,
6610        schema_cols: &[ColumnSchema],
6611        alias: &str,
6612        snapshot: &spg_storage::snapshot::Snapshot,
6613    ) -> Option<QueryResult> {
6614        use spg_sql::ast::SelectItem;
6615        if stmt.distinct
6616            || stmt.limit_with_ties
6617            || stmt.group_by.is_some()
6618            || stmt.having.is_some()
6619            || !stmt.unions.is_empty()
6620            || !stmt.order_by.is_empty()
6621            || stmt.limit.is_some()
6622            || stmt.offset.is_some()
6623            || stmt.items.len() != 1
6624        {
6625            return None;
6626        }
6627        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6628            return None;
6629        };
6630        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6631            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6632        if !is_count_star {
6633            return None;
6634        }
6635        let where_expr = stmt.where_.as_ref()?;
6636        let count = crate::index_access::try_range_count(
6637            where_expr,
6638            schema_cols,
6639            table,
6640            alias,
6641            snapshot,
6642            self.speaks_mysql,
6643        )?;
6644        let columns = alloc::vec![ColumnSchema::new(
6645            "count".to_string(),
6646            spg_storage::DataType::BigInt,
6647            false,
6648        )];
6649        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6650        Some(QueryResult::Rows { columns, rows })
6651    }
6652
6653    /// Single-table aggregate path: filter the (optionally index-seeked)
6654    /// rows, then hand off to the aggregate executor which does its own
6655    /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6656    fn run_single_table_aggregate<'a>(
6657        &self,
6658        stmt: &SelectStatement,
6659        table: &'a spg_storage::Table,
6660        schema_cols: &'a [ColumnSchema],
6661        alias: &str,
6662        indexed_rows: Option<crate::index_access::Seeked<'a>>,
6663        cancel: CancelToken<'_>,
6664    ) -> Result<QueryResult, EngineError> {
6665        // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6666        // REPEATABLE (see run_single_table_scan). Aggregates
6667        // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6668        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6669        let ctx = self
6670            .ev_ctx(schema_cols, Some(alias))
6671            .with_sample_rng(&sample_cell);
6672        // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6673        // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6674        // and every abandoned buffer on the way stays resident: RSS is a
6675        // high-water mark, so the intermediates are paid for even though
6676        // they are freed. Round 656 measured the scan at 17 bytes/row
6677        // where the survivor list itself only needs 8.
6678        let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6679            Vec::with_capacity(table.rows().len())
6680        } else {
6681            // With a WHERE, the row count is an UPPER bound and reserving it
6682            // is the worse trade: `… WHERE id = 5` over 50M rows would take
6683            // 400 MB of pointers to hold one survivor. Let it grow.
6684            Vec::new()
6685        };
6686        // v6.2.6 — Memoize: per-query LRU cache for correlated
6687        // scalar subqueries. Fresh per row-loop entry so each
6688        // SELECT execution gets an isolated cache.
6689        let mut memo = memoize::MemoizeCache::new();
6690        // v7.37 (perf) — single-table aggregate's WHERE filter
6691        // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6692        // correlated`) per row, even for subquery-free WHEREs that
6693        // the single-table SCAN path has compiled since v7.32
6694        // (perf knife D). The asymmetry meant a fold-to-filter
6695        // rewrite (joinfold) that swapped a JOIN for a single-table
6696        // aggregate over a compiled WHERE saw the tree-walker
6697        // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6698        // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6699        // step. Compile once if eligible; fall back to the walker
6700        // for subquery-bearing or non-compilable WHEREs.
6701        let compiled_where: Option<eval::CompiledExpr> = stmt
6702            .where_
6703            .as_ref()
6704            .filter(|w| eval::fully_compilable(w))
6705            .map(|w| {
6706                // v7.38.8 — the scan filter runs the cheap half of its
6707                // conjunction first. Called from HERE and not from
6708                // `eval::compiled`, deliberately: the row loop lives in
6709                // that file, and adding a function to it cost this
6710                // query 11 % through layout alone while doing no work
6711                // for it. See `crate::qualorder`.
6712                match crate::qualorder::reordered(w) {
6713                    Some(r) => eval::compile_expr(&r, &ctx),
6714                    None => eval::compile_expr(w, &ctx),
6715                }
6716            });
6717        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6718        let mut row_passes_where = |row: &Row<'static>,
6719                                    eval_stack: &mut Vec<Value<'static>>,
6720                                    memo: &mut memoize::MemoizeCache|
6721         -> Result<bool, EngineError> {
6722            match (&compiled_where, &stmt.where_) {
6723                (Some(cw), _) => {
6724                    // v7.39 (round 479) — the predicate wants a bool, not a
6725                    // Value. The owned entry ended in `Value::into_owned`
6726                    // and the caller then dropped it, once per row; round
6727                    // 478's profile put that pair above the comparison
6728                    // itself.
6729                    Ok(eval::compiled::eval_compiled_pred(
6730                        cw,
6731                        row,
6732                        &ctx,
6733                        eval_stack,
6734                        ctx.mysql_dialect,
6735                    )
6736                    .map_err(EngineError::Eval)?)
6737                }
6738                (None, Some(w)) => {
6739                    let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6740                    Ok(crate::eval::predicate_is_true(
6741                        &cond,
6742                        "WHERE",
6743                        ctx.mysql_dialect,
6744                    )?)
6745                }
6746                (None, None) => Ok(true),
6747            }
6748        };
6749        if let Some(seeked) = &indexed_rows {
6750            // v7.38.19 — an EXACT seek has already applied the whole
6751            // predicate, so asking again is asking the index's question
6752            // a second time, once per row.
6753            //
6754            // Profiled on `count(*) FROM events WHERE project_id = 3`
6755            // over 200,000 rows: `try_index_seek` 1,814 leaf samples and
6756            // `binop::compare` 1,633 — and `compare`'s first arm is
6757            // `(Int, Int) => a.cmp(b)`, so it was never that a
6758            // comparison is expensive. It was that 25,000 of them were
6759            // re-deciding what the walk had decided. The same query with
6760            // `GROUP BY project_id` bolted on ran in half the time,
6761            // doing strictly more work, because that path reached the
6762            // rows differently.
6763            //
6764            // `exact` is false for every arm that has not proven it —
6765            // the GIN, trigram and jsonb walks, an `AND` whose other
6766            // conjuncts went unapplied, a collated key, a type whose key
6767            // cannot name it. See `index_access::Seeked`.
6768            if seeked.exact {
6769                filtered.extend(seeked.rows.iter().map(Cow::as_ref));
6770            } else {
6771                for cow in &seeked.rows {
6772                    let row = cow.as_ref();
6773                    if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6774                        continue;
6775                    }
6776                    filtered.push(row);
6777                }
6778            }
6779        }
6780        // v7.36 (cold-tier coverage) — single-table aggregate's
6781        // non-indexed full scan was hot-only and silently lost cold
6782        // rows on COUNT/SUM/etc. Materialise cold rows once into
6783        // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6784        // shape stays unchanged; the cold rows live until the end of
6785        // the aggregate run.
6786        let cold_rows_storage = if indexed_rows.is_none() {
6787            self.iter_cold_rows_of_table(table)
6788        } else {
6789            Vec::new()
6790        };
6791        if indexed_rows.is_none() {
6792            // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6793            // single-table aggregate full-scan path. Mirrors the gate on
6794            // `run_single_table_scan`: this is a user-query result path,
6795            // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6796            // reader's snapshot cannot see (e.g. tombstoned versions),
6797            // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6798            // under the default gate-off: every hot row is frozen or
6799            // committed-and-alive, so `is_row_visible` returns true.
6800            // Cold-tier rows are frozen (visible) by definition — left
6801            // ungated, matching the plain-scan path.
6802            let scan_snapshot = self.current_snapshot();
6803            // v7.39 (pg_stat knife B) — this full-scan branch walks
6804            // headers directly (serial and sharded alike); count the
6805            // sequential scan here.
6806            table.note_seq_scan();
6807            // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6808            // filter dominate the pre-aggregate wall time on big
6809            // scans (P1's ground truth: accumulation is only ~17%).
6810            // Shard THAT work when the host injected an executor and
6811            // the WHERE is compiled (the compiled evaluator is pure
6812            // over &row; the tree-walker fallback can hit correlated
6813            // subqueries and stays serial). Shards return surviving
6814            // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6815            // 'static bound — and the main thread only dereferences.
6816            let n = table.row_count();
6817            let par = self.parallel_runner.0.as_deref().filter(|_| {
6818                n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6819            });
6820            // v7.38.11 — ask the BRIN summary first. When it prunes,
6821            // the work left is a few thousand rows and sharding it
6822            // costs more than it saves, so the serial pruned loop below
6823            // takes it; the shard machinery is left exactly as it was
6824            // rather than taught about slots.
6825            let brin_slots = stmt
6826                .where_
6827                .as_ref()
6828                .and_then(|w| crate::brin::candidate_slots(w, table));
6829            let brin_prunes = brin_slots
6830                .as_ref()
6831                .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6832            if let Some(r) = par
6833                && !brin_prunes
6834            {
6835                let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6836                let chunk = n.div_ceil(n_shards);
6837                type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6838                let cw = &compiled_where;
6839                let snap_ref = &scan_snapshot;
6840                let results = r.run_shards(n_shards, &|s| {
6841                    let lo = s * chunk;
6842                    let hi = ((s + 1) * chunk).min(n);
6843                    let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6844                    // EvalContext carries Cells (sampler / row counters)
6845                    // and is !Sync — each shard builds its own from the
6846                    // same Sync inputs. The compiled WHERE is gated to
6847                    // the pure-scalar whitelist, which reads none of the
6848                    // session state the engine-built ctx would add
6849                    // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6850                    // sampled scans never take this branch).
6851                    let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6852                    let mut stack: Vec<Value<'static>> = Vec::new();
6853                    let out: ShardOut = (|| {
6854                        for i in lo..hi {
6855                            if !table.is_row_visible(i, snap_ref) {
6856                                continue;
6857                            }
6858                            let row = &table.rows()[i];
6859                            // v7.39 (round 480) — the parallel full-scan
6860                            // shard is the path the aggregate benchmark
6861                            // actually takes, and it was still on the OWNED
6862                            // entry: round 480's profile attributed 68.7 %
6863                            // of `drop_glue<Value>` to this closure, which
6864                            // is why round 479's fix to the indexed path
6865                            // barely moved the total.
6866                            //
6867                            // The `matches!(…, Value::Bool(true))` form was
6868                            // also a narrower reading than the rest of the
6869                            // engine uses — `predicate_is_true` is what
6870                            // handles NULL and MySQL truthiness — so the
6871                            // bool entry fixes the shape as well as the cost.
6872                            let pass = match cw {
6873                                Some(c) => eval::compiled::eval_compiled_pred(
6874                                    c,
6875                                    row,
6876                                    &shard_ctx,
6877                                    &mut stack,
6878                                    shard_ctx.mysql_dialect,
6879                                )
6880                                .map_err(EngineError::Eval)?,
6881                                None => true,
6882                            };
6883                            if pass {
6884                                keep.push(i);
6885                            }
6886                        }
6887                        Ok(keep)
6888                    })();
6889                    alloc::boxed::Box::new(out)
6890                });
6891                // v7.39 (round 567) — `rows()` is a 32-way trie, so
6892                // indexing it is four dependent loads and a scan that
6893                // reads every row paid them every row. A profile of
6894                // `SELECT sum(id)` over 500k rows put 37.8% of the
6895                // connection thread's CPU on THIS ONE LINE. The cursor
6896                // holds the leaf, making that one descent per 32.
6897                let mut rows_cur = table.rows().run_cursor();
6898                for boxed in results {
6899                    let shard = boxed
6900                        .downcast::<ShardOut>()
6901                        .expect("runner echoes the closure's box");
6902                    for i in (*shard)? {
6903                        if let Some(row) = rows_cur.get(i) {
6904                            filtered.push(row);
6905                        }
6906                    }
6907                }
6908            } else {
6909                let mut rows_cur = table.rows().run_cursor();
6910                // v7.38.11 — the slots the BRIN summary could not rule
6911                // out. The predicate still runs on every row that
6912                // survives: the summary decides what to SKIP, never
6913                // what to return.
6914                let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6915                for range in ranges {
6916                    for i in range {
6917                        if !table.is_row_visible(i, &scan_snapshot) {
6918                            continue;
6919                        }
6920                        let Some(row) = rows_cur.get(i) else { continue };
6921                        if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6922                            continue;
6923                        }
6924                        filtered.push(row);
6925                    }
6926                }
6927            }
6928            for row in &cold_rows_storage {
6929                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6930                    continue;
6931                }
6932                filtered.push(row);
6933            }
6934        }
6935        // v7.29 — a per-query memo so correlated scalar
6936        // subqueries batch-evaluate once (group map) instead of
6937        // executing per group.
6938        let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6939        let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6940            self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6941                .map_err(|err| match err {
6942                    EngineError::Eval(ev) => ev,
6943                    other => eval::EvalError::TypeMismatch {
6944                        detail: alloc::format!("{other}"),
6945                    },
6946                })
6947        };
6948        // v7.39 (round 656) — the plain relational scan. This collect() was
6949        // the measured defect: one 64-byte `RowRef` per surviving row to
6950        // wrap an 8-byte pointer `filtered` already holds. Scalar
6951        // aggregates measured ~81 bytes/row of working memory because of
6952        // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6953        // one number. `AggRows::Ptrs` reads the pointers directly.
6954        let agg = aggregate::run(
6955            stmt,
6956            crate::join::AggRows::Ptrs(&filtered),
6957            schema_cols,
6958            Some(alias),
6959            Some(&agg_correlated),
6960            self.parallel_runner.0.as_deref(),
6961            Some(self.active_catalog()),
6962            Some(self),
6963        )?;
6964        self.finish_agg_result(agg, stmt, cancel)
6965    }
6966
6967    /// Single-table scan + projection path: WHERE filter (compiled when
6968    /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6969    /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6970    fn run_single_table_scan<'a>(
6971        &self,
6972        stmt: &SelectStatement,
6973        table: &'a spg_storage::Table,
6974        schema_cols: &'a [ColumnSchema],
6975        alias: &str,
6976        indexed_rows: Option<crate::index_access::Seeked<'a>>,
6977        cancel: CancelToken<'_>,
6978    ) -> Result<QueryResult, EngineError> {
6979        // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6980        // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6981        // deterministic `__tsm_fract(seed)` draws share one scan-local
6982        // state (isolated from the global random() PRNG); a fresh cell per
6983        // scan makes a repeat / rescan reproduce the same sample. Unused
6984        // and cheap when the query carries no sample.
6985        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6986        let ctx = self
6987            .ev_ctx(schema_cols, Some(alias))
6988            .with_sample_rng(&sample_cell);
6989        let projection = build_projection(
6990            &stmt.items,
6991            schema_cols,
6992            alias,
6993            self.speaks_mysql,
6994            Some(self.active_catalog()),
6995        )?;
6996        // v7.19 P5 — single-table SELECT path for SRF
6997        // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6998        // unnest in the projection list. When present, the
6999        // per-row processor emits one output row per array
7000        // element (broadcasting non-SRF projections from the
7001        // same input row). Empty / NULL arrays emit zero rows
7002        // for that input — PG semantics.
7003        // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
7004        let srf_idxs = self.srf_target_idxs(&projection);
7005        let srf_position = srf_idxs.first().copied();
7006        // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
7007        let mut srf_plan = if srf_position.is_some() {
7008            Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
7009        } else {
7010            None
7011        };
7012
7013        // Materialise the filter pass into `(order_key, projected_row)`
7014        // tuples. The order key is `None` when there's no ORDER BY clause.
7015        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
7016        // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
7017        // output row to the per-query byte budget as it is built, so a
7018        // fat single-table scan / sort REJECTS with QueryBytesExceeded
7019        // at ~the ceiling instead of materialising the whole table and
7020        // only noticing at the final enforce_row_limit check. Without
7021        // this, N concurrent fat scans peak at N×table and OOM the host.
7022        // `max_query_bytes = None` (the embedded default) = no ceiling,
7023        // so existing unbudgeted behaviour is byte-identical.
7024        let mut budget = ByteBudget::new(self.max_query_bytes);
7025        // v6.2.6 — Memoize per-row WHERE eval shares one cache.
7026        let mut memo = memoize::MemoizeCache::new();
7027        // v7.32 (perf knife D) — subquery-free WHERE compiles once;
7028        // the row loop then runs a flat step program instead of a
7029        // tree interpretation per row.
7030        let compiled_where: Option<eval::CompiledExpr> = stmt
7031            .where_
7032            .as_ref()
7033            .filter(|w| eval::fully_compilable(w))
7034            .map(|w| {
7035                // v7.38.8 — the scan filter runs the cheap half of its
7036                // conjunction first. Called from HERE and not from
7037                // `eval::compiled`, deliberately: the row loop lives in
7038                // that file, and adding a function to it cost this
7039                // query 11 % through layout alone while doing no work
7040                // for it. See `crate::qualorder`.
7041                match crate::qualorder::reordered(w) {
7042                    Some(r) => eval::compile_expr(&r, &ctx),
7043                    None => eval::compile_expr(w, &ctx),
7044                }
7045            });
7046        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7047        // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
7048        // SELECT-item scalar subquery for the PK-probe fast path. The
7049        // analysis (gate checks + catalog lookups) takes ~500 ns; doing
7050        // it once per query instead of once per row × 100 rows saves
7051        // ~50 µs and lets the per-row evaluation reduce to a single
7052        // index probe + outer-column read.
7053        let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
7054            .iter()
7055            .map(|p| {
7056                if let Expr::ScalarSubquery(inner) = &p.expr {
7057                    self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
7058                } else {
7059                    None
7060                }
7061            })
7062            .collect();
7063        let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
7064        // v7.39 (round 487) — a projection item that is a bare column
7065        // reference binds its position ONCE per query.
7066        //
7067        // Per row it used to walk `eval_expr_with_correlated` (a memo
7068        // lookup for "does this have a subquery", then an un-memoised
7069        // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
7070        // then `resolve_column`, which finds the column by scanning the
7071        // schema and comparing NAMES. On `SELECT g FROM h` that chain was
7072        // 19 % of self time for what is ultimately one cell read.
7073        //
7074        // `compile_column_pos` is the Step VM's resolver, already
7075        // `pub(crate)` and already reused by the aggregate's bind-once
7076        // path: it mirrors `resolve_column`'s happy layers and returns
7077        // None for anything that would reach an error, an ambiguity, or a
7078        // miss, so those still go the interpreter's way and keep its
7079        // exact message. A composite column is excluded for the same
7080        // reason `compile_into` excludes it — it must be rehydrated from
7081        // stored JSON, which is not a cell read.
7082        let proj_direct = bind_direct_columns(&projection, &ctx);
7083        let any_proj_direct = proj_direct.iter().any(Option::is_some);
7084        // v7.39 (round 605) — a projection item that cannot depend on the row
7085        // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
7086        // allocations a row against one for a plain column, `'abc' || 'def'`
7087        // six and `upper('abc')` five, all of them producing the same value
7088        // 50,000 times. An item that fails to evaluate is left alone, so its
7089        // error still comes from the row loop in the interpreter's wording.
7090        let proj_const: Vec<Option<Value<'static>>> = projection
7091            .iter()
7092            .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
7093            .collect();
7094        let any_proj_const = proj_const.iter().any(Option::is_some);
7095        crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
7096        // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
7097        // projection. Statement prep (`resolve_order_by_position`) can only map
7098        // `ORDER BY 1` onto the first SELECT item when that item is an
7099        // expression; a `*` is not one, so the literal survived to here and was
7100        // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
7101        // at all. The parser rewrites `SELECT unnest(a) x` into
7102        // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
7103        // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
7104        // back in input order. The projection is built by now, so the Nth output
7105        // column is known — resolve against it.
7106        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
7107        // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
7108        // EXPANDED rows, so a key naming a select-list item reads that item.
7109        let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
7110            srf_order_output_cols(&order_by, &projection)
7111        } else {
7112            Vec::new()
7113        };
7114        let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
7115        // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
7116        // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
7117        // Hoisted above the closure so the projection-eval path can
7118        // gate `memo` passing on it: the SELECT-item correlated-scalar
7119        // batch path scans the FULL inner table once (~5 ms for 12.5 k
7120        // rows) and is only a win when N outer rows is large; for small
7121        // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
7122        let early_cap: Option<usize> = if order_by.is_empty()
7123            && !stmt.distinct
7124            && !stmt.limit_with_ties
7125            && srf_position.is_none()
7126            && stmt.where_.is_none()
7127        {
7128            stmt.limit_literal()
7129                .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
7130        } else {
7131            None
7132        };
7133        // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
7134        // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
7135        // full-sort by the test gate) keep only the running top-`keep`
7136        // rows in memory instead of materialising every projected row,
7137        // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
7138        // space, not O(rows). `None` = accumulate everything (the prior
7139        // behaviour). The final `partial_sort_tagged(keep)` below still
7140        // runs and produces the identical rows.
7141        // v7.39 (round 683) — the declared collation for each ORDER BY
7142        // position, resolved once and carried beside `descs` for the same
7143        // reason `descs` is carried: it is per key position, not per row.
7144        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
7145        let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
7146            && !stmt.distinct
7147            && !stmt.limit_with_ties
7148            && srf_position.is_none()
7149            && !self.env_cfg().disable_topk
7150        {
7151            stmt.limit_literal().and_then(|l| {
7152                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
7153                (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
7154            })
7155        } else {
7156            None
7157        };
7158        // v7.38.19 — when the sort column is one the projection already
7159        // carries, build no key at all and sort by reading it.
7160        //
7161        // Restricted to the FULL sort: a top-N compares against a stored
7162        // boundary key and `WITH TIES` extends past the limit through the
7163        // keys, both of which need one to exist. DISTINCT keys on them
7164        // too, and an SRF's keys come from the EXPANDED row.
7165        // A COLLATION does not rule it out, but it has to be one that
7166        // orders these values the way bytes do -- decided on the values
7167        // themselves, further down, once they exist.
7168        let sort_by_output: Option<Vec<usize>> = if stmt.distinct
7169            || stmt.limit_with_ties
7170            || srf_position.is_some()
7171            || topk_stream.is_some()
7172        {
7173            None
7174        } else {
7175            order_by_output_cols_if_identical(&order_by, &projection, schema_cols)
7176        };
7177        // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
7178        // kept rows in `tagged`. Probing on the PROJECTED row as soon as
7179        // it is built means a duplicate costs neither a build_order_keys
7180        // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
7181        // a tagged slot, and the sort below runs over u survivors, not
7182        // n input rows — PG's hash-distinct-then-sort plan shape.
7183        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
7184            hashbrown::HashMap::new();
7185        let distinct_hb = hashbrown::DefaultHashBuilder::default();
7186        // v7.38.13 — which output positions must NOT fold. Built once per
7187        // scan from the projection, which carries the source column's
7188        // byte-wise-ness; see `FoldSpec`.
7189        let distinct_mask = fold_mask(&projection);
7190        // v7.39 (round 485) — one projection buffer for the whole scan
7191        // rather than a fresh `Vec` per input row. A row that survives
7192        // the DISTINCT probe takes the buffer with it (`mem::take`) and
7193        // the next row allocates a new one; a row that duplicates an
7194        // earlier one leaves the buffer — and its capacity — in place.
7195        // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
7196        // projected rows are duplicates, so that is 49 900 allocate /
7197        // free pairs the scan no longer performs. Shapes where every row
7198        // survives (plain projection, `DISTINCT` over a unique column)
7199        // allocate exactly as often as before.
7200        let mut proj_buf: Vec<Value<'static>> = Vec::new();
7201        // v7.39 (round 571) — buffers handed back by the top-N trim.
7202        // Round 485 made the scan share ONE projection buffer, but a
7203        // surviving row takes it (`mem::take`) and without DISTINCT
7204        // almost every row survives, so the next one starts from zero
7205        // capacity and allocates. The trim drops `keep` rows at a time
7206        // and their buffers come back here instead of being freed.
7207        let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
7208        let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
7209        // v7.39 (round 581) — the worst row the accumulator is currently
7210        // keeping. Anything that loses to it cannot reach the answer, so
7211        // it is dropped before its projection is ever built.
7212        let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
7213        // v7.38.20 — the boundary's own leading eight bytes, so a losing
7214        // row can be turned away before a key is built for it. Kept
7215        // beside the boundary and refreshed with it; `None` whenever the
7216        // boundary's first key is not one this can read, which sends
7217        // every row down the ordinary path.
7218        // v7.38.21 — and whether those bytes may be trusted under the
7219        // collation in force, which is the boundary's own text to answer.
7220        let mut topk_boundary_prefix: Option<(crate::orderby::PrefixKind, u64, bool)> = None;
7221        // v7.39 (round 582) — resolve each ORDER BY column once, not
7222        // once per row. See `order_by_bound_positions`.
7223        let order_bound =
7224            crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
7225        // v7.39.12 — a correlated scalar subquery in ORDER BY is
7226        // resolved for the row before its key is built.
7227        //
7228        // Uncorrelated subqueries are replaced by a literal before
7229        // execution; a correlated one cannot be, so it reached the
7230        // per-row evaluator — the one place that cannot run a subquery
7231        // — and the statement raised "subquery reached row eval".
7232        // Reported by sentori against 7.39.11; see
7233        // `Engine::order_by_resolved_for_row`.
7234        //
7235        // The `any` runs once, here, so an ordinary ORDER BY pays one
7236        // bool per row and nothing else.
7237        let order_has_subquery = order_by
7238            .iter()
7239            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
7240        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
7241        // v7.39 (round 581) — and it stops asking when the answer is
7242        // always "keep".
7243        //
7244        // The check earns its place only on rows it rejects. Over
7245        // ascending ids, `ORDER BY id DESC` never rejects one — every
7246        // row beats the current worst — so the comparison is pure
7247        // overhead there, measured at +5.5% in three batches out of
7248        // three. After a window of rows it looks at what it has
7249        // actually rejected and switches itself off if the shape is not
7250        // paying. The answers do not depend on it either way.
7251        // v7.38.21 — resolved once per query, not per row.
7252        //
7253        // No collation at all is the case v7.38.20 shipped. A DECLARED
7254        // one may still be answered by bytes, and which collations those
7255        // are is `Collated::ascii_byte_order`'s to say — the same
7256        // allowlist `byte_order_answers_the_collation` consults, so the
7257        // two cannot come to disagree about a collation. What that
7258        // allowlist requires of the TEXT is checked per row and on the
7259        // boundary, because a streaming top-N has no batch to check.
7260        let boundary_no_collation = order_colls.iter().all(Option::is_none);
7261        let boundary_collations_permit = boundary_no_collation
7262            || order_colls
7263                .iter()
7264                .flatten()
7265                .all(crate::collate::Collated::ascii_byte_order);
7266        const BOUNDARY_WINDOW: u32 = 8192;
7267        let mut boundary_checks: u32 = 0;
7268        let mut boundary_rejects: u32 = 0;
7269        let mut boundary_check_on = true;
7270        // Inline the per-row work in a closure so the indexed and full-
7271        // scan branches share the body.
7272        // v7.38.19 — `check_where` is per CALL SITE, not per closure: the
7273        // full-scan loops below must apply the predicate, and the
7274        // indexed loop must not when the seek already did. A captured
7275        // flag would have to be right for both.
7276        let mut process_row = |row: &Row<'static>,
7277                               loop_idx: usize,
7278                               check_where: bool|
7279         -> Result<(), EngineError> {
7280            if loop_idx.is_multiple_of(256) {
7281                cancel.check()?;
7282            }
7283            if !check_where {
7284                // The seek answered the whole predicate. See
7285                // `index_access::Seeked`.
7286            } else if let Some(cw) = &compiled_where {
7287                let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7288                    .map_err(EngineError::Eval)?;
7289                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7290                    return Ok(());
7291                }
7292            } else if let Some(where_expr) = &stmt.where_ {
7293                let cond =
7294                    self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7295                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7296                    return Ok(());
7297                }
7298            }
7299            // Under DISTINCT the keys are built AFTER the dup probe
7300            // (survivors only); the non-distinct order is unchanged.
7301            // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7302            // row further down, and building them here would evaluate the
7303            // ORDER BY against the INPUT row: a key naming the SRF's own
7304            // output became a scalar call to it, which is where
7305            // "function unnest(integer[]) does not exist" came from.
7306            let order_keys = if order_by.is_empty()
7307                || stmt.distinct
7308                || srf_position.is_some()
7309                // v7.38.19 — the branch below builds whatever key it
7310                // needs from the projected values, collation included,
7311                // so nothing has to be built here for it.
7312                //
7313                // A draft that skipped them here but still let the
7314                // COLLATED case fall through to the key-based sort put a
7315                // mixed column back in INSERT order: every key empty,
7316                // every row equal, a stable sort faithfully preserving
7317                // nothing. The rule is one decision, not two.
7318                || sort_by_output.is_some()
7319            {
7320                Vec::new()
7321            } else {
7322                // v7.38.20 — turn a decisively losing row away before
7323                // its key is built. Only the FIRST key is read, and only
7324                // its leading eight bytes; a tie there decides nothing
7325                // and falls through to the full path below.
7326                //
7327                // ASC only: under DESC the boundary is the largest kept
7328                // key and the comparison flips, which this deliberately
7329                // does not try to express — a second direction in a
7330                // fast-path predicate is how one of them ends up wrong.
7331                if boundary_check_on
7332                    && let Some((_, descs)) = &topk_stream
7333                    && !descs.first().copied().unwrap_or(false)
7334                    && order_by.len() == 1
7335                    && boundary_collations_permit
7336                    && let Some((bkind, bp, boundary_is_ascii)) = topk_boundary_prefix
7337                    && let Some((rkind, rp, row_is_ascii)) =
7338                        crate::orderby::first_key_prefix(&order_bound, row)
7339                    && bkind == rkind
7340                    && (boundary_no_collation || (boundary_is_ascii && row_is_ascii))
7341                    && rp > bp
7342                {
7343                    boundary_checks += 1;
7344                    boundary_rejects += 1;
7345                    if boundary_checks == BOUNDARY_WINDOW {
7346                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7347                    }
7348                    return Ok(());
7349                }
7350                let mut buf = key_pool.pop().unwrap_or_default();
7351                if order_has_subquery {
7352                    // A substituted literal is no longer a bound column.
7353                    let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7354                    crate::orderby::build_order_keys_bound(
7355                        per_row.as_deref().unwrap_or(&order_by),
7356                        &unbound,
7357                        &order_colls,
7358                        row,
7359                        &ctx,
7360                        &mut buf,
7361                    )?;
7362                } else {
7363                    crate::orderby::build_order_keys_bound(
7364                        &order_by,
7365                        &order_bound,
7366                        &order_colls,
7367                        row,
7368                        &ctx,
7369                        &mut buf,
7370                    )?;
7371                }
7372                // v7.39 (round 581) — reject before projecting.
7373                //
7374                // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7375                // 50 distinct `g` decides nearly every row on the FIRST
7376                // key, and PG answers it FASTER than the single-key form
7377                // (7.4 ms against 10.4) because a rejected row costs it
7378                // one comparison. SPG built both keys AND the projected
7379                // row for all 500k before throwing them away. The keys
7380                // are needed to compare; the projection is not.
7381                if boundary_check_on
7382                    && let Some((_, descs)) = &topk_stream
7383                    && let Some(b) = &topk_boundary
7384                {
7385                    boundary_checks += 1;
7386                    let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7387                        == core::cmp::Ordering::Greater;
7388                    if loses {
7389                        boundary_rejects += 1;
7390                    }
7391                    if boundary_checks == BOUNDARY_WINDOW {
7392                        // Keep asking only if it has been rejecting at
7393                        // least a quarter of what it saw.
7394                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7395                    }
7396                    if loses {
7397                        buf.clear();
7398                        key_pool.push(buf);
7399                        return Ok(());
7400                    }
7401                }
7402                buf
7403            };
7404            if srf_position.is_some() {
7405                let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7406                for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7407                    if stmt.distinct {
7408                        let bucket = seen_distinct
7409                            .entry(norm_hash_row(
7410                                &out,
7411                                &distinct_hb,
7412                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7413                            ))
7414                            .or_default();
7415                        if bucket.iter().any(|i| {
7416                            row_eq_norm(
7417                                &tagged[i].1,
7418                                &out,
7419                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7420                            )
7421                        }) {
7422                            continue;
7423                        }
7424                        bucket.push(tagged.len());
7425                    }
7426                    budget.charge(approx_row_bytes(&out))?;
7427                    // The keys come from THIS expanded row: a key naming a
7428                    // select-list item reads its value, anything else is
7429                    // still evaluated against the input row.
7430                    let keys = if order_by.is_empty() {
7431                        Vec::new()
7432                    } else {
7433                        let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7434                        for (k, ob) in order_by.iter().enumerate() {
7435                            kv.push(match srf_order_cols.get(k).copied().flatten() {
7436                                Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7437                                None => eval::eval_expr(&ob.expr, row, &ctx)
7438                                    .map_err(EngineError::Eval)?,
7439                            });
7440                        }
7441                        // Packed by the same code every other ORDER BY uses,
7442                        // so DESC / NULLS FIRST / the MySQL rule are not
7443                        // restated here.
7444                        let key_row = Row::new(kv);
7445                        let mut buf = Vec::new();
7446                        crate::orderby::build_order_keys_bound(
7447                            &order_by,
7448                            &srf_key_bound,
7449                            &order_colls,
7450                            &key_row,
7451                            &ctx,
7452                            &mut buf,
7453                        )?;
7454                        buf
7455                    };
7456                    tagged.push((keys, out));
7457                }
7458            } else {
7459                let values = &mut proj_buf;
7460                values.clear();
7461                values.reserve(projection.len());
7462                for (i, p) in projection.iter().enumerate() {
7463                    // v7.37.x (docker-fair SCALARSQ attack) — pre-
7464                    // analysed PK-probe fast path. The per-row work is
7465                    // a read of outer.col from the row plus an index
7466                    // probe — no Expr clone, no walker, no
7467                    // `eval_expr_with_correlated` framework.
7468                    if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7469                        values.push(self.probe_with_pk_fast_path(fp, row));
7470                        continue;
7471                    }
7472                    // v7.39 (round 605) — the same value every row.
7473                    if any_proj_const && let Some(v) = &proj_const[i] {
7474                        values.push(v.clone());
7475                        continue;
7476                    }
7477                    // v7.39 (round 487) — bound column: read the cell.
7478                    // This is `rehydrate_cell`'s body for a non-composite
7479                    // column, which is what the whole chain below reduces
7480                    // to once the name has been resolved.
7481                    if any_proj_direct && let Some(pos) = proj_direct[i] {
7482                        crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7483                        values.push(row.values[pos].clone().into_owned());
7484                        continue;
7485                    }
7486                    // v7.24 (round-16 B) — correlated-aware.
7487                    // v7.37.x (docker-fair SCALARSQ attack) — share the
7488                    // per-row memo with projection. Required for the
7489                    // batch-evaluated correlated-scalar path to fire on
7490                    // SELECT-item scalar subqueries; otherwise each row
7491                    // re-executes the inner.
7492                    //
7493                    // Skip the memo when the outer row count is small
7494                    // (early-limited): the batch path scans the FULL
7495                    // inner table to build a GroupMap (~5 ms for a
7496                    // 12.5 k-row inner), while per-row execution with a
7497                    // PK index seek is ~5 µs per call — much cheaper for
7498                    // N ≤ ~1000 outer rows.
7499                    let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7500                    let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7501                    values.push(
7502                        self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7503                    );
7504                }
7505                crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7506                if stmt.distinct {
7507                    let bucket = seen_distinct
7508                        .entry(norm_hash_values(
7509                            &proj_buf,
7510                            &distinct_hb,
7511                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7512                        ))
7513                        .or_default();
7514                    if bucket.iter().any(|i| {
7515                        values_eq_norm(
7516                            &tagged[i].1.values,
7517                            &proj_buf,
7518                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7519                        )
7520                    }) {
7521                        crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7522                        return Ok(());
7523                    }
7524                    bucket.push(tagged.len());
7525                }
7526                let out = Row::new(core::mem::replace(
7527                    &mut proj_buf,
7528                    proj_pool.pop().unwrap_or_default(),
7529                ));
7530                let order_keys = if stmt.distinct && !order_by.is_empty() {
7531                    // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7532                    // the bound-cell path precisely so an ORDER BY key that
7533                    // names a column is READ instead of evaluated, and the
7534                    // non-DISTINCT branch above has passed it ever since;
7535                    // this branch never did, so `SELECT DISTINCT k .. ORDER
7536                    // BY k` resolved "k" by string for every surviving row.
7537                    let mut buf = key_pool.pop().unwrap_or_default();
7538                    if order_has_subquery {
7539                        // A substituted literal is no longer a bound column.
7540                        let per_row =
7541                            self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
7542                        crate::orderby::build_order_keys_bound(
7543                            per_row.as_deref().unwrap_or(&order_by),
7544                            &unbound,
7545                            &order_colls,
7546                            row,
7547                            &ctx,
7548                            &mut buf,
7549                        )?;
7550                    } else {
7551                        crate::orderby::build_order_keys_bound(
7552                            &order_by,
7553                            &order_bound,
7554                            &order_colls,
7555                            row,
7556                            &ctx,
7557                            &mut buf,
7558                        )?;
7559                    }
7560                    buf
7561                } else {
7562                    order_keys
7563                };
7564                budget.charge(approx_row_bytes(&out))?;
7565                tagged.push((order_keys, out));
7566            }
7567            // Streaming top-N: bound the accumulator to O(keep) rows.
7568            if let Some((k, descs)) = &topk_stream {
7569                crate::orderby::topk_trim_recycling(
7570                    &mut tagged,
7571                    *k,
7572                    descs,
7573                    &mut proj_pool,
7574                    &mut key_pool,
7575                    &mut topk_boundary,
7576                );
7577                // The prefix follows the boundary it summarises.
7578                topk_boundary_prefix = topk_boundary
7579                    .as_ref()
7580                    .and_then(|b| b.first())
7581                    .and_then(crate::orderby::order_key_prefix);
7582            }
7583            Ok(())
7584        };
7585        // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7586        // load-bearing full-scan path. This is the primary single-table
7587        // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7588        // in-place writers retain dead/old versions, an ungated scan
7589        // here would return them, so the gate must land BEFORE the
7590        // writers flip (see the plan's activation-order rule). A no-op
7591        // today: every hot row is frozen or committed-and-alive under
7592        // the reader's snapshot, so `is_row_visible` returns true for
7593        // all of them (verified by the full e2e suite staying green).
7594        let scan_snapshot = self.current_snapshot();
7595        let mut emitted: usize = 0;
7596        if let Some(seeked) = &indexed_rows {
7597            let recheck = !seeked.exact;
7598            for (loop_idx, cow) in seeked.rows.iter().enumerate() {
7599                if let Some(cap) = early_cap
7600                    && emitted >= cap
7601                {
7602                    break;
7603                }
7604                process_row(cow.as_ref(), loop_idx, recheck)?;
7605                emitted = emitted.saturating_add(1);
7606            }
7607        } else {
7608            // v7.39 (round 570) — the row store is a 32-way trie, so
7609            // indexing it is four dependent loads. Round 567 measured
7610            // -18% on the aggregate scan from holding the leaf between
7611            // rows; this is the same loop for the projecting scan.
7612            let mut rows_cur = table.rows().run_cursor();
7613            // v7.38.11 — see the aggregate scan above: a BRIN index on a
7614            // column this WHERE bounds says which slots cannot match.
7615            let brin_slots = stmt
7616                .where_
7617                .as_ref()
7618                .and_then(|w| crate::brin::candidate_slots(w, table))
7619                .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7620            for i in brin_slots.into_iter().flatten() {
7621                if let Some(cap) = early_cap
7622                    && emitted >= cap
7623                {
7624                    break;
7625                }
7626                // Skip rows this snapshot cannot see (invisible rows do
7627                // not count toward the LIMIT).
7628                if !table.is_row_visible(i, &scan_snapshot) {
7629                    continue;
7630                }
7631                let Some(row) = rows_cur.get(i) else { continue };
7632                process_row(row, i, true)?;
7633                emitted = emitted.saturating_add(1);
7634            }
7635            // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7636            // rows into the same loop. The full-scan path here is the
7637            // load-bearing single-table SELECT executor, and pre-
7638            // 7.35.1 it only walked `table.rows()` (hot), so any
7639            // `SELECT … FROM t` against a table with cold segments
7640            // silently returned a subset.
7641            let cold_rows = self.iter_cold_rows_of_table(table);
7642            for (offset, row) in cold_rows.iter().enumerate() {
7643                if let Some(cap) = early_cap
7644                    && emitted >= cap
7645                {
7646                    break;
7647                }
7648                process_row(row, table.row_count() + offset, true)?;
7649                emitted = emitted.saturating_add(1);
7650            }
7651        }
7652
7653        // (DISTINCT already de-duped STREAMING inside process_row, so the
7654        // sort below only sees the u survivors and the partial-sort
7655        // budget applies to DISTINCT too.)
7656        if !order_by.is_empty() {
7657            // Partial-sort fast path: when LIMIT is small relative to
7658            // the row count, select_nth_unstable + sort just the
7659            // prefix is O(n + k log k) instead of O(n log n).
7660            // WITH TIES needs the full sort so the tie extension can
7661            // scan past `limit` to find rows that share the last-kept
7662            // row's key.
7663            let keep = if stmt.limit_with_ties
7664                // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7665                // forces the full-sort fallback by suppressing the
7666                // partial-sort `keep` budget. See
7667                // `xtests/sigil/test-mode-gucs.md`.
7668                || self.env_cfg().disable_topk
7669            {
7670                None
7671            } else {
7672                stmt.limit_literal()
7673                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7674            };
7675            let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7676            if let Some(cols) = &sort_by_output {
7677                // No keys were built; the sort reads the projected row.
7678                // The comparator is the value-level one the window
7679                // functions and the key path both defer to, so DESC,
7680                // NULLS placement, the MySQL fold and the collation are
7681                // not restated here.
7682                let terms: Vec<(usize, bool, Option<bool>)> = cols
7683                    .iter()
7684                    .zip(order_by.iter())
7685                    .map(|(c, o)| (*c, o.desc, o.nulls_first))
7686                    .collect();
7687                let mysql = ctx.mysql_dialect;
7688                // v7.38.19 — sort a PERMUTATION carrying the first eight
7689                // bytes, not the rows.
7690                //
7691                // The elements above are `(Vec<OrderKey>, Row)`, 48 bytes,
7692                // and driftsort moves them ~n log n times: 7.4 M moves at
7693                // 400,000 rows. Worse, every comparison chases three
7694                // dependent loads PER SIDE to reach the byte it wants --
7695                // the row's `Vec`, the `Value`, then the string's own
7696                // buffer -- and a profile of this sort put 35% of its
7697                // working samples in the sort machinery around that.
7698                //
7699                // A `(u64, u32)` is 16 bytes and the comparison reads it
7700                // straight out of the array. The u64 is the first eight
7701                // bytes big-endian, zero-padded, which ORDERS THE SAME as
7702                // the string: if two differ inside those bytes they differ
7703                // at the same index either way, and a string shorter than
7704                // eight pads with zeros exactly where `[u8]`'s own
7705                // comparison runs out. Equal prefixes fall through to the
7706                // full comparator, so nothing rests on the padding being
7707                // clever.
7708                //
7709                // The tail-break on the index is what keeps the sort
7710                // STABLE, which `sort_by` was giving for free and an
7711                // unstable sort over a permutation would not.
7712                // v7.38.19 — three ways to sort these rows, and which
7713                // one is right turns on the values, which is why it is
7714                // decided here rather than at plan time.
7715                //
7716                //   * the collation orders these values the way bytes do
7717                //     -- take the eight-byte key below
7718                //   * it does not, but there IS a collation -- build its
7719                //     sort key once per row and order the permutation on
7720                //     those, which is what the key path did, done from
7721                //     the projected value instead of during the scan
7722                //   * no collation at all -- the eight-byte key again
7723                //
7724                // The middle case is the one a draft got wrong by
7725                // leaving the rows to a key path whose keys it had just
7726                // skipped building.
7727                let mut keep_sorted = false;
7728                let bytes_answer = byte_order_answers_the_collation(&tagged, &terms, &order_colls);
7729                if !bytes_answer && let Some(coll) = order_colls.first().and_then(Option::as_ref) {
7730                    let (first_col, first_desc, _) = terms[0];
7731                    let mut order: Vec<(Vec<u8>, u32)> = Vec::with_capacity(tagged.len());
7732                    for (i, row) in tagged.iter().enumerate() {
7733                        let k = match row.1.values.get(first_col) {
7734                            Some(Value::Text(t)) => coll.sort_key_of(t).unwrap_or_else(|| {
7735                                let mut v = Vec::with_capacity(t.len() + 1);
7736                                v.push(0);
7737                                v.extend_from_slice(t.as_bytes());
7738                                v
7739                            }),
7740                            _ => Vec::new(),
7741                        };
7742                        order.push((k, u32::try_from(i).unwrap_or(u32::MAX)));
7743                    }
7744                    order.sort_by(|(ka, ia), (kb, ib)| {
7745                        let c = ka.cmp(kb);
7746                        let c = if first_desc { c.reverse() } else { c };
7747                        if c != core::cmp::Ordering::Equal {
7748                            return c;
7749                        }
7750                        row_cmp_by_index(&tagged, &terms, &order_colls, mysql, *ia, *ib)
7751                            .then_with(|| ia.cmp(ib))
7752                    });
7753                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7754                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7755                    tagged = order
7756                        .iter()
7757                        .map(|&(_, i)| {
7758                            slots[i as usize]
7759                                .take()
7760                                .expect("the permutation names each row once")
7761                        })
7762                        .collect();
7763                    keep_sorted = true;
7764                }
7765                // v7.38.20 — a key that does NOT discriminate is still
7766                // worth sorting on, as long as the runs it leaves are
7767                // handled once instead of n log n times.
7768                //
7769                // `text (26 values)` is two hundred identical characters
7770                // drawn from twenty-six letters, so every eight-byte
7771                // prefix inside a letter is the same and 15,384 rows tie
7772                // on it. A comparison sort then asks ~7.4 M questions of
7773                // which nearly all are a two-hundred-byte `memcmp`
7774                // answering EQUAL: profiled, 30% of the working samples
7775                // sat in `memcmp` and 37% in the sort machinery.
7776                //
7777                // Sorting the integer keys is cheap. What each run needs
7778                // afterwards is ONE pass: if every value in it is equal,
7779                // input order already IS the stable answer, and proving
7780                // that costs n-1 comparisons rather than n log n. Only a
7781                // run that is not all-equal gets sorted.
7782                //
7783                // Single-term only. With a second ORDER BY column an
7784                // all-equal first term does not settle the row order --
7785                // the later terms still speak -- and the shortcut would
7786                // drop them.
7787                let all_keys = if keep_sorted {
7788                    None
7789                } else {
7790                    sort_keys_of(&tagged, terms[0].0)
7791                };
7792                let low_card = !keep_sorted
7793                    && terms.len() == 1
7794                    && all_keys
7795                        .as_ref()
7796                        .is_some_and(|(keys, exact)| !*exact && !key_discriminates(keys));
7797                let keyed =
7798                    all_keys.filter(|(keys, exact)| *exact || key_discriminates(keys) || low_card);
7799                if keep_sorted {
7800                    // The collated permutation above already placed every
7801                    // row. A draft let the byte-order fallback run after
7802                    // it and undo the whole thing.
7803                } else if let Some((mut order, exact)) = keyed {
7804                    let (first_col, first_desc, _) = terms[0];
7805                    let row_cmp = |ia: u32, ib: u32| -> core::cmp::Ordering {
7806                        let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
7807                        for (col, desc, nf) in &terms {
7808                            let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col))
7809                            else {
7810                                continue;
7811                            };
7812                            let ord = match (va, vb) {
7813                                (Value::Text(x), Value::Text(y)) if !mysql => {
7814                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7815                                    if *desc { c.reverse() } else { c }
7816                                }
7817                                _ => {
7818                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7819                                }
7820                            };
7821                            if ord != core::cmp::Ordering::Equal {
7822                                return ord;
7823                            }
7824                        }
7825                        core::cmp::Ordering::Equal
7826                    };
7827                    let _ = first_col;
7828                    if low_card {
7829                        // Integer sort first, then one pass per run.
7830                        order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
7831                            let c = pa.cmp(&pb);
7832                            let c = if first_desc { c.reverse() } else { c };
7833                            c.then_with(|| ia.cmp(&ib))
7834                        });
7835                        let mut lo = 0;
7836                        while lo < order.len() {
7837                            let mut hi = lo + 1;
7838                            while hi < order.len() && order[hi].0 == order[lo].0 {
7839                                hi += 1;
7840                            }
7841                            if hi - lo > 1 {
7842                                let head = tagged[order[lo].1 as usize].1.values.get(first_col);
7843                                let uniform = order[lo + 1..hi].iter().all(|&(_, i)| {
7844                                    tagged[i as usize].1.values.get(first_col) == head
7845                                });
7846                                if !uniform {
7847                                    order[lo..hi].sort_by(|&(_, ia), &(_, ib)| {
7848                                        row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
7849                                    });
7850                                }
7851                                // A uniform run is already in index
7852                                // order, which IS the stable answer.
7853                            }
7854                            lo = hi;
7855                        }
7856                    } else {
7857                        order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
7858                            let c = pa.cmp(&pb);
7859                            let c = if first_desc { c.reverse() } else { c };
7860                            if c != core::cmp::Ordering::Equal {
7861                                return c;
7862                            }
7863                            // An EXACT key that ties means the values are
7864                            // equal, so only the remaining terms can speak.
7865                            // A prefix that ties has decided nothing yet and
7866                            // the first term must be asked again, which
7867                            // `row_cmp` does by walking every term from the
7868                            // start.
7869                            if exact && terms.len() == 1 {
7870                                return ia.cmp(&ib);
7871                            }
7872                            row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
7873                        });
7874                    }
7875                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7876                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7877                    tagged = order
7878                        .iter()
7879                        .map(|&(_, i)| {
7880                            slots[i as usize]
7881                                .take()
7882                                .expect("the permutation names each row once")
7883                        })
7884                        .collect();
7885                } else {
7886                    tagged.sort_by(|a, b| {
7887                        for (i, (col, desc, nf)) in terms.iter().enumerate() {
7888                            let va = a.1.values.get(*col);
7889                            let vb = b.1.values.get(*col);
7890                            let (Some(va), Some(vb)) = (va, vb) else {
7891                                continue;
7892                            };
7893                            let _ = i;
7894                            // v7.38.19 — two non-NULL strings, no MySQL fold, is
7895                            // where a text sort spends every one of its ~7 M
7896                            // comparisons, and the shared comparator cannot be
7897                            // inlined into this loop: it carries NULL placement,
7898                            // the fold, the NUMERIC bignum gate and the float
7899                            // total order. Answering that one pair here is the
7900                            // same answer by the same route — `value_cmp`'s
7901                            // leading same-variant arm is `x.cmp(y)`, and the
7902                            // raw comparator's last act is this reverse.
7903                            let ord = match (va, vb) {
7904                                (Value::Text(x), Value::Text(y)) if !mysql => {
7905                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7906                                    if *desc { c.reverse() } else { c }
7907                                }
7908                                _ => {
7909                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7910                                }
7911                            };
7912                            if ord != core::cmp::Ordering::Equal {
7913                                return ord;
7914                            }
7915                        }
7916                        core::cmp::Ordering::Equal
7917                    });
7918                }
7919            } else {
7920                crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7921            }
7922        }
7923
7924        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7925        // past the truncated tail through every row that shares the
7926        // last-kept row's ORDER BY key. The tie check uses the
7927        // already-computed `(order_keys, row)` pairs so it matches
7928        // the sort comparator exactly. DISTINCT + WITH TIES falls
7929        // through to the no-ties path (PG also disallows their
7930        // combination; SPG silently drops the tie extension here so
7931        // the customer doesn't see a hard error mid-query — the
7932        // user-visible result is still correct, just narrower).
7933        let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7934            apply_offset_and_limit_tagged(
7935                &mut tagged,
7936                stmt.offset_literal(),
7937                stmt.limit_literal(),
7938                true,
7939            );
7940            tagged.into_iter().map(|(_, r)| r).collect()
7941        } else {
7942            // DISTINCT already de-duped pre-sort above.
7943            let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7944            apply_offset_and_limit(
7945                &mut output_rows,
7946                stmt.offset_literal(),
7947                stmt.limit_literal(),
7948            );
7949            output_rows
7950        };
7951
7952        let columns: Vec<ColumnSchema> = projection
7953            .into_iter()
7954            .map(|p| p.to_column_schema())
7955            .collect();
7956
7957        Ok(QueryResult::Rows {
7958            columns,
7959            rows: output_rows,
7960        })
7961    }
7962
7963    /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7964    /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7965    /// select items for the surviving rows only — PG's Result-above-
7966    /// Limit shape, where SubPlan loops equal the OUTPUT row count
7967    /// (50) instead of the group count (24k).
7968    fn finish_agg_result(
7969        &self,
7970        mut agg: aggregate::AggResult,
7971        stmt: &SelectStatement,
7972        cancel: CancelToken<'_>,
7973    ) -> Result<QueryResult, EngineError> {
7974        apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7975        if !agg.deferred.is_empty() {
7976            apply_offset_and_limit(
7977                &mut agg.synth_rows,
7978                stmt.offset_literal(),
7979                stmt.limit_literal(),
7980            );
7981            let ctx = EvalContext::new(&agg.synth_schema, None);
7982            let mut memo = memoize::MemoizeCache::default();
7983            // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7984            // Deferred subqueries are referenced only by surviving
7985            // select-list rows (≤ LIMIT), so their correlation keys are
7986            // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7987            // each batchable subquery's group map over just those keys
7988            // via per-key index seek; the per-row splice loop below then
7989            // reuses the seeded map. A join-shaped or un-indexed inner
7990            // falls through to the all-keys batch inside the call (built
7991            // eagerly here instead of lazily on row 0 — same cost), so
7992            // it still pays the full scan, never the 715 ms per-row
7993            // direct eval; its index-nested-loop probe is the next
7994            // knife. Genuinely non-batchable shapes return None and are
7995            // left unseeded for the loop's per-row resolver, as before.
7996            for (_, expr) in &agg.deferred {
7997                let mut subs: Vec<&SelectStatement> = Vec::new();
7998                collect_scalar_subqueries(expr, &mut subs);
7999                for sub in subs {
8000                    let repr = alloc::format!("{sub}");
8001                    if memo.group_maps.contains_key(&repr) {
8002                        continue;
8003                    }
8004                    if let Some(gm) = self.try_batch_correlated_scalar(
8005                        sub,
8006                        Some((&agg.synth_rows, &ctx)),
8007                        cancel,
8008                    )? {
8009                        memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
8010                    }
8011                }
8012            }
8013            for (ri, srow) in agg.synth_rows.iter().enumerate() {
8014                cancel.check()?;
8015                for (col, expr) in &agg.deferred {
8016                    let v =
8017                        self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
8018                    if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
8019                        *cell = v;
8020                    }
8021                }
8022            }
8023        }
8024        Ok(QueryResult::Rows {
8025            columns: agg.columns,
8026            rows: agg.rows,
8027        })
8028    }
8029
8030    /// v7.37 — streaming projection for the joined-non-aggregate
8031    /// shape (multi-table FROM, all projection items bound, no
8032    /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
8033    /// UNION). Walks the deferred join survivors and emits
8034    /// `&[&Value]` borrowed straight out of the source tables — no
8035    /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
8036    /// on the mailrs `PROJ` shape (about 4 ms saved).
8037    ///
8038    /// Returns `Ok(None)` when the shape doesn't qualify; the caller
8039    /// then falls back to the materialising path.
8040    /// v7.37 (round 831) — stream a joinless SELECT straight off the
8041    /// stored table, one row at a time, without ever building a row set.
8042    ///
8043    /// Returns `Ok(None)` for anything this cannot serve, and the caller
8044    /// falls through to the deferred-join path exactly as before: a
8045    /// missing table, or a cold tier whose hydration the fallback handles.
8046    /// Sort a single-table scan through the external sorter, so the
8047    /// answer's size is bounded by `work_mem` and not by the input.
8048    ///
8049    /// Sorting held every row twice — the scan's `Vec<Row>` and the
8050    /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
8051    /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
8052    /// enough ORDER BY took the server down, which is a liveness
8053    /// problem before it is a performance one.
8054    ///
8055    /// A SEPARATE walk rather than a change to `run_single_table_scan`,
8056    /// following what round 831 did for the joinless shape. That
8057    /// function is 552 lines whose projection loop is entangled with
8058    /// DISTINCT (which indexes back into the tagged vector) and with
8059    /// streaming top-N (whose boundary moves as the scan runs); both
8060    /// assume the projection has already happened when a row is
8061    /// pushed, which is exactly what spilling has to defer. Two earlier
8062    /// attempts tried to rework that loop and were reverted. Here the
8063    /// existing path is untouched and this one only claims shapes it
8064    /// can serve, so a decline costs nothing.
8065    ///
8066    /// Records are SOURCE rows, not projected ones: `finish` re-derives
8067    /// keys from what it decodes, and an ORDER BY key need not be in
8068    /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
8069    fn try_spill_sorted_scan(
8070        &self,
8071        stmt: &SelectStatement,
8072        from: &FromClause,
8073        cancel: CancelToken<'_>,
8074    ) -> Result<Option<QueryResult>, EngineError> {
8075        // Shapes this walk does not serve. Each one either needs the
8076        // whole tagged vector addressable (DISTINCT probes back into
8077        // it, WITH TIES re-reads its tail) or is already bounded
8078        // without spilling (a LIMIT makes the partial sort O(keep)).
8079        if !self.can_spill()
8080            || stmt.order_by.is_empty()
8081            || stmt.distinct
8082            || stmt.limit_with_ties
8083            || stmt.limit_literal().is_some()
8084            || !from.joins.is_empty()
8085            || from.primary.lateral_subquery.is_some()
8086            || from.primary.unnest_expr.is_some()
8087            || from.primary.generate_series_args.is_some()
8088            || select_has_window(stmt)
8089        {
8090            return Ok(None);
8091        }
8092        // A parent's rows are its children's. These walks scan the named
8093        // relation alone, so a partitioned or inherited parent comes back
8094        // short — and silently: the corpus caught `SELECT id FROM pr
8095        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8096        // parent's own rows instead of the partitions'. `ONLY` is exactly
8097        // the case that does not fan out, so it stays, which is the test
8098        // the FROM-clause fan-out itself makes.
8099        if !from.primary.only
8100            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8101        {
8102            return Ok(None);
8103        }
8104        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8105            return Ok(None);
8106        };
8107        // Cold-tier rows live outside `rows()`; this walk would drop
8108        // them silently, the same reason round 831's walk declines.
8109        if table.has_cold_rows_fast() {
8110            return Ok(None);
8111        }
8112
8113        let alias = from
8114            .primary
8115            .alias
8116            .as_deref()
8117            .unwrap_or(from.primary.name.as_str());
8118        let cols = table.schema().columns.clone();
8119        let sess = self.dml_session();
8120        let ctx = EvalContext::new(&cols, Some(alias))
8121            .with_catalog(self.active_catalog())
8122            .with_session(&sess);
8123        let projection = build_projection(
8124            &stmt.items,
8125            &cols,
8126            alias,
8127            self.speaks_mysql,
8128            Some(self.active_catalog()),
8129        )?;
8130        let order_by = stmt.order_by.clone();
8131        // The same one-shot resolution the general path does (round
8132        // 582): each ORDER BY column is bound once, not once per row.
8133        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8134        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8135        // Resolved BEFORE the scan, because it now decides what the sort
8136        // STORES and not just what it decodes (round 995).
8137        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8138
8139        // v7.38.22 — resolved HERE, because this path did not resolve
8140        // them at all.
8141        //
8142        // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
8143        // "en_US.utf8"` in BYTE order on this path — and swallowed an
8144        // unknown collation name rather than raising — because the sorter
8145        // below compared with an empty collation slice. The materialising
8146        // path honoured both. Which answer a query got depended on which
8147        // path the planner took, and this is the path a plain single-table
8148        // SELECT takes.
8149        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
8150        // v7.39.12 — a correlated scalar subquery in ORDER BY is
8151        // resolved for the row before its key is built.
8152        //
8153        // Uncorrelated subqueries are replaced by a literal before
8154        // execution; a correlated one cannot be, so it reached the
8155        // per-row evaluator — the one place that cannot run a subquery
8156        // — and the statement raised "subquery reached row eval".
8157        // Reported by sentori against 7.39.11; see
8158        // `Engine::order_by_resolved_for_row`.
8159        //
8160        // The `any` runs once, here, so an ordinary ORDER BY pays one
8161        // bool per row and nothing else.
8162        let order_has_subquery = order_by
8163            .iter()
8164            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
8165        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
8166        let mut sorter = crate::extsort::ExternalSorter::new(
8167            self.temp_run_factory,
8168            self.session_work_mem_bytes(),
8169            cols.clone(),
8170            &descs,
8171            &order_colls,
8172        )
8173        .with_stats(&self.spill_stats)
8174        .with_pruned(&needed);
8175        let snapshot = self.current_snapshot();
8176        // One key buffer for the whole scan: `push` drains it and leaves
8177        // the capacity behind.
8178        let mut keys: Vec<OrderKey> = Vec::new();
8179        // r1024 — compile the predicate once for the scan.
8180        //
8181        // These two sorted-spill scans are the paths a single-table SELECT
8182        // with an ORDER BY takes, and they were the last row-returning ones
8183        // still walking the expression tree per row. r1023 did the
8184        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8185        // exactly this shape.
8186        //
8187        // Found from the profile's CALL TREE rather than its leaves. The
8188        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8189        // 261, `mod_op` 178 — and two attempts at reasoning out which
8190        // function asked for it were both wrong. The tree names the caller
8191        // chain, and it named this one.
8192        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8193            .where_
8194            .as_ref()
8195            .filter(|w| crate::eval::fully_compilable(w))
8196            .map(|w| crate::eval::compile_expr(w, &ctx));
8197        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8198        for (i, row) in table.scan_visible_from(0, &snapshot) {
8199            if i.is_multiple_of(256) {
8200                cancel.check()?;
8201            }
8202            if let Some(c) = &compiled_where {
8203                if !crate::eval::compiled::eval_compiled_pred(
8204                    c,
8205                    row,
8206                    &ctx,
8207                    &mut eval_stack,
8208                    ctx.mysql_dialect,
8209                )? {
8210                    continue;
8211                }
8212            } else if let Some(w) = &stmt.where_ {
8213                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8214                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8215                    continue;
8216                }
8217            }
8218            keys.clear();
8219            // The same collations the sorter compares with, and the
8220            // re-derivation below is handed the same ones. `finish`'s
8221            // contract is that a key comes back the way it was pushed;
8222            // a collation is part of the way it was pushed.
8223            if order_has_subquery {
8224                // A substituted literal is no longer a bound column.
8225                let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
8226                crate::orderby::build_order_keys_bound(
8227                    per_row.as_deref().unwrap_or(&order_by),
8228                    &unbound,
8229                    &order_colls,
8230                    row,
8231                    &ctx,
8232                    &mut keys,
8233                )?;
8234            } else {
8235                crate::orderby::build_order_keys_bound(
8236                    &order_by,
8237                    &order_bound,
8238                    &order_colls,
8239                    row,
8240                    &ctx,
8241                    &mut keys,
8242                )?;
8243            }
8244            sorter.push(&mut keys, row)?;
8245        }
8246
8247        let key_ctx = &ctx;
8248        let rows = sorter.finish(
8249            |src, buf| {
8250                crate::orderby::build_order_keys_rederived(
8251                    &order_by,
8252                    &order_bound,
8253                    &order_colls,
8254                    src,
8255                    key_ctx,
8256                    buf,
8257                )
8258            },
8259            |src| {
8260                let mut values = Vec::with_capacity(projection.len());
8261                for p in &projection {
8262                    values.push(
8263                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8264                    );
8265                }
8266                Ok(Row::new(values))
8267            },
8268        )?;
8269
8270        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8271        Ok(Some(QueryResult::Rows { columns, rows }))
8272    }
8273
8274    /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
8275    /// handing each row to the consumer instead of collecting the answer.
8276    ///
8277    /// That walk bounds the SORT and then returns `QueryResult::Rows`,
8278    /// which holds every output row. Measured at `work_mem = 4 MB` over
8279    /// 200-byte rows, RSS above the server's own baseline while the
8280    /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
8281    /// at 400k — linear — while the spill underneath worked correctly
8282    /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
8283    /// removes each file, so a count taken afterwards reads 0 whatever
8284    /// happened, and an earlier reading of "no spill at all" was that
8285    /// blind witness). The growth is the collected result, not the sort.
8286    ///
8287    /// Emitting makes peak the budget, one buffer per run and a single
8288    /// row — the state a merge already holds at every step. It also
8289    /// frees each projected row as the next is built rather than
8290    /// accumulating them, which is where the time is: a profile of the
8291    /// collecting walk put the allocator at 586 samples, more than every
8292    /// sort comparison combined (420), against 19 for `push` itself.
8293    /// v7.37 (round 923) — which of a sort record's columns the output half
8294    /// reads. The record is the SOURCE row (round 836), so a narrow projection
8295    /// decoded every column: skipping one 200-byte text halves a decode
8296    /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
8297    ///
8298    /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
8299    /// column reads NULL. Answers only when every projection item is a bare
8300    /// column reference AND every ORDER BY key is a bound column; anything
8301    /// else returns empty, decoding everything as before.
8302    /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
8303    /// drops references from expression kinds it does not enumerate.
8304    ///
8305    /// ORDER BY columns are included — the merge re-derives keys from the
8306    /// decoded row on the spilled path, so pruning one would sort NULLs.
8307    pub(crate) fn sort_record_columns_needed(
8308        items: &[SelectItem],
8309        order_bound: &[Option<usize>],
8310        arity: usize,
8311        ctx: &EvalContext,
8312    ) -> Vec<bool> {
8313        let all_bare = items.iter().all(|i| {
8314            matches!(
8315                i,
8316                SelectItem::Expr {
8317                    expr: Expr::Column(_),
8318                    ..
8319                }
8320            )
8321        });
8322        if !all_bare || order_bound.iter().any(Option::is_none) {
8323            return Vec::new();
8324        }
8325        let mut mask = alloc::vec![false; arity];
8326        for item in items {
8327            if let SelectItem::Expr {
8328                expr: Expr::Column(c),
8329                ..
8330            } = item
8331            {
8332                match crate::eval::find_column_pos(c, ctx) {
8333                    Some(p) if p < arity => mask[p] = true,
8334                    _ => return Vec::new(),
8335                }
8336            }
8337        }
8338        for p in order_bound.iter().flatten() {
8339            if *p < arity {
8340                mask[*p] = true;
8341            } else {
8342                return Vec::new();
8343            }
8344        }
8345        mask
8346    }
8347
8348    /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
8349    /// of sorting.
8350    ///
8351    /// PG serves such an ordering from the index and never sorts. We sorted:
8352    /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
8353    /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
8354    /// the sorter's own round trip — `ExternalSorter::finish_each` →
8355    /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
8356    /// Every row is encoded into the sorter's arena and decoded back out,
8357    /// for an order the index already holds.
8358    ///
8359    /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
8360    /// because it was built for top-N. This is the unbounded sibling.
8361    ///
8362    /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
8363    /// from a btree, so walking one would silently drop those rows. That is
8364    /// exactly the defect r1020 fixed on the top-N path, where it had
8365    /// shipped.
8366    /// r1044 — the index this statement's ORDER BY can be WALKED on,
8367    /// instead of sorted, or `None`.
8368    ///
8369    /// Extracted so `EXPLAIN` can ask the same question the executor
8370    /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
8371    /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
8372    /// while the executor walked the primary key — 34.9 ms against
8373    /// 147.0 for the same query ordered by an unindexed column, so the
8374    /// walk was plainly running. Round 551 fixed a different case of
8375    /// this and wrote the reason down: EXPLAIN is the first thing any
8376    /// performance question opens, and an instrument that misnames the
8377    /// access path is worse than one that says nothing.
8378    ///
8379    /// The gate is here once. Two copies of it is how the plan and the
8380    /// executor come to disagree again.
8381    /// v7.39.13 — the shape refusals both ordered-walk gates make.
8382    ///
8383    /// One list, because two of them would be two answers to "can this
8384    /// statement walk an index", and a walk that runs where EXPLAIN says
8385    /// it does not is the defect r1044 exists to prevent.
8386    /// v7.39.13 — `WHERE lead = <literal> ORDER BY next [DESC] LIMIT n`
8387    /// behind an index on `(lead, next, …)`: one seek to the key prefix,
8388    /// then n steps inside it.
8389    ///
8390    /// Sentori's busiest read, and the one shape they have reported
8391    /// unchanged for three versions: `WHERE project_id = ? ORDER BY
8392    /// received_at DESC LIMIT 20`. PostgreSQL 18 answers it with
8393    /// `Limit -> Index Scan`; SPG planned `Sort -> Seq Scan` and sorted
8394    /// the table to return twenty rows, roughly 250x behind.
8395    ///
8396    /// The ordered walk that existed could only start at an index's
8397    /// LEADING column, so an index on `(project_id, received_at)` could
8398    /// serve `ORDER BY project_id` and nothing else. What was missing is
8399    /// below it: a tree walk bounded by a key prefix, which
8400    /// `Index::iter_prefix_desc` now provides.
8401    ///
8402    /// The equality conjunct only NARROWS the walk — the statement's own
8403    /// `WHERE` still runs per row — so picking the wrong conjunct can
8404    /// cost time and cannot change an answer.
8405    pub(crate) fn index_prefix_walk_target(
8406        &self,
8407        stmt: &SelectStatement,
8408        from: &FromClause,
8409    ) -> Option<(String, usize, alloc::vec::Vec<spg_storage::IndexKey>)> {
8410        if self.walk_shape_refused(stmt, from) {
8411            return None;
8412        }
8413        // One ORDER BY term for now: a second one would have to be the
8414        // next key column again, and the tree walks one direction.
8415        if stmt.order_by.len() != 1 || stmt.distinct {
8416            return None;
8417        }
8418        let table = self.active_catalog().get(&from.primary.name)?;
8419        let alias = from
8420            .primary
8421            .alias
8422            .as_deref()
8423            .unwrap_or(from.primary.name.as_str());
8424        let cols = &table.schema().columns;
8425        let order = &stmt.order_by[0];
8426        let Expr::Column(oc) = &order.expr else {
8427            return None;
8428        };
8429        if let Some(q) = &oc.qualifier
8430            && !q.eq_ignore_ascii_case(alias)
8431        {
8432            return None;
8433        }
8434        let order_pos = cols
8435            .iter()
8436            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8437        // The walk comes out in the tree's order, so it may only take an
8438        // ORDER BY whose order that IS — the same question the leading-
8439        // column gate asks, for the same reason.
8440        let order_col = cols.get(order_pos)?;
8441        if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8442            && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8443        {
8444            return None;
8445        }
8446        // A NULL key is not in the tree, and this walk has no separate
8447        // pass for those rows the way the leading-column one does.
8448        if order_col.nullable {
8449            return None;
8450        }
8451        let where_ = stmt.where_.as_ref()?;
8452        for index in table.indices() {
8453            if !matches!(index.kind, spg_storage::IndexKind::BTreeMulti(_))
8454                || index.expression.is_some()
8455                || index.partial_predicate.is_some()
8456            {
8457                continue;
8458            }
8459            // The ORDER BY column must be the key component that follows
8460            // the equality-bound prefix.
8461            if index.extra_column_positions.first() != Some(&order_pos) {
8462                continue;
8463            }
8464            let lead_pos = index.column_position;
8465            let lead_col = cols.get(lead_pos)?;
8466            // The prefix is compared with the tree's own ordering, so the
8467            // leading column has to be one the tree orders bytewise too.
8468            if crate::index_access::collated_column(lead_col, table.db_collation()).is_none()
8469                && !crate::collate::column_key_is_bytewise(lead_col, self.speaks_mysql)
8470            {
8471                continue;
8472            }
8473            let Some(key) = self.eq_literal_key_for(where_, lead_pos, cols, alias) else {
8474                continue;
8475            };
8476            return Some((index.name.clone(), order_pos, alloc::vec![key]));
8477        }
8478        None
8479    }
8480
8481    /// The index key a top-level `AND` conjunct binds `col_pos` to, when
8482    /// one of them is `col = <literal>` (either way round).
8483    ///
8484    /// Only literals: a column reference or a function would have to be
8485    /// evaluated per row, and this runs once for the whole statement.
8486    fn eq_literal_key_for(
8487        &self,
8488        where_: &Expr,
8489        col_pos: usize,
8490        cols: &[ColumnSchema],
8491        alias: &str,
8492    ) -> Option<spg_storage::IndexKey> {
8493        let col = cols.get(col_pos)?;
8494        let mut found: Option<spg_storage::IndexKey> = None;
8495        let mut stack: alloc::vec::Vec<&Expr> = alloc::vec![where_];
8496        while let Some(e) = stack.pop() {
8497            match e {
8498                Expr::Binary {
8499                    lhs,
8500                    op: spg_sql::ast::BinOp::And,
8501                    rhs,
8502                } => {
8503                    stack.push(lhs);
8504                    stack.push(rhs);
8505                }
8506                Expr::Binary {
8507                    lhs,
8508                    op: spg_sql::ast::BinOp::Eq,
8509                    rhs,
8510                } => {
8511                    let names_col = |x: &Expr| match x {
8512                        Expr::Column(c) => {
8513                            c.name.eq_ignore_ascii_case(&col.name)
8514                                && c.qualifier
8515                                    .as_ref()
8516                                    .is_none_or(|q| q.eq_ignore_ascii_case(alias))
8517                        }
8518                        _ => false,
8519                    };
8520                    let lit = if names_col(lhs) {
8521                        Some(&**rhs)
8522                    } else if names_col(rhs) {
8523                        Some(&**lhs)
8524                    } else {
8525                        None
8526                    };
8527                    // v7.39.13 — a BARE literal means whatever the
8528                    // COLUMN says it means, and
8529                    // `literal_as_column_value` is the one place that
8530                    // decision is made. Asking
8531                    // `literal_expr_to_value` instead made this the
8532                    // fifth copy of it, and it read every string
8533                    // literal as text: `WHERE k = '\x07'` on a `bytea`
8534                    // column built no key at all, so the walk declined
8535                    // and the plan went back to sorting the table —
8536                    // while the EQUALITY seek beside it, which does ask
8537                    // the one funnel, used the very same index.
8538                    //
8539                    // Anything that is not a bare literal — a cast, a
8540                    // negation — already carries its own type, and
8541                    // `from_value_for_column` decides whether that type
8542                    // keys for this column.
8543                    let v = match lit {
8544                        Some(Expr::Literal(l)) => {
8545                            crate::index_access::literal_as_column_value(l, col, col_pos)
8546                        }
8547                        Some(other) => {
8548                            crate::conversions::literal_expr_to_value(other.clone()).ok()
8549                        }
8550                        None => None,
8551                    };
8552                    if let Some(v) = v
8553                        && !v.is_null()
8554                        && let Some(k) = spg_storage::IndexKey::from_value_for_column(&v, col.ty)
8555                    {
8556                        found = Some(k);
8557                    }
8558                }
8559                _ => {}
8560            }
8561        }
8562        found
8563    }
8564
8565    fn walk_shape_refused(&self, stmt: &SelectStatement, from: &FromClause) -> bool {
8566        // A non-literal count is refused: `LIMIT $1` is rewritten to a
8567        // literal by `resolve_limit_exprs` before dispatch, so anything
8568        // still carrying a placeholder here has not been through it.
8569        let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8570            matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8571        };
8572        if stmt.order_by.is_empty()
8573            || !stmt.distinct_on.is_empty()
8574            || stmt.limit_with_ties
8575            || !literal_count(&stmt.limit)
8576            || !literal_count(&stmt.offset)
8577            || stmt.having.is_some()
8578            || stmt.group_by.is_some()
8579            || !stmt.unions.is_empty()
8580            || !from.joins.is_empty()
8581            || from.primary.lateral_subquery.is_some()
8582            || from.primary.unnest_expr.is_some()
8583            || from.primary.as_of_segment.is_some()
8584            || from.primary.generate_series_args.is_some()
8585            || select_has_window(stmt)
8586            || aggregate::uses_aggregate(stmt)
8587        {
8588            return true;
8589        }
8590        if stmt
8591            .items
8592            .iter()
8593            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8594        {
8595            return true;
8596        }
8597        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8598            return true;
8599        };
8600        if table.has_cold_rows_fast() {
8601            return true;
8602        }
8603        !from.primary.only
8604            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8605    }
8606
8607    pub(crate) fn index_order_walk_target(
8608        &self,
8609        stmt: &SelectStatement,
8610        from: &FromClause,
8611    ) -> Option<(String, usize)> {
8612        // v7.39.11 — LIMIT / OFFSET join the walk instead of refusing it.
8613        //
8614        // Reported by sentori against 7.39.10 and measured on their own
8615        // busiest read: "the most recent N events for this project",
8616        // backed by an index on exactly that ordering. PostgreSQL 18
8617        // answered it with `Limit -> Index Scan`; SPG with
8618        // `Limit -> Sort -> Seq Scan`, 4.998 ms against 0.021 — the
8619        // whole table sorted to return twenty rows.
8620        //
8621        // The walk was built for this shape — `iter_desc`'s own doc says
8622        // "the ORDER BY <indexed col> DESC + LIMIT N executor path" —
8623        // and then the gate refused every statement that had a LIMIT, so
8624        // the one query it was written for could never reach it. The
8625        // capability was here; the routing was not.
8626        //
8627        // A non-literal count is refused: `LIMIT $1` is rewritten to a
8628        // literal by `resolve_limit_exprs` before dispatch, so anything
8629        // still carrying a placeholder here has not been through it.
8630        let literal_count = |e: &Option<spg_sql::ast::LimitExpr>| {
8631            matches!(e, None | Some(spg_sql::ast::LimitExpr::Literal(_)))
8632        };
8633        if self.walk_shape_refused(stmt, from) {
8634            return None;
8635        }
8636        let table = self.active_catalog().get(&from.primary.name)?;
8637        let alias = from
8638            .primary
8639            .alias
8640            .as_deref()
8641            .unwrap_or(from.primary.name.as_str());
8642        let cols = &table.schema().columns;
8643        let order = &stmt.order_by[0];
8644        let Expr::Column(oc) = &order.expr else {
8645            return None;
8646        };
8647        if let Some(q) = &oc.qualifier
8648            && !q.eq_ignore_ascii_case(alias)
8649        {
8650            return None;
8651        }
8652        let order_pos = cols
8653            .iter()
8654            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8655        // r1047 — DISTINCT joins the walk when the projection IS the
8656        // order column, and only then. The index's keys are canonical
8657        // (r1039: representation equality is value equality — the
8658        // property every seek already depends on), so one key is one
8659        // distinct value and the walk can emit the first passing row of
8660        // each key group instead of hashing every row. On the release
8661        // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
8662        // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
8663        // with an ablation floor of 14.8, because the hash must
8664        // normalize and probe ALL the rows; the walk visits each key
8665        // once. A wider projection makes DISTINCT about the whole tuple,
8666        // not the key, so anything else still declines.
8667        if stmt.distinct {
8668            let only_the_order_column = stmt.items.len() == 1
8669                && match &stmt.items[0] {
8670                    SelectItem::Expr {
8671                        expr: Expr::Column(c),
8672                        ..
8673                    } => {
8674                        c.name.eq_ignore_ascii_case(&oc.name)
8675                            && match &c.qualifier {
8676                                Some(q) => q.eq_ignore_ascii_case(alias),
8677                                None => true,
8678                            }
8679                    }
8680                    _ => false,
8681                };
8682            if !only_the_order_column {
8683                return None;
8684            }
8685        }
8686        // r1046 — a nullable key no longer refuses the walk; it changes
8687        // what the walk has to do. A NULL key is not in the btree, so
8688        // walking alone would silently drop those rows — the r1020
8689        // defect, which shipped once. The walk emits them separately, at
8690        // the end SQL puts them.
8691        //
8692        // Refusing was costing every nullable indexed column a 3.4x:
8693        // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
8694        // 72.0 ms with the column nullable and 20.2 with the same data
8695        // under NOT NULL. `NOT NULL` is not the default, so that was the
8696        // common case paying for the uncommon one.
8697        // v7.39.11 — the walk comes out in the tree's order, so it may
8698        // only take an ORDER BY whose order that IS.
8699        //
8700        // The B-tree walks in BYTE order unless the column's keys are
8701        // ICU sort keys. `try_pk_walk_top_n` has asked this since
8702        // v7.38.18; this gate never did, and the answer changed when an
8703        // index appeared. Measured on `alpha / Beta / GAMMA / delta`
8704        // over a MySQL-dialect session, `SELECT t FROM s ORDER BY t`:
8705        //
8706        //   no index   alpha Beta delta GAMMA   (MySQL's own order)
8707        //   indexed    Beta GAMMA alpha delta   (bytes)
8708        //
8709        // No row is wrong and nothing raises; only the order changes,
8710        // and it changes because an index exists. Ordering is the one
8711        // thing a walk contributes, so when it is the wrong ordering
8712        // there is nothing left to keep.
8713        let order_col = cols.get(order_pos)?;
8714        if crate::index_access::collated_column(order_col, table.db_collation()).is_none()
8715            && !crate::collate::column_key_is_bytewise(order_col, self.speaks_mysql)
8716        {
8717            return None;
8718        }
8719        // v7.39.11 — a composite B-tree LEADING on the ORDER BY column
8720        // walks it too, which is what `try_pk_walk_top_n` has always
8721        // done and what this gate did not know.
8722        //
8723        // Keys sort by the whole tuple, so the leading component comes
8724        // out in order — `Index::iter_asc` says so, and the materialising
8725        // top-N walk has relied on it since v7.38.1. The consequence of
8726        // the two gates disagreeing was the thing r1044 exists to
8727        // prevent: measured on a table indexed `(a, b)`, `SELECT a FROM
8728        // m ORDER BY a LIMIT 2` planned as `Limit -> Sort -> Seq Scan`
8729        // while the executor plainly walked the index — a projection
8730        // that divides by zero on the last row in key order returned two
8731        // rows instead of raising. EXPLAIN is the first thing any
8732        // performance question opens, and an instrument that misnames
8733        // the access path is worse than one that says nothing.
8734        let index = table
8735            .index_on(order_pos)
8736            .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8737            .or_else(|| {
8738                table.indices().iter().find(|i| {
8739                    matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8740                        && i.column_position == order_pos
8741                })
8742            })?;
8743        if index.expression.is_some() || index.partial_predicate.is_some() {
8744            return None;
8745        }
8746        // v7.39.11 — more than one ORDER BY term walks when the index
8747        // holds exactly that ordering.
8748        //
8749        // Keys sort by the whole tuple, so `iter_asc` over a composite
8750        // B-tree IS `ORDER BY a, b` — the walk needs no new machinery,
8751        // only permission. Reported by sentori against 7.39.10:
8752        // `ORDER BY a, b LIMIT 10` planned as `Seq Scan -> Sort` here
8753        // against an `Incremental Sort` over an index scan on
8754        // PostgreSQL 18, on a table indexed for it.
8755        //
8756        // Three things have to hold, and each of them is the tree's
8757        // limitation rather than a conservative choice:
8758        //
8759        //   * the terms are the index's key columns, in its order, from
8760        //     the leading one — a suffix or a permutation is a different
8761        //     ordering;
8762        //   * every term runs the same direction, because the tree is
8763        //     walked one way for all of them. `(a, b DESC)` is what
8764        //     PostgreSQL serves from an index whose SECOND key is
8765        //     descending, and SPG's tree does not scan per column;
8766        //   * every key column is NOT NULL. A NULL key is not in the
8767        //     tree at all, and the separate pass that emits those rows
8768        //     (r1046) knows how to place them for ONE column, not for a
8769        //     tuple.
8770        if stmt.order_by.len() > 1 {
8771            let keys: Vec<usize> = core::iter::once(index.column_position)
8772                .chain(index.extra_column_positions.iter().copied())
8773                .collect();
8774            if stmt.order_by.len() > keys.len() {
8775                return None;
8776            }
8777            let desc = stmt.order_by[0].desc;
8778            for (term, &key_pos) in stmt.order_by.iter().zip(keys.iter()) {
8779                if term.desc != desc {
8780                    return None;
8781                }
8782                let Expr::Column(c) = &term.expr else {
8783                    return None;
8784                };
8785                if let Some(q) = &c.qualifier
8786                    && !q.eq_ignore_ascii_case(alias)
8787                {
8788                    return None;
8789                }
8790                let pos = cols
8791                    .iter()
8792                    .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
8793                if pos != key_pos {
8794                    return None;
8795                }
8796                let col = cols.get(pos)?;
8797                if col.nullable {
8798                    return None;
8799                }
8800                if crate::index_access::collated_column(col, table.db_collation()).is_none()
8801                    && !crate::collate::column_key_is_bytewise(col, self.speaks_mysql)
8802                {
8803                    return None;
8804                }
8805            }
8806        }
8807        Some((index.name.clone(), order_pos))
8808    }
8809
8810    fn try_index_order_stream<F>(
8811        &self,
8812        stmt: &SelectStatement,
8813        from: &FromClause,
8814        cancel: CancelToken<'_>,
8815        emit: &mut F,
8816    ) -> Result<Option<usize>, EngineError>
8817    where
8818        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8819    {
8820        // r1044 — the shape gate lives in `index_order_walk_target`, so
8821        // `EXPLAIN` answers the same question. What stays here is the
8822        // part that RAISES (an illegal ORDER BY has to keep erroring
8823        // from where it did) and the bindings the walk needs.
8824        crate::orderby::check_order_by_legality(stmt)?;
8825        crate::orderby::check_order_by_positions(stmt)?;
8826        crate::window::reject_window_in_row_clauses(stmt)?;
8827        // v7.39.13 — the prefix walk first: it serves a shape the
8828        // leading-column walk cannot, and refuses everything that one
8829        // takes.
8830        let (order_pos, prefix) = match self.index_prefix_walk_target(stmt, from) {
8831            Some((_, pos, keys)) => (pos, Some(keys)),
8832            None => match self.index_order_walk_target(stmt, from) {
8833                Some((_, pos)) => (pos, None),
8834                None => return Ok(None),
8835            },
8836        };
8837        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8838            return Ok(None);
8839        };
8840        let alias = from
8841            .primary
8842            .alias
8843            .as_deref()
8844            .unwrap_or(from.primary.name.as_str());
8845        let cols = table.schema().columns.clone();
8846        let order = &stmt.order_by[0];
8847        // v7.39.11 — the same lookup the gate made; see
8848        // `index_order_walk_target`.
8849        let Some(index) = (if prefix.is_some() {
8850            // The prefix planner named an index whose FIRST extra key
8851            // column is the order column; the lookup below looks for one
8852            // whose LEADING column is, and would find the wrong tree.
8853            table.indices().iter().find(|i| {
8854                matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8855                    && i.extra_column_positions.first() == Some(&order_pos)
8856                    && i.expression.is_none()
8857                    && i.partial_predicate.is_none()
8858            })
8859        } else {
8860            table
8861                .index_on(order_pos)
8862                .filter(|i| matches!(i.kind, spg_storage::IndexKind::BTree(_)))
8863                .or_else(|| {
8864                    table.indices().iter().find(|i| {
8865                        matches!(i.kind, spg_storage::IndexKind::BTreeMulti(_))
8866                            && i.column_position == order_pos
8867                    })
8868                })
8869        }) else {
8870            return Ok(None);
8871        };
8872
8873        let sess = self.dml_session();
8874        let ctx = EvalContext::new(&cols, Some(alias))
8875            .with_catalog(self.active_catalog())
8876            .with_session(&sess);
8877        let projection = build_projection(
8878            &stmt.items,
8879            &cols,
8880            alias,
8881            self.speaks_mysql,
8882            Some(self.active_catalog()),
8883        )?;
8884        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8885        emit(crate::StreamItem::Header(&columns))?;
8886        let bound_pos: Vec<Option<usize>> = projection
8887            .iter()
8888            .map(|p| match &p.expr {
8889                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8890                    Ok(Some(pos)) => Some(pos),
8891                    _ => None,
8892                },
8893                _ => None,
8894            })
8895            .collect();
8896
8897        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8898            .where_
8899            .as_ref()
8900            .filter(|w| crate::eval::fully_compilable(w))
8901            .map(|w| crate::eval::compile_expr(w, &ctx));
8902        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8903        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8904        let snapshot = self.current_snapshot();
8905
8906        // A btree holds one locator per row VERSION, so a row whose key was
8907        // updated can sit under two keys and a dead one can sit beside its
8908        // replacement. The visibility gate drops the dead; `seen` drops a
8909        // live row that the walk reaches twice, which would otherwise be a
8910        // duplicated output row rather than a slow one.
8911        let mut emitted_rows = alloc::vec![false; table.rows().len()];
8912
8913        // r1046 — the rows the index cannot hold.
8914        //
8915        // A NULL key is not in the btree, so the walk below never reaches
8916        // those rows; they are emitted here, at the end SQL puts them.
8917        // PG's default is NULLS LAST ascending and NULLS FIRST
8918        // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
8919        // the same rule `order_by_value_cmp_raw` applies to the sort this
8920        // replaces, so the two orders agree.
8921        //
8922        // Finding them costs one pass over the column. That pass is why
8923        // this is still worth doing: the sort it replaces encodes and
8924        // decodes every row, and the walk plus the pass measured 72.0 ms
8925        // down to about 22 on 400,000 rows.
8926        let nulls_first = order.nulls_first.unwrap_or(order.desc);
8927        // r1047 — under DISTINCT the walk emits the FIRST passing row of
8928        // each key group and skips the rest; the gate admits DISTINCT
8929        // only when the projection is the order column itself, so one
8930        // canonical key is one output row. NULL is one distinct value,
8931        // so the NULL pass stops at its first emit too.
8932        let distinct = stmt.distinct;
8933        let mut count = 0usize;
8934        let mut visited = 0usize;
8935        // v7.39.11 — OFFSET and LIMIT, applied as the walk goes.
8936        //
8937        // Both count PASSING rows, so a skipped row still has to run the
8938        // predicate and the projection — `stream_filter_project` is
8939        // `stream_project_row` without the emit, which is exactly that.
8940        // Stopping at `remaining == 0` is the whole point: twenty rows
8941        // off the end of an index instead of a sorted table.
8942        let mut to_skip = stmt.offset_literal().unwrap_or(0) as usize;
8943        let mut remaining: Option<usize> = stmt.limit_literal().map(|l| l as usize);
8944        let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
8945                                  eval_stack: &mut Vec<Value<'static>>,
8946                                  values: &mut Vec<Value<'static>>,
8947                                  visited: &mut usize,
8948                                  to_skip: &mut usize,
8949                                  remaining: &mut Option<usize>,
8950                                  emit: &mut F|
8951         -> Result<usize, EngineError> {
8952            if !cols[order_pos].nullable {
8953                return Ok(0);
8954            }
8955            // v7.39.11 — nothing to emit once the LIMIT is met, and
8956            // finding that out must not cost a scan.
8957            //
8958            // This pass looks for NULL-keyed rows by walking the whole
8959            // heap, because they are not in the tree. That is the price
8960            // r1046 measured and accepted for an UNBOUNDED order. With
8961            // a LIMIT the walk above has usually already produced every
8962            // row the caller asked for, and scanning 400,000 rows to
8963            // add none of them is the whole cost of the query: the
8964            // release sweep's `SELECT pad FROM t ORDER BY n LIMIT 10`
8965            // over a nullable indexed NUMERIC went 0.237 ms at 50,000
8966            // rows and 2.251 at 400,000 — linear, against PostgreSQL's
8967            // 0.155 and 0.182 — the moment this gate started accepting
8968            // LIMIT. The `remaining` check below sits after the
8969            // per-row filters, so it could never be reached.
8970            if *remaining == Some(0) {
8971                return Ok(0);
8972            }
8973            let mut n = 0usize;
8974            for (ri, row) in table.rows().iter().enumerate() {
8975                if !matches!(row.values.get(order_pos), Some(Value::Null)) {
8976                    continue;
8977                }
8978                if emitted_rows.get(ri).copied().unwrap_or(true) {
8979                    continue;
8980                }
8981                if !table.is_row_visible(ri, &snapshot) {
8982                    continue;
8983                }
8984                *visited += 1;
8985                if visited.is_multiple_of(256) {
8986                    cancel.check()?;
8987                }
8988                emitted_rows[ri] = true;
8989                if *remaining == Some(0) {
8990                    break;
8991                }
8992                let passed = if *to_skip > 0 {
8993                    let p = Self::stream_filter_project(
8994                        row,
8995                        stmt.where_.as_ref(),
8996                        compiled_where.as_ref(),
8997                        eval_stack,
8998                        &projection,
8999                        &bound_pos,
9000                        &ctx,
9001                        values,
9002                    )?;
9003                    if p {
9004                        *to_skip -= 1;
9005                    }
9006                    false
9007                } else {
9008                    Self::stream_project_row(
9009                        row,
9010                        stmt.where_.as_ref(),
9011                        compiled_where.as_ref(),
9012                        eval_stack,
9013                        &projection,
9014                        &bound_pos,
9015                        &ctx,
9016                        values,
9017                        emit,
9018                    )?
9019                };
9020                if passed {
9021                    n += 1;
9022                    if let Some(r) = remaining.as_mut() {
9023                        *r -= 1;
9024                        if *r == 0 {
9025                            break;
9026                        }
9027                    }
9028                    if distinct {
9029                        break;
9030                    }
9031                }
9032            }
9033            Ok(n)
9034        };
9035
9036        if nulls_first {
9037            count += emit_null_rows(
9038                &mut emitted_rows,
9039                &mut eval_stack,
9040                &mut values,
9041                &mut visited,
9042                &mut to_skip,
9043                &mut remaining,
9044                emit,
9045            )?;
9046        }
9047
9048        // v7.39.13 — a prefix walk when the statement binds the index's
9049        // leading column, the whole tree otherwise. The key is not read
9050        // by the loop, so the two shapes meet as posting lists.
9051        let walker: alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> =
9052            match prefix.as_ref().and_then(|p| {
9053                if order.desc {
9054                    index.iter_prefix_desc(p).map(
9055                        |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9056                            alloc::boxed::Box::new(it.map(|(_, l)| l))
9057                        },
9058                    )
9059                } else {
9060                    index.iter_prefix_asc(p).map(
9061                        |it| -> alloc::boxed::Box<dyn Iterator<Item = &spg_storage::PostingList>> {
9062                            alloc::boxed::Box::new(it.map(|(_, l)| l))
9063                        },
9064                    )
9065                }
9066            }) {
9067                Some(it) => it,
9068                None if order.desc => alloc::boxed::Box::new(index.iter_desc().map(|(_, l)| l)),
9069                None => alloc::boxed::Box::new(index.iter_asc().map(|(_, l)| l)),
9070            };
9071        'walk: for locators in walker {
9072            if remaining == Some(0) {
9073                break;
9074            }
9075            for loc in locators {
9076                let spg_storage::RowLocator::Hot(ri) = *loc else {
9077                    continue;
9078                };
9079                if emitted_rows.get(ri).copied().unwrap_or(true) {
9080                    continue;
9081                }
9082                if !table.is_row_visible(ri, &snapshot) {
9083                    continue;
9084                }
9085                let Some(row) = table.rows().get(ri) else {
9086                    continue;
9087                };
9088                visited += 1;
9089                if visited.is_multiple_of(256) {
9090                    cancel.check()?;
9091                }
9092                emitted_rows[ri] = true;
9093                // v7.39.11 — a skipped row still runs the predicate and
9094                // the projection, because OFFSET counts rows that PASS;
9095                // it just does not reach the client.
9096                let passed = if to_skip > 0 {
9097                    let p = Self::stream_filter_project(
9098                        row,
9099                        stmt.where_.as_ref(),
9100                        compiled_where.as_ref(),
9101                        &mut eval_stack,
9102                        &projection,
9103                        &bound_pos,
9104                        &ctx,
9105                        &mut values,
9106                    )?;
9107                    if p {
9108                        to_skip -= 1;
9109                    }
9110                    false
9111                } else {
9112                    Self::stream_project_row(
9113                        row,
9114                        stmt.where_.as_ref(),
9115                        compiled_where.as_ref(),
9116                        &mut eval_stack,
9117                        &projection,
9118                        &bound_pos,
9119                        &ctx,
9120                        &mut values,
9121                        emit,
9122                    )?
9123                };
9124                if passed {
9125                    count += 1;
9126                    if let Some(r) = remaining.as_mut() {
9127                        *r -= 1;
9128                        if *r == 0 {
9129                            break 'walk;
9130                        }
9131                    }
9132                    // One row per key group: the rest are the same value.
9133                    if distinct {
9134                        break;
9135                    }
9136                }
9137            }
9138        }
9139
9140        if !nulls_first {
9141            count += emit_null_rows(
9142                &mut emitted_rows,
9143                &mut eval_stack,
9144                &mut values,
9145                &mut visited,
9146                &mut to_skip,
9147                &mut remaining,
9148                emit,
9149            )?;
9150        }
9151        Ok(Some(count))
9152    }
9153
9154    /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
9155    /// building an `OrderKey` vector per row.
9156    ///
9157    /// The row-returning sorted scan allocates twice per row: one
9158    /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
9159    /// projection. Counted over 400 k rows (r1030,
9160    /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
9161    /// allocations and 208 MB of traffic for an answer of four hundred
9162    /// thousand integers.
9163    ///
9164    /// The key half is pure ceremony on this shape.
9165    /// `sort_tagged_by_inline_int_key` already sorts indices rather than
9166    /// rows, so the per-row vector is built, has one integer taken out of
9167    /// it, and is then dragged through the permutation — it exists to carry
9168    /// a number the row's column already held. This lane carries the number
9169    /// instead, in a fixed-size array that lives inside the buffer element
9170    /// and allocates nothing. Same idea as the predicate VM's integer lane.
9171    ///
9172    /// Declines to `None` for anything it does not cover, and every caller
9173    /// falls through to the general path, so the gate list is the
9174    /// specification.
9175    ///
9176    /// Ties: equal keys keep scan order, as the stable sort on the general
9177    /// path does. Rows that tie on every ORDER BY term are entitled to any
9178    /// order among themselves either way — see `STABILITY.md`.
9179    fn try_int_key_sorted_stream<F>(
9180        &self,
9181        stmt: &SelectStatement,
9182        from: &FromClause,
9183        cancel: CancelToken<'_>,
9184        emit: &mut F,
9185    ) -> Result<Option<usize>, EngineError>
9186    where
9187        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9188    {
9189        /// Sort terms this lane carries inline. Four covers every ORDER BY
9190        /// in the endpoint sweep and in the dogfood corpus; wider ones fall
9191        /// through rather than growing the buffer element for everybody.
9192        const MAX_KEYS: usize = 4;
9193
9194        if stmt.order_by.is_empty()
9195            || stmt.order_by.len() > MAX_KEYS
9196            // v7.38.14 — DISTINCT is admitted when the projected set is
9197            // exactly the ORDER BY set, and only then. This lane sorts, and
9198            // when the sort key determines the projected row every duplicate
9199            // lands ADJACENT to its twin -- so the de-duplication is a
9200            // comparison with the previous row rather than a hash table, and
9201            // the reason this lane declined DISTINCT disappears with it. The
9202            // seen-set it could not offer held indices into a materialised
9203            // vector; there is no seen-set now.
9204            //
9205            // The gate is as narrow as the bare-GROUP-BY rewrite's for the
9206            // same reason: `ORDER BY a` over a projection of `a, b` does NOT
9207            // place duplicates of the PAIR adjacent, so set EQUALITY, never
9208            // overlap.
9209            || (stmt.distinct && !Self::distinct_is_adjacent_after_sort(stmt))
9210            || stmt.limit_with_ties
9211            || stmt.limit.is_some()
9212            || stmt.offset.is_some()
9213            || stmt.having.is_some()
9214            || stmt.group_by.is_some()
9215            || !stmt.unions.is_empty()
9216            || !from.joins.is_empty()
9217            || from.primary.lateral_subquery.is_some()
9218            || from.primary.unnest_expr.is_some()
9219            || from.primary.as_of_segment.is_some()
9220            || from.primary.generate_series_args.is_some()
9221            || select_has_window(stmt)
9222            || aggregate::uses_aggregate(stmt)
9223        {
9224            return Ok(None);
9225        }
9226        if stmt
9227            .items
9228            .iter()
9229            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9230        {
9231            return Ok(None);
9232        }
9233        crate::orderby::check_order_by_legality(stmt)?;
9234        crate::orderby::check_order_by_positions(stmt)?;
9235        crate::window::reject_window_in_row_clauses(stmt)?;
9236        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9237            return Ok(None);
9238        };
9239        if table.has_cold_rows_fast() {
9240            return Ok(None);
9241        }
9242        if !from.primary.only
9243            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9244        {
9245            return Ok(None);
9246        }
9247        let alias = from
9248            .primary
9249            .alias
9250            .as_deref()
9251            .unwrap_or(from.primary.name.as_str());
9252        let cols = table.schema().columns.clone();
9253
9254        // Every ORDER BY term must be a NOT NULL integer column of this
9255        // table. NOT NULL is what lets the key be a bare integer: with
9256        // NULLs the lane would have to carry their ordering too, and
9257        // getting that subtly wrong is the r1020 defect.
9258        let mut key_pos = [0usize; MAX_KEYS];
9259        let mut descs = [false; MAX_KEYS];
9260        // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
9261        // which the AST records as `None`; `unwrap_or(desc)` is how the
9262        // rest of the engine resolves it.
9263        let mut nulls_first = [false; MAX_KEYS];
9264        let n_keys = stmt.order_by.len();
9265        for (slot, order) in stmt.order_by.iter().enumerate() {
9266            let Expr::Column(oc) = &order.expr else {
9267                return Ok(None);
9268            };
9269            if let Some(q) = &oc.qualifier
9270                && !q.eq_ignore_ascii_case(alias)
9271            {
9272                return Ok(None);
9273            }
9274            let Some(pos) = cols
9275                .iter()
9276                .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
9277            else {
9278                return Ok(None);
9279            };
9280            if !matches!(
9281                cols[pos].ty,
9282                spg_storage::DataType::SmallInt
9283                    | spg_storage::DataType::Int
9284                    | spg_storage::DataType::BigInt
9285            ) {
9286                return Ok(None);
9287            }
9288            key_pos[slot] = pos;
9289            descs[slot] = order.desc;
9290            nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
9291        }
9292
9293        let sess = self.dml_session();
9294        let ctx = EvalContext::new(&cols, Some(alias))
9295            .with_catalog(self.active_catalog())
9296            .with_session(&sess);
9297        let projection = build_projection(
9298            &stmt.items,
9299            &cols,
9300            alias,
9301            self.speaks_mysql,
9302            Some(self.active_catalog()),
9303        )?;
9304        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9305        let bound_pos: Vec<Option<usize>> = projection
9306            .iter()
9307            .map(|p| match &p.expr {
9308                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9309                    Ok(Some(pos)) => Some(pos),
9310                    _ => None,
9311                },
9312                _ => None,
9313            })
9314            .collect();
9315        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9316            .where_
9317            .as_ref()
9318            .filter(|w| crate::eval::fully_compilable(w))
9319            .map(|w| crate::eval::compile_expr(w, &ctx));
9320
9321        // The same first-observable point the materialising planner fires,
9322        // placed after the gates so it fires exactly once: this lane runs
9323        // BEFORE that planner and would otherwise be a hole in the
9324        // panic-isolation and cancellation-race coverage rather than a
9325        // faster path through it.
9326        crate::injection_point!("planner_first_row_fetch", &stmt.from);
9327
9328        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9329        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9330        let mut budget = ByteBudget::new(self.max_query_bytes);
9331        let snapshot = self.current_snapshot();
9332        // Keys, a NULL bit per key slot, and the row. The bitmask keeps
9333        // the element small: a nullable key still costs one bit rather
9334        // than a second array.
9335        let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
9336
9337        for (ri, row) in table.rows().iter().enumerate() {
9338            if ri.is_multiple_of(256) {
9339                cancel.check()?;
9340            }
9341            if !table.is_row_visible(ri, &snapshot) {
9342                continue;
9343            }
9344            // The key comes from the STORED row, before projection: an
9345            // ORDER BY column need not appear in the select list.
9346            let mut keys = [0i64; MAX_KEYS];
9347            let mut nulls = 0u8;
9348            let mut keyed = true;
9349            for slot in 0..n_keys {
9350                match row.values.get(key_pos[slot]) {
9351                    Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
9352                    Some(Value::Int(v)) => keys[slot] = i64::from(*v),
9353                    Some(Value::BigInt(v)) => keys[slot] = *v,
9354                    Some(Value::Null) | None => nulls |= 1 << slot,
9355                    // An integer column holding something else is a row
9356                    // this lane cannot order; hand the whole query back
9357                    // rather than guess at it.
9358                    _ => {
9359                        keyed = false;
9360                        break;
9361                    }
9362                }
9363            }
9364            if !keyed {
9365                return Ok(None);
9366            }
9367            if !Self::stream_filter_project(
9368                row,
9369                stmt.where_.as_ref(),
9370                compiled_where.as_ref(),
9371                &mut eval_stack,
9372                &projection,
9373                &bound_pos,
9374                &ctx,
9375                &mut values,
9376            )? {
9377                continue;
9378            }
9379            budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
9380            sorted.push((keys, nulls, core::mem::take(&mut values)));
9381            values.reserve(projection.len());
9382        }
9383
9384        sorted.sort_by(|a, b| {
9385            use core::cmp::Ordering;
9386            for slot in 0..n_keys {
9387                let bit = 1u8 << slot;
9388                let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
9389                    (true, true) => Ordering::Equal,
9390                    // Where the NULLs go is already decided — `nulls_first`
9391                    // resolved DESC's default when it was read. Reversing
9392                    // this for DESC as well would apply the direction
9393                    // twice and put them at the wrong end.
9394                    (true, false) => {
9395                        if nulls_first[slot] {
9396                            Ordering::Less
9397                        } else {
9398                            Ordering::Greater
9399                        }
9400                    }
9401                    (false, true) => {
9402                        if nulls_first[slot] {
9403                            Ordering::Greater
9404                        } else {
9405                            Ordering::Less
9406                        }
9407                    }
9408                    (false, false) => {
9409                        let o = a.0[slot].cmp(&b.0[slot]);
9410                        if descs[slot] { o.reverse() } else { o }
9411                    }
9412                };
9413                if ord != Ordering::Equal {
9414                    return ord;
9415                }
9416            }
9417            Ordering::Equal
9418        });
9419
9420        emit(crate::StreamItem::Header(&columns))?;
9421        // v7.38.14 — DISTINCT, de-duplicated against the PREVIOUS row.
9422        //
9423        // The gate above only admits DISTINCT when the sort key determines
9424        // the projected row, so every duplicate is adjacent to its twin by
9425        // the time this loop runs and one comparison replaces a hash table
9426        // of every row seen. Equality is `values_eq_norm` with the same mask
9427        // the materialising path builds -- deliberately the same function,
9428        // because a de-duplication that disagreed with the one on the other
9429        // path would make the answer depend on which lane a query took.
9430        //
9431        // A query that did not ask for DISTINCT pays one already-false bool
9432        // test per row: the short-circuit means the comparison never runs
9433        // and `prev` is never written.
9434        let dedup_mask = fold_mask(&projection);
9435        let fold = FoldSpec::of(self.speaks_mysql, &dedup_mask);
9436        let mut count = 0usize;
9437        let mut prev: Option<&[Value<'static>]> = None;
9438        for (_, _, vals) in &sorted {
9439            if stmt.distinct
9440                && let Some(p) = prev
9441                && values_eq_norm(p, vals, fold)
9442            {
9443                continue;
9444            }
9445            emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
9446            count += 1;
9447            if stmt.distinct {
9448                prev = Some(vals);
9449            }
9450        }
9451        Ok(Some(count))
9452    }
9453
9454    /// v7.38.14 — would sorting place every duplicate next to its twin?
9455    ///
9456    /// True when the projected expressions and the ORDER BY expressions are the
9457    /// same SET. Then the sort key determines the projected row, so equal rows
9458    /// are adjacent afterwards and an adjacent comparison de-duplicates exactly
9459    /// as a hash would -- and, because both sort paths are stable, the survivor
9460    /// is the first-seen row, which is the one the hash keeps too.
9461    ///
9462    /// A wildcard's expansion is not known here, so it is not a set this can
9463    /// compare; an ordinal ORDER BY names a select-list position rather than a
9464    /// value and is left alone.
9465    fn distinct_is_adjacent_after_sort(stmt: &SelectStatement) -> bool {
9466        if stmt.order_by.is_empty() || !stmt.distinct_on.is_empty() {
9467            return false;
9468        }
9469        let mut projected: alloc::vec::Vec<&Expr> =
9470            alloc::vec::Vec::with_capacity(stmt.items.len());
9471        for item in &stmt.items {
9472            match item {
9473                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return false,
9474                SelectItem::Expr { expr, .. } => projected.push(expr),
9475            }
9476        }
9477        if projected.is_empty() {
9478            return false;
9479        }
9480        let keys: alloc::vec::Vec<&Expr> = stmt.order_by.iter().map(|o| &o.expr).collect();
9481        if keys
9482            .iter()
9483            .any(|k| matches!(k, Expr::Literal(spg_sql::ast::Literal::Integer(_))))
9484        {
9485            return false;
9486        }
9487        projected.iter().all(|p| keys.contains(p)) && keys.iter().all(|k| projected.contains(k))
9488    }
9489
9490    fn try_spill_sorted_stream<F>(
9491        &self,
9492        stmt: &SelectStatement,
9493        from: &FromClause,
9494        cancel: CancelToken<'_>,
9495        emit: &mut F,
9496    ) -> Result<Option<usize>, EngineError>
9497    where
9498        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9499    {
9500        // The shapes `try_spill_sorted_scan` declines, plus the ones the
9501        // streaming executor does not carry (a LIMIT is already bounded
9502        // by a partial sort; the rest need the answer addressable).
9503        if !self.can_spill()
9504            || stmt.order_by.is_empty()
9505            || stmt.distinct
9506            || stmt.limit_with_ties
9507            || stmt.limit.is_some()
9508            || stmt.offset.is_some()
9509            || stmt.having.is_some()
9510            || stmt.group_by.is_some()
9511            || !stmt.unions.is_empty()
9512            || !from.joins.is_empty()
9513            || from.primary.lateral_subquery.is_some()
9514            || from.primary.unnest_expr.is_some()
9515            || from.primary.as_of_segment.is_some()
9516            || from.primary.generate_series_args.is_some()
9517            || select_has_window(stmt)
9518            || aggregate::uses_aggregate(stmt)
9519        {
9520            return Ok(None);
9521        }
9522        if stmt
9523            .items
9524            .iter()
9525            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9526        {
9527            return Ok(None);
9528        }
9529        // Everything `exec_bare_select_cancel` does before it scans runs
9530        // BELOW this path, so a statement claimed here skips it. Three of
9531        // those were missed on the way in and each was caught by a
9532        // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
9533        // ORDER BY 2` sorted happily instead of raising 42P10), the
9534        // cancellation check by another, the partition fan-out by the
9535        // differential corpus. What is reconciled, item by item: with-ties
9536        // needs ORDER BY (gated above), USING/NATURAL and RLS join
9537        // rewrites (joins gated above), the single-table RLS predicate
9538        // (the dispatcher declines a policy-subject table before this is
9539        // reached), the meta-view dispatch (those names are not in the
9540        // catalog, so the lookup below declines). These three are calls,
9541        // so the message and SQLSTATE are the ones the fall-back gives —
9542        // `select_has_window` above reads the select list and ORDER BY but
9543        // not WHERE, which is the case the third one covers.
9544        crate::orderby::check_order_by_legality(stmt)?;
9545        crate::orderby::check_order_by_positions(stmt)?;
9546        crate::window::reject_window_in_row_clauses(stmt)?;
9547        // A parent's rows are its children's. These walks scan the named
9548        // relation alone, so a partitioned or inherited parent comes back
9549        // short — and silently: the corpus caught `SELECT id FROM pr
9550        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
9551        // parent's own rows instead of the partitions'. `ONLY` is exactly
9552        // the case that does not fan out, so it stays, which is the test
9553        // the FROM-clause fan-out itself makes.
9554        if !from.primary.only
9555            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
9556        {
9557            return Ok(None);
9558        }
9559        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9560            return Ok(None);
9561        };
9562        // Cold-tier rows live outside `rows()`; this walk would drop
9563        // them silently, the same reason round 831's walk declines.
9564        if table.has_cold_rows_fast() {
9565            return Ok(None);
9566        }
9567
9568        let alias = from
9569            .primary
9570            .alias
9571            .as_deref()
9572            .unwrap_or(from.primary.name.as_str());
9573        let cols = table.schema().columns.clone();
9574        let sess = self.dml_session();
9575        let ctx = EvalContext::new(&cols, Some(alias))
9576            .with_catalog(self.active_catalog())
9577            .with_session(&sess);
9578        let projection = build_projection(
9579            &stmt.items,
9580            &cols,
9581            alias,
9582            self.speaks_mysql,
9583            Some(self.active_catalog()),
9584        )?;
9585        let order_by = stmt.order_by.clone();
9586        // The same one-shot resolution the general path does (round
9587        // 582): each ORDER BY column is bound once, not once per row.
9588        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
9589        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
9590        // Resolved BEFORE the scan, because it now decides what the sort
9591        // STORES and not just what it decodes (round 995).
9592        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
9593
9594        // v7.38.22 — resolved HERE, because this path did not resolve
9595        // them at all.
9596        //
9597        // Every published SPG through 7.38.21 answered `ORDER BY s COLLATE
9598        // "en_US.utf8"` in BYTE order on this path — and swallowed an
9599        // unknown collation name rather than raising — because the sorter
9600        // below compared with an empty collation slice. The materialising
9601        // path honoured both. Which answer a query got depended on which
9602        // path the planner took, and this is the path a plain single-table
9603        // SELECT takes.
9604        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
9605        // v7.39.12 — a correlated scalar subquery in ORDER BY is
9606        // resolved for the row before its key is built.
9607        //
9608        // Uncorrelated subqueries are replaced by a literal before
9609        // execution; a correlated one cannot be, so it reached the
9610        // per-row evaluator — the one place that cannot run a subquery
9611        // — and the statement raised "subquery reached row eval".
9612        // Reported by sentori against 7.39.11; see
9613        // `Engine::order_by_resolved_for_row`.
9614        //
9615        // The `any` runs once, here, so an ordinary ORDER BY pays one
9616        // bool per row and nothing else.
9617        let order_has_subquery = order_by
9618            .iter()
9619            .any(|o| crate::subquery::expr_has_subquery(&o.expr));
9620        let unbound: Vec<Option<usize>> = alloc::vec![None; order_by.len()];
9621        let mut sorter = crate::extsort::ExternalSorter::new(
9622            self.temp_run_factory,
9623            self.session_work_mem_bytes(),
9624            cols.clone(),
9625            &descs,
9626            &order_colls,
9627        )
9628        .with_stats(&self.spill_stats)
9629        .with_pruned(&needed);
9630        let snapshot = self.current_snapshot();
9631        // One key buffer for the whole scan: `push` drains it and leaves
9632        // the capacity behind.
9633        let mut keys: Vec<OrderKey> = Vec::new();
9634        // r1024 — compile the predicate once for the scan.
9635        //
9636        // These two sorted-spill scans are the paths a single-table SELECT
9637        // with an ORDER BY takes, and they were the last row-returning ones
9638        // still walking the expression tree per row. r1023 did the
9639        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
9640        // exactly this shape.
9641        //
9642        // Found from the profile's CALL TREE rather than its leaves. The
9643        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
9644        // 261, `mod_op` 178 — and two attempts at reasoning out which
9645        // function asked for it were both wrong. The tree names the caller
9646        // chain, and it named this one.
9647        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9648            .where_
9649            .as_ref()
9650            .filter(|w| crate::eval::fully_compilable(w))
9651            .map(|w| crate::eval::compile_expr(w, &ctx));
9652        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9653        for (i, row) in table.scan_visible_from(0, &snapshot) {
9654            if i.is_multiple_of(256) {
9655                cancel.check()?;
9656            }
9657            if let Some(c) = &compiled_where {
9658                if !crate::eval::compiled::eval_compiled_pred(
9659                    c,
9660                    row,
9661                    &ctx,
9662                    &mut eval_stack,
9663                    ctx.mysql_dialect,
9664                )? {
9665                    continue;
9666                }
9667            } else if let Some(w) = &stmt.where_ {
9668                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
9669                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9670                    continue;
9671                }
9672            }
9673            keys.clear();
9674            // The same collations the sorter compares with, and the
9675            // re-derivation below is handed the same ones. `finish`'s
9676            // contract is that a key comes back the way it was pushed;
9677            // a collation is part of the way it was pushed.
9678            if order_has_subquery {
9679                // A substituted literal is no longer a bound column.
9680                let per_row = self.order_by_resolved_for_row(&order_by, row, &ctx, cancel)?;
9681                crate::orderby::build_order_keys_bound(
9682                    per_row.as_deref().unwrap_or(&order_by),
9683                    &unbound,
9684                    &order_colls,
9685                    row,
9686                    &ctx,
9687                    &mut keys,
9688                )?;
9689            } else {
9690                crate::orderby::build_order_keys_bound(
9691                    &order_by,
9692                    &order_bound,
9693                    &order_colls,
9694                    row,
9695                    &ctx,
9696                    &mut keys,
9697                )?;
9698            }
9699            sorter.push(&mut keys, row)?;
9700        }
9701
9702        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9703        emit(crate::StreamItem::Header(&columns))?;
9704
9705        let key_ctx = &ctx;
9706        let mut emitted_since_check = 0usize;
9707        let n = sorter.finish_each(
9708            |src, buf| {
9709                crate::orderby::build_order_keys_rederived(
9710                    &order_by,
9711                    &order_bound,
9712                    &order_colls,
9713                    src,
9714                    key_ctx,
9715                    buf,
9716                )
9717            },
9718            |src, values| {
9719                for p in &projection {
9720                    values.push(
9721                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
9722                    );
9723                }
9724                Ok(())
9725            },
9726            |cells| {
9727                // The merge is the long half of a big sort, and the scan's
9728                // check above stops running once it ends: a cancelled
9729                // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
9730                // anyway. Same stride as the scan.
9731                emitted_since_check += 1;
9732                if emitted_since_check >= 256 {
9733                    emitted_since_check = 0;
9734                    cancel.check()?;
9735                }
9736                emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
9737            },
9738        )?;
9739        Ok(Some(n))
9740    }
9741
9742    /// One row of the single-table streaming walk: the WHERE test, the
9743    /// projection, the emit. Returns whether a row was emitted.
9744    ///
9745    /// v7.39 (round 970) — factored out because the walk now has two ways
9746    /// to reach a row, the sequential scan and an index seek's candidate
9747    /// positions, and both must do IDENTICALLY this. A copy in each is how
9748    /// two paths for one job drift; this file already carries the cost of
9749    /// that lesson twice (rounds 823 and 961, both resolvers).
9750    ///
9751    /// `#[inline]` so the scan loop keeps the shape round 957 measured it
9752    /// in — a shared hot path pays for a new abstraction whether or not it
9753    /// uses it, and this one is on the scan.
9754    #[inline]
9755    #[allow(clippy::too_many_arguments)]
9756    fn stream_filter_project(
9757        row: &spg_storage::Row<'static>,
9758        where_: Option<&Expr>,
9759        // r1023 — the same WHERE, compiled once by the caller. `None` means
9760        // the expression did not qualify and `where_` is evaluated as before.
9761        compiled_where: Option<&crate::eval::CompiledExpr>,
9762        eval_stack: &mut Vec<Value<'static>>,
9763        projection: &[ProjectedItem],
9764        bound_pos: &[Option<usize>],
9765        ctx: &crate::eval::EvalContext<'_>,
9766        values: &mut Vec<Value<'static>>,
9767    ) -> Result<bool, EngineError> {
9768        // r1023 — this scan ran its predicate through the TREE INTERPRETER,
9769        // once per row, and it was the only row-returning path that did.
9770        // The aggregate path, `table_access`, and the PK walker all compile
9771        // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
9772        // server's live samples were `eval_expr` 99, `apply_binary` 81,
9773        // `mod_op` 29 — the interpreter, not delivery.
9774        //
9775        // The arithmetic accounted for it exactly. Over the wire, the same
9776        // filter costs 6.375 ms returning rows and 0.679 ms counting them;
9777        // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
9778        // which is what an interpreted predicate costs against the compiled
9779        // lane's 11.7. It was named "delivery after a filter" before this
9780        // profile, and it was never delivery.
9781        if let Some(c) = compiled_where {
9782            if !crate::eval::compiled::eval_compiled_pred(
9783                c,
9784                row,
9785                ctx,
9786                eval_stack,
9787                ctx.mysql_dialect,
9788            )? {
9789                return Ok(false);
9790            }
9791        } else if let Some(w) = where_ {
9792            let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
9793            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9794                return Ok(false);
9795            }
9796        }
9797        values.clear();
9798        for (p, bound) in projection.iter().zip(bound_pos) {
9799            values.push(match bound {
9800                Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
9801                None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
9802            });
9803        }
9804        Ok(true)
9805    }
9806
9807    /// The same filter and projection, then emit. Split from
9808    /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
9809    /// before it can emit them — a sort — runs the identical predicate and
9810    /// projection rather than a second copy of them.
9811    #[allow(clippy::too_many_arguments)]
9812    fn stream_project_row<F>(
9813        row: &spg_storage::Row<'static>,
9814        where_: Option<&Expr>,
9815        compiled_where: Option<&crate::eval::CompiledExpr>,
9816        eval_stack: &mut Vec<Value<'static>>,
9817        projection: &[ProjectedItem],
9818        bound_pos: &[Option<usize>],
9819        ctx: &crate::eval::EvalContext<'_>,
9820        values: &mut Vec<Value<'static>>,
9821        emit: &mut F,
9822    ) -> Result<bool, EngineError>
9823    where
9824        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9825    {
9826        if !Self::stream_filter_project(
9827            row,
9828            where_,
9829            compiled_where,
9830            eval_stack,
9831            projection,
9832            bound_pos,
9833            ctx,
9834            values,
9835        )? {
9836            return Ok(false);
9837        }
9838        emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
9839        Ok(true)
9840    }
9841
9842    fn try_stream_single_table<F>(
9843        &self,
9844        stmt: &SelectStatement,
9845        from: &FromClause,
9846        cancel: CancelToken<'_>,
9847        emit: &mut F,
9848    ) -> Result<Option<usize>, EngineError>
9849    where
9850        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9851    {
9852        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9853            return Ok(None);
9854        };
9855        // Cold-tier rows live outside `rows()`; the materialising fallback
9856        // covers both tiers and this walk would silently drop them.
9857        if table.has_cold_rows_fast() {
9858            return Ok(None);
9859        }
9860        let alias = from
9861            .primary
9862            .alias
9863            .as_deref()
9864            .unwrap_or(from.primary.name.as_str());
9865        let cols = table.schema().columns.clone();
9866        let sess = self.dml_session();
9867        let ctx = EvalContext::new(&cols, Some(alias))
9868            .with_catalog(self.active_catalog())
9869            .with_session(&sess);
9870        let projection = build_projection(
9871            &stmt.items,
9872            &cols,
9873            alias,
9874            self.speaks_mysql,
9875            Some(self.active_catalog()),
9876        )?;
9877
9878        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9879        emit(crate::StreamItem::Header(&columns))?;
9880
9881        // v7.37 (round 957) — resolve each bare-column projection ONCE
9882        // instead of once per row. `find_column_pos`-style resolution is a
9883        // linear walk of the schema comparing column-name strings, and the
9884        // row loop below ran it for every cell of every row: measured at
9885        // 400k rows, binding it out of the loop took `SELECT pad` from
9886        // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
9887        //
9888        // ORDER BY has bound its keys this way since round 582
9889        // (`order_by_bound_positions`); the projection never did.
9890        //
9891        // `locate_column` is the same resolution `resolve_column` performs,
9892        // returning the site instead of the value, so the two cannot drift
9893        // apart the way a second hand-written resolver would. Anything it
9894        // declines — an expression, a whole-row reference, a name that does
9895        // not resolve — binds to `None` and takes the general path below,
9896        // errors included, so an empty table still reports nothing rather
9897        // than raising at bind time.
9898        let bound_pos: Vec<Option<usize>> = projection
9899            .iter()
9900            .map(|p| match &p.expr {
9901                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9902                    Ok(Some(pos)) => Some(pos),
9903                    _ => None,
9904                },
9905                _ => None,
9906            })
9907            .collect();
9908
9909        // One snapshot for the whole scan, as the materialising path takes.
9910        let snapshot = self.current_snapshot();
9911
9912        // v7.39 (round 970) — ask the indices BEFORE walking the table.
9913        //
9914        // This walk had no index step at all, and it is preferred over the
9915        // materialising path, which does have one (`pick_indexed_rows` ->
9916        // `try_index_seek`). So a primary-key point lookup — the commonest
9917        // statement there is — read every row: measured on 500k rows,
9918        // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
9919        // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
9920        // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
9921        //
9922        // The control that named it: `... OFFSET 0` — semantically the same
9923        // query — answered in 0.159 ms, because OFFSET is one of the shape
9924        // gates that declines this walk and sends the statement to the path
9925        // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
9926        // no semantics in common; what they share is making this function
9927        // stand down.
9928        //
9929        // The seek only NARROWS: every candidate still goes through the
9930        // full WHERE below, exactly as the mutation paths use it, so a
9931        // partial index match cannot change an answer. Positions come back
9932        // already visibility-filtered and already capped at a quarter of the
9933        // table (round 490), so a seek can never cost more than the scan it
9934        // replaces, and `None` means "walk the table" as before.
9935        //
9936        // Sorted because the scan would have produced table order and the
9937        // index produces key order. Without an ORDER BY neither is promised,
9938        // but a walk that silently reorders its answer when an index happens
9939        // to exist is a difference nobody asked for.
9940        let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
9941            crate::index_access::try_index_seek_positions(
9942                w,
9943                &cols,
9944                table,
9945                alias,
9946                &snapshot,
9947                self.speaks_mysql,
9948            )
9949        });
9950
9951        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9952        // r1023 — compile the predicate once for the whole scan. Same gate
9953        // every other path uses: `fully_compilable` or keep the interpreter,
9954        // so a shape the VM cannot take answers exactly as it did before.
9955        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9956            .where_
9957            .as_ref()
9958            .filter(|w| crate::eval::fully_compilable(w))
9959            .map(|w| crate::eval::compile_expr(w, &ctx));
9960        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9961        let mut count: usize = 0;
9962        match seek_positions {
9963            Some(mut positions) => {
9964                positions.sort_unstable();
9965                for (n, pos) in positions.into_iter().enumerate() {
9966                    if n.is_multiple_of(256) {
9967                        cancel.check()?;
9968                    }
9969                    let Some(row) = table.rows().get(pos) else {
9970                        continue;
9971                    };
9972                    if Self::stream_project_row(
9973                        row,
9974                        stmt.where_.as_ref(),
9975                        compiled_where.as_ref(),
9976                        &mut eval_stack,
9977                        &projection,
9978                        &bound_pos,
9979                        &ctx,
9980                        &mut values,
9981                        emit,
9982                    )? {
9983                        count += 1;
9984                    }
9985                }
9986            }
9987            None => {
9988                // v7.38.11 — the streaming scan is the path a client
9989                // reaches over the wire, so it is the one that has to
9990                // ask the BRIN summary which slots can be skipped. The
9991                // predicate still runs on every row that survives.
9992                let slots = stmt
9993                    .where_
9994                    .as_ref()
9995                    .and_then(|w| crate::brin::candidate_slots(w, table))
9996                    .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
9997                for (i, row) in table.scan_visible_slots(slots, &snapshot) {
9998                    if i.is_multiple_of(256) {
9999                        cancel.check()?;
10000                    }
10001                    if Self::stream_project_row(
10002                        row,
10003                        stmt.where_.as_ref(),
10004                        compiled_where.as_ref(),
10005                        &mut eval_stack,
10006                        &projection,
10007                        &bound_pos,
10008                        &ctx,
10009                        &mut values,
10010                        emit,
10011                    )? {
10012                        count += 1;
10013                    }
10014                }
10015            }
10016        }
10017        Ok(Some(count))
10018    }
10019
10020    pub(crate) fn try_exec_joined_streaming<F>(
10021        &self,
10022        stmt: &SelectStatement,
10023        cancel: CancelToken<'_>,
10024        emit: &mut F,
10025    ) -> Result<Option<usize>, EngineError>
10026    where
10027        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
10028    {
10029        // Shape gates — keep the streamable surface narrow on
10030        // purpose. The fall-back path still handles everything else.
10031        let Some(from) = &stmt.from else {
10032            return Ok(None);
10033        };
10034        // v7.37 (round 830) — decline anything a row-security policy binds
10035        // for this session. Policies are injected in
10036        // `exec_bare_select_cancel`, below this path, so a statement claimed
10037        // here would read the table unfiltered: measured, `SELECT val FROM
10038        // sec` returned all three rows to a session whose policy allows two,
10039        // while `SELECT upper(val) FROM sec` — declined by the shape gates
10040        // and so materialised — returned the correct two.
10041        //
10042        // Declining sends it to the path that enforces. Teaching this one to
10043        // inject the predicate itself would keep the streaming benefit for
10044        // RLS tables and is the better end state; it is not what a
10045        // correctness fix should carry, and the fall-back is exactly as
10046        // correct, only slower.
10047        if self.select_reads_policy_subject_table(stmt) {
10048            return Ok(None);
10049        }
10050        // r1058 — a WITH list this path never materialises: the CTE
10051        // name would be resolved as a physical relation and error
10052        // ("relation \"big\" does not exist" over the extended
10053        // protocol, caught by the perm-runner's wire legs). The
10054        // materialising fallback owns CTE execution.
10055        if !stmt.ctes.is_empty() {
10056            return Ok(None);
10057        }
10058        // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
10059        // tables` and kin) exist only as synth arms on the
10060        // materialising path; claiming one here errored "relation
10061        // does not exist" over the extended protocol for a query the
10062        // simple protocol answered. Prefix test only — a genuinely
10063        // missing relation must keep erroring in-path.
10064        if from.primary.name.starts_with("__spg_")
10065            || from
10066                .joins
10067                .iter()
10068                .any(|j| j.table.name.starts_with("__spg_"))
10069        {
10070            return Ok(None);
10071        }
10072        // r1058 — decline partitioned / inheritance parents, same
10073        // shape of bug as the RLS decline above: this path scans the
10074        // named table's own (empty) heap, so `SELECT id, region FROM
10075        // cust` on a partition parent streamed ZERO rows over the wire
10076        // while COUNT(*) — an aggregate, materialised below — said 3.
10077        // Caught by the perm-runner's server permutations; the
10078        // materialising fallback expands children correctly.
10079        if crate::partition::has_children(self.active_catalog(), &from.primary.name)
10080            || from
10081                .joins
10082                .iter()
10083                .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
10084        {
10085            return Ok(None);
10086        }
10087        // v7.39 (round 790) — single-table SELECTs stream too. This
10088        // gate said "joins only" because the path was written for
10089        // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
10090        // fell to the materialising fallback, which builds the whole
10091        // `Vec<Row<'static>>` and only then iterates it. Measured on
10092        // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
10093        // reached through a one-row JOIN — 2.6x, purely for lacking a
10094        // join. The deferred-join structure handles one source as the
10095        // degenerate stride-1 case, so the walk below is unchanged.
10096        let _single_table = from.joins.is_empty();
10097        // An ORDER BY that the bounded sort can serve streams; everything
10098        // else still falls to the materialising fallback below.
10099        // r1025 — an ordering the index already holds needs no sort at all.
10100        // Tried before the spill sort, which is the path it replaces.
10101        if !stmt.order_by.is_empty()
10102            && from.joins.is_empty()
10103            && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
10104        {
10105            return Ok(Some(n));
10106        }
10107        if !stmt.order_by.is_empty()
10108            && from.joins.is_empty()
10109            && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
10110        {
10111            return Ok(Some(n));
10112        }
10113        // r1031 — integer keys carried inline instead of an `OrderKey`
10114        // vector per row. Tried AFTER the spill sort on purpose: this lane
10115        // buffers the whole answer, so anything the spill path would take
10116        // must keep taking it rather than be turned back into an in-memory
10117        // sort that answers with a budget error.
10118        if !stmt.order_by.is_empty()
10119            && from.joins.is_empty()
10120            && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
10121        {
10122            return Ok(Some(n));
10123        }
10124        if !stmt.order_by.is_empty()
10125            || stmt.limit.is_some()
10126            || stmt.offset.is_some()
10127            || stmt.having.is_some()
10128            || stmt.group_by.is_some()
10129            || stmt.distinct
10130            || !stmt.unions.is_empty()
10131            || stmt.limit_with_ties
10132        {
10133            return Ok(None);
10134        }
10135        if aggregate::uses_aggregate(stmt) {
10136            return Ok(None);
10137        }
10138        // No window / SRF on the streaming path.
10139        if select_has_window(stmt) {
10140            return Ok(None);
10141        }
10142        if stmt
10143            .items
10144            .iter()
10145            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
10146        {
10147            return Ok(None);
10148        }
10149        // v7.37 (round 831) — a joinless FROM over a plain stored table
10150        // never needs the deferred structure, and building one costs the
10151        // whole table. `materialise_table_ref_filtered` clones every row
10152        // into a `Vec<Row<'static>>` before anything is filtered or
10153        // projected, so peak cost tracks the TABLE, not the result:
10154        // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
10155        // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
10156        // projection saving nothing, while an arithmetic projection — which
10157        // the shape gates decline, so it materialises through the ordinary
10158        // executor — cost +21 MB.
10159        //
10160        // Scanning in batches and releasing each one is what `cursor_fill`
10161        // already does for a lazy cursor, and it is the same walk: resume
10162        // from a slot, take visible rows, evaluate, hand them over, drop
10163        // them. Round 800's finding stands and is why this reads rows OUT
10164        // rather than seeding the join by index — touching the stored
10165        // `PersistentVec` in place makes the whole table resident, which is
10166        // worse than the copy. Each batch is copied, then freed.
10167        if from.joins.is_empty()
10168            && from.primary.unnest_expr.is_none()
10169            && from.primary.lateral_subquery.is_none()
10170            && from.primary.as_of_segment.is_none()
10171            && from.primary.generate_series_args.is_none()
10172            && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
10173        {
10174            return Ok(Some(n));
10175        }
10176        // Build the deferred join under the regular byte budget.
10177        let mut budget = ByteBudget::new(self.max_query_bytes);
10178        let deferred = {
10179            let mut needed = alloc::collections::BTreeSet::new();
10180            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10181            self.build_joined_filtered_rows(
10182                from,
10183                stmt.where_.as_ref(),
10184                cancel,
10185                if prunable { Some(&needed) } else { None },
10186                &mut budget,
10187            )?
10188        };
10189        let combined_schema = &deferred.combined_schema;
10190        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10191        // `::regclass` / enum cast in a joined projection or HAVING needs it.
10192        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10193        // the same predicate the unjoined shape carries.
10194        let joined_sess = self.dml_session();
10195        // v7.38.18 — and the DIALECT. This context carried the catalog and
10196        // the session and not the one field that decides how text
10197        // compares, so a joined row was evaluated in PostgreSQL
10198        // semantics inside a MySQL session.
10199        //
10200        // It showed up only where the two sides had DIFFERENT text types:
10201        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10202        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10203        // were fine and the same comparison inside one table was fine.
10204        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10205        // so the wrong semantics were invisible until a CHAR's padding
10206        // had to be stripped and PostgreSQL's arm does not strip it.
10207        //
10208        // `with_engine` is what sets it; the next line already reaches
10209        // for `self.backslash_escapes`, so the dialect was in hand.
10210        let ctx = EvalContext::new(combined_schema, None)
10211            .with_catalog(self.active_catalog())
10212            .with_engine(self)
10213            .with_session(&joined_sess);
10214        let projection = build_projection(
10215            &stmt.items,
10216            combined_schema,
10217            "",
10218            self.speaks_mysql,
10219            Some(self.active_catalog()),
10220        )?;
10221        // Every projection item must be a bound qualified column —
10222        // anything that needs `eval_expr_with_correlated` keeps the
10223        // materialising path.
10224        let bound_pos = |e: &Expr| -> Option<usize> {
10225            match e {
10226                // v7.39 (round 822) — an UNQUALIFIED column resolves here
10227                // too. The `qualifier.is_some()` guard this replaces meant
10228                // `SELECT pad FROM big` — the commonest projection there is
10229                // — never reached the streaming walk: it fell out at this
10230                // gate and re-ran on the materialising path, after the
10231                // deferred join structure had already been built and paid
10232                // for. Measured (round 821, statement_timeout=120 over 400k
10233                // rows): `big.pad` and `b.pad` streamed and cancelled at
10234                // ~65k rows in 0.14 s, while bare `pad` ran to completion in
10235                // 0.80 s with the timeout never consulted. `find_column_pos`
10236                // has always handled the unqualified case (it falls through
10237                // to a by-name match), so the guard narrowed the gate for no
10238                // reason it recorded.
10239                Expr::Column(c) => eval::find_column_pos(c, &ctx),
10240                _ => None,
10241            }
10242        };
10243        let proj_decomposed: Vec<(usize, usize)> = {
10244            let mut out = Vec::with_capacity(projection.len());
10245            for p in &projection {
10246                let Some(abs) = bound_pos(&p.expr) else {
10247                    return Ok(None);
10248                };
10249                let Some(k) = deferred
10250                    .offsets
10251                    .partition_point(|&o| o <= abs)
10252                    .checked_sub(1)
10253                else {
10254                    return Ok(None);
10255                };
10256                out.push((k, abs - deferred.offsets[k]));
10257            }
10258            out
10259        };
10260        // Emit columns once.
10261        let columns: Vec<ColumnSchema> = projection
10262            .iter()
10263            // v7.39 (read01 round 54) — keep the column's enum identity through
10264            // the projection (it lives outside the DataType lattice), or a
10265            // derived table / UNION / windowed result forgets it and any outer
10266            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
10267            .map(|p| p.to_column_schema())
10268            .collect();
10269        emit(crate::StreamItem::Header(&columns))?;
10270        let sources_ref = &deferred.sources;
10271        let stride = deferred.stride;
10272        let survivors_ref = &deferred.survivors;
10273        let n_surv = if stride == 0 {
10274            0
10275        } else {
10276            survivors_ref.len() / stride
10277        };
10278        // Reused per-row cell-ref scratch — pushes are zero-alloc
10279        // after the first row.
10280        let null_value = Value::Null;
10281        let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
10282        let mut count: usize = 0;
10283        for surv_i in 0..n_surv {
10284            if surv_i.is_multiple_of(256) {
10285                cancel.check()?;
10286            }
10287            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10288            cell_refs.clear();
10289            for &(k, col_in_src) in &proj_decomposed {
10290                let ri = tuple[k];
10291                let v: &Value = if ri == usize::MAX {
10292                    &null_value
10293                } else {
10294                    sources_ref[k]
10295                        .get(ri)
10296                        .and_then(|r| r.values.get(col_in_src))
10297                        .unwrap_or(&null_value)
10298                };
10299                cell_refs.push(v);
10300            }
10301            emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
10302            count += 1;
10303        }
10304        Ok(Some(count))
10305    }
10306
10307    fn exec_joined_select(
10308        &self,
10309        stmt: &SelectStatement,
10310        from: &FromClause,
10311        cancel: CancelToken<'_>,
10312    ) -> Result<QueryResult, EngineError> {
10313        // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
10314        // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
10315        // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
10316        // FROM B WHERE B.k = A.k)` into
10317        //   SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
10318        //   WHERE B.k IS NULL
10319        // The general join executor builds a hash, probes every outer
10320        // tuple, materialises (left_padded_with_null) for every miss,
10321        // then runs the aggregate over the result set. For COUNT(*) we
10322        // only need the count — skip the tuple materialisation. Build
10323        // a HashSet of B's unique join values, scan A's PK index, and
10324        // increment the counter on each miss. PG's Merge Anti-Join
10325        // does roughly this; ours becomes a simple HashSet probe.
10326        if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
10327            return Ok(out);
10328        }
10329        // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
10330        // When ORDER BY is on an indexed primary column, walking the
10331        // btree in the requested direction lets the streamer break
10332        // after `LIMIT + OFFSET` survivors without ever materialising
10333        // the rest of the join — the 80 ms `mailrs_prod_not_exists`
10334        // plateau is exactly this shape.
10335        if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
10336            return Ok(out);
10337        }
10338        // v7.30.3 (mailrs round-26) — the bounded single-join path
10339        // first; peak memory scales with LIMIT instead of the table.
10340        if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
10341            return Ok(out);
10342        }
10343        // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
10344        // WHERE materialisation to the shared helper so the LATERAL
10345        // / UNNEST / regular-catalog paths route through one place.
10346        // (`build_joined_filtered_rows` carries LATERAL support as
10347        // of Phase 3.P0-41.) Downstream we still handle aggregate /
10348        // projection / ORDER BY / DISTINCT / LIMIT inline because
10349        // those depend on the SelectStatement's items list.
10350        let mut budget = ByteBudget::new(self.max_query_bytes);
10351        let deferred = {
10352            let mut needed = alloc::collections::BTreeSet::new();
10353            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
10354            self.build_joined_filtered_rows(
10355                from,
10356                stmt.where_.as_ref(),
10357                cancel,
10358                if prunable { Some(&needed) } else { None },
10359                &mut budget,
10360            )?
10361        };
10362        let combined_schema = &deferred.combined_schema;
10363        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
10364        // `::regclass` / enum cast in a joined projection or HAVING needs it.
10365        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
10366        // the same predicate the unjoined shape carries.
10367        let joined_sess = self.dml_session();
10368        // v7.38.18 — and the DIALECT. This context carried the catalog and
10369        // the session and not the one field that decides how text
10370        // compares, so a joined row was evaluated in PostgreSQL
10371        // semantics inside a MySQL session.
10372        //
10373        // It showed up only where the two sides had DIFFERENT text types:
10374        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
10375        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
10376        // were fine and the same comparison inside one table was fine.
10377        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
10378        // so the wrong semantics were invisible until a CHAR's padding
10379        // had to be stripped and PostgreSQL's arm does not strip it.
10380        //
10381        // `with_engine` is what sets it; the next line already reaches
10382        // for `self.backslash_escapes`, so the dialect was in hand.
10383        let ctx = EvalContext::new(combined_schema, None)
10384            .with_catalog(self.active_catalog())
10385            .with_engine(self)
10386            .with_session(&joined_sess);
10387        // Aggregate path: handle GROUP BY / aggregate calls over the
10388        // joined+filtered rows.
10389        if aggregate::uses_aggregate(stmt) {
10390            // v7.32 (P4 borrow channel, increment 2) — borrow each
10391            // surviving join tuple as a RowRef::Tuple; the aggregate
10392            // engine reads source cells by reference (bound fast path =
10393            // zero clone) instead of consuming materialised combined
10394            // Rows. This is where the +211k materialise_tuple_vals
10395            // clones disappear for the join+aggregate shape.
10396            let refs = deferred.row_refs();
10397            // v7.29 — a per-query memo so correlated scalar
10398            // subqueries batch-evaluate once (group map) instead of
10399            // executing per group.
10400            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
10401            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
10402                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
10403                    .map_err(|err| match err {
10404                        EngineError::Eval(ev) => ev,
10405                        other => eval::EvalError::TypeMismatch {
10406                            detail: alloc::format!("{other}"),
10407                        },
10408                    })
10409            };
10410            let agg = aggregate::run(
10411                stmt,
10412                crate::join::AggRows::Refs(&refs),
10413                combined_schema,
10414                None,
10415                Some(&agg_correlated),
10416                self.parallel_runner.0.as_deref(),
10417                Some(self.active_catalog()),
10418                Some(self),
10419            )?;
10420            return self.finish_agg_result(agg, stmt, cancel);
10421        }
10422
10423        let projection = build_projection(
10424            &stmt.items,
10425            combined_schema,
10426            "",
10427            self.speaks_mysql,
10428            Some(self.active_catalog()),
10429        )?;
10430        // v7.39 (round 734) — a set-returning projection over a JOIN.
10431        // This executor's projection loop treats every item as a scalar,
10432        // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
10433        // "function unnest(integer[]) does not exist" where PG expands
10434        // it. The row-set executor already carries the full SRF pipeline
10435        // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
10436        // sharding): materialise the joined survivors and hand over. The
10437        // WHERE is cleared — the join already applied it, and combined
10438        // columns resolve identically in both executors.
10439        if !self.srf_target_idxs(&projection).is_empty() {
10440            let refs = deferred.row_refs();
10441            let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
10442            let mut s2 = stmt.clone();
10443            s2.where_ = None;
10444            let schema = combined_schema.clone();
10445            return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
10446        }
10447        // v7.33 (P4 borrow channel, increment 3) — project directly off
10448        // the deferred row-index tuples instead of materialising an
10449        // intermediate combined Row per survivor. A bound qualified
10450        // column is read by reference (`RowRef::get` → `tuple_value`) and
10451        // cloned ONCE into the output row; the old `materialise()` (a full
10452        // combined Row plus a source→intermediate clone per referenced
10453        // cell, for every survivor) is gone. A row materialises on demand
10454        // only when a projection or ORDER BY expression needs the eval
10455        // path (subquery / function / arithmetic / unqualified column).
10456        // Same bind-once classification the aggregate input fast path uses
10457        // (`accumulate_groups`), reading the same `tuple_value` mapping the
10458        // differential gate already covers.
10459        let refs = deferred.row_refs();
10460        let bound_pos = |e: &Expr| -> Option<usize> {
10461            match e {
10462                Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
10463                _ => None,
10464            }
10465        };
10466        let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
10467        let all_proj_bound = proj_pos.iter().all(Option::is_some);
10468        // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
10469        // pre-decompose each bound projection position into
10470        // `(source_k, col_in_source)` so the per-row column read
10471        // skips the per-cell `tuple_value` partition_point + slice
10472        // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
10473        // calls) that walk dominated; this version reaches into
10474        // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
10475        let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
10476            .iter()
10477            .map(|p| {
10478                p.and_then(|abs| {
10479                    let k = deferred
10480                        .offsets
10481                        .partition_point(|&o| o <= abs)
10482                        .checked_sub(1)?;
10483                    Some((k, abs - deferred.offsets[k]))
10484                })
10485            })
10486            .collect();
10487        // v7.39 (round 962) — which projection items are whole-row
10488        // references, and to which join source. The test is
10489        // `locate_column` declining the name, which is the SAME resolver
10490        // the evaluation path uses, so this cannot drift from it: a real
10491        // column carrying an alias's name resolves to a position and is
10492        // not reported here. The source index comes from the alias
10493        // prefix, the way the combined schema names its columns.
10494        let whole_row_src: Vec<Option<usize>> = projection
10495            .iter()
10496            .map(|p| {
10497                let Expr::Column(c) = &p.expr else {
10498                    return None;
10499                };
10500                if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
10501                    return None;
10502                }
10503                let prefix = alloc::format!("{name}.", name = c.name);
10504                let abs = deferred
10505                    .combined_schema
10506                    .iter()
10507                    .position(|s| s.name.starts_with(&prefix))?;
10508                deferred
10509                    .offsets
10510                    .partition_point(|&o| o <= abs)
10511                    .checked_sub(1)
10512            })
10513            .collect();
10514        // ORDER BY (when present) still evaluates against a materialised
10515        // Row — keep the order-key encoder correct rather than fork it.
10516        let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
10517        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
10518        let mut proj_memo = memoize::MemoizeCache::default();
10519        let sources_ref = &deferred.sources;
10520        let stride = deferred.stride;
10521        let survivors_ref = &deferred.survivors;
10522        let n_surv = survivors_ref.len() / stride.max(1);
10523        // v7.38 (read01 B8) — streaming top-N budget (see the sibling
10524        // single-table path). Bounds this JOIN projection's accumulator
10525        // to O(keep) for `ORDER BY … LIMIT k`.
10526        let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
10527            && !stmt.distinct
10528            && !stmt.limit_with_ties
10529            && !self.env_cfg().disable_topk
10530        {
10531            stmt.limit_literal().and_then(|l| {
10532                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
10533                (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
10534            })
10535        } else {
10536            None
10537        };
10538        // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
10539        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
10540            hashbrown::HashMap::new();
10541        let distinct_hb = hashbrown::DefaultHashBuilder::default();
10542        // v7.38.13 — which output positions must NOT fold. Built once per
10543        // scan from the projection, which carries the source column's
10544        // byte-wise-ness; see `FoldSpec`.
10545        let distinct_mask = fold_mask(&projection);
10546        for surv_i in 0..n_surv {
10547            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
10548            let row = &refs[surv_i];
10549            let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
10550                Some(row.as_row())
10551            } else {
10552                None
10553            };
10554            let mut values = Vec::with_capacity(projection.len());
10555            for (i, p) in projection.iter().enumerate() {
10556                if let Some((k, col_in_src)) = proj_decomposed[i] {
10557                    // v7.36 — direct (source_k, col) lookup, no
10558                    // partition_point. tuple[k] is the row index in
10559                    // sources[k]; LEFT-NULL slots are `usize::MAX`.
10560                    let ri = tuple[k];
10561                    let v: Value<'static> = if ri == usize::MAX {
10562                        Value::Null
10563                    } else {
10564                        sources_ref[k]
10565                            .get(ri)
10566                            .and_then(|r| r.values.get(col_in_src))
10567                            .cloned()
10568                            .map(Value::into_owned)
10569                            .unwrap_or(Value::Null)
10570                    };
10571                    values.push(v);
10572                } else if let Some(pos) = proj_pos[i] {
10573                    // Bound but couldn't decompose (shouldn't normally
10574                    // happen — keep as a safe path).
10575                    values.push(
10576                        row.get(pos)
10577                            .cloned()
10578                            .map(Value::into_owned)
10579                            .unwrap_or(Value::Null),
10580                    );
10581                } else if let Some(k) = whole_row_src[i]
10582                    && tuple[k] == usize::MAX
10583                {
10584                    // v7.39 (round 962) — a whole-row reference to a side
10585                    // an OUTER join null-extended is NULL, not a
10586                    // composite whose fields are all NULL. PG18.4 answers
10587                    // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
10588                    // an empty cell; round 961 answered `(,)`.
10589                    //
10590                    // The evaluator below cannot tell the two apart: it
10591                    // reads the MATERIALISED combined row, where a
10592                    // null-extended side is indistinguishable from a real
10593                    // row whose every column is NULL — and that row is
10594                    // `(,)` in PG too, so guessing by "all fields NULL"
10595                    // would trade one wrong answer for another. The
10596                    // tuple, which is still in hand here, does know:
10597                    // `usize::MAX` is the sentinel the join writes for
10598                    // exactly this.
10599                    values.push(Value::Null);
10600                } else {
10601                    // Eval path — `materialised` is Some whenever any
10602                    // projection item is non-bound (need_eval_row true).
10603                    // v7.24 (round-16 B) — select-list subqueries under a
10604                    // JOIN go through the correlated-aware evaluator too.
10605                    let mrow = materialised.as_deref().expect("materialised for eval");
10606                    values.push(self.eval_expr_with_correlated(
10607                        &p.expr,
10608                        mrow,
10609                        &ctx,
10610                        cancel,
10611                        Some(&mut proj_memo),
10612                    )?);
10613                }
10614            }
10615            let out_row = Row::new(values);
10616            // v7.37.16 — streaming DISTINCT (see the scan-path twin):
10617            // probe on the projected row; duplicates skip the
10618            // build_order_keys eval and never enter `tagged`.
10619            if stmt.distinct {
10620                let bucket = seen_distinct
10621                    .entry(norm_hash_row(
10622                        &out_row,
10623                        &distinct_hb,
10624                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10625                    ))
10626                    .or_default();
10627                if bucket.iter().any(|i| {
10628                    row_eq_norm(
10629                        &tagged[i].1,
10630                        &out_row,
10631                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
10632                    )
10633                }) {
10634                    continue;
10635                }
10636                bucket.push(tagged.len());
10637            }
10638            let order_keys = if stmt.order_by.is_empty() {
10639                Vec::new()
10640            } else {
10641                let mrow = materialised.as_deref().expect("materialised for order by");
10642                build_order_keys(&stmt.order_by, mrow, &ctx)?
10643            };
10644            budget.charge(approx_row_bytes(&out_row))?;
10645            tagged.push((order_keys, out_row));
10646            if let Some((k, descs)) = &topk_stream {
10647                topk_trim(&mut tagged, *k, descs);
10648            }
10649        }
10650        if !stmt.order_by.is_empty() {
10651            // v7.38 元机制 D acceptor — see other call site above.
10652            let keep = if self.env_cfg().disable_topk {
10653                None
10654            } else {
10655                stmt.limit_literal()
10656                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
10657            };
10658            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
10659            // v7.39 (round 688) — the join's ORDER BY resolves its keys
10660            // against `ctx`, which is built from `build_combined_schema`, so
10661            // this is where a declared collation reaches the sort. There was
10662            // exactly ONE resolver call in the engine before this — the
10663            // single-table scan's — which is why every other shape sorted by
10664            // bytes no matter what the schemas carried.
10665            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
10666            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
10667        }
10668        let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
10669        apply_offset_and_limit(
10670            &mut output_rows,
10671            stmt.offset_literal(),
10672            stmt.limit_literal(),
10673        );
10674        let columns: Vec<ColumnSchema> = projection
10675            .into_iter()
10676            .map(|p| p.to_column_schema())
10677            .collect();
10678        Ok(QueryResult::Rows {
10679            columns,
10680            rows: output_rows,
10681        })
10682    }
10683}
10684
10685impl Engine {
10686    /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
10687    /// by id, decodes each row body against the table's current
10688    /// schema, applies the SELECT's projection + optional WHERE +
10689    /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
10690    /// / ORDER BY are unsupported on this path (STABILITY carve-
10691    /// out); operators wanting them should restore the segment
10692    /// into a regular table first.
10693    fn exec_select_as_of_segment(
10694        &self,
10695        stmt: &SelectStatement,
10696        from: &spg_sql::ast::FromClause,
10697        segment_id: u32,
10698    ) -> Result<QueryResult, EngineError> {
10699        // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
10700        // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
10701        if !from.joins.is_empty()
10702            || stmt.group_by.is_some()
10703            || stmt.having.is_some()
10704            || !stmt.unions.is_empty()
10705            || !stmt.order_by.is_empty()
10706            || stmt.offset.is_some()
10707            || stmt.distinct
10708            || aggregate::uses_aggregate(stmt)
10709        {
10710            return Err(EngineError::Unsupported(
10711                "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
10712                 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
10713                    .into(),
10714            ));
10715        }
10716        let table = self
10717            .active_catalog()
10718            .get(&from.primary.name)
10719            .ok_or_else(|| StorageError::TableNotFound {
10720                name: from.primary.name.clone(),
10721            })?;
10722        let schema = table.schema().clone();
10723        let schema_cols = &schema.columns;
10724        let alias = from
10725            .primary
10726            .alias
10727            .as_deref()
10728            .unwrap_or(from.primary.name.as_str());
10729        let ctx = self.ev_ctx(schema_cols, Some(alias));
10730        let seg = self
10731            .active_catalog()
10732            .cold_segment(segment_id)
10733            .ok_or_else(|| {
10734                EngineError::Unsupported(alloc::format!(
10735                    "AS OF SEGMENT: cold segment {segment_id} not registered"
10736                ))
10737            })?;
10738        let mut out_rows: Vec<Row<'static>> = Vec::new();
10739        let mut limit_remaining: Option<usize> =
10740            stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
10741        for (_key, body) in seg.scan() {
10742            let (row, _consumed) =
10743                spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
10744                    .map_err(EngineError::Storage)?;
10745            if let Some(where_expr) = &stmt.where_ {
10746                let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
10747                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
10748                    continue;
10749                }
10750            }
10751            // Projection.
10752            let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
10753            out_rows.push(projected);
10754            if let Some(rem) = limit_remaining.as_mut() {
10755                if *rem == 0 {
10756                    out_rows.pop();
10757                    break;
10758                }
10759                *rem -= 1;
10760            }
10761        }
10762        // Output column schema: derive from SELECT items.
10763        let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
10764        Ok(QueryResult::Rows {
10765            columns,
10766            rows: out_rows,
10767        })
10768    }
10769
10770    /// v6.10.2 — simple-path WHERE eval that doesn't go through
10771    /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
10772    /// scan paths predicate against a snapshot frozen segment, no
10773    /// cross-row state.
10774    fn eval_expr_simple(
10775        &self,
10776        expr: &Expr,
10777        row: &Row<'static>,
10778        ctx: &EvalContext,
10779    ) -> Result<Value<'static>, EngineError> {
10780        let cancel = CancelToken::none();
10781        self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
10782    }
10783}
10784
10785// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
10786
10787/// One row-producing projection: an expression to evaluate, the resulting
10788/// column's user-visible name, its inferred type, and nullability.
10789#[derive(Debug, Clone)]
10790pub(crate) struct ProjectedItem {
10791    pub(crate) expr: Expr,
10792    pub(crate) output_name: String,
10793    pub(crate) ty: DataType,
10794    pub(crate) nullable: bool,
10795    /// v7.39 (read01 round 54) — a projected enum column keeps its enum
10796    /// identity. Enum-ness lives outside the DataType lattice (the value is a
10797    /// Text), so a projection that dropped this made the RESULT schema forget
10798    /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
10799    /// that schema, silently fell back to TEXT order instead of member order.
10800    pub(crate) user_enum_type: Option<String>,
10801    /// v7.39 (round 425) — a projected MySQL temporal column keeps its
10802    /// declared fractional-seconds precision, so the renderer can pad to
10803    /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
10804    /// a whole second). Like `user_enum_type` this lives outside the
10805    /// DataType lattice, so a projection that dropped it made the RESULT
10806    /// schema forget how wide the fraction should print.
10807    pub(crate) mysql_fsp: Option<u8>,
10808    /// v7.39 (round 688) — and its declared collation, the third thing to
10809    /// live outside the DataType lattice and the third to be lost the same
10810    /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
10811    /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
10812    /// projection rebuilt the output column and the ORDER BY resolves
10813    /// against THAT schema.
10814    pub(crate) collation_name: Option<String>,
10815    /// v7.38.13 — and whether this position must NOT fold when DISTINCT
10816    /// de-dups it. The fourth thing to live outside the DataType lattice
10817    /// and the fourth to be lost the same way: a column declared
10818    /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
10819    /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
10820    /// returns two.
10821    ///
10822    /// A BOOL rather than the `Collation` enum on purpose. The enum's
10823    /// storage default is `Binary`, but the FOLD default under MySQL is
10824    /// case-insensitive — carrying the enum would silently mean
10825    /// "exempt" for every projected expression that is not a column.
10826    /// This field states the question it answers.
10827    pub(crate) fold_exempt: bool,
10828    /// v7.38.18 — does this column's collation make trailing spaces
10829    /// insignificant? A separate question from `fold_exempt`:
10830    /// `utf8mb4_bin` is fold-exempt AND pads, `utf8mb4_0900_ai_ci`
10831    /// folds and does not. Read off the same column, at the same
10832    /// place, so the two masks cannot drift apart.
10833    pub(crate) pads: bool,
10834}
10835
10836impl ProjectedItem {
10837    /// v7.38.14 — the output column this projected item describes.
10838    ///
10839    /// There were TWENTY-ONE places converting a `ProjectedItem` into a
10840    /// `ColumnSchema`, each written as `ColumnSchema::new(..)` followed by a
10841    /// hand-picked list of attributes to copy after it, and the lists did not
10842    /// agree: six carried enum identity, the collation NAME and MySQL fsp; ten
10843    /// carried the first and last but not the name; five carried nothing at
10844    /// all. Not one carried `collation`, the enum every MySQL text comparison
10845    /// actually reads.
10846    ///
10847    /// That is how a declared collation vanished between a subquery and the
10848    /// query that selects from it: the inner SELECT's output schema claimed
10849    /// `ColumnSchema::new`'s default, which is `Binary` — a value downstream
10850    /// reads as "byte-wise ON PURPOSE" rather than as "unknown", so the loss
10851    /// presents as a deliberate declaration.
10852    ///
10853    /// One conversion, so a field added to either type has one place to be
10854    /// remembered instead of twenty-one.
10855    pub(crate) fn to_column_schema(&self) -> ColumnSchema {
10856        let mut c = ColumnSchema::new(self.output_name.clone(), self.ty, self.nullable);
10857        c.user_enum_type.clone_from(&self.user_enum_type);
10858        c.collation_name.clone_from(&self.collation_name);
10859        c.mysql_fsp = self.mysql_fsp;
10860        // `fold_exempt` is the projection's answer to the same question
10861        // `ColumnSchema::collation` answers downstream, and it was computed
10862        // from the source column. Keeping the two in step here is what stops
10863        // a de-duplication site further on from asking the schema and being
10864        // told the opposite of what the projection knew.
10865        c.collation = if self.fold_exempt {
10866            spg_storage::Collation::Binary
10867        } else {
10868            spg_storage::Collation::CaseInsensitive
10869        };
10870        c
10871    }
10872}
10873
10874/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
10875/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
10876/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
10877/// the spec's "two NULLs are not distinct"; the second is a tolerated
10878/// quirk for v1 (no NaN literals are reachable from the SQL surface).
10879/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
10880fn expr_is_aggregate_call(e: &Expr) -> bool {
10881    match e {
10882        Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
10883        Expr::AggregateOrdered { .. } => true,
10884        _ => false,
10885    }
10886}
10887
10888/// Collect distinct top-level aggregate call expressions (dedup by value). Does
10889/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
10890/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
10891/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
10892/// than today — never a regression on a working query).
10893fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
10894    if expr_is_aggregate_call(e) {
10895        if !out.iter().any(|x| x == e) {
10896            out.push(e.clone());
10897        }
10898        return;
10899    }
10900    match e {
10901        Expr::Binary { lhs, rhs, .. } => {
10902            collect_agg_exprs(lhs, out);
10903            collect_agg_exprs(rhs, out);
10904        }
10905        Expr::Unary { expr, .. }
10906        | Expr::Cast { expr, .. }
10907        | Expr::IsNull { expr, .. }
10908        | Expr::BoolTest { expr, .. }
10909        | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
10910        Expr::FunctionCall { args, .. } => {
10911            for a in args {
10912                collect_agg_exprs(a, out);
10913            }
10914        }
10915        Expr::Like { expr, pattern, .. } => {
10916            collect_agg_exprs(expr, out);
10917            collect_agg_exprs(pattern, out);
10918        }
10919        Expr::Extract { source, .. } => collect_agg_exprs(source, out),
10920        Expr::WindowFunction {
10921            args,
10922            partition_by,
10923            order_by,
10924            ..
10925        } => {
10926            for a in args {
10927                collect_agg_exprs(a, out);
10928            }
10929            for p in partition_by {
10930                collect_agg_exprs(p, out);
10931            }
10932            for (o, _, _) in order_by {
10933                collect_agg_exprs(o, out);
10934            }
10935        }
10936        _ => {}
10937    }
10938}
10939
10940/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
10941fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
10942    if expr_is_aggregate_call(e) {
10943        if let Some(idx) = aggs.iter().position(|x| x == e) {
10944            *e = Expr::Column(ColumnName {
10945                qualifier: None,
10946                name: alloc::format!("__agg{idx}"),
10947            });
10948        }
10949        return;
10950    }
10951    match e {
10952        Expr::Binary { lhs, rhs, .. } => {
10953            replace_agg_exprs(lhs, aggs);
10954            replace_agg_exprs(rhs, aggs);
10955        }
10956        Expr::Unary { expr, .. }
10957        | Expr::Cast { expr, .. }
10958        | Expr::IsNull { expr, .. }
10959        | Expr::BoolTest { expr, .. }
10960        | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
10961        Expr::FunctionCall { args, .. } => {
10962            for a in args {
10963                replace_agg_exprs(a, aggs);
10964            }
10965        }
10966        Expr::Like { expr, pattern, .. } => {
10967            replace_agg_exprs(expr, aggs);
10968            replace_agg_exprs(pattern, aggs);
10969        }
10970        Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
10971        Expr::WindowFunction {
10972            args,
10973            partition_by,
10974            order_by,
10975            ..
10976        } => {
10977            for a in args {
10978                replace_agg_exprs(a, aggs);
10979            }
10980            for p in partition_by {
10981                replace_agg_exprs(p, aggs);
10982            }
10983            for (o, _, _) in order_by {
10984                replace_agg_exprs(o, aggs);
10985            }
10986        }
10987        _ => {}
10988    }
10989}
10990
10991/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
10992/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
10993/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
10994/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
10995/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
10996/// Returns None outside the bounded subset (leaves current behaviour). Only fires
10997/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
10998/// window-only / aggregate-only queries.
10999fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
11000    if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
11001        return None;
11002    }
11003    // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
11004    if !stmt.unions.is_empty() {
11005        return None;
11006    }
11007    let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
11008    if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
11009        return None;
11010    }
11011    stmt.from.as_ref()?;
11012    // Collect the aggregate calls to hoist from projection + outer ORDER BY.
11013    let mut aggs: Vec<Expr> = Vec::new();
11014    for item in &stmt.items {
11015        if let SelectItem::Expr { expr, .. } = item {
11016            collect_agg_exprs(expr, &mut aggs);
11017        }
11018    }
11019    for ob in &stmt.order_by {
11020        collect_agg_exprs(&ob.expr, &mut aggs);
11021    }
11022    // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
11023    let mut inner_items: Vec<SelectItem> = Vec::new();
11024    for g in &group_cols {
11025        inner_items.push(SelectItem::Expr {
11026            expr: g.clone(),
11027            alias: None,
11028        });
11029    }
11030    for (i, a) in aggs.iter().enumerate() {
11031        inner_items.push(SelectItem::Expr {
11032            expr: a.clone(),
11033            alias: Some(alloc::format!("__agg{i}")),
11034        });
11035    }
11036    let inner = SelectStatement {
11037        items: inner_items,
11038        distinct: false,
11039        distinct_on: Vec::new(),
11040        unions: Vec::new(),
11041        order_by: Vec::new(),
11042        limit: None,
11043        offset: None,
11044        limit_with_ties: false,
11045        window_check_exprs: Vec::new(),
11046        ..stmt.clone()
11047    };
11048    let derived = TableRef {
11049        name: "__aggwin".into(),
11050        alias: Some("__aggwin".into()),
11051        only: false,
11052        as_of_segment: None,
11053        unnest_expr: None,
11054        unnest_column_aliases: Vec::new(),
11055        with_ordinality: false,
11056        generate_series_args: None,
11057        lateral_subquery: Some(alloc::boxed::Box::new(inner)),
11058        jsonb_each_text_arg: None,
11059        table_fn_call: None,
11060        rows_from: None,
11061        json_table: None,
11062        scalar_fn_item: false,
11063    };
11064    // Outer window query over the derived rows: aggregates → __aggN column refs.
11065    let mut outer_items = stmt.items.clone();
11066    for item in &mut outer_items {
11067        if let SelectItem::Expr { expr, alias } = item {
11068            // Preserve PG's column label for a bare aggregate projection.
11069            if alias.is_none()
11070                && let Expr::FunctionCall { name, .. } = expr
11071                && crate::aggregate::is_aggregate_name(name)
11072            {
11073                *alias = Some(name.to_ascii_lowercase());
11074            }
11075            replace_agg_exprs(expr, &aggs);
11076        }
11077    }
11078    let mut outer_order = stmt.order_by.clone();
11079    for ob in &mut outer_order {
11080        replace_agg_exprs(&mut ob.expr, &aggs);
11081    }
11082    let mut outer_distinct_on = stmt.distinct_on.clone();
11083    for e in &mut outer_distinct_on {
11084        replace_agg_exprs(e, &aggs);
11085    }
11086    Some(SelectStatement {
11087        locking: None,
11088        ctes: Vec::new(),
11089        distinct: stmt.distinct,
11090        distinct_on: outer_distinct_on,
11091        items: outer_items,
11092        from: Some(FromClause {
11093            primary: derived,
11094            joins: Vec::new(),
11095        }),
11096        where_: None,
11097        group_by: None,
11098        group_by_all: false,
11099        having: None,
11100        unions: Vec::new(),
11101        order_by: outer_order,
11102        limit: stmt.limit.clone(),
11103        offset: stmt.offset.clone(),
11104        limit_with_ties: stmt.limit_with_ties,
11105        window_check_exprs: Vec::new(),
11106    })
11107}
11108
11109/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
11110/// membership.
11111///
11112/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
11113/// there?", and all four answered by scanning the whole right side once per
11114/// left row. The cost was (left rows x right rows), which is why
11115/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
11116/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
11117/// row that does not pays for all of it. Over 100k left rows, raising the
11118/// right side from 100 to 10,000 took 35 ms to 2848.
11119///
11120/// This is the shape round 485 already solved for DISTINCT, and it reuses
11121/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
11122/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
11123/// every bucket with the exact comparator, so a collision costs time and
11124/// never an answer.
11125struct PeerIndex<'r> {
11126    bh: hashbrown::DefaultHashBuilder,
11127    buckets: hashbrown::HashMap<u64, Vec<usize>>,
11128    rows: &'r [Row<'static>],
11129    fold: FoldSpec<'r>,
11130}
11131
11132impl<'r> PeerIndex<'r> {
11133    fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
11134        // ONE hasher for the whole pass: the default builder is seeded per
11135        // instance, so a fresh one per row would put equal rows in different
11136        // buckets.
11137        let bh = hashbrown::DefaultHashBuilder::default();
11138        let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
11139            hashbrown::HashMap::with_capacity(rows.len());
11140        for (i, r) in rows.iter().enumerate() {
11141            buckets
11142                .entry(norm_hash_row(r, &bh, fold))
11143                .or_default()
11144                .push(i);
11145        }
11146        Self {
11147            bh,
11148            buckets,
11149            rows,
11150            fold,
11151        }
11152    }
11153
11154    fn contains(&self, r: &Row<'static>) -> bool {
11155        let h = norm_hash_row(r, &self.bh, self.fold);
11156        self.buckets
11157            .get(&h)
11158            .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
11159    }
11160
11161    /// Remove ONE occurrence, so the multiset forms cancel row for row the
11162    /// way the pool they replaced did.
11163    fn take_one(&mut self, r: &Row<'static>) -> bool {
11164        let h = norm_hash_row(r, &self.bh, self.fold);
11165        let Some(b) = self.buckets.get_mut(&h) else {
11166            return false;
11167        };
11168        let Some(pos) = b
11169            .iter()
11170            .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
11171        else {
11172            return false;
11173        };
11174        b.swap_remove(pos);
11175        true
11176    }
11177}
11178
11179pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
11180    dedup_by_row(rows, |r| r, fold)
11181}
11182
11183/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
11184/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
11185/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
11186/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
11187/// order is preserved, and correctness needs only the one-way guarantee
11188/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
11189/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
11190fn dedup_by_row<T>(
11191    items: Vec<T>,
11192    row_of: impl Fn(&T) -> &Row<'static>,
11193    fold: FoldSpec<'_>,
11194) -> Vec<T> {
11195    if items.len() <= 32 {
11196        let mut out: Vec<T> = Vec::with_capacity(items.len());
11197        for it in items {
11198            if !out
11199                .iter()
11200                .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
11201            {
11202                out.push(it);
11203            }
11204        }
11205        return out;
11206    }
11207    // ONE BuildHasher instance for the whole pass — the default builder
11208    // is randomly seeded PER INSTANCE, so a fresh one per row would give
11209    // equal rows different hashes and never dedup.
11210    let bh = hashbrown::DefaultHashBuilder::default();
11211    let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
11212    let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
11213        hashbrown::HashMap::with_capacity(items.len());
11214    for it in items {
11215        let h = norm_hash_row(row_of(&it), &bh, fold);
11216        let bucket = buckets.entry(h).or_default();
11217        if !bucket
11218            .iter()
11219            .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
11220        {
11221            bucket.push(out.len());
11222            out.push(it);
11223        }
11224    }
11225    out
11226}
11227
11228/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
11229/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
11230/// rows may collide (buckets are re-checked with the exact comparator).
11231///
11232/// Domain design mirrors `value_cmp`'s equivalence classes:
11233/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
11234///   shares one domain: a value that is an integer fitting i64 hashes the
11235///   i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
11236///   anything else hashes the f64 approximation computed by THE SAME
11237///   formula the value_cmp float arms use (`numeric_to_f64`), so
11238///   `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
11239///   hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
11240///   Known un-closable corner: an integer in [2^53, 2^63) can compare
11241///   Equal to a float via value_cmp's lossy f64 arm while hashing in the
11242///   exact-i64 domain — mixed int/float rows at that magnitude may miss a
11243///   dedup (PG itself compares int8↔float8 in the lossy float8 domain).
11244/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
11245///   compares them blank-insensitively; plain Text pairs that differ only
11246///   in trailing blanks merely collide and are separated exactly).
11247/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
11248///   hash their fields under a distinct tag.
11249/// - Everything value_cmp falls back to debug-format ordering for
11250///   (Json, arrays, vectors, geometry, ranges, …) shares one constant
11251///   bucket — degrades to the exact linear scan, never wrong.
11252fn norm_hash_row(
11253    row: &Row<'static>,
11254    bh: &hashbrown::DefaultHashBuilder,
11255    fold: FoldSpec<'_>,
11256) -> u64 {
11257    norm_hash_values(&row.values, bh, fold)
11258}
11259
11260/// v7.39 (round 485) — the same hash over a bare value slice, so the
11261/// DISTINCT probe can run against a reused buffer instead of demanding a
11262/// `Row` that has to be allocated first (see `values_eq_norm`).
11263fn norm_hash_values(
11264    values: &[Value<'static>],
11265    bh: &hashbrown::DefaultHashBuilder,
11266    fold: FoldSpec<'_>,
11267) -> u64 {
11268    use core::hash::{BuildHasher, Hash, Hasher};
11269    let mut h = bh.build_hasher();
11270    for (i, v) in values.iter().enumerate() {
11271        // v7.39 (round 410) — hash the folded key when the MySQL collation
11272        // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
11273        // `'A'` vs `'a '`) share a hash bucket.
11274        //
11275        // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
11276        // byte-wise column that folded here while the comparator did not
11277        // would scatter equal rows across buckets and stop de-duplicating
11278        // at all; the hash and the comparator have to read the same mask.
11279        if fold.folds(i)
11280            && let Some(folded) = mysql_dedup_fold(v, fold.pads_at(i))
11281        {
11282            folded.hash(&mut h);
11283            continue;
11284        }
11285        norm_hash_value(v, &mut h);
11286    }
11287    h.finish()
11288}
11289
11290/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
11291///
11292/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
11293const fn pow10_i128(p: u16) -> Option<i128> {
11294    const P: [i128; 39] = {
11295        let mut t = [1i128; 39];
11296        let mut i = 1;
11297        while i < 39 {
11298            t[i] = t[i - 1] * 10;
11299            i += 1;
11300        }
11301        t
11302    };
11303    if (p as usize) < P.len() {
11304        Some(P[p as usize])
11305    } else {
11306        None
11307    }
11308}
11309
11310fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
11311    const TAG_NULL: u8 = 0;
11312    const TAG_BOOL: u8 = 1;
11313    const TAG_NUM_I64: u8 = 2;
11314    const TAG_NUM_F64: u8 = 3;
11315    const TAG_TEXT: u8 = 4;
11316    const TAG_DATE: u8 = 6;
11317    const TAG_TIME: u8 = 7;
11318    const TAG_TIMESTAMP: u8 = 8;
11319    const TAG_TIMETZ: u8 = 10;
11320    const TAG_UUID: u8 = 11;
11321    const TAG_MONEY: u8 = 12;
11322    const TAG_BYTES: u8 = 13;
11323    const TAG_INTERVAL: u8 = 14;
11324    const TAG_CHAR1: u8 = 15;
11325    const TAG_OPAQUE: u8 = 255;
11326    // One shared writer for the numeric family: an integer value
11327    // representable as i64 goes exact (round-trip probe — no_std, so no
11328    // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
11329    // through 0i64, folding it into 0.0 as value_cmp requires.
11330    let num_f64 = |h: &mut H, x: f64| {
11331        if x.is_nan() {
11332            h.write_u8(TAG_NUM_F64);
11333            h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
11334            return;
11335        }
11336        const TWO63: f64 = 9_223_372_036_854_775_808.0;
11337        if (-TWO63..TWO63).contains(&x) {
11338            #[allow(clippy::cast_possible_truncation)]
11339            let n = x as i64;
11340            #[allow(clippy::cast_precision_loss)]
11341            if (n as f64) == x {
11342                h.write_u8(TAG_NUM_I64);
11343                h.write_i64(n);
11344                return;
11345            }
11346        }
11347        h.write_u8(TAG_NUM_F64);
11348        h.write_u64(x.to_bits());
11349    };
11350    match v {
11351        Value::Null => h.write_u8(TAG_NULL),
11352        Value::Bool(b) => {
11353            h.write_u8(TAG_BOOL);
11354            h.write_u8(u8::from(*b));
11355        }
11356        Value::SmallInt(n) => {
11357            h.write_u8(TAG_NUM_I64);
11358            h.write_i64(i64::from(*n));
11359        }
11360        Value::Int(n) => {
11361            h.write_u8(TAG_NUM_I64);
11362            h.write_i64(i64::from(*n));
11363        }
11364        Value::BigInt(n) => {
11365            h.write_u8(TAG_NUM_I64);
11366            h.write_i64(*n);
11367        }
11368        Value::Float(x) => num_f64(h, *x),
11369        Value::Numeric {
11370            scaled,
11371            scale,
11372            kind,
11373        } => match kind {
11374            spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
11375            spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
11376            spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
11377            spg_storage::NumericKind::Finite => {
11378                // Reduce trailing fractional zeros so 1.50 and 1.5 share a
11379                // representation, then: exact integers fitting i64 go to the
11380                // i64 domain; everything else uses numeric_to_f64 — the SAME
11381                // formula value_cmp's Numeric↔Float arm compares with.
11382                // r1044 — the reduction is required (`1.5` and `1.50` are
11383                // one value and must land in one bucket) and it used to
11384                // walk one digit at a time. That is O(scale), and scale
11385                // is not small in practice: `n / 100` on a NUMERIC
11386                // column stores `9.1900000000000000`, scale 16, so the
11387                // loop ran fourteen times PER ROW.
11388                //
11389                // Priced by ablation rather than guessed at — removing
11390                // the loop entirely took `SELECT DISTINCT n FROM t ORDER
11391                // BY n` over 400,000 rows from 52 ms to 14.8, against
11392                // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
11393                // tried first moved it not at all, which is why this one
11394                // was measured before it was written.
11395                //
11396                // Binary search over the same powers finds the whole
11397                // run of trailing zeros in at most six tests and one
11398                // division, instead of one test and one division per
11399                // digit.
11400                let (mut s, mut sc) = (*scaled, *scale);
11401                if sc > 0 && s != 0 {
11402                    let mut lo: u16 = 0;
11403                    let mut hi: u16 = sc;
11404                    while lo < hi {
11405                        let mid = (lo + hi).div_ceil(2);
11406                        match pow10_i128(mid) {
11407                            Some(p) if s % p == 0 => lo = mid,
11408                            _ => hi = mid - 1,
11409                        }
11410                    }
11411                    if lo > 0 {
11412                        if let Some(p) = pow10_i128(lo) {
11413                            s /= p;
11414                            sc -= lo;
11415                        }
11416                    }
11417                }
11418                if sc == 0 {
11419                    if let Ok(n) = i64::try_from(s) {
11420                        h.write_u8(TAG_NUM_I64);
11421                        h.write_i64(n);
11422                    } else {
11423                        num_f64(h, crate::orderby::numeric_to_f64(s, 0));
11424                    }
11425                } else {
11426                    num_f64(h, crate::orderby::numeric_to_f64(s, sc));
11427                }
11428            }
11429        },
11430        // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
11431        // value that also fits i128 reuses the Numeric path above so
11432        // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
11433        // any i128-representable value — constant bucket is safe.
11434        Value::NumericBig(b) => match b.to_i128() {
11435            Some(s) => norm_hash_value(
11436                &Value::Numeric {
11437                    scaled: s,
11438                    scale: b.scale(),
11439                    kind: spg_storage::NumericKind::Finite,
11440                },
11441                h,
11442            ),
11443            None => h.write_u8(TAG_OPAQUE),
11444        },
11445        // value_cmp compares Text↔BpChar blank-insensitively (both sides
11446        // trimmed), so both hash the trimmed bytes. Text pairs differing
11447        // only in trailing blanks collide and are split exactly in-bucket.
11448        Value::Text(s) | Value::BpChar(s) => {
11449            h.write_u8(TAG_TEXT);
11450            h.write(s.trim_end_matches(' ').as_bytes());
11451        }
11452        Value::Char1(c) => {
11453            h.write_u8(TAG_CHAR1);
11454            h.write_u8(*c);
11455        }
11456        Value::Date(d) => {
11457            h.write_u8(TAG_DATE);
11458            h.write_i32(*d);
11459        }
11460        Value::Time(t) => {
11461            h.write_u8(TAG_TIME);
11462            h.write_i64(*t);
11463        }
11464        Value::Timestamp(t) => {
11465            h.write_u8(TAG_TIMESTAMP);
11466            h.write_i64(*t);
11467        }
11468        Value::TimeTz { us, offset_secs } => {
11469            h.write_u8(TAG_TIMETZ);
11470            h.write_i64(*us);
11471            h.write_i32(*offset_secs);
11472        }
11473        Value::Uuid(u) => {
11474            h.write_u8(TAG_UUID);
11475            h.write(u);
11476        }
11477        Value::Money(c) => {
11478            h.write_u8(TAG_MONEY);
11479            h.write_i64(*c);
11480        }
11481        Value::Bytes(b) => {
11482            h.write_u8(TAG_BYTES);
11483            h.write(b.as_ref());
11484        }
11485        Value::Interval {
11486            months,
11487            days,
11488            micros,
11489            kind,
11490        } => {
11491            h.write_u8(TAG_INTERVAL);
11492            h.write_i32(*months);
11493            h.write_i32(*days);
11494            h.write_i64(*micros);
11495        }
11496        // v7.37.16 — REAL joined the numeric value_cmp family (widened
11497        // to f64, same formulas as the arms), so it hashes in the shared
11498        // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
11499        // f32→f64 is exact, so equal-under-cmp implies equal bits here.
11500        Value::Real(x) => num_f64(h, f64::from(*x)),
11501        // Json (structural equality), vector families (float rendering),
11502        // arrays / geometry / net / ranges / composites (debug-format
11503        // fallback): one constant bucket — exact linear within.
11504        _ => h.write_u8(TAG_OPAQUE),
11505    }
11506}
11507
11508/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
11509/// treats numerically-equal exact values as one regardless of type or scale
11510/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
11511/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
11512/// `Row` `==` would keep them distinct.
11513/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
11514/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
11515/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
11516/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
11517/// the folded comparison key for a text value, None for anything else (which
11518/// keeps the byte-exact `value_cmp` path).
11519fn mysql_dedup_fold(v: &Value, pads: bool) -> Option<String> {
11520    match v {
11521        // v7.38.17 — CHAR's trailing spaces are padding; TEXT's are
11522        // data. The comment above named `utf8mb4_uca1400_ai_ci`, which
11523        // is MariaDB's default and PAD SPACE. SPG advertises MySQL 8.0,
11524        // whose default is NO PAD, so `'alpha'` and `'alpha  '` are two
11525        // rows to a `SELECT DISTINCT` and one to `count(DISTINCT)` was
11526        // the same question answered twice.
11527        // v7.38.18 — a CHAR's padding is the TYPE's and never counts; a
11528        // TEXT's is the collation's, which `pads` carries per position.
11529        Value::BpChar(s) => Some(spg_storage::mysql_compare_fold_char(s)),
11530        Value::Text(s) if pads => Some(spg_storage::mysql_compare_fold_char(s)),
11531        Value::Text(s) => Some(spg_storage::mysql_compare_fold(s)),
11532        _ => None,
11533    }
11534}
11535
11536/// v7.39 (round 485) — how many projected rows the single-table scan
11537/// builds, and how many of those the DISTINCT probe throws away again.
11538///
11539/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
11540/// 21 % of all samples in malloc/free called straight from the scan
11541/// closure. The closure's one per-row allocation is the projected
11542/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
11543/// instructions later — but "most" is a guess until it is a number, so
11544/// these count it. (Round 480 was spent acting on an inference about a
11545/// branch that turned out never to run.)
11546/// v7.39 (round 488) — reachability counters for round 487's projection
11547/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
11548/// and a never-called-function probe rules out code layout — so the
11549/// question is whether that shape reaches this code at all, which is a
11550/// number, not an inference.
11551pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11552pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11553
11554pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
11555pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
11556    core::sync::atomic::AtomicU64::new(0);
11557
11558/// v7.38.13 — how DISTINCT must compare one row of output.
11559///
11560/// The MySQL default collation folds case and trailing spaces when it
11561/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
11562/// must not fold — `e2e_mysql_collate_binary_round370` calls the
11563/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
11564/// one when the schema asked to keep them apart", and names DISTINCT as
11565/// one of the sites that has to honour it.
11566///
11567/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
11568/// value in a MySQL session, because a bool cannot see a column. The
11569/// GROUP BY path consults the schema and was right all along; the test
11570/// only ever exercised that spelling, so the DISTINCT hole was never
11571/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
11572///
11573/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
11574/// which is what a caller with no schema to offer gets.
11575#[derive(Clone, Copy)]
11576pub(crate) struct FoldSpec<'c> {
11577    mysql: bool,
11578    binary: &'c [bool],
11579    /// v7.38.18 — the padding mask, in lockstep with `binary`. Read the
11580    /// note on `folds`: a hash and its comparator must consult the same
11581    /// masks or equal rows scatter across buckets.
11582    pads: &'c [bool],
11583}
11584
11585impl<'c> FoldSpec<'c> {
11586    /// No column information — every Text position folds under MySQL.
11587    pub(crate) const fn dialect(mysql: bool) -> Self {
11588        Self {
11589            mysql,
11590            binary: &[],
11591            pads: &[],
11592        }
11593    }
11594
11595    /// The mask read off the output columns.
11596    pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
11597        Self {
11598            mysql,
11599            binary,
11600            pads: &[],
11601        }
11602    }
11603
11604    /// The masks read off the output columns — fold-exemption AND
11605    /// padding, which are different questions about the same collation.
11606    pub(crate) fn of_masks(mysql: bool, binary: &'c [bool], pads: &'c [bool]) -> Self {
11607        Self {
11608            mysql,
11609            binary,
11610            pads,
11611        }
11612    }
11613
11614    /// Does position `i` treat trailing spaces as insignificant?
11615    #[inline]
11616    fn pads_at(&self, i: usize) -> bool {
11617        self.pads.get(i).copied().unwrap_or(false)
11618    }
11619
11620    /// Does position `i` fold?
11621    #[inline]
11622    fn folds(&self, i: usize) -> bool {
11623        self.mysql && !self.binary.get(i).copied().unwrap_or(false)
11624    }
11625}
11626
11627/// The fold-exempt mask for a projection.
11628///
11629/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
11630/// projection rebuilds that schema through `ColumnSchema::new`, whose
11631/// collation default is `Binary` — a mask built from it would mark
11632/// EVERY column byte-wise and stop DISTINCT folding at all.
11633/// The padding mask for a projection, read off the same items as
11634/// [`fold_mask`] so the two cannot come from different places.
11635pub(crate) fn pad_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11636    projection.iter().map(|p| p.pads).collect()
11637}
11638
11639pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
11640    projection.iter().map(|p| p.fold_exempt).collect()
11641}
11642
11643/// v7.38.14 — the same mask, from an OUTPUT SCHEMA instead of a
11644/// projection.
11645///
11646/// Some de-duplication sites hold `Vec<ColumnSchema>` and never see the
11647/// `ProjectedItem`s it came from. `ProjectedItem::fold_exempt` is built
11648/// from exactly this test (`select.rs`, `build_projection`), so the two
11649/// must keep answering identically -- a site that decided "byte-wise" one
11650/// way while its neighbour decided the other is how the answer came to
11651/// depend on which executor ran the query.
11652///
11653/// The direction matters: `Collation::Binary` is `ColumnSchema::new`'s
11654/// DEFAULT, so a schema rebuilt without carrying the field reads as
11655/// "byte-wise on purpose" here. That is a real trap and it has caught
11656/// five fields so far; it is why S4 of this release exists.
11657/// v7.38.18 — the padding mask from output columns, the sibling of
11658/// [`fold_mask_of_columns`]. Whether a column folds and whether it
11659/// pads are different questions about the same collation.
11660pub(crate) fn pad_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11661    columns
11662        .iter()
11663        .map(|c| crate::collate::pads_space(c.collation_name.as_deref()))
11664        .collect()
11665}
11666
11667pub(crate) fn fold_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11668    columns
11669        .iter()
11670        .map(|c| matches!(c.collation, spg_storage::Collation::Binary))
11671        .collect()
11672}
11673
11674pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
11675    values_eq_norm(&a.values, &b.values, fold)
11676}
11677
11678/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
11679/// DISTINCT probe can compare a reused projection buffer against a kept
11680/// row without building a `Row` for it.
11681pub(crate) fn values_eq_norm(
11682    a: &[Value<'static>],
11683    b: &[Value<'static>],
11684    fold: FoldSpec<'_>,
11685) -> bool {
11686    a.len() == b.len()
11687        && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
11688            if fold.folds(i)
11689                && let (Some(fx), Some(fy)) = (
11690                    mysql_dedup_fold(x, fold.pads_at(i)),
11691                    mysql_dedup_fold(y, fold.pads_at(i)),
11692                )
11693            {
11694                return fx == fy;
11695            }
11696            crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
11697        })
11698}
11699
11700/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
11701/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
11702/// order via the byte values; vectors are not sortable.
11703pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
11704    // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
11705    // so values sharing a ≥6-byte common prefix (`product_001` vs
11706    // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
11707    // order by their exact bytes instead of the old lossy f64 coarse key.
11708    // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
11709    // matches PG's default C / binary text collation. Every other type
11710    // keeps the lossless-enough `f64` fast path below.
11711    if let Value::Text(s) = v {
11712        return Ok(OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())));
11713    }
11714    // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
11715    // byte order (PG bpcharcmp under C collation), so mixed-pad values of
11716    // the same logical string order equal.
11717    if let Value::BpChar(s) = v {
11718        return Ok(OrderKey::Text(crate::orderby::CompactText::new(
11719            s.trim_end_matches(' '),
11720        )));
11721    }
11722    // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
11723    // carry the parsed value and compare it structurally (see
11724    // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
11725    if let Value::Json(s) = v {
11726        return Ok(match crate::json::parse(s) {
11727            Ok(jv) => OrderKey::Json(jv),
11728            Err(_) => OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())),
11729        });
11730    }
11731    // v7.37 — byte-orderable types PG sorts byte-wise but that have no
11732    // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
11733    // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
11734    // matching PG's network ordering.
11735    match v {
11736        Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
11737        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
11738        Value::NumericBig(b) => {
11739            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11740                spg_storage::NumericKey::from_big(b),
11741            )));
11742        }
11743        Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
11744        Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11745        Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11746        Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
11747        Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
11748            let mut key = alloc::vec::Vec::with_capacity(18);
11749            key.push(*family);
11750            key.extend_from_slice(addr);
11751            key.push(*bits);
11752            return Ok(OrderKey::Bytes(key));
11753        }
11754        _ => {}
11755    }
11756    // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
11757    // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
11758    // own OrderKey so integer arrays sort numerically; a NULL element rides to
11759    // the end via the +INF sentinel.
11760    let inf = || OrderKey::NullBig;
11761    let arr = match v {
11762        Value::IntArray(a) => Some(
11763            a.iter()
11764                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11765                .collect(),
11766        ),
11767        Value::SmallIntArray(a) => Some(
11768            a.iter()
11769                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11770                .collect(),
11771        ),
11772        Value::BigIntArray(a) => Some(
11773            a.iter()
11774                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11775                .collect(),
11776        ),
11777        Value::BoolArray(a) => Some(
11778            a.iter()
11779                .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
11780                .collect(),
11781        ),
11782        Value::TextArray(a) => Some(
11783            a.iter()
11784                .map(|o| {
11785                    o.as_ref()
11786                        .map_or_else(inf, |s| OrderKey::Text(crate::orderby::CompactText::new(s)))
11787                })
11788                .collect(),
11789        ),
11790        #[allow(clippy::cast_precision_loss)]
11791        Value::FloatArray(a) => Some(
11792            a.iter()
11793                .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
11794                .collect(),
11795        ),
11796        // r1040 — array elements take the same exact key their scalar
11797        // form does; an f64 projection here would order `{0.1}` against
11798        // `{0.1000000000000000001}` by luck.
11799        Value::NumericArray(a) => Some(
11800            a.iter()
11801                .map(|o| {
11802                    o.map_or_else(inf, |(m, s)| {
11803                        OrderKey::Numeric(alloc::boxed::Box::new(
11804                            spg_storage::NumericKey::from_numeric(
11805                                m,
11806                                s,
11807                                spg_storage::NumericKind::Finite,
11808                            ),
11809                        ))
11810                    })
11811                })
11812                .collect(),
11813        ),
11814        Value::DateArray(a) => Some(
11815            a.iter()
11816                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11817                .collect(),
11818        ),
11819        _ => None,
11820    };
11821    if let Some(elements) = arr {
11822        return Ok(OrderKey::Array(elements));
11823    }
11824    // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
11825    // right, which is exactly the lexicographic element order an Array key
11826    // already gives: `(2,'b') < (9,'a')` because the leading field decides.
11827    if let Value::Composite(fields) = v {
11828        let elements = fields
11829            .iter()
11830            .map(|(_, fv)| value_to_order_key(fv))
11831            .collect::<Result<alloc::vec::Vec<_>, _>>()?;
11832        return Ok(OrderKey::Array(elements));
11833    }
11834    // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
11835    // Projecting these to f64 (the historic path) silently collapses BigInt /
11836    // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
11837    // the wrong order for large ids and microsecond timestamps.
11838    match v {
11839        Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11840        Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
11841        Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11842        // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
11843        // integer (days / micros / cents / calendar year); TIMETZ by the
11844        // UTC-equivalent micros (local wall - offset) so the same physical
11845        // instant in different zones sorts equal.
11846        Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
11847        Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
11848        Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
11849        Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
11850        // v7.39.13 — the UTC instant is only HALF the key.
11851        //
11852        // This ordered by the instant alone, so the values that share
11853        // one were called equal and a stable sort then returned them in
11854        // insertion order — an answer, not a tie-break. Measured on
11855        // PostgreSQL 18.6 against this engine, six rows, one column:
11856        //
11857        // ```text
11858        //   PG 18.6        SPG 7.39.12
11859        //   07:00:00+01    07:00:00+01
11860        //   06:59:59+00    06:59:59+00
11861        //   09:00:00+02    07:00:00+00   <- the four that share
11862        //   07:00:00+00    02:00:00-05      07:00 UTC, in the
11863        //   02:00:00-05    09:00:00+02      order they were written
11864        //   01:00:00-06    01:00:00-06
11865        // ```
11866        //
11867        // PostgreSQL breaks the tie by OFFSET DESCENDING, and
11868        // `'07:00:00+00' = '02:00:00-05'` is FALSE there. Shifting the
11869        // instant left by 32 bits leaves room for the offset underneath
11870        // it — `i128` holds both exactly, where `i64` could not — and
11871        // `compare` in `eval::binop` orders the same pair the same way,
11872        // from the same measurement.
11873        Value::TimeTz { us, offset_secs } => {
11874            return Ok(OrderKey::Int(i128::from(spg_storage::timetz_sort_key(
11875                *us,
11876                *offset_secs,
11877            ))));
11878        }
11879        Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
11880        _ => {}
11881    }
11882    let num = match v {
11883        // Callers without NULLS FIRST/LAST context (array elements,
11884        // histogram sampling) put NULL last, as before.
11885        Value::Null => return Ok(OrderKey::NullBig),
11886        // v7.17.0 Phase 3.P0-38 — range ordering is not supported
11887        // in v7.17.0 (needs lex-then-inclusivity tiebreak).
11888        Value::Range { .. } => {
11889            return Err(EngineError::Unsupported(
11890                "ORDER BY of a range value is not supported in v7.17.0".into(),
11891            ));
11892        }
11893        // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
11894        Value::Hstore(_) => {
11895            return Err(EngineError::Unsupported(
11896                "ORDER BY of a hstore value is not supported".into(),
11897            ));
11898        }
11899        // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
11900        Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
11901            return Err(EngineError::Unsupported(
11902                "ORDER BY of a 2D array is not supported in v7.17.0".into(),
11903            ));
11904        }
11905        // r1039/r1040 — the exact canonical key, not an f64 projection.
11906        //
11907        // r1039 fixed the three specials, which carry a canonical zero in
11908        // `scaled` and so all sorted as the number 0. The projection
11909        // itself was the rest of the defect: "precision losses here only
11910        // matter for tie-breaks well past 15 significant digits" was the
11911        // comment, and the measurement disagreed — f64 called
11912        // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
11913        // returned them in insertion order. Three of ten values came back
11914        // in the wrong place against PG18.4.
11915        Value::Numeric {
11916            scaled,
11917            scale,
11918            kind,
11919        } => {
11920            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11921                spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
11922            )));
11923        }
11924        Value::Float(x) => *x,
11925        // v7.37.16 — REAL sorts by its exact f64 widening (it had no
11926        // arm and fell through to the unsupported error).
11927        Value::Real(x) => f64::from(*x),
11928        Value::Bool(b) => {
11929            if *b {
11930                1.0
11931            } else {
11932                0.0
11933            }
11934        }
11935        Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
11936            return Err(EngineError::Unsupported(
11937                "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
11938            ));
11939        }
11940        // v7.37 — PG orders INTERVAL by its total time, treating a month as
11941        // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
11942        // f64 is exact for any interval under ~285 years, and only ORDER BY
11943        // tie-breaks past that magnitude lose precision. Matches the
11944        // min/max(interval) comparator in aggregate.rs.
11945        #[allow(clippy::cast_precision_loss)]
11946        Value::Interval {
11947            months,
11948            days,
11949            micros,
11950            kind,
11951        } => {
11952            let total = i128::from(*months) * 30 * 86_400_000_000
11953                + i128::from(*days) * 86_400_000_000
11954                + i128::from(*micros);
11955            total as f64
11956        }
11957        Value::Json(_) => {
11958            return Err(EngineError::Unsupported(
11959                "ORDER BY of a JSON value is not supported — cast the document to text first"
11960                    .into(),
11961            ));
11962        }
11963        // v7.5.0 — Value is #[non_exhaustive]; future variants need
11964        // an explicit ORDER BY mapping. Surface as Unsupported until
11965        // engine support is added.
11966        _ => {
11967            return Err(EngineError::Unsupported(
11968                "ORDER BY of this value type is not supported".into(),
11969            ));
11970        }
11971    };
11972    Ok(OrderKey::Num(num))
11973}
11974
11975/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
11976/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
11977/// `EngineError` so the projection-build path keeps `UnknownQualifier`
11978/// vs `ColumnNotFound` distinct.
11979/// PG's name for the physical row identity. It is reserved there — no table
11980/// can have a column called this — which is what lets `*` skip it by name.
11981pub(crate) const CTID_COLUMN: &str = "ctid";
11982
11983/// v7.39 (round 512) — PG's system columns, in the order they are appended.
11984/// All six are reserved names there, which is what lets `*` skip them and
11985/// lets a scan tell them from a user column without a flag.
11986pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
11987
11988/// Is this name one of them?
11989pub(crate) fn is_system_column(name: &str) -> bool {
11990    SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
11991}
11992
11993/// Where the scan's appended system columns begin, if this schema carries
11994/// them: the trailing six, named in order. A catalog view with a column of
11995/// its own called `xmin` does not match, which is the point.
11996fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
11997    let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
11998    cols[start..]
11999        .iter()
12000        .zip(SYSTEM_COLUMNS)
12001        .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
12002        .then_some(start)
12003}
12004
12005/// v7.39 (round 540) — which positions `*` must skip.
12006///
12007/// The rule stays round 512's — the synthetic columns are the trailing
12008/// six of a relation's block, matched by POSITION so a genuine `xmin`
12009/// column is not lost — but a JOINED schema names its columns
12010/// `alias.column` and lays the peers out end to end, so a peer's six sit
12011/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
12012/// "trailing six" test back on the block it was written for.
12013fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
12014    let mut skip = alloc::vec![false; cols.len()];
12015    fn qualifier(n: &str) -> Option<&str> {
12016        n.rsplit_once('.').map(|(q, _)| q)
12017    }
12018    fn bare(n: &str) -> &str {
12019        n.rsplit('.').next().unwrap_or(n)
12020    }
12021    let mut i = 0;
12022    while i < cols.len() {
12023        let q = qualifier(&cols[i].name);
12024        let mut end = i;
12025        while end < cols.len() && qualifier(&cols[end].name) == q {
12026            end += 1;
12027        }
12028        if let Some(start) = (end - i)
12029            .checked_sub(SYSTEM_COLUMNS.len())
12030            .map(|off| i + off)
12031            && cols[start..end]
12032                .iter()
12033                .zip(SYSTEM_COLUMNS)
12034                .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
12035        {
12036            for s in skip.iter_mut().take(end).skip(start) {
12037                *s = true;
12038            }
12039        }
12040        i = end;
12041    }
12042    skip
12043}
12044
12045/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
12046/// read? Only then is the column materialised.
12047pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
12048    let mut found = false;
12049    crate::expr_analysis::visit_expr_columns_and_subqueries(
12050        e,
12051        &mut |c| {
12052            if is_system_column(&c.name) {
12053                found = true;
12054            }
12055        },
12056        &mut |_| {},
12057    );
12058    found
12059}
12060
12061fn references_ctid(stmt: &SelectStatement) -> bool {
12062    let in_expr = expr_references_ctid;
12063    stmt.items.iter().any(|i| match i {
12064        SelectItem::Expr { expr, .. } => in_expr(expr),
12065        _ => false,
12066    }) || stmt.where_.as_ref().is_some_and(in_expr)
12067        || stmt.order_by.iter().any(|o| in_expr(&o.expr))
12068        || stmt
12069            .group_by
12070            .as_ref()
12071            .is_some_and(|g| g.iter().any(in_expr))
12072        || stmt.having.as_ref().is_some_and(in_expr)
12073}
12074
12075/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
12076/// is a name the projection has to TYPE before any row exists.
12077///
12078/// Evaluation has answered this since round T9 (`resolve_column` builds a
12079/// `Value::Composite` of every column), but the typing side below had no
12080/// such branch and raised `column "t" does not exist` first — so the
12081/// feature was unreachable through a projection. Measured against PG18.4:
12082/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
12083///
12084/// The type is `Jsonb` + a composite marker, which is exactly how a
12085/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
12086/// the value travels as a `Value::Composite` and renders in the canonical
12087/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
12088/// so the marker names the alias and no rehydration keys off it — the
12089/// value arrives already built.
12090fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
12091    let mut s = ColumnSchema::new(
12092        alloc::string::String::from(alias),
12093        spg_storage::DataType::Jsonb,
12094        true,
12095    );
12096    s.user_composite_type = Some(alloc::string::String::from(alias));
12097    s
12098}
12099
12100/// v7.39.3 — `mysql` makes the name comparison case-INSENSITIVE, which
12101/// is MySQL's rule for column names (measured on 9.7.2: `mycol`,
12102/// `MYCOL` and a backquoted `MyCol` all resolve the same column).
12103///
12104/// SPG compared byte for byte and its lexer folds an UNQUOTED
12105/// identifier, so a table restored from a `mysqldump` — where every
12106/// identifier is backquoted and keeps its case — had every mixed-case
12107/// column unreachable from ordinary unquoted SQL. Same "two spellings,
12108/// two things" defect v7.39.1 closed for relation names.
12109pub(crate) fn resolve_projection_column<'a>(
12110    c: &ColumnName,
12111    schema_cols: &'a [ColumnSchema],
12112    table_alias: &str,
12113    mysql: bool,
12114) -> Result<Cow<'a, ColumnSchema>, EngineError> {
12115    let same = |a: &str, b: &str| {
12116        if mysql {
12117            a.eq_ignore_ascii_case(b)
12118        } else {
12119            a == b
12120        }
12121    };
12122    if let Some(q) = &c.qualifier {
12123        let composite = alloc::format!("{q}.{name}", name = c.name);
12124        if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &composite)) {
12125            return Ok(Cow::Borrowed(s));
12126        }
12127        // Single-table case: the qualifier may equal the active alias —
12128        // then look for the bare column name.
12129        if same(q, table_alias)
12130            && let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name))
12131        {
12132            return Ok(Cow::Borrowed(s));
12133        }
12134        // For multi-table schemas the qualifier is unknown only if no
12135        // column bears the "<q>." prefix. For single-table, the alias
12136        // mismatch alone is enough.
12137        let prefix = alloc::format!("{q}.");
12138        let qualifier_known =
12139            same(q, table_alias) || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
12140        if !qualifier_known {
12141            return Err(EngineError::Eval(EvalError::UnknownQualifier {
12142                qualifier: q.clone(),
12143                column: c.name.clone(),
12144            }));
12145        }
12146        return Err(EngineError::Eval(EvalError::ColumnNotFound {
12147            name: c.name.clone(),
12148        }));
12149    }
12150    if let Some(s) = schema_cols.iter().find(|s| same(&s.name, &c.name)) {
12151        return Ok(Cow::Borrowed(s));
12152    }
12153    let suffix = alloc::format!(".{name}", name = c.name);
12154    let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
12155    let first = matches.next();
12156    let extra = matches.next();
12157    match (first, extra) {
12158        (Some(s), None) => Ok(Cow::Borrowed(s)),
12159        (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
12160            detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
12161        })),
12162        // The whole-row reference, checked LAST so a real column carrying
12163        // the alias's name still wins — the same precedence
12164        // `resolve_column` applies on the evaluation side.
12165        //
12166        // Two schema shapes reach here. A single-table (or subquery, or
12167        // CTE) scan carries its alias and bare column names, so the name
12168        // has to equal the alias. A JOIN's combined schema carries no
12169        // alias at all and qualifies every column `alias.col`, so the
12170        // alias is identified by the prefix instead — which is exactly
12171        // how `whole_row_composite` picks the fields out on the
12172        // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
12173        // answers `(7,z)` on PG18.4 and errored here until this arm
12174        // covered the joined shape too.
12175        _ if !table_alias.is_empty() && c.name == table_alias => {
12176            Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
12177        }
12178        _ if table_alias.is_empty() && {
12179            let prefix = alloc::format!("{name}.", name = c.name);
12180            schema_cols.iter().any(|s| s.name.starts_with(&prefix))
12181        } =>
12182        {
12183            Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
12184        }
12185        _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
12186            name: c.name.clone(),
12187        })),
12188    }
12189}
12190
12191/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
12192/// parser to carry per-branch GROUPING() masks into a grouping-set query's
12193/// ORDER BY. They must never reach the output. No-op unless such a column is
12194/// present, so the common path is untouched.
12195/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
12196///
12197/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
12198/// a `LIMIT 2` that should have answered two groups answered one.
12199fn apply_deferred_limit(
12200    rows: alloc::vec::Vec<Row<'static>>,
12201    deferred: &(
12202        Option<spg_sql::ast::LimitExpr>,
12203        Option<spg_sql::ast::LimitExpr>,
12204    ),
12205) -> alloc::vec::Vec<Row<'static>> {
12206    let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
12207        Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
12208        _ => None,
12209    };
12210    let mut rows = rows;
12211    if let Some(off) = count(&deferred.1) {
12212        rows = rows.split_off(off.min(rows.len()));
12213    }
12214    if let Some(lim) = count(&deferred.0) {
12215        rows.truncate(lim);
12216    }
12217    rows
12218}
12219
12220fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
12221    let QueryResult::Rows { columns, rows } = result else {
12222        return result;
12223    };
12224    if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
12225        return QueryResult::Rows { columns, rows };
12226    }
12227    let keep: Vec<usize> = columns
12228        .iter()
12229        .enumerate()
12230        .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
12231        .map(|(i, _)| i)
12232        .collect();
12233    let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
12234    let new_rows: Vec<Row<'static>> = rows
12235        .into_iter()
12236        .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
12237        .collect();
12238    QueryResult::Rows {
12239        columns: new_cols,
12240        rows: new_rows,
12241    }
12242}
12243
12244/// v7.39 (round 487) — bind every projection item that is a bare column
12245/// reference to its position, once per query.
12246///
12247/// `#[inline(never)]` and out of line on purpose. Round 486 established
12248/// that adding code inside these scan bodies moves neighbouring hot
12249/// functions around under fat LTO: the first version of this had the loop
12250/// inline in `run_single_table_scan` and four aggregate shapes that never
12251/// touch that function — `full_agg`, `join_agg`, `group_500k`,
12252/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
12253/// the same machine. Keeping it out of line kept them still.
12254#[inline(never)]
12255fn bind_direct_columns(
12256    projection: &[ProjectedItem],
12257    ctx: &eval::EvalContext<'_>,
12258) -> Vec<Option<usize>> {
12259    projection
12260        .iter()
12261        .map(|p| match &p.expr {
12262            Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
12263                // Same exclusion `compile_into` makes: a composite column
12264                // has to be rehydrated from stored JSON, which is not a
12265                // cell read.
12266                ctx.columns
12267                    .get(*pos)
12268                    .is_none_or(|sc| sc.user_composite_type.is_none())
12269            }),
12270            _ => None,
12271        })
12272        .collect()
12273}
12274
12275/// v7.39 (round 505) — the name an un-aliased projected expression reports.
12276///
12277/// PG18 names a call for its function and everything else `?column?`;
12278/// measured with `\gdesc`. SPG used to print the parsed expression back
12279/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
12280/// name-keyed row access found nothing under `upper`.
12281///
12282/// The MySQL half is NOT this rule and is deliberately left alone here:
12283/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
12284/// which needs the parser to hand over spans the AST does not carry yet.
12285/// Until it does, a MySQL session keeps the printed form — closer to what
12286/// MariaDB answers than `?column?` would be.
12287pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
12288    if mysql {
12289        return expr.to_string();
12290    }
12291    spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
12292}
12293
12294pub(crate) fn build_projection(
12295    items: &[SelectItem],
12296    schema_cols: &[ColumnSchema],
12297    table_alias: &str,
12298    mysql: bool,
12299    cat: Option<&Catalog>,
12300) -> Result<Vec<ProjectedItem>, EngineError> {
12301    build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0, cat)
12302}
12303
12304/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
12305/// invisible to `*`.
12306///
12307/// The windowed-SELECT path appends a synthetic `__win_N` column per window
12308/// function so the rewritten projection can reference the computed values as
12309/// ordinary columns. `*` then expanded them too, and
12310/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
12311/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
12312/// silent one: the row simply had one more field than the client asked for.
12313///
12314/// Hidden by POSITION rather than by name, for the reason round 512 recorded
12315/// about the system columns: a name test looks safe until a real column
12316/// happens to carry the name. These are appended last, so the count is what
12317/// identifies them.
12318pub(crate) fn build_projection_hiding_tail(
12319    items: &[SelectItem],
12320    schema_cols: &[ColumnSchema],
12321    table_alias: &str,
12322    mysql: bool,
12323    hidden_tail: usize,
12324    // v7.38.19 — the catalog, so a user-defined function's DECLARED
12325    // return type reaches the projection. Without it `describe_expr`
12326    // cannot type `f_sql()` and the column falls back to text, which is
12327    // what psql reads to decide alignment: `SELECT 7::bigint, f_sql()`
12328    // right-aligned one cell and left-aligned the other while both held
12329    // a bigint. Reported by sentori against 7.38.18 (their §2.2), who
12330    // also established that the EXECUTOR was never confused -- CTAS off
12331    // the same expression gives a bigint column, and arithmetic on it
12332    // works. Only the type travelling in the RowDescription was wrong.
12333    cat: Option<&Catalog>,
12334) -> Result<Vec<ProjectedItem>, EngineError> {
12335    let visible = schema_cols.len().saturating_sub(hidden_tail);
12336    // v7.39 (round 462) — a join's combined schema qualifies every column
12337    // `alias.col` so the deferred-join cell lookups resolve by composite
12338    // name. That is an internal convention, and `*` was handing it to the
12339    // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
12340    // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
12341    // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
12342    // already learned this for `q.*`; plain `*` never got the same rule.
12343    //
12344    // The signal is the schema itself, not the call site: only a combined
12345    // join schema arrives with no table alias AND every column qualified.
12346    // A single-table schema carries its alias, an empty schema has nothing
12347    // to strip, and a synthetic schema's names carry no dot.
12348    let joined_schema = table_alias.is_empty()
12349        && !schema_cols.is_empty()
12350        && schema_cols.iter().all(|c| c.name.contains('.'));
12351    let bare_name = |name: &str| -> String {
12352        if !joined_schema {
12353            return name.to_string();
12354        }
12355        match name.split_once('.') {
12356            Some((_, rest)) if !rest.is_empty() => rest.to_string(),
12357            _ => name.to_string(),
12358        }
12359    };
12360    let mut out = Vec::new();
12361    for item in items {
12362        match item {
12363            SelectItem::Wildcard => {
12364                // v7.39 (round 511) — `*` never expands a system column, as
12365                // PG's does not. They join the schema only when the statement
12366                // asked for them, so this matters for the mixed shape
12367                // `SELECT *, ctid FROM t`.
12368                //
12369                // v7.39 (round 512) — by POSITION, not by name. Matching on
12370                // the name alone looked safe because PG reserves them, and it
12371                // is not: `pg_replication_slots` genuinely has a column called
12372                // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
12373                // Only the trailing six, in the order the scan appends them,
12374                // are the synthetic ones.
12375                let sys_skip = synthetic_system_positions(schema_cols);
12376                for (idx, col) in schema_cols.iter().enumerate() {
12377                    if sys_skip[idx] || idx >= visible {
12378                        continue;
12379                    }
12380                    out.push(ProjectedItem {
12381                        expr: Expr::Column(ColumnName {
12382                            qualifier: None,
12383                            name: col.name.clone(),
12384                        }),
12385                        output_name: bare_name(&col.name),
12386                        ty: col.ty,
12387                        nullable: col.nullable,
12388                        user_enum_type: col.user_enum_type.clone(),
12389                        mysql_fsp: col.mysql_fsp,
12390                        collation_name: col.collation_name.clone(),
12391                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12392                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
12393                    });
12394                }
12395            }
12396            // v7.39 (round 128) — `q.*` expands to every column belonging to
12397            // the qualifier `q`. Single-table schemas carry bare column names
12398            // reachable via `table_alias`; a join's combined schema carries
12399            // `alias.col` names, so a column belongs to `q` when its name has
12400            // the `q.` prefix. PG labels the expanded columns by their bare
12401            // name, so the `alias.` prefix is stripped from the output name.
12402            SelectItem::QualifiedWildcard(q) => {
12403                let prefix = alloc::format!("{q}.");
12404                let single_table = !table_alias.is_empty() && q == table_alias;
12405                let mut matched = 0usize;
12406                for col in &schema_cols[..visible] {
12407                    let belongs =
12408                        col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
12409                    if !belongs {
12410                        continue;
12411                    }
12412                    matched += 1;
12413                    let output_name = col
12414                        .name
12415                        .strip_prefix(&prefix)
12416                        .unwrap_or(&col.name)
12417                        .to_string();
12418                    out.push(ProjectedItem {
12419                        expr: Expr::Column(ColumnName {
12420                            qualifier: None,
12421                            name: col.name.clone(),
12422                        }),
12423                        output_name,
12424                        ty: col.ty,
12425                        nullable: col.nullable,
12426                        user_enum_type: col.user_enum_type.clone(),
12427                        mysql_fsp: col.mysql_fsp,
12428                        collation_name: col.collation_name.clone(),
12429                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
12430                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
12431                    });
12432                }
12433                if matched == 0 {
12434                    // `q.*` names no column, so the reference IS the star.
12435                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
12436                        qualifier: q.clone(),
12437                        column: alloc::string::String::from("*"),
12438                    }));
12439                }
12440            }
12441            SelectItem::Expr { expr, alias } => {
12442                // Plain column ref keeps full schema info (real type +
12443                // nullability). For compound expressions try the
12444                // describe-side function-return-type table first
12445                // (e.g. `SELECT now()` → Timestamptz, `SELECT
12446                // concat(…)` → Text). Falls back to nullable Text
12447                // for shapes the describe path can't resolve.
12448                if let Expr::Column(c) = expr {
12449                    let sch = resolve_projection_column(c, schema_cols, table_alias, mysql)?;
12450                    let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
12451                    out.push(ProjectedItem {
12452                        expr: expr.clone(),
12453                        output_name,
12454                        ty: sch.ty,
12455                        nullable: sch.nullable,
12456                        // v7.39 (read01 round 54) — a bare enum column keeps
12457                        // its enum identity through the projection.
12458                        user_enum_type: sch.user_enum_type.clone(),
12459                        mysql_fsp: sch.mysql_fsp,
12460                        collation_name: sch.collation_name.clone(),
12461                        // v7.38.13 — and its byte-wise-ness. This is the
12462                        // site `SELECT DISTINCT t FROM t` arrives at.
12463                        fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
12464                        pads: crate::collate::pads_space(sch.collation_name.as_deref()),
12465                    });
12466                } else if let Some(shape) = describe::describe_expr_in(expr, schema_cols, cat) {
12467                    let output_name = alias
12468                        .clone()
12469                        .unwrap_or_else(|| default_output_name(expr, mysql));
12470                    out.push(ProjectedItem {
12471                        expr: expr.clone(),
12472                        // v7.38.18 — a projected EXPRESSION has no column collation
12473                        // to read, so it takes the session default, which is MySQL
12474                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12475                        pads: false,
12476                        output_name,
12477                        ty: shape.ty,
12478                        // v7.39 (round 258) — a projected EXPRESSION keeps its
12479                        // enum identity too, not just a bare column. `FROM
12480                        // (VALUES ('happy'::mood), …) t(m)` lowers to constant
12481                        // SELECTs, so the derived column arrived here as a cast
12482                        // and lost the enum — making the outer ORDER BY / min /
12483                        // max / array_agg sort by the label's TEXT.
12484                        nullable: shape.nullable,
12485                        user_enum_type: None,
12486                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12487                        // A bare column reference keeps its collation; any
12488                        // other expression produces a new value and has none.
12489                        collation_name: match expr {
12490                            Expr::Column(c) => schema_cols
12491                                .iter()
12492                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12493                                .and_then(|sc| sc.collation_name.clone()),
12494                            _ => None,
12495                        },
12496                        fold_exempt: match expr {
12497                            Expr::Column(c) => schema_cols
12498                                .iter()
12499                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12500                                .is_some_and(|sc| {
12501                                    matches!(sc.collation, spg_storage::Collation::Binary)
12502                                }),
12503                            // Not a column: no declared collation to honour,
12504                            // so the session default applies and it folds.
12505                            _ => false,
12506                        },
12507                    });
12508                } else {
12509                    let output_name = alias
12510                        .clone()
12511                        .unwrap_or_else(|| default_output_name(expr, mysql));
12512                    out.push(ProjectedItem {
12513                        expr: expr.clone(),
12514                        // v7.38.18 — a projected EXPRESSION has no column collation
12515                        // to read, so it takes the session default, which is MySQL
12516                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
12517                        pads: false,
12518                        output_name,
12519                        // A user ENUM has no DataType of its own, so
12520                        // `describe_expr` cannot type `'ok'::mood` and the
12521                        // item lands HERE, defaulting to text — which is why
12522                        // pg_typeof answered `text` and a derived table sorted
12523                        // enum values by their label.
12524                        ty: DataType::Text,
12525                        nullable: true,
12526                        user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
12527                            .map(alloc::string::String::from),
12528                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
12529                        collation_name: match expr {
12530                            Expr::Column(c) => schema_cols
12531                                .iter()
12532                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12533                                .and_then(|sc| sc.collation_name.clone()),
12534                            _ => None,
12535                        },
12536                        fold_exempt: match expr {
12537                            Expr::Column(c) => schema_cols
12538                                .iter()
12539                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
12540                                .is_some_and(|sc| {
12541                                    matches!(sc.collation, spg_storage::Collation::Binary)
12542                                }),
12543                            // Not a column: no declared collation to honour,
12544                            // so the session default applies and it folds.
12545                            _ => false,
12546                        },
12547                    });
12548                }
12549            }
12550        }
12551    }
12552    Ok(out)
12553}
12554
12555// ---- v4.12 window-function helpers ----
12556// The (partition-key, order-key, original-index) tuple shape used
12557// across these helpers is intrinsic to the planner. Factoring it
12558// into a typedef adds indirection without making the code clearer,
12559// so several lints are allowed inline on the affected functions
12560// rather than module-wide.
12561
12562/// v4.22: pick more specific column types from observed rows when
12563/// the projection builder defaulted to Text (the v1.x behavior for
12564/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
12565/// land an Int column in the CTE storage table rather than failing
12566/// the insert with "expected TEXT, got INT".
12567pub(crate) fn infer_column_types(
12568    columns: &[ColumnSchema],
12569    rows: &[Row<'static>],
12570) -> Vec<ColumnSchema> {
12571    let mut out = columns.to_vec();
12572    for (col_idx, col) in out.iter_mut().enumerate() {
12573        if col.ty != DataType::Text {
12574            continue;
12575        }
12576        let mut inferred: Option<DataType> = None;
12577        let mut all_null = true;
12578        for row in rows {
12579            let Some(v) = row.values.get(col_idx) else {
12580                continue;
12581            };
12582            let ty = match v {
12583                Value::Null => continue,
12584                Value::SmallInt(_) => DataType::SmallInt,
12585                Value::Int(_) => DataType::Int,
12586                Value::BigInt(_) => DataType::BigInt,
12587                Value::Float(_) => DataType::Float,
12588                Value::Bool(_) => DataType::Bool,
12589                Value::Vector(_) => DataType::Vector {
12590                    dim: 0,
12591                    encoding: VecEncoding::F32,
12592                },
12593                // v7.38 (read01 U16) — carry array values through with an
12594                // array type so a recursive CTE that projects an array
12595                // (e.g. a SEARCH/CYCLE ord / path column) types the working
12596                // column as an array, not Text.
12597                Value::TextArray(_) => DataType::TextArray,
12598                Value::IntArray(_) => DataType::IntArray,
12599                Value::BigIntArray(_) => DataType::BigIntArray,
12600                Value::SmallIntArray(_) => DataType::SmallIntArray,
12601                Value::FloatArray(_) => DataType::FloatArray,
12602                Value::BoolArray(_) => DataType::BoolArray,
12603                // v7.39 (GUC knife 2) — an interval projection describes
12604                // as INTERVAL (typed drivers read the RowDescription OID).
12605                Value::Interval { .. } => DataType::Interval,
12606                _ => DataType::Text,
12607            };
12608            all_null = false;
12609            inferred = Some(match inferred {
12610                None => ty,
12611                Some(prev) if prev == ty => prev,
12612                Some(_) => DataType::Text,
12613            });
12614        }
12615        if let Some(t) = inferred {
12616            col.ty = t;
12617            col.nullable = true;
12618        } else if all_null {
12619            col.nullable = true;
12620        }
12621    }
12622    out
12623}
12624
12625/// Numeric widening rank for UNION type resolution (higher = wider).
12626fn numeric_rank(t: DataType) -> Option<u8> {
12627    match t {
12628        DataType::SmallInt => Some(1),
12629        DataType::Int => Some(2),
12630        DataType::BigInt => Some(3),
12631        DataType::Numeric { .. } => Some(4),
12632        DataType::Float => Some(5),
12633        _ => None,
12634    }
12635}
12636
12637/// Resolve the common result type for a UNION / VALUES column from the
12638/// set of concrete (non-NULL) branch types, following the safe subset
12639/// of PG's type resolution:
12640///   * all-numeric  → the widest numeric (int ∪ bigint → bigint, … ∪
12641///     numeric → numeric, … ∪ float → float);
12642///   * DATE ∪ TIMESTAMP → TIMESTAMP;
12643///   * exactly one concrete non-TEXT type mixed with TEXT literals →
12644///     that concrete type (the TEXT cells get parsed into it).
12645/// Returns `None` for anything ambiguous, so the caller leaves the
12646/// column untouched rather than risk a wrong or failing coercion.
12647fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
12648    // NB: types are collected from RUNTIME values, which are coarser
12649    // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
12650    // a single-concrete-type fast path must NOT overwrite the column
12651    // type — it would downgrade tstz to ts. NULL-only unification (PG:
12652    // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
12653    // row's pg_typeof) needs schema-level resolution — recorded, not
12654    // attempted here.
12655    if types.len() < 2 {
12656        return None;
12657    }
12658    if types.iter().all(|t| numeric_rank(*t).is_some()) {
12659        return types
12660            .iter()
12661            .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
12662            .copied();
12663    }
12664    let non_text: Vec<&DataType> = types
12665        .iter()
12666        .filter(|t| !matches!(t, DataType::Text))
12667        .collect();
12668    // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
12669    // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
12670    // if any is timestamp the result is timestamp (ts ∪ date). All values are
12671    // the same UTC-micros instant, so widening date/ts to tstz is lossless.
12672    if non_text.iter().all(|t| {
12673        matches!(
12674            t,
12675            DataType::Date | DataType::Timestamp | DataType::Timestamptz
12676        )
12677    }) && non_text
12678        .iter()
12679        .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
12680    {
12681        if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
12682            return Some(DataType::Timestamptz);
12683        }
12684        return Some(DataType::Timestamp);
12685    }
12686    // A single concrete non-TEXT type mixed with TEXT literals.
12687    if non_text.len() == 1 {
12688        return Some(*non_text[0]);
12689    }
12690    // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
12691    // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
12692    // text): resolve the concrete set first (PG treats the unknown-
12693    // typed string literals as castable to whatever the knowns
12694    // resolve to), then the TEXT cells parse into that target — the
12695    // caller's coercion dry-run still abandons the column if any
12696    // literal doesn't parse.
12697    if !non_text.is_empty() && non_text.len() < types.len() {
12698        let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
12699        return resolve_union_common_type(&concrete);
12700    }
12701    None
12702}
12703
12704/// Coerce every cell of a UNION / VALUES result column to one common
12705/// type (see [`resolve_union_common_type`]). Conservative: a column
12706/// whose branches already agree, or whose types don't resolve, or where
12707/// any cell fails to coerce, is left exactly as it was — this never
12708/// turns a previously-working query into an error.
12709fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
12710    for col_idx in 0..columns.len() {
12711        let mut seen: Vec<DataType> = Vec::new();
12712        for row in rows.iter() {
12713            if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
12714                if !seen.contains(&dt) {
12715                    seen.push(dt);
12716                }
12717            }
12718        }
12719        // v7.37.16 — a single concrete runtime type under a TEXT-typed
12720        // column means the column type came off a NULL (or unknown-text)
12721        // branch: NULL literals describe as TEXT (`L::Null → Text`), so
12722        // `VALUES (NULL),(1.5)` left the column "text" while every
12723        // non-NULL cell is numeric. Adopt the concrete type — schema
12724        // only, no cell changes. tstz-safe by construction: a real
12725        // timestamptz column's schema type is Timestamptz, not Text, so
12726        // the coarser runtime type (Value::Timestamp) can't downgrade it
12727        // through this arm; and a real text column's non-NULL cells are
12728        // Text, which keeps seen == [Text] and skips it.
12729        if seen.len() == 1
12730            && matches!(columns[col_idx].ty, DataType::Text)
12731            && !matches!(seen[0], DataType::Text)
12732        {
12733            columns[col_idx].ty = seen[0];
12734            continue;
12735        }
12736        let Some(target) = resolve_union_common_type(&seen) else {
12737            continue;
12738        };
12739        // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
12740        // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
12741        // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
12742        // existing numeric cell untouched and only promote integers (to scale 0)
12743        // rather than rescaling everything to the widest scale.
12744        let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
12745        // Dry-run the coercion; abandon the whole column if any fails.
12746        let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
12747        let mut ok = true;
12748        for row in rows.iter() {
12749            match row.values.get(col_idx) {
12750                Some(Value::Numeric { .. }) if scale_preserving_numeric => {
12751                    coerced.push(Some(row.values[col_idx].clone()));
12752                }
12753                Some(v) => {
12754                    let cell_target = if scale_preserving_numeric {
12755                        DataType::Numeric {
12756                            precision: 0,
12757                            scale: 0,
12758                        }
12759                    } else {
12760                        target
12761                    };
12762                    match crate::conversions::coerce_value(
12763                        v.clone(),
12764                        cell_target,
12765                        &columns[col_idx].name,
12766                        col_idx,
12767                    ) {
12768                        Ok(cv) => coerced.push(Some(cv)),
12769                        Err(_) => {
12770                            ok = false;
12771                            break;
12772                        }
12773                    }
12774                }
12775                None => coerced.push(None),
12776            }
12777        }
12778        if !ok {
12779            continue;
12780        }
12781        for (row, cv) in rows.iter_mut().zip(coerced) {
12782            if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
12783                *slot = nv;
12784            }
12785        }
12786        columns[col_idx].ty = target;
12787    }
12788}
12789
12790/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
12791/// dedup inside the recursive iteration. Crude but deterministic
12792/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
12793fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
12794    let mut out = Vec::new();
12795    for v in &row.values {
12796        // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
12797        // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
12798        // like PG (and like GROUP BY, which already normalizes). The old
12799        // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
12800        // the exact-decimal family through one scale-stripped canonical form.
12801        match v {
12802            Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12803            Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12804            Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12805            Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
12806            other => {
12807                let s = alloc::format!("{other:?}|");
12808                out.extend_from_slice(s.as_bytes());
12809            }
12810        }
12811    }
12812    out
12813}
12814
12815/// Append a scale-independent canonical key for an exact-decimal value: strip
12816/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
12817/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
12818fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
12819    while scale > 0 && scaled % 10 == 0 {
12820        scaled /= 10;
12821        scale -= 1;
12822    }
12823    let s = alloc::format!("\u{1}{scaled}e-{scale}|");
12824    out.extend_from_slice(s.as_bytes());
12825}
12826
12827/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
12828/// (uncorrelated; outer refs were substituted upstream), then zip
12829/// them in parallel, NULL-padding shorter arrays to the longest
12830/// (PG's ROWS FROM shorthand). Shared by the primary-position
12831/// executor and the join-position materialiser, which both detect
12832/// the parser's `__unnest_zip` marker call.
12833pub(crate) fn unnest_zip_rows(
12834    args: &[Expr],
12835) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
12836    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12837    let ctx = EvalContext::new(&empty_schema, None);
12838    let dummy_row = Row::new(alloc::vec::Vec::new());
12839    let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
12840    let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
12841        alloc::vec::Vec::with_capacity(args.len());
12842    for a in args {
12843        let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
12844        // v7.39.13 — the element menu the rest of the workspace already
12845        // has, not a third copy of a shortened one.
12846        //
12847        // This arm listed Text, Int and BigInt and refused everything
12848        // else, so `unnest(uuid[], text[])` raised while
12849        // `unnest(uuid[])` — a different path — did not. A shipped
12850        // endpoint of a customer's returned 500 on every call because
12851        // of it. `array_elements` and `array_element_type` are the two
12852        // halves of the menu that `array_element_at`'s own comment
12853        // describes: "previously only matched Text/Int/BigInt arrays
12854        // and errored on every other element type". Same sentence,
12855        // third arm.
12856        let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = if matches!(v, Value::Null) {
12857            (DataType::Text, alloc::vec::Vec::new())
12858        } else if let Some(items) = crate::eval::values::array_elements(&v) {
12859            let dt = v
12860                .data_type()
12861                .and_then(crate::describe::array_element_type)
12862                .unwrap_or(DataType::Text);
12863            (dt, items)
12864        } else {
12865            return Err(EngineError::Unsupported(alloc::format!(
12866                "unnest() expects array arguments, got {}",
12867                crate::conversions::pg_type_name_for_error_opt(v.data_type())
12868            )));
12869        };
12870        dtypes.push(dt);
12871        columns.push(items);
12872    }
12873    let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
12874    let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
12875    for i in 0..max_len {
12876        let vals: alloc::vec::Vec<Value<'static>> = columns
12877            .iter()
12878            .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
12879            .collect();
12880        rows.push(Row::new(vals));
12881    }
12882    Ok((dtypes, rows))
12883}
12884
12885/// Detect the parser's multi-arg unnest marker on an unnest_expr.
12886pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
12887    match expr {
12888        Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
12889        _ => None,
12890    }
12891}
12892
12893/// Evaluate generate_series arguments (uncorrelated — outer refs
12894/// were substituted upstream where applicable) and build the row
12895/// stream. Dispatches on the start value's shape and rejects
12896/// mixed-shape calls early (e.g. start = timestamp, stop =
12897/// integer) so the caller gets a clean error rather than a panic.
12898/// Shared by the primary-position executor and the join-position
12899/// materialiser.
12900pub(crate) fn generate_series_rows(
12901    args: &[Expr],
12902    cancel: &CancelToken<'_>,
12903) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12904    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12905    let ctx = EvalContext::new(&empty_schema, None);
12906    let dummy_row = Row::new(alloc::vec::Vec::new());
12907    let mut arg_values: alloc::vec::Vec<Value<'static>> =
12908        alloc::vec::Vec::with_capacity(args.len());
12909    for a in args {
12910        arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
12911    }
12912    generate_series_from_values(arg_values, args, cancel)
12913}
12914
12915/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
12916/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
12917/// full integer / numeric / timestamp overload set with the FROM-clause path.
12918/// Before this split the target-list arm reimplemented only the integer case,
12919/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
12920/// NULL for the timestamp column instead of the series. `arg_values` are the
12921/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
12922/// timestamp type resolution (it inspects the argument expressions' types).
12923pub(crate) fn generate_series_from_values(
12924    mut arg_values: alloc::vec::Vec<Value<'static>>,
12925    args: &[Expr],
12926    cancel: &CancelToken<'_>,
12927) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12928    // PG: a NULL bound or step yields zero rows (also keeps the
12929    // NULL-padded lateral probe alive — schema without data).
12930    if arg_values.iter().any(|v| matches!(v, Value::Null)) {
12931        return Ok((DataType::BigInt, alloc::vec::Vec::new()));
12932    }
12933    // PG resolves `generate_series(date, date, interval)` to the
12934    // timestamp/timestamptz overload by implicitly casting each date
12935    // bound up to a timestamp at midnight (verified vs live PG18.4:
12936    // date args yield rows anchored at 00:00:00). SPG's TZ-naive
12937    // timestamp model renders the same instants, so fold any Date
12938    // bound to its midnight Timestamp (canonical `days *
12939    // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
12940    // the shape match so the existing timestamp arm drives the walk.
12941    // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
12942    // `generate_series(date, date, interval)` has no date overload, and among
12943    // the two candidates PG prefers the timestamptz one (timestamptz is the
12944    // preferred type of the datetime category), so the column comes back
12945    // `timestamp with time zone` — the rows render with a `+00` offset. A
12946    // timestamptz bound obviously lands there too. Only genuinely
12947    // timestamp-typed bounds keep the TZ-naive result type.
12948    let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12949    let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
12950        || args.iter().any(|a| {
12951            crate::describe::describe_expr(a, &empty_cols)
12952                .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
12953        });
12954    for v in &mut arg_values {
12955        if let Value::Date(d) = *v {
12956            *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
12957        }
12958    }
12959    match arg_values.as_slice() {
12960        [Value::Timestamp(start), Value::Timestamp(stop), step] => {
12961            let interval_step = match step {
12962                Value::Interval { .. } => step.clone(),
12963                // v7.38 (read01) — PG resolves an unknown-type string step
12964                // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
12965                // a bare text step by parsing it the same way `::interval` does.
12966                Value::Text(s) => crate::conversions::coerce_value(
12967                    Value::text(s.as_ref()),
12968                    DataType::Interval,
12969                    "",
12970                    0,
12971                )
12972                .map_err(|_| {
12973                    EngineError::Unsupported(alloc::format!(
12974                        "generate_series(timestamp, timestamp, …): \
12975                         could not parse step {s:?} as INTERVAL"
12976                    ))
12977                })?,
12978                other => {
12979                    return Err(EngineError::Unsupported(alloc::format!(
12980                        "generate_series(timestamp, timestamp, …): \
12981                         step must be INTERVAL, got {}",
12982                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
12983                    )));
12984                }
12985            };
12986            let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
12987            Ok((
12988                if tz {
12989                    DataType::Timestamptz
12990                } else {
12991                    DataType::Timestamp
12992                },
12993                rows,
12994            ))
12995        }
12996        [start, stop, step]
12997            if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
12998        {
12999            let s = value_to_i64(start);
13000            let e = value_to_i64(stop);
13001            let st = value_to_i64(step);
13002            // PG types the series by the argument type: int4 args → int4
13003            // elements, int8 (bigint) args → int8. Any BigInt operand widens.
13004            let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
13005            let rows = generate_series_integers(s, e, st, wide, cancel)?;
13006            Ok((
13007                if wide {
13008                    DataType::BigInt
13009                } else {
13010                    DataType::Int
13011                },
13012                rows,
13013            ))
13014        }
13015        [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
13016            let s = value_to_i64(start);
13017            let e = value_to_i64(stop);
13018            let wide = value_is_bigint(start) || value_is_bigint(stop);
13019            let rows = generate_series_integers(s, e, 1, wide, cancel)?;
13020            Ok((
13021                if wide {
13022                    DataType::BigInt
13023                } else {
13024                    DataType::Int
13025                },
13026                rows,
13027            ))
13028        }
13029        // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
13030        // series in exact numeric arithmetic; NaN / infinity bounds and a
13031        // zero step get dedicated wordings, and a mixed int/numeric call
13032        // resolves here via the implicit int→numeric cast.
13033        [_, _] | [_, _, _]
13034            if arg_values
13035                .iter()
13036                .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
13037                && arg_values.iter().all(|v| {
13038                    matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
13039                }) =>
13040        {
13041            use spg_storage::NumericKind as K;
13042            let words: [(&str, &str); 3] = [
13043                (
13044                    "start value cannot be NaN",
13045                    "start value cannot be infinity",
13046                ),
13047                ("stop value cannot be NaN", "stop value cannot be infinity"),
13048                ("step size cannot be NaN", "step size cannot be infinity"),
13049            ];
13050            for (i, v) in arg_values.iter().enumerate() {
13051                if let Value::Numeric { kind, .. } = v {
13052                    if *kind != K::Finite {
13053                        let (nan_w, inf_w) = words[i];
13054                        return Err(EngineError::Unsupported(
13055                            if *kind == K::NaN { nan_w } else { inf_w }.into(),
13056                        ));
13057                    }
13058                }
13059            }
13060            let big =
13061                |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
13062            let start = big(&arg_values[0]);
13063            let stop = big(&arg_values[1]);
13064            let step = if arg_values.len() == 3 {
13065                big(&arg_values[2])
13066            } else {
13067                spg_storage::bignum::BigNumeric::from_i128(1, 0)
13068            };
13069            if step.is_zero() {
13070                return Err(EngineError::Unsupported(
13071                    "step size cannot equal zero".into(),
13072                ));
13073            }
13074            let descending = step.parts().0;
13075            let mut rows = alloc::vec::Vec::new();
13076            let mut cur = start;
13077            const MAX_ROWS: usize = 10_000_000;
13078            loop {
13079                cancel.check()?;
13080                let c = cur.cmp(&stop);
13081                if descending {
13082                    if c == core::cmp::Ordering::Less {
13083                        break;
13084                    }
13085                } else if c == core::cmp::Ordering::Greater {
13086                    break;
13087                }
13088                if rows.len() >= MAX_ROWS {
13089                    return Err(EngineError::Unsupported(alloc::format!(
13090                        "generate_series() result exceeds {MAX_ROWS} rows"
13091                    )));
13092                }
13093                rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
13094                    cur.clone()
13095                )]));
13096                cur = cur.add(&step);
13097            }
13098            Ok((
13099                DataType::Numeric {
13100                    precision: 0,
13101                    scale: 0,
13102                },
13103                rows,
13104            ))
13105        }
13106        _ => Err(EngineError::Unsupported(alloc::format!(
13107            "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
13108             argument shapes; got {}",
13109            arg_values
13110                .iter()
13111                .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
13112                .collect::<alloc::vec::Vec<_>>()
13113                .join(", ")
13114        ))),
13115    }
13116}
13117
13118/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
13119/// Step direction follows the sign: positive step iterates upward
13120/// (stops when current > stop); negative iterates downward; zero
13121/// errors. Caller-facing row stream is `BigInt`-typed so a single
13122/// projection schema covers SmallInt / Int / BigInt callers.
13123fn generate_series_integers(
13124    start: i64,
13125    stop: i64,
13126    step: i64,
13127    wide: bool,
13128    cancel: &CancelToken<'_>,
13129) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13130    if step == 0 {
13131        return Err(EngineError::Unsupported(
13132            "step size cannot equal zero".into(),
13133        ));
13134    }
13135    let mut out = alloc::vec::Vec::new();
13136    let mut cur = start;
13137    // Hard cap to keep a runaway call from eating all memory. PG
13138    // has no such cap but does honour query timeout; SPG's cancel
13139    // token will fire too — this is a defense-in-depth backstop.
13140    const MAX_ROWS: usize = 10_000_000;
13141    loop {
13142        cancel.check()?;
13143        if step > 0 && cur > stop {
13144            break;
13145        }
13146        if step < 0 && cur < stop {
13147            break;
13148        }
13149        out.push(Row::new(alloc::vec![if wide {
13150            Value::BigInt(cur)
13151        } else {
13152            Value::Int(cur as i32)
13153        }]));
13154        if out.len() > MAX_ROWS {
13155            return Err(EngineError::Unsupported(alloc::format!(
13156                "generate_series(): exceeded {MAX_ROWS} rows; \
13157                 narrow start/stop or use a larger step"
13158            )));
13159        }
13160        cur = match cur.checked_add(step) {
13161            Some(n) => n,
13162            None => break,
13163        };
13164    }
13165    Ok(out)
13166}
13167
13168/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
13169/// `Value::Interval { months, micros }` per the caller's guard;
13170/// each iteration adds the interval via `apply_binary_interval`
13171/// so month-shifting handles short-month rollover (PG semantics).
13172fn generate_series_timestamps(
13173    start: i64,
13174    stop: i64,
13175    step: Value,
13176    cancel: &CancelToken<'_>,
13177) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
13178    let (months, days, micros) = match &step {
13179        Value::Interval {
13180            months,
13181            days,
13182            micros,
13183            kind,
13184        } => (*months, *days, *micros),
13185        _ => unreachable!("caller guards step.is_interval"),
13186    };
13187    if months == 0 && days == 0 && micros == 0 {
13188        return Err(EngineError::Unsupported(
13189            "generate_series(): INTERVAL step cannot be zero".into(),
13190        ));
13191    }
13192    let ascending = months > 0 || days > 0 || micros > 0;
13193    let mut out = alloc::vec::Vec::new();
13194    let mut cur = Value::Timestamp(start);
13195    const MAX_ROWS: usize = 10_000_000;
13196    loop {
13197        cancel.check()?;
13198        let cur_t = match cur {
13199            Value::Timestamp(t) => t,
13200            _ => unreachable!("loop invariant: cur is Timestamp"),
13201        };
13202        if ascending && cur_t > stop {
13203            break;
13204        }
13205        if !ascending && cur_t < stop {
13206            break;
13207        }
13208        out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
13209        if out.len() > MAX_ROWS {
13210            return Err(EngineError::Unsupported(alloc::format!(
13211                "generate_series(): exceeded {MAX_ROWS} rows; \
13212                 narrow start/stop or use a larger step"
13213            )));
13214        }
13215        let next = eval::apply_binary_interval(
13216            spg_sql::ast::BinOp::Add,
13217            &cur,
13218            &Value::Interval {
13219                months,
13220                days,
13221                micros,
13222                kind: spg_storage::IntervalKind::Finite,
13223            },
13224        )
13225        .map_err(EngineError::Eval)?;
13226        cur = match next {
13227            Some(v) => v,
13228            None => break,
13229        };
13230    }
13231    Ok(out)
13232}
13233
13234/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
13235/// WITH TIES` requires an `ORDER BY`. Without one, there's no
13236/// way to identify "ties" deterministically, so PG errors at
13237/// plan time. SPG mirrors that surface so the same DDL / app
13238/// behaviour holds on cutover.
13239fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
13240    if stmt.limit_with_ties && stmt.order_by.is_empty() {
13241        return Err(EngineError::Unsupported(alloc::string::String::from(
13242            "WITH TIES cannot be specified without ORDER BY clause",
13243        )));
13244    }
13245    Ok(())
13246}
13247
13248/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
13249/// (case-insensitive). Used by `exec_select_cancel`'s
13250/// projection loop to detect Set-Returning-Function rows that
13251/// need per-row expansion. Only the top-level call counts —
13252/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
13253/// projection's perspective; it would surface as an "unknown
13254/// function" mismatch downstream, which is what we want
13255/// (multi-SRF / nested SRF is documented carve-out for v7.19).
13256fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
13257    top_level_srf_kind(expr).is_some()
13258}
13259
13260/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
13261/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
13262/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
13263/// source row.
13264#[derive(Clone, Copy, PartialEq, Eq)]
13265pub(crate) enum SrfKind {
13266    Unnest,
13267    /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
13268    /// list. It used to be handled ONLY by the parser's lift into FROM, so a
13269    /// second one in the same list came back as "unknown function".
13270    GenerateSeries,
13271    GenerateSubscripts,
13272    /// `_text` variants unwrap scalars to their lexeme; the plain forms render
13273    /// every value as compact JSON text.
13274    ArrayElements {
13275        as_text: bool,
13276    },
13277    PathQuery,
13278    RegexpMatches,
13279    Each {
13280        as_text: bool,
13281    },
13282    ObjectKeys,
13283}
13284
13285/// Case-insensitive match against any of `names`.
13286fn name_is(name: &str, names: &[&str]) -> bool {
13287    names.iter().any(|n| name.eq_ignore_ascii_case(n))
13288}
13289
13290pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
13291    let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13292        return None;
13293    };
13294    let n = args.len();
13295    // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
13296    // SELECT list (it returned an array there before) and shares the unnest
13297    // expansion machinery.
13298    if n == 1 && name.eq_ignore_ascii_case("unnest") {
13299        return Some(SrfKind::Unnest);
13300    }
13301    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
13302        return Some(SrfKind::GenerateSeries);
13303    }
13304    if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
13305        return Some(SrfKind::GenerateSubscripts);
13306    }
13307    // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
13308    // per element / match in the SELECT list; they collapsed to a single row
13309    // (a TextArray, or an "unknown function" error for `each`) before.
13310    if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
13311        return Some(SrfKind::ArrayElements { as_text: false });
13312    }
13313    if n == 1
13314        && name_is(
13315            name,
13316            &["jsonb_array_elements_text", "json_array_elements_text"],
13317        )
13318    {
13319        return Some(SrfKind::ArrayElements { as_text: true });
13320    }
13321    // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
13322    if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
13323        return Some(SrfKind::PathQuery);
13324    }
13325    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
13326        return Some(SrfKind::RegexpMatches);
13327    }
13328    if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
13329        return Some(SrfKind::Each { as_text: false });
13330    }
13331    if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
13332        return Some(SrfKind::Each { as_text: true });
13333    }
13334    if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
13335        return Some(SrfKind::ObjectKeys);
13336    }
13337    None
13338}
13339
13340/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
13341/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
13342/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
13343/// rows, as in PG).
13344pub(crate) fn top_level_srf_output(
13345    expr: &spg_sql::ast::Expr,
13346    row: &Row<'static>,
13347    ctx: &EvalContext<'_>,
13348) -> Result<Vec<Value<'static>>, EngineError> {
13349    let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
13350        (top_level_srf_kind(expr), expr)
13351    else {
13352        return Err(EngineError::Unsupported(
13353            "expected a SELECT-list SRF call".into(),
13354        ));
13355    };
13356    match kind {
13357        SrfKind::Unnest => {
13358            // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
13359            // the elements DIRECTLY: the old path built the whole
13360            // Value::Array (one eval + a clone per element) only for
13361            // array_value_to_elements to clone every element back out.
13362            // Any other argument shape (a column, a function result)
13363            // keeps the build-then-split path.
13364            if let spg_sql::ast::Expr::Array(items) = &args[0] {
13365                return items
13366                    .iter()
13367                    .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
13368                    .collect();
13369            }
13370            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13371            array_value_to_elements(&arr)
13372        }
13373        SrfKind::GenerateSeries => {
13374            // v7.39 (read01 round 96) — evaluate the args against the actual
13375            // row, then hand off to the shared core so the numeric and
13376            // timestamp/timestamptz overloads work here too (this arm used to
13377            // handle only integers, silently NULLing a temporal/numeric series
13378            // when it shared a target list with another SRF).
13379            let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
13380            for a in args {
13381                arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13382            }
13383            let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
13384            Ok(rows
13385                .into_iter()
13386                .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
13387                .collect())
13388        }
13389        SrfKind::GenerateSubscripts => {
13390            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13391            let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13392            if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
13393                return Ok(Vec::new());
13394            }
13395            let len = array_value_to_elements(&arr)?.len();
13396            Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
13397        }
13398        // One Value per array element (`_text` → text / SQL NULL, plain → the
13399        // element's compact JSON text) — the element list the FROM-clause form
13400        // materialises.
13401        SrfKind::ArrayElements { as_text } => {
13402            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13403            if matches!(arg, Value::Null) {
13404                return Ok(Vec::new());
13405            }
13406            let items =
13407                crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13408            Ok(items
13409                .into_iter()
13410                .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13411                .collect())
13412        }
13413        // The scalar form already yields a TextArray of the keys (or errors on
13414        // a non-object, like PG); expand it into rows.
13415        SrfKind::ObjectKeys => {
13416            let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
13417            array_value_to_elements(&v)
13418        }
13419        // One row per match, each a text[] of the pattern's capture groups.
13420        SrfKind::RegexpMatches => {
13421            let vals: Vec<Value<'static>> = args
13422                .iter()
13423                .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
13424                .collect::<Result<_, _>>()?;
13425            crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
13426        }
13427        // One composite `(key, value)` row per object member (plain → jsonb
13428        // value, `_text` → text / SQL NULL).
13429        SrfKind::Each { as_text } => {
13430            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13431            if matches!(arg, Value::Null) {
13432                return Ok(Vec::new());
13433            }
13434            let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
13435            Ok(pairs
13436                .into_iter()
13437                .map(|(k, v)| {
13438                    let val = if as_text {
13439                        v.map(Value::text).unwrap_or(Value::Null)
13440                    } else {
13441                        v.map(Value::json).unwrap_or(Value::Null)
13442                    };
13443                    Value::Composite(alloc::vec![
13444                        ("key".to_string(), Value::text(k)),
13445                        ("value".to_string(), val),
13446                    ])
13447                })
13448                .collect())
13449        }
13450        // One Value per matched JSON value.
13451        SrfKind::PathQuery => {
13452            let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
13453            let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
13454            // v7.39 — optional vars document (3rd arg).
13455            let vars = match args.get(2) {
13456                Some(a) => {
13457                    let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
13458                    crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
13459                }
13460                None => None,
13461            };
13462            match crate::json::path_query_vars(&doc, &path, vars.as_ref())
13463                .map_err(EngineError::Eval)?
13464            {
13465                Value::Null => Ok(Vec::new()),
13466                Value::TextArray(items) => Ok(items
13467                    .into_iter()
13468                    .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
13469                    .collect()),
13470                other => Ok(alloc::vec![other]),
13471            }
13472        }
13473    }
13474}
13475
13476/// v7.19 P5 — turn an array-typed `Value` into the element list
13477/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
13478/// = (no rows)`). Non-array values fall through to a type-mismatch
13479/// error.
13480pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
13481    // v7.39 (round 236) — PG unnests a multidimensional array into its
13482    // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
13483    // rows). SPG stores 2-D arrays as their own variants, which fell
13484    // through to the type-mismatch arm below.
13485    if let Some(flat) = crate::eval::values::flatten_2d(v) {
13486        return array_value_to_elements(&flat);
13487    }
13488    // v7.39.11 — every array-family value, through the one element
13489    // menu. The arms below name int / bigint / text / json and stop, so
13490    // `SELECT unnest(ARRAY[1,2]::smallint[])` raised "expects an array
13491    // argument, got smallint[]" — the type it had just been given —
13492    // and so did every catalog vector. Found while closing sentori's
13493    // §4 against 7.39.10; the FROM-clause unnest has the same arm.
13494    if crate::eval::values::array_len(v).is_some() {
13495        if let Some(elems) = crate::eval::values::array_elements(v) {
13496            return Ok(elems);
13497        }
13498    }
13499    match v {
13500        Value::Null => Ok(Vec::new()),
13501        Value::TextArray(items) => Ok(items
13502            .iter()
13503            .map(|opt| {
13504                opt.as_ref()
13505                    .map(|s| Value::text(s.clone()))
13506                    .unwrap_or(Value::Null)
13507            })
13508            .collect()),
13509        Value::IntArray(items) => Ok(items
13510            .iter()
13511            .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
13512            .collect()),
13513        Value::BigIntArray(items) => Ok(items
13514            .iter()
13515            .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
13516            .collect()),
13517        // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
13518        // range per canonical span.
13519        Value::Multirange { kind, ranges } => Ok(ranges
13520            .iter()
13521            .map(|s| Value::Range {
13522                kind: *kind,
13523                lower: s.lower.clone(),
13524                upper: s.upper.clone(),
13525                lower_inc: s.lower_inc,
13526                upper_inc: s.upper_inc,
13527                empty: false,
13528            })
13529            .collect()),
13530        other => Err(EngineError::Eval(EvalError::TypeMismatch {
13531            detail: alloc::format!(
13532                "unnest() expects an array argument, got {}",
13533                crate::conversions::pg_type_name_for_error_opt(other.data_type())
13534            ),
13535        })),
13536    }
13537}
13538
13539impl Engine {
13540    /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
13541    /// the SELECT's FROM / JOIN graph, re-parse each view's body
13542    /// source, and prepend it as a synthetic CTE on the
13543    /// returned SelectStatement. Returns `None` when no view
13544    /// references are found (caller proceeds with the original
13545    /// statement); returns `Some(rewritten)` otherwise (caller
13546    /// re-runs exec_select_cancel on the rewritten form so the
13547    /// regular CTE materialiser handles it).
13548    fn expand_views_in_select(
13549        &self,
13550        stmt: &SelectStatement,
13551    ) -> Result<Option<SelectStatement>, EngineError> {
13552        let cat = self.active_catalog();
13553        let mut referenced: Vec<String> = Vec::new();
13554        if let Some(from) = &stmt.from {
13555            collect_view_refs(&from.primary, cat, &mut referenced);
13556            for j in &from.joins {
13557                collect_view_refs(&j.table, cat, &mut referenced);
13558            }
13559        }
13560        // Don't expand a view name that's already shadowed by a
13561        // CTE on the same SELECT — the CTE wins per PG.
13562        referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
13563        if referenced.is_empty() {
13564            return Ok(None);
13565        }
13566        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
13567        for name in &referenced {
13568            let view = cat.view(name).ok_or_else(|| {
13569                EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13570                    "view {name:?} disappeared mid-expansion"
13571                )))
13572            })?;
13573            let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
13574                EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
13575            })?;
13576            let Statement::Select(body) = parsed else {
13577                return Err(EngineError::Unsupported(alloc::format!(
13578                    "view {name:?} body is not a SELECT (catalog corruption)"
13579                )));
13580            };
13581            new_ctes.push(spg_sql::ast::Cte {
13582                name: name.clone(),
13583                body: spg_sql::ast::CteBody::Select(body),
13584                recursive: false,
13585                column_overrides: view.columns.clone(),
13586                search: None,
13587                cycle: None,
13588            });
13589        }
13590        let mut out = stmt.clone();
13591        // Prepend so view CTEs are visible to caller-supplied CTEs.
13592        new_ctes.extend(out.ctes);
13593        out.ctes = new_ctes;
13594        Ok(Some(out))
13595    }
13596
13597    /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
13598    /// any partition-parent table, rewrite the SELECT so each parent
13599    /// reference resolves to a CTE whose body is a `UNION ALL` over the
13600    /// children that pass the WHERE-derived partition-key range. Returns
13601    /// `None`(no rewrite needed)when no parent is referenced or all
13602    /// references are shadowed by a same-name CTE.
13603    ///
13604    /// Pruning vocabulary at v7.37.6-B:
13605    ///   * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
13606    ///     and `<key> BETWEEN literal AND literal`.
13607    ///   * Anything outside that(OR / nested IN / function call on the
13608    ///     key)defaults to "no pruning" — every child + DEFAULT lands
13609    ///     in the UNION. Correctness is preserved; only the plan size
13610    ///     widens.
13611    fn expand_partition_parents_in_select(
13612        &self,
13613        stmt: &SelectStatement,
13614    ) -> Result<Option<SelectStatement>, EngineError> {
13615        let cat = self.active_catalog();
13616        let Some(from) = &stmt.from else {
13617            return Ok(None);
13618        };
13619        let mut parent_refs: Vec<String> = Vec::new();
13620        collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
13621        for j in &from.joins {
13622            collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
13623        }
13624        // Drop names shadowed by a CTE on the same SELECT(PG semantics
13625        // — same as view expansion above).
13626        parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
13627        if parent_refs.is_empty() {
13628            return Ok(None);
13629        }
13630        // Synthesise a CTE name per parent so the existing
13631        // "CTE shadows a real table" guard doesn't fire (the parent
13632        // IS a real table in the catalog, unlike VIEW expansion's
13633        // case). The FROM-clause TableRef walker below rewrites
13634        // every parent reference to point at the synthetic CTE.
13635        let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
13636        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
13637        let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
13638        for parent_name in &parent_refs {
13639            // No children = no rewrite. The parent itself is a real
13640            // (empty-rows) table — the regular FROM-resolution path
13641            // will scan it and return 0 rows, matching the
13642            // "partition parent with no children" plan. Skipping the
13643            // CTE here also avoids `SELECT * FROM parent` re-entering
13644            // this rewrite on the synthetic body (infinite recursion).
13645            let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
13646                continue;
13647            };
13648            new_ctes.push(spg_sql::ast::Cte {
13649                name: synth_name(parent_name),
13650                body: spg_sql::ast::CteBody::Select(body),
13651                recursive: false,
13652                column_overrides: Vec::new(),
13653                search: None,
13654                cycle: None,
13655            });
13656            expanded_parents.push(parent_name.clone());
13657        }
13658        if expanded_parents.is_empty() {
13659            return Ok(None);
13660        }
13661        let mut out = stmt.clone();
13662        if let Some(from) = out.from.as_mut() {
13663            rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
13664            for j in &mut from.joins {
13665                rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
13666            }
13667        }
13668        new_ctes.extend(out.ctes);
13669        out.ctes = new_ctes;
13670        Ok(Some(out))
13671    }
13672
13673    /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
13674    /// Children include every overlap-hit `Range` plus(always)the
13675    /// `Default` child(if any). Returns `Ok(None)` when no children
13676    /// would survive — caller skips the CTE injection and lets the
13677    /// parent fall through to the regular(empty-rows)scan path,
13678    /// avoiding the infinite recursion that an empty-body CTE
13679    /// referencing the parent name would trigger.
13680    /// v7.37.16 (16.10) — public helper invoked from explain.rs to
13681    /// surface "which children survive the WHERE-clause prune" in
13682    /// EXPLAIN output. Returns `None` when `parent_name` isn't
13683    /// actually a partition parent; otherwise returns the list of
13684    /// children the planner would scan (same algorithm as
13685    /// [`Self::build_partition_parent_union_body`] but without the
13686    /// SQL re-parse).
13687    /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
13688    /// expression (the PG-shaped EXPLAIN's scan builder has no full
13689    /// SelectStatement in hand). Wraps the original by synthesising a
13690    /// minimal statement carrying just the predicate.
13691    pub(crate) fn explain_partition_kept_children_by_where(
13692        &self,
13693        parent_name: &str,
13694        where_: Option<&spg_sql::ast::Expr>,
13695    ) -> Option<Vec<alloc::string::String>> {
13696        let mut synth = SelectStatement::default();
13697        synth.where_ = where_.cloned();
13698        self.explain_partition_kept_children(parent_name, &synth)
13699    }
13700
13701    pub(crate) fn explain_partition_kept_children(
13702        &self,
13703        parent_name: &str,
13704        outer: &SelectStatement,
13705    ) -> Option<Vec<alloc::string::String>> {
13706        use spg_storage::PartitionRole;
13707        let cat = self.active_catalog();
13708        let parent = cat.get(parent_name)?;
13709        let (key_position, parent_kind) = match &parent.schema().partition_role {
13710            Some(PartitionRole::Parent {
13711                key_column_positions,
13712                kind,
13713                ..
13714            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13715            _ => return None,
13716        };
13717        let key_col_name = parent.schema().columns[key_position].name.clone();
13718        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13719            Some(expr) => extract_key_range(expr, &key_col_name),
13720            None => (None, None),
13721        };
13722        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13723            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13724            None => None,
13725        };
13726        let children = crate::partition::children_of_parent(cat, parent_name);
13727        let mut kept: Vec<alloc::string::String> = Vec::new();
13728        let mut default_child: Option<alloc::string::String> = None;
13729        for child_name in &children {
13730            let Some(child) = cat.get(child_name) else {
13731                continue;
13732            };
13733            match &child.schema().partition_role {
13734                Some(PartitionRole::Range { lower, upper, .. }) => {
13735                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13736                        kept.push(child_name.clone());
13737                    }
13738                }
13739                Some(PartitionRole::List { values, .. }) => match &eq_value {
13740                    Some(v) => {
13741                        if values.iter().any(|b| b.equals_value(v)) {
13742                            kept.push(child_name.clone());
13743                        }
13744                    }
13745                    None => kept.push(child_name.clone()),
13746                },
13747                Some(PartitionRole::Hash {
13748                    modulus, remainder, ..
13749                }) => match &eq_value {
13750                    Some(v) => {
13751                        let h = crate::partition::pg_compatible_hash(v);
13752                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13753                            kept.push(child_name.clone());
13754                        }
13755                    }
13756                    None => kept.push(child_name.clone()),
13757                },
13758                Some(PartitionRole::Default { .. }) => {
13759                    default_child = Some(child_name.clone());
13760                }
13761                _ => {}
13762            }
13763        }
13764        let _ = parent_kind;
13765        if let Some(d) = default_child {
13766            if kept.is_empty() || eq_value.is_none() {
13767                kept.push(d);
13768            }
13769        }
13770        Some(kept)
13771    }
13772
13773    fn build_partition_parent_union_body(
13774        &self,
13775        parent_name: &str,
13776        outer: &SelectStatement,
13777    ) -> Result<Option<SelectStatement>, EngineError> {
13778        use spg_storage::PartitionRole;
13779        let cat = self.active_catalog();
13780        let parent = cat.get(parent_name).ok_or_else(|| {
13781            EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13782                "partition parent {parent_name:?} disappeared mid-expansion"
13783            )))
13784        })?;
13785        let (key_position, parent_kind) = match &parent.schema().partition_role {
13786            Some(PartitionRole::Parent {
13787                key_column_positions,
13788                kind,
13789                ..
13790            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13791            // v7.39 (round 645) — an INHERITANCE parent, which has no
13792            // role of its own: the relationship is recorded only in the
13793            // children. Three things differ from a partition parent and
13794            // all three are in this body.
13795            //
13796            //   * The parent HOLDS ROWS, so it is a term of the union —
13797            //     `FROM ONLY`, or expanding it would recurse.
13798            //   * There is no partition key, so there is nothing to
13799            //     prune: every child is a term.
13800            //   * A child may declare columns of its own, so the terms
13801            //     name the PARENT's columns rather than `*`. PG's
13802            //     `SELECT * FROM parent` returns the parent's shape.
13803            //
13804            // Answered from this match rather than a branch before it —
13805            // round 644 measured what an extra early return beside an
13806            // existing test costs in this file.
13807            _ if crate::partition::has_inheritance_children(cat, parent_name) => {
13808                let cols = parent
13809                    .schema()
13810                    .columns
13811                    .iter()
13812                    .map(|c| quote_ident_for_sql(&c.name))
13813                    .collect::<Vec<_>>()
13814                    .join(", ");
13815                let carry_sys = references_ctid(outer);
13816                let sys = if carry_sys {
13817                    let mut t = alloc::string::String::new();
13818                    for s in SYSTEM_COLUMNS {
13819                        t.push_str(", ");
13820                        t.push_str(s);
13821                    }
13822                    t
13823                } else {
13824                    alloc::string::String::new()
13825                };
13826                let mut body = alloc::format!(
13827                    "SELECT {cols}{sys} FROM ONLY {}",
13828                    quote_ident_for_sql(parent_name)
13829                );
13830                for child in crate::partition::children_of_parent(cat, parent_name) {
13831                    body.push_str(&alloc::format!(
13832                        " UNION ALL SELECT {cols}{sys} FROM {}",
13833                        quote_ident_for_sql(&child)
13834                    ));
13835                }
13836                return parse_select_or_corrupt(&body).map(Some);
13837            }
13838            _ => {
13839                return Err(EngineError::Unsupported(alloc::format!(
13840                    "partition expansion: {parent_name:?} is not a parent"
13841                )));
13842            }
13843        };
13844        let key_col_name = parent.schema().columns[key_position].name.clone();
13845        // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
13846        // off the WHERE; for LIST / HASH we extract a single `=`
13847        // literal (and the rest of the planner falls back to "keep
13848        // every child" — same conservative path as 16.1/16.2).
13849        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13850            Some(expr) => extract_key_range(expr, &key_col_name),
13851            None => (None, None),
13852        };
13853        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13854            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13855            None => None,
13856        };
13857        let children = crate::partition::children_of_parent(cat, parent_name);
13858        let mut kept: Vec<String> = Vec::new();
13859        let mut default_child: Option<String> = None;
13860        // First pass — apply per-strategy gates, defer DEFAULT until
13861        // we know whether some non-DEFAULT child matched.
13862        for child_name in &children {
13863            let Some(child) = cat.get(child_name) else {
13864                continue;
13865            };
13866            match &child.schema().partition_role {
13867                Some(PartitionRole::Range { lower, upper, .. }) => {
13868                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13869                        kept.push(child_name.clone());
13870                    }
13871                }
13872                // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
13873                // = <lit>`, only the child whose values contain that
13874                // literal survives. Otherwise (no equality predicate
13875                // or planner couldn't extract one) keep the child
13876                // conservatively.
13877                Some(PartitionRole::List { values, .. }) => match &eq_value {
13878                    Some(v) => {
13879                        if values.iter().any(|b| b.equals_value(v)) {
13880                            kept.push(child_name.clone());
13881                        }
13882                    }
13883                    None => kept.push(child_name.clone()),
13884                },
13885                // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
13886                // we know the residue class deterministically, so
13887                // only the matching REMAINDER child survives.
13888                Some(PartitionRole::Hash {
13889                    modulus, remainder, ..
13890                }) => match &eq_value {
13891                    Some(v) => {
13892                        let h = crate::partition::pg_compatible_hash(v);
13893                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13894                            kept.push(child_name.clone());
13895                        }
13896                    }
13897                    None => kept.push(child_name.clone()),
13898                },
13899                Some(PartitionRole::Default { .. }) => {
13900                    default_child = Some(child_name.clone());
13901                }
13902                _ => {}
13903            }
13904        }
13905        // PG-style DEFAULT semantics: the DEFAULT child must be
13906        // scanned iff some row could fall outside every concrete
13907        // child's bound predicate. We approximate that as "no
13908        // concrete child matched" (== full prune) — strictly
13909        // conservative for LIST / HASH (DEFAULT also catches rows
13910        // outside the union of value-sets / residues), and matches
13911        // PG for the equality case where we *do* know the routing
13912        // outcome.
13913        let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
13914        if let Some(d) = default_child {
13915            if kept.is_empty() {
13916                kept.push(d);
13917            } else if eq_value.is_none() {
13918                // Without an equality literal, the DEFAULT child may
13919                // still hold matching rows (e.g. LIKE on TEXT keys
13920                // for which a LIST partition exists). Keep it.
13921                kept.push(d);
13922            }
13923        }
13924        // Build the UNION ALL body text and re-parse — keeps the
13925        // rewrite expressible in surface SQL so the engine's existing
13926        // parser path handles the AST shape uniformly.
13927        if kept.is_empty() {
13928            // No children survive — caller falls back to scanning the
13929            // (empty) parent table. Returning None here is what
13930            // prevents the synthetic CTE from referring back to the
13931            // parent name and re-entering this rewrite pass.
13932            let _ = parent_name;
13933            return Ok(None);
13934        }
13935        // v7.39 (round 622, S05a) — the system columns of the CHILD the row
13936        // actually lives in.
13937        //
13938        // The parent is read through a synthetic CTE, so a `tableoid` on it
13939        // resolved against that CTE: every row of every child reported
13940        // `__spg_partition_pm`, an internal name no user ever typed, where
13941        // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
13942        // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
13943        // one asks "which partition is this row in", answering 0 rows where
13944        // PG answers 1. `ctid` had the same shape: it numbered the CTE's
13945        // output, so rows in different children got distinct ctids instead
13946        // of each child's own physical position.
13947        //
13948        // Naming them in the term is what carries them: the child scan
13949        // materialises its own six because the statement now references
13950        // them, and they land in SYSTEM_COLUMNS order right after the user
13951        // columns — the exact layout the positional `*` skip already
13952        // expects. Only done when the outer statement asks for one, so a
13953        // plain `SELECT * FROM parent` scans exactly what it scanned.
13954        let carry_sys = references_ctid(outer);
13955        let mut body = alloc::string::String::new();
13956        for (i, child_name) in kept.iter().enumerate() {
13957            if i > 0 {
13958                body.push_str(" UNION ALL ");
13959            }
13960            body.push_str("SELECT *");
13961            if carry_sys {
13962                for sys in SYSTEM_COLUMNS {
13963                    body.push_str(", ");
13964                    body.push_str(sys);
13965                }
13966            }
13967            body.push_str(" FROM ");
13968            body.push_str(&quote_ident_for_sql(child_name));
13969        }
13970        parse_select_or_corrupt(&body).map(Some)
13971    }
13972}
13973
13974/// Rewrite a `TableRef` pointing at a partition parent so it
13975/// references the synthetic CTE created by the expansion. If the
13976/// original ref had no alias, preserve the parent name as an alias
13977/// so column references like `events_partitioned.received_at`
13978/// keep resolving.
13979fn rewrite_partition_parent_table_ref(
13980    t: &mut spg_sql::ast::TableRef,
13981    parents: &[alloc::string::String],
13982    synth_name: &impl Fn(&str) -> alloc::string::String,
13983) {
13984    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13985        return;
13986    }
13987    // v7.39 (round 644) — an ONLY reference stays pointed at the parent
13988    // itself. The rewrite is keyed on the NAME, so in
13989    // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
13990    // parent list and this then rewrote BOTH — including the one that
13991    // asked not to descend. PG answers 0 for that join; SPG answered 2.
13992    // Folded into the existing test — see the note in
13993    // `collect_partition_parent_refs` for what a separate one cost.
13994    if t.only || !parents.iter().any(|p| p == &t.name) {
13995        return;
13996    }
13997    if t.alias.is_none() {
13998        t.alias = Some(t.name.clone());
13999    }
14000    t.name = synth_name(&t.name);
14001}
14002
14003/// Walk a `TableRef` and push its `name` if it resolves to a partition
14004/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
14005/// `generate_series_args` references — those aren't catalog tables.
14006fn collect_partition_parent_refs(
14007    t: &spg_sql::ast::TableRef,
14008    cat: &spg_storage::Catalog,
14009    out: &mut Vec<alloc::string::String>,
14010) {
14011    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
14012        return;
14013    }
14014    // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
14015    // The keyword used to be absorbed at parse time, so this fanned out
14016    // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
14017    // answered 2 where PG answers 0.
14018    //
14019    // Folded into the existing test rather than given an early return of
14020    // its own: as two extra lines in this function's body it cost
14021    // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
14022    // outside the panel. Rounds 641 and 643 met the same wall from the
14023    // other two directions — adding to a hot function and taking away
14024    // from a cold one. What goes in a body near the row loop is a
14025    // codegen decision whatever its shape.
14026    if !t.only && crate::partition::has_children(cat, &t.name) {
14027        out.push(t.name.clone());
14028    }
14029}
14030
14031/// v7.37.6-B partition-key range derived from a WHERE expression.
14032/// `i64` microseconds since epoch with the same sign convention as
14033/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
14034/// / `=`),`false` ⇒ exclusive(`>` / `<`).
14035#[derive(Debug, Clone, Copy)]
14036pub(crate) struct PartitionFilterBound {
14037    pub micros: i64,
14038    pub inclusive: bool,
14039}
14040
14041/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
14042/// shapes; tighten the running lo / hi as we go. Anything outside that
14043/// (OR / nested calls / non-key columns)is ignored — caller treats
14044/// `None` as "no constraint on that side."
14045fn extract_key_range(
14046    expr: &spg_sql::ast::Expr,
14047    key_col: &str,
14048) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
14049    let mut lo: Option<PartitionFilterBound> = None;
14050    let mut hi: Option<PartitionFilterBound> = None;
14051    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14052    while let Some(e) = stack.pop() {
14053        match e {
14054            spg_sql::ast::Expr::Binary {
14055                lhs,
14056                op: spg_sql::ast::BinOp::And,
14057                rhs,
14058            } => {
14059                stack.push(lhs);
14060                stack.push(rhs);
14061            }
14062            // BETWEEN is desugared at parse time into `lhs >= low AND
14063            // lhs <= high`, so it lands here as two regular Binary
14064            // arms via the AND walker above.
14065            spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
14066                let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
14067                    (Some(lhs.as_ref()), rhs.as_ref(), false)
14068                } else if is_column_ref(rhs, key_col) {
14069                    (Some(rhs.as_ref()), lhs.as_ref(), true)
14070                } else {
14071                    (None, lhs.as_ref(), false)
14072                };
14073                if col_ref.is_none() {
14074                    continue;
14075                }
14076                let Some(lit) = literal_to_micros(lit_side) else {
14077                    continue;
14078                };
14079                use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
14080                let effective_op = if swapped {
14081                    match op {
14082                        Lt => Gt,
14083                        LtEq => GtEq,
14084                        Gt => Lt,
14085                        GtEq => LtEq,
14086                        other => *other,
14087                    }
14088                } else {
14089                    *op
14090                };
14091                match effective_op {
14092                    Eq => {
14093                        tighten_lo(
14094                            &mut lo,
14095                            PartitionFilterBound {
14096                                micros: lit,
14097                                inclusive: true,
14098                            },
14099                        );
14100                        tighten_hi(
14101                            &mut hi,
14102                            PartitionFilterBound {
14103                                micros: lit,
14104                                inclusive: true,
14105                            },
14106                        );
14107                    }
14108                    GtEq => {
14109                        tighten_lo(
14110                            &mut lo,
14111                            PartitionFilterBound {
14112                                micros: lit,
14113                                inclusive: true,
14114                            },
14115                        );
14116                    }
14117                    Gt => {
14118                        tighten_lo(
14119                            &mut lo,
14120                            PartitionFilterBound {
14121                                micros: lit,
14122                                inclusive: false,
14123                            },
14124                        );
14125                    }
14126                    LtEq => {
14127                        tighten_hi(
14128                            &mut hi,
14129                            PartitionFilterBound {
14130                                micros: lit,
14131                                inclusive: true,
14132                            },
14133                        );
14134                    }
14135                    Lt => {
14136                        tighten_hi(
14137                            &mut hi,
14138                            PartitionFilterBound {
14139                                micros: lit,
14140                                inclusive: false,
14141                            },
14142                        );
14143                    }
14144                    _ => {}
14145                }
14146            }
14147            _ => {}
14148        }
14149    }
14150    (lo, hi)
14151}
14152
14153fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14154    match slot {
14155        None => *slot = Some(new),
14156        Some(cur) => {
14157            if new.micros > cur.micros
14158                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14159            {
14160                *slot = Some(new);
14161            }
14162        }
14163    }
14164}
14165
14166fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
14167    match slot {
14168        None => *slot = Some(new),
14169        Some(cur) => {
14170            if new.micros < cur.micros
14171                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
14172            {
14173                *slot = Some(new);
14174            }
14175        }
14176    }
14177}
14178
14179fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
14180    if let spg_sql::ast::Expr::Column(c) = e {
14181        c.name.eq_ignore_ascii_case(key_col)
14182    } else {
14183        false
14184    }
14185}
14186
14187/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
14188/// `key_col = <literal>` predicate out for LIST/HASH partition
14189/// pruning. Returns `None` when no equality literal can be lifted
14190/// (planner then keeps every child — correctness preserved). The
14191/// returned `Value<'static>` is an owned coercion so the caller can
14192/// outlive any AST node it was extracted from.
14193pub(crate) fn extract_key_eq_value(
14194    expr: &spg_sql::ast::Expr,
14195    key_col: &str,
14196) -> Option<spg_storage::Value<'static>> {
14197    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
14198    while let Some(e) = stack.pop() {
14199        match e {
14200            spg_sql::ast::Expr::Binary {
14201                lhs,
14202                op: spg_sql::ast::BinOp::And,
14203                rhs,
14204            } => {
14205                stack.push(lhs);
14206                stack.push(rhs);
14207            }
14208            spg_sql::ast::Expr::Binary {
14209                lhs,
14210                op: spg_sql::ast::BinOp::Eq,
14211                rhs,
14212            } => {
14213                let lit_side = if is_column_ref(lhs, key_col) {
14214                    rhs.as_ref()
14215                } else if is_column_ref(rhs, key_col) {
14216                    lhs.as_ref()
14217                } else {
14218                    continue;
14219                };
14220                let cloned = lit_side.clone();
14221                let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
14222                    continue;
14223                };
14224                // Coerce to an owned Value<'static> so the caller
14225                // can hold it past the WHERE expression's lifetime.
14226                let owned: spg_storage::Value<'static> = match v {
14227                    spg_storage::Value::Text(s) => {
14228                        spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
14229                    }
14230                    spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
14231                    spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
14232                    spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
14233                    spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
14234                    spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
14235                    spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
14236                    spg_storage::Value::Null => spg_storage::Value::Null,
14237                    // Anything else (Vector / Json / Bytes / Numeric /
14238                    // arrays / interval / …) isn't a current partition
14239                    // key type; skip without pruning.
14240                    _ => continue,
14241                };
14242                return Some(owned);
14243            }
14244            _ => {}
14245        }
14246    }
14247    None
14248}
14249
14250/// Coerce a literal Expr(after the parser folded sequence calls etc.)
14251/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
14252/// pruning and routing agree on the literal vocabulary. Returns
14253/// `None` when the literal isn't recognised(planner then skips
14254/// pruning on that branch — correctness preserved).
14255fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
14256    let cloned = e.clone();
14257    let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
14258    match value {
14259        spg_storage::Value::Timestamp(m) => Some(m),
14260        spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
14261        spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
14262        _ => None,
14263    }
14264}
14265
14266/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
14267/// satisfying the WHERE-derived filter range. PG-style half-open:
14268/// child upper exclusive. Filter inclusivity is honoured per-bound.
14269fn range_satisfies_filter(
14270    range_lo: &spg_storage::PartitionBound,
14271    range_hi: &spg_storage::PartitionBound,
14272    filter_lo: Option<&PartitionFilterBound>,
14273    filter_hi: Option<&PartitionFilterBound>,
14274) -> bool {
14275    use spg_storage::PartitionBound;
14276    // For each filter side, reject children that can't host any row
14277    // matching the predicate.
14278    if let Some(lo) = filter_lo {
14279        // child upper bound vs filter lower:
14280        //   if filter is x >= L, child rejects iff child.hi <= L
14281        //   if filter is x  > L, child rejects iff child.hi <= L
14282        //   (child.hi exclusive, so equality with L still rejects)
14283        match range_hi {
14284            PartitionBound::MinValue => return false,
14285            PartitionBound::MaxValue => {}
14286            PartitionBound::TimestampTz(hi) => {
14287                if *hi <= lo.micros {
14288                    return false;
14289                }
14290            }
14291            // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
14292            // matched against TIMESTAMPTZ filters here; keep child
14293            // (conservative: don't prune).
14294            PartitionBound::BigInt(_)
14295            | PartitionBound::Int(_)
14296            | PartitionBound::SmallInt(_)
14297            | PartitionBound::Date(_)
14298            | PartitionBound::Text(_) => {}
14299        }
14300    }
14301    if let Some(hi) = filter_hi {
14302        // child lower bound vs filter upper:
14303        //   if filter is x <= U, child rejects iff child.lo > U
14304        //   if filter is x  < U, child rejects iff child.lo >= U
14305        match range_lo {
14306            PartitionBound::MaxValue => return false,
14307            PartitionBound::MinValue => {}
14308            PartitionBound::TimestampTz(lo) => {
14309                let rejects = if hi.inclusive {
14310                    *lo > hi.micros
14311                } else {
14312                    *lo >= hi.micros
14313                };
14314                if rejects {
14315                    return false;
14316                }
14317            }
14318            PartitionBound::BigInt(_)
14319            | PartitionBound::Int(_)
14320            | PartitionBound::SmallInt(_)
14321            | PartitionBound::Date(_)
14322            | PartitionBound::Text(_) => {}
14323        }
14324    }
14325    true
14326}
14327
14328fn quote_ident_for_sql(name: &str) -> alloc::string::String {
14329    // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
14330    // identifier, otherwise quoted). Conservative: always quote so
14331    // children with reserved names round-trip safely through the
14332    // CTE-body parse.
14333    let mut out = alloc::string::String::with_capacity(name.len() + 2);
14334    out.push('"');
14335    for c in name.chars() {
14336        if c == '"' {
14337            out.push('"');
14338        }
14339        out.push(c);
14340    }
14341    out.push('"');
14342    out
14343}
14344
14345fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
14346    let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
14347        EngineError::Unsupported(alloc::format!(
14348            "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
14349        ))
14350    })?;
14351    let Statement::Select(body) = parsed else {
14352        return Err(EngineError::Unsupported(alloc::format!(
14353            "partition expansion: generated SQL {sql:?} is not a SELECT"
14354        )));
14355    };
14356    Ok(body)
14357}
14358
14359/// v7.39 (read01 round 65/66) — the column shape a set-returning function
14360/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
14361/// yields ONE column named after the call's alias when there is one (`FROM
14362/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
14363/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
14364fn setof_column_shape_from(
14365    declared: &str,
14366    name: &str,
14367    alias: Option<&str>,
14368    got: &[ColumnSchema],
14369) -> alloc::vec::Vec<ColumnSchema> {
14370    let upper = declared.to_ascii_uppercase();
14371    if upper.starts_with("TABLE(") {
14372        let raw = &declared["TABLE(".len()..declared.len() - 1];
14373        return raw
14374            .split(',')
14375            .zip(got.iter())
14376            .map(|(decl, g)| {
14377                let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
14378                ColumnSchema::new(cname.to_string(), g.ty, true)
14379            })
14380            .collect();
14381    }
14382    let cname = alias.unwrap_or(name);
14383    got.first()
14384        .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
14385        .unwrap_or_default()
14386}
14387
14388/// The plpgsql twin: the interpreter hands back raw value rows, so the types
14389/// come off the first row.
14390fn setof_column_shape(
14391    declared: &str,
14392    name: &str,
14393    alias: Option<&str>,
14394    first_row: Option<&alloc::vec::Vec<Value<'static>>>,
14395) -> alloc::vec::Vec<ColumnSchema> {
14396    let got: alloc::vec::Vec<ColumnSchema> = first_row
14397        .map(|r| {
14398            r.iter()
14399                .enumerate()
14400                .map(|(i, v)| {
14401                    ColumnSchema::new(
14402                        alloc::format!("col{i}"),
14403                        v.data_type().unwrap_or(DataType::Text),
14404                        true,
14405                    )
14406                })
14407                .collect()
14408        })
14409        .unwrap_or_default();
14410    setof_column_shape_from(declared, name, alias, &got)
14411}
14412
14413/// v7.39 (read01 round 67) — expand every set-returning call in a target list
14414/// for ONE input row, PG's ProjectSet semantics.
14415///
14416/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
14417/// output has as many rows as the LONGEST of them, and a shorter one is padded
14418/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
14419/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
14420/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
14421/// is zero rows, not one NULL row.
14422///
14423/// Non-SRF items repeat, evaluated once per output row from the same input row.
14424/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
14425/// used to reach the scalar function dispatcher, which reported the aggregate as
14426/// an *unknown function* — the same "symptom two layers above the cause" shape
14427/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
14428/// sees a call, not the clause it came from. The statement knows.
14429/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
14430/// clause may appear.
14431///
14432/// PG rejects `FOR UPDATE` on exactly the shapes that have no
14433/// identifiable base row to lock, each with its own wording. SPG
14434/// accepted all of them and locked nothing, so a query that PG refuses
14435/// outright came back looking like it had taken locks.
14436///
14437/// Every wording read off live PG 18.4.
14438impl crate::Engine {
14439    /// v7.39.2 — a column name in WHERE / ORDER BY / GROUP BY / HAVING
14440    /// that names nothing is refused before the scan, not when a row
14441    /// reaches it.
14442    ///
14443    /// The projection resolves its names eagerly; a predicate only meets
14444    /// them per row. So on an EMPTY table `SELECT a FROM t WHERE nosuch
14445    /// = 1` answered zero rows and no error, and the same statement over
14446    /// a table with one row raised. Measured on PostgreSQL 18.6 and
14447    /// MySQL 9.7.2: both refuse it whatever the row count. A typo in a
14448    /// predicate therefore passed a test written against an empty
14449    /// fixture and failed in production — or, worse, ran nightly over an
14450    /// empty window and reported nothing.
14451    ///
14452    /// Deliberately narrow: ONE plain base table, nothing else. A join,
14453    /// a CTE, a set operation, a lateral or function source, or a
14454    /// subquery in the clause all bring a second scope into which a name
14455    /// may legitimately resolve, and refusing one of those would be a
14456    /// worse defect than the one this closes. Those shapes keep the
14457    /// old behaviour; the walk below does not descend into a subquery
14458    /// for the same reason.
14459    /// v7.39.2 — refuse a call whose argument count no overload accepts,
14460    /// BEFORE the scan rather than per row.
14461    ///
14462    /// `SELECT lower(t, n) FROM t` answered zero rows and no error over
14463    /// an EMPTY table and raised the moment the table had one row in it,
14464    /// because the arity check lives inside the row-time dispatch. It is
14465    /// the same shape as the unknown-column-in-a-predicate defect closed
14466    /// earlier in this release, and it hides in the same place: a query
14467    /// written against an empty fixture passes its test.
14468    ///
14469    /// The accepted counts come from `eval::arity::REFUSED_ARITIES`,
14470    /// which is derived by asking the dispatch itself offline and can
14471    /// only ever UNDER-refuse — see that file for why the two other
14472    /// candidate oracles were refuted.
14473    /// v7.39.3 — MySQL's column names are case-insensitive; PostgreSQL's
14474    /// quoted ones are not. See `EvalContext::col_eq`.
14475    fn col_name_eq(&self, a: &str, b: &str) -> bool {
14476        if self.speaks_mysql {
14477            a.eq_ignore_ascii_case(b)
14478        } else {
14479            a == b
14480        }
14481    }
14482
14483    pub(crate) fn validate_function_arity(
14484        &self,
14485        stmt: &SelectStatement,
14486    ) -> Result<(), EngineError> {
14487        let mut calls: Vec<(alloc::string::String, Vec<Expr>)> = Vec::new();
14488        for it in &stmt.items {
14489            if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14490                collect_function_calls(expr, &mut calls);
14491            }
14492        }
14493        if let Some(w) = &stmt.where_ {
14494            collect_function_calls(w, &mut calls);
14495        }
14496        for o in &stmt.order_by {
14497            collect_function_calls(&o.expr, &mut calls);
14498        }
14499        // The columns a name in this statement could resolve to. Only
14500        // plain base tables; anything else and the types are not
14501        // statically knowable, so nothing is refused early.
14502        let cat = self.active_catalog();
14503        let mut cols: Vec<ColumnSchema> = Vec::new();
14504        if let Some(from) = &stmt.from {
14505            for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14506                if let Some(table) = cat.get(&t.name) {
14507                    cols.extend(table.schema().columns.iter().cloned());
14508                }
14509            }
14510        }
14511        for (name, args) in calls {
14512            let Ok(i) = crate::eval::arity::REFUSED_ARITIES
14513                .binary_search_by(|(n, _)| (*n).cmp(name.as_str()))
14514            else {
14515                continue;
14516            };
14517            if !crate::eval::arity::REFUSED_ARITIES[i]
14518                .1
14519                .contains(&args.len())
14520            {
14521                continue;
14522            }
14523            // v7.39.2 — PostgreSQL names the SIGNATURE it could not
14524            // match, and before the scan there are no values to read a
14525            // type from. Where every argument's type is knowable
14526            // statically — a column of a source table, or a literal —
14527            // the sentence is PostgreSQL's exactly; where one is not,
14528            // this leaves the call to the row-time raise, which has the
14529            // values. Refusing early with a WORSE message would trade
14530            // one defect for another.
14531            let mut types: Vec<alloc::string::String> = Vec::new();
14532            for a in &args {
14533                let Some(t) = static_arg_type(a, &cols) else {
14534                    types.clear();
14535                    break;
14536                };
14537                types.push(t);
14538            }
14539            if types.len() != args.len() {
14540                continue;
14541            }
14542            return Err(EngineError::Eval(EvalError::WrongArity {
14543                name,
14544                types: types.join(", "),
14545            }));
14546        }
14547        Ok(())
14548    }
14549
14550    pub(crate) fn validate_clause_columns(
14551        &self,
14552        stmt: &SelectStatement,
14553    ) -> Result<(), EngineError> {
14554        let Some(from) = &stmt.from else {
14555            return Ok(());
14556        };
14557        if !stmt.ctes.is_empty() {
14558            return Ok(());
14559        }
14560        // v7.39.2 — every source, not just the first. A join is checkable
14561        // for the same reason one table is: with no CTE and no
14562        // subquery-shaped source, a bare name has to come from one of
14563        // them. Refusing the check for joins left `SELECT … FROM a JOIN b
14564        // … WHERE nosuch = 1` labelled `'field list'` where MySQL 9.7.2
14565        // says `'where clause'`.
14566        let plain = |t: &spg_sql::ast::TableRef| -> bool {
14567            t.unnest_expr.is_none()
14568                && t.generate_series_args.is_none()
14569                && t.lateral_subquery.is_none()
14570                && t.jsonb_each_text_arg.is_none()
14571                && t.table_fn_call.is_none()
14572                && t.rows_from.is_none()
14573                && t.json_table.is_none()
14574                && !t.scalar_fn_item
14575        };
14576        let cat = self.active_catalog();
14577        let mut sources: Vec<(String, &spg_storage::Table)> = Vec::new();
14578        for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
14579            if !plain(t) {
14580                return Ok(());
14581            }
14582            let Some(table) = cat.get(&t.name) else {
14583                return Ok(());
14584            };
14585            sources.push((t.alias.clone().unwrap_or_else(|| t.name.clone()), table));
14586        }
14587        let known = |c: &spg_sql::ast::ColumnName| -> bool {
14588            // A system column is not in a table's list and is a perfectly
14589            // good predicate: `WHERE ctid = '(0,4)'::tid` and `WHERE
14590            // tableoid::regclass::text = 'pm_a'` are both real, and the
14591            // first draft of this check refused them. The e2e suite said
14592            // so immediately, which is what it is for.
14593            if is_system_column(&c.name) {
14594                return true;
14595            }
14596            if let Some(q) = &c.qualifier {
14597                // A qualifier must name one of this statement's sources,
14598                // and that source must carry the column. An alias
14599                // REPLACES the written name, which is PostgreSQL's rule
14600                // and MySQL's: `FROM pg_cast c WHERE pg_cast.oid <> 0`
14601                // is an error on both.
14602                return match sources.iter().find(|(a, _)| a == q) {
14603                    Some((_, t)) => t
14604                        .schema()
14605                        .columns
14606                        .iter()
14607                        .any(|sc| self.col_name_eq(&sc.name, &c.name)),
14608                    None => false,
14609                };
14610            }
14611            sources
14612                .iter()
14613                .any(|(_, t)| {
14614                    t.schema()
14615                        .columns
14616                        .iter()
14617                        .any(|sc| self.col_name_eq(&sc.name, &c.name))
14618                })
14619                // An output name the statement itself defines: ORDER BY,
14620                // GROUP BY and HAVING may all name one.
14621                || stmt.items.iter().any(|it| match it {
14622                    SelectItem::Expr { expr, alias } => {
14623                        alias.as_deref() == Some(c.name.as_str())
14624                            || matches!(expr, Expr::Column(pc) if pc.name == c.name)
14625                    }
14626                    _ => false,
14627                })
14628        };
14629        // v7.39.2 — the CLAUSE travels with the reference, because MySQL
14630        // names it: `Unknown column 'x' in 'where clause'`, `'order
14631        // clause'`, `'group statement'`, `'having clause'`. Measured on
14632        // 9.7.2, and a driver's error handling reads the sentence as well
14633        // as the number. PostgreSQL says only `column "x" does not
14634        // exist`, with no clause, so its wording is unchanged.
14635        //
14636        // This walk is the only place the clause is still known: by the
14637        // time a row-time resolver meets the name, the expression has
14638        // been detached from the statement that held it.
14639        let mut refs: Vec<(spg_sql::ast::ColumnName, &'static str)> = Vec::new();
14640        let mut push = |e: &Expr, ctx: &'static str, out: &mut Vec<_>| {
14641            let mut here: Vec<spg_sql::ast::ColumnName> = Vec::new();
14642            collect_plain_column_refs(e, &mut here);
14643            out.extend(here.into_iter().map(|c| (c, ctx)));
14644        };
14645        if let Some(w) = &stmt.where_ {
14646            push(w, "where clause", &mut refs);
14647        }
14648        if let Some(g) = &stmt.group_by {
14649            for e in g {
14650                push(e, "group statement", &mut refs);
14651            }
14652        }
14653        if let Some(h) = &stmt.having {
14654            push(h, "having clause", &mut refs);
14655        }
14656        for o in &stmt.order_by {
14657            push(&o.expr, "order clause", &mut refs);
14658        }
14659        // v7.39.2 — and the join predicates, which MySQL calls the `on
14660        // clause`. Measured on 9.7.2: `Unknown column 'j1.nosuch' in 'on
14661        // clause'`, qualifier and all.
14662        for j in &from.joins {
14663            if let Some(on) = &j.on {
14664                push(on, "on clause", &mut refs);
14665            }
14666        }
14667        for (c, ctx) in &refs {
14668            if !known(c) {
14669                if self.speaks_mysql {
14670                    // The QUALIFIER travels with it: MySQL 9.7.2 answers
14671                    // `Unknown column 'j1.nosuch' in 'on clause'`, not the
14672                    // bare name. Measured.
14673                    let shown = match &c.qualifier {
14674                        Some(q) => alloc::format!("{q}.{}", c.name),
14675                        None => c.name.clone(),
14676                    };
14677                    return Err(EngineError::Eval(EvalError::TypeMismatch {
14678                        detail: alloc::format!("Unknown column '{shown}' in '{ctx}'"),
14679                    }));
14680                }
14681                // PostgreSQL 18.6 names the missing TABLE when the
14682                // qualifier is the part that resolves to nothing
14683                // (`missing FROM-clause entry for table "pg_cast"`) and
14684                // the COLUMN otherwise. Raising the column error for both
14685                // dropped the table name a caller matches on.
14686                if let Some(q) = &c.qualifier
14687                    && !sources.iter().any(|(a, _)| a == q)
14688                {
14689                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
14690                        qualifier: q.clone(),
14691                        column: c.name.clone(),
14692                    }));
14693                }
14694                // v7.39.2 — and a qualified reference whose qualifier
14695                // DOES resolve prints the whole thing, unquoted:
14696                // `column ea.no_such does not exist` (measured on PG
14697                // 18.6). The bare `column "no_such" does not exist` drops
14698                // the alias a caller matches on, which is what the
14699                // sqlx round-20 pin says.
14700                if let Some(q) = &c.qualifier {
14701                    return Err(EngineError::Eval(EvalError::QualifiedColumnNotFound {
14702                        qualifier: q.clone(),
14703                        column: c.name.clone(),
14704                    }));
14705                }
14706                return Err(EngineError::Eval(EvalError::ColumnNotFound {
14707                    name: c.name.clone(),
14708                }));
14709            }
14710        }
14711        Ok(())
14712    }
14713}
14714
14715/// v7.39.2 — the column references of an expression, NOT descending into
14716/// a subquery.
14717///
14718/// A correlated subquery resolves its names against an outer scope this
14719/// walk cannot see, so descending would refuse valid queries. Missing a
14720/// typo inside one is the safe direction; refusing a good query is not.
14721/// v7.39.2 — the type PostgreSQL would name for an argument, when it
14722/// can be known without a row: a column of a source table, or a
14723/// literal. `None` for anything else, which is what keeps the pre-scan
14724/// refusal from printing a worse sentence than the row-time one.
14725pub(crate) fn static_arg_type(e: &Expr, cols: &[ColumnSchema]) -> Option<alloc::string::String> {
14726    use spg_sql::ast::Literal as L;
14727    match e {
14728        Expr::Column(c) => cols
14729            .iter()
14730            .find(|s| s.name.eq_ignore_ascii_case(&c.name))
14731            .map(|s| crate::conversions::pg_type_name_for_error(s.ty)),
14732        // A bare literal has no type yet on PostgreSQL — it names it
14733        // `unknown` in this very sentence — except where the lexeme
14734        // fixes one.
14735        Expr::Literal(L::String(_)) | Expr::Literal(L::Null) => {
14736            Some(alloc::string::String::from("unknown"))
14737        }
14738        Expr::Literal(L::Integer(_)) => Some(alloc::string::String::from("integer")),
14739        Expr::Literal(L::Bool(_)) => Some(alloc::string::String::from("boolean")),
14740        _ => None,
14741    }
14742}
14743
14744/// v7.39.2 — the function calls of an expression, name and argument
14745/// count, NOT descending into a subquery (its scope is its own).
14746fn collect_function_calls(e: &Expr, out: &mut Vec<(alloc::string::String, Vec<Expr>)>) {
14747    match e {
14748        Expr::FunctionCall { name, args } => {
14749            out.push((name.to_ascii_lowercase(), args.clone()));
14750            for a in args {
14751                collect_function_calls(a, out);
14752            }
14753        }
14754        Expr::Binary { lhs, rhs, .. } => {
14755            collect_function_calls(lhs, out);
14756            collect_function_calls(rhs, out);
14757        }
14758        Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14759            collect_function_calls(expr, out);
14760        }
14761        _ => {}
14762    }
14763}
14764
14765fn collect_plain_column_refs(e: &Expr, out: &mut Vec<spg_sql::ast::ColumnName>) {
14766    match e {
14767        Expr::Column(c) => out.push(c.clone()),
14768        Expr::Binary { lhs, rhs, .. } => {
14769            collect_plain_column_refs(lhs, out);
14770            collect_plain_column_refs(rhs, out);
14771        }
14772        Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => {
14773            collect_plain_column_refs(expr, out);
14774        }
14775        Expr::FunctionCall { args, .. } => {
14776            for a in args {
14777                collect_plain_column_refs(a, out);
14778            }
14779        }
14780        _ => {}
14781    }
14782}
14783
14784fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
14785    let Some(lock) = &stmt.locking else {
14786        return Ok(());
14787    };
14788    let verb = lock_clause_verb(lock.strength);
14789    let refuse = |what: &str| {
14790        Err(EngineError::Unsupported(alloc::format!(
14791            "{verb} is not allowed with {what}"
14792        )))
14793    };
14794    if !stmt.unions.is_empty() {
14795        return refuse("UNION/INTERSECT/EXCEPT");
14796    }
14797    if stmt.distinct || !stmt.distinct_on.is_empty() {
14798        return refuse("DISTINCT clause");
14799    }
14800    if stmt.group_by.is_some() || stmt.group_by_all {
14801        return refuse("GROUP BY clause");
14802    }
14803    let has_agg = stmt.items.iter().any(|it| match it {
14804        spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
14805        _ => false,
14806    });
14807    if has_agg {
14808        return refuse("aggregate functions");
14809    }
14810    // `FOR UPDATE OF t` must name a relation that is actually in FROM.
14811    for want in &lock.of_tables {
14812        if !locking_from_names(stmt)
14813            .iter()
14814            .any(|n| n.eq_ignore_ascii_case(want))
14815        {
14816            return Err(EngineError::Unsupported(alloc::format!(
14817                "relation \"{want}\" in {verb} clause not found in FROM clause"
14818            )));
14819        }
14820    }
14821    Ok(())
14822}
14823
14824/// How PG names the clause in its diagnostics.
14825const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
14826    use spg_sql::ast::LockStrength as LS;
14827    match s {
14828        LS::Update => "FOR UPDATE",
14829        LS::NoKeyUpdate => "FOR NO KEY UPDATE",
14830        LS::Share => "FOR SHARE",
14831        LS::KeyShare => "FOR KEY SHARE",
14832    }
14833}
14834
14835/// Every relation name (or alias) the FROM clause exposes.
14836fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
14837    let mut out = alloc::vec::Vec::new();
14838    if let Some(f) = &stmt.from {
14839        let mut push = |t: &spg_sql::ast::TableRef| {
14840            if let Some(a) = &t.alias {
14841                out.push(a.clone());
14842            }
14843            out.push(t.name.clone());
14844        };
14845        push(&f.primary);
14846        for j in &f.joins {
14847            push(&j.table);
14848        }
14849    }
14850    out
14851}
14852
14853fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
14854    use spg_sql::ast::Expr;
14855    if let Some(w) = &stmt.where_
14856        && aggregate::contains_aggregate(w)
14857    {
14858        return Err(EngineError::Unsupported(
14859            "aggregate functions are not allowed in WHERE".into(),
14860        ));
14861    }
14862    let mut nested = false;
14863    let mut check = |e: &Expr| {
14864        let mut probe = e.clone();
14865        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14866            let args = match n {
14867                Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
14868                _ => return false,
14869            };
14870            if args.iter().any(aggregate::contains_aggregate) {
14871                nested = true;
14872            }
14873            false
14874        });
14875    };
14876    for it in &stmt.items {
14877        if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
14878            check(expr);
14879        }
14880    }
14881    if let Some(h) = &stmt.having {
14882        check(h);
14883    }
14884    for o in &stmt.order_by {
14885        check(&o.expr);
14886    }
14887    if nested {
14888        return Err(EngineError::Unsupported(
14889            "aggregate function calls cannot be nested".into(),
14890        ));
14891    }
14892    Ok(())
14893}
14894
14895/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
14896/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
14897/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
14898/// to a set and then applies the enclosing expression once per element. SPG only
14899/// ever recognised an SRF that WAS the item, so everything above died on
14900/// "unknown function unnest" — the set-returning call, wrapped in anything at
14901/// all, fell through to the scalar function dispatcher which has no such name.
14902///
14903/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
14904/// rewritten to read that column, and the rewritten expression is evaluated once
14905/// per output row against the input row extended with the lifted values. The
14906/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
14907/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
14908/// executors (the single-table scan, the synthetic-table pipeline, and the
14909/// unnest FROM path) each evaluated the key as an ordinary expression, where the
14910/// literal `n` is just the constant n — the same sort key for every row. The
14911/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
14912/// back in input order, not in a wrong order. Statement prep resolves the common
14913/// case, but only when the SELECT item is an expression — a `*` is not one, and
14914/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
14915/// spelling landed on exactly the shape prep could not resolve.
14916///
14917/// A set-returning item is left alone: copying it into ORDER BY would make the
14918/// key "the whole set", evaluated once per INPUT row.
14919fn resolve_positional_order_by(
14920    order_by: &[spg_sql::ast::OrderBy],
14921    projection: &[ProjectedItem],
14922) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
14923    order_by
14924        .iter()
14925        .filter_map(|o| {
14926            let mut o = o.clone();
14927            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
14928                && *n >= 1
14929                && let Ok(idx) = usize::try_from(*n - 1)
14930                && let Some(item) = projection.get(idx)
14931                && !expr_contains_builtin_srf(&item.expr)
14932            {
14933                // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
14934                // item is itself an integer LITERAL must not be
14935                // substituted textually: the literal would read as an
14936                // ordinal again downstream, and `SELECT 10 … ORDER BY
14937                // 1` died with "position 10 is not in select list"
14938                // where PG happily returns the rows. Ordering by a
14939                // constant orders nothing, so the key drops.
14940                if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
14941                    return None;
14942                }
14943                o.expr = item.expr.clone();
14944            }
14945            Some(o)
14946        })
14947        .collect()
14948}
14949
14950/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
14951/// this expression? Statement preparation (`resolve_order_by_position`) runs
14952/// before any catalog is in hand, and it only needs to know "is this item's value
14953/// a set", which the builtin SRFs answer syntactically.
14954pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
14955    let mut found = false;
14956    let mut probe = e.clone();
14957    crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14958        if is_top_level_unnest(n) {
14959            found = true;
14960            return true;
14961        }
14962        false
14963    });
14964    found
14965}
14966
14967/// v7.39 (round 599) — everything about a target-list SRF that does not
14968/// depend on the row.
14969///
14970/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
14971/// each SRF-bearing projection expression, walked and rewrote the tree,
14972/// formatted a `__srf_N` name per node, and copied the whole column schema.
14973/// A counting allocator put the path at 24 allocations per input row for a
14974/// single-element `unnest`, against 0 for the same scan without one — 211 MB
14975/// where the plain scan took 4.3 — and the shape held whatever the array
14976/// contained, which is what invariant work looks like.
14977struct SrfPlan {
14978    /// The lifted SRF calls, in slot order.
14979    nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
14980    /// Per projection position, the expression with its SRF calls replaced
14981    /// by `__srf_N` column references. `None` means the item has none.
14982    rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
14983    /// The input schema followed by one column per slot. Only the slots'
14984    /// TYPES vary per row, and they are patched in place.
14985    ext_cols: alloc::vec::Vec<ColumnSchema>,
14986    /// v7.39 (round 743) — the rewritten projection COMPILED against the
14987    /// extended schema, once per plan. The per-output-row evaluation ran
14988    /// the interpreter (~560 ns/row on the unnest panel cell); the Step
14989    /// VM reads the `__srf_N` slots as plain columns. `None` = that item
14990    /// is not fully compilable and keeps the interpreter.
14991    compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
14992    base_cols: usize,
14993}
14994
14995fn build_srf_plan(
14996    engine: &Engine,
14997    projection: &[ProjectedItem],
14998    srf_idxs: &[usize],
14999    ctx: &EvalContext<'_>,
15000) -> Result<SrfPlan, EngineError> {
15001    // Lift every SRF node out of every item that contains one.
15002    let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
15003    let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
15004    let mut reject: Option<EngineError> = None;
15005    for &i in srf_idxs {
15006        let mut e = projection[i].expr.clone();
15007        crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
15008            if reject.is_some() {
15009                return true;
15010            }
15011            // PG refuses a set-returning function inside a conditional: the set
15012            // would have to be produced before anyone knows whether the branch
15013            // is even taken.
15014            let conditional = match n {
15015                spg_sql::ast::Expr::Case { .. } => Some("CASE"),
15016                spg_sql::ast::Expr::FunctionCall { name, .. }
15017                    if name.eq_ignore_ascii_case("coalesce") =>
15018                {
15019                    Some("COALESCE")
15020                }
15021                _ => None,
15022            };
15023            if let Some(kind) = conditional
15024                && engine.expr_contains_srf(n)
15025            {
15026                reject = Some(EngineError::Unsupported(alloc::format!(
15027                    "set-returning functions are not allowed in {kind}"
15028                )));
15029                return true;
15030            }
15031            if !engine.is_srf_node(n) {
15032                return false;
15033            }
15034            let slot = nodes.len();
15035            nodes.push(n.clone());
15036            *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
15037                qualifier: None,
15038                name: alloc::format!("__srf_{slot}"),
15039            });
15040            true
15041        });
15042        rewritten[i] = Some(e);
15043    }
15044    if let Some(err) = reject {
15045        return Err(err);
15046    }
15047    let base_cols = ctx.columns.len();
15048    let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
15049    for slot in 0..nodes.len() {
15050        ext_cols.push(ColumnSchema::new(
15051            alloc::format!("__srf_{slot}"),
15052            DataType::Text,
15053            true,
15054        ));
15055    }
15056    // v7.39 (round 743) — compile the rewritten items against the
15057    // EXTENDED schema. The slot columns' declared type is a per-row
15058    // patched detail the compiled column read does not consult.
15059    let compiled: Vec<Option<eval::CompiledExpr>> = {
15060        let mut ext_ctx = ctx.clone();
15061        ext_ctx.columns = &ext_cols;
15062        projection
15063            .iter()
15064            .enumerate()
15065            .map(|(i, p)| {
15066                let e = rewritten[i].as_ref().unwrap_or(&p.expr);
15067                if eval::fully_compilable(e) {
15068                    Some(eval::compile_expr(e, &ext_ctx))
15069                } else {
15070                    None
15071                }
15072            })
15073            .collect()
15074    };
15075    Ok(SrfPlan {
15076        nodes,
15077        rewritten,
15078        ext_cols,
15079        compiled,
15080        base_cols,
15081    })
15082}
15083
15084/// One input row expanded through a plan built once for the whole scan.
15085/// v7.39 (round 621) — expand a projection whose target list contains
15086/// set-returning items, remembering which INPUT row each output row came from.
15087///
15088/// The three materialised-source tails — `FROM unnest(…)`, `FROM
15089/// generate_series(…)`, and the one that serves VALUES / a derived table /
15090/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
15091/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
15092/// v(x)` answered `function unnest(integer[]) does not exist` on all the
15093/// others, for a query PG answers. Sharing the expansion is the point: a
15094/// fourth copy would have been the fourth place to forget.
15095fn expand_projection_srfs(
15096    engine: &Engine,
15097    projection: &[ProjectedItem],
15098    srf_idxs: &[usize],
15099    filtered: &[Row<'static>],
15100    ctx: &EvalContext<'_>,
15101) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
15102    let mut out = alloc::vec::Vec::with_capacity(filtered.len());
15103    let mut src = alloc::vec::Vec::with_capacity(filtered.len());
15104    // v7.39 (round 726) — ONE plan for the whole scan. The per-row
15105    // spelling rebuilt it for every input row: a full clone of the
15106    // rewritten projection trees and the extended schema, 50k times on
15107    // the panel's unnest cell.
15108    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15109    // v7.39 (round 733) — shard the expansion. Each shard clones the
15110    // plan (its ext_cols slot types are per-row mutable) and builds a
15111    // MINIMAL context — EvalContext is not Sync — which is sound only
15112    // when every expression involved is pure: the whole projection and
15113    // every SRF argument must be fully_compilable, or the row loop
15114    // stays serial with the full session context.
15115    // The projection is judged in its REWRITTEN form — the SRF call
15116    // itself is never compilable, but after the lift it is a plain
15117    // `__srf_N` column reference.
15118    let all_pure = projection
15119        .iter()
15120        .enumerate()
15121        .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
15122        && plan.nodes.iter().all(|n| match n {
15123            Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
15124            other => eval::fully_compilable(other),
15125        });
15126    if all_pure
15127        && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
15128        && let Some(r) = engine.parallel_runner.0.as_deref()
15129    {
15130        let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
15131        let chunk = filtered.len().div_ceil(n_shards);
15132        type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
15133        let schema_cols = ctx.columns;
15134        let alias = ctx.table_alias;
15135        let mysql = ctx.mysql_dialect;
15136        let style = ctx.render_style;
15137        let plan_ref = &plan;
15138        let results = r.run_shards(n_shards, &|si| {
15139            let lo = si * chunk;
15140            let hi = ((si + 1) * chunk).min(filtered.len());
15141            let mut sctx = eval::EvalContext::new(schema_cols, alias);
15142            sctx.mysql_dialect = mysql;
15143            sctx.render_style = style;
15144            // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
15145            // compiled programs); each shard rebuilds it, which also
15146            // recompiles against the shard's own context. Build errors
15147            // were already surfaced by the outer build above.
15148            let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
15149                Ok(p) => p,
15150                Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
15151            };
15152            let mut run = || -> ShardOut {
15153                let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
15154                let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
15155                for (i, row) in filtered[lo..hi].iter().enumerate() {
15156                    let expanded =
15157                        expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
15158                    sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
15159                    o.extend(expanded);
15160                }
15161                Ok((o, sidx))
15162            };
15163            alloc::boxed::Box::new(run())
15164        });
15165        for boxed in results {
15166            let shard = boxed
15167                .downcast::<ShardOut>()
15168                .expect("runner echoes the closure's box");
15169            let (o, sidx) = (*shard)?;
15170            out.extend(o);
15171            src.extend(sidx);
15172        }
15173        return Ok((out, src));
15174    }
15175    for (i, row) in filtered.iter().enumerate() {
15176        let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
15177        src.extend(core::iter::repeat_n(i, expanded.len()));
15178        out.extend(expanded);
15179    }
15180    Ok((out, src))
15181}
15182
15183/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
15184///
15185/// A key that names a select-list item reads it out of the EXPANDED row,
15186/// because PG sorts after the expansion. A key that names a source column the
15187/// query does not project is evaluated against the input row that output row
15188/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
15189fn srf_order_key(
15190    ob: &spg_sql::ast::OrderBy,
15191    out_col: Option<usize>,
15192    out: &Row<'static>,
15193    src: &Row<'static>,
15194    ctx: &EvalContext<'_>,
15195) -> Result<Value<'static>, EngineError> {
15196    match out_col {
15197        Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
15198        None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
15199    }
15200}
15201
15202fn expand_srf_row_with(
15203    engine: &Engine,
15204    plan: &mut SrfPlan,
15205    projection: &[ProjectedItem],
15206    row: &Row<'static>,
15207    ctx: &EvalContext<'_>,
15208) -> Result<Vec<Row<'static>>, EngineError> {
15209    let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
15210    for n in &plan.nodes {
15211        lists.push(engine.srf_values(n, row, ctx)?);
15212    }
15213    let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
15214    // Only the slots' element types depend on the row; the names and the
15215    // input schema around them do not.
15216    for (slot, list) in lists.iter().enumerate() {
15217        plan.ext_cols[plan.base_cols + slot].ty = list
15218            .iter()
15219            .find_map(|v| v.data_type())
15220            .unwrap_or(DataType::Text);
15221    }
15222    let mut ext_ctx = ctx.clone();
15223    ext_ctx.columns = &plan.ext_cols;
15224    let mut out = Vec::with_capacity(n_rows);
15225    // v7.39 (round 726) — the base columns are the SAME for every
15226    // expanded row; clone them once and rewrite only the SRF slots per
15227    // k. The old form cloned the whole input row per OUTPUT row — for
15228    // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
15229    // TEXT column the projection never reads.
15230    let base_len = row.values.len();
15231    let mut ext_vals = row.values.clone();
15232    ext_vals.resize(base_len + lists.len(), Value::Null);
15233    let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15234    for k in 0..n_rows {
15235        for (slot, list) in lists.iter().enumerate() {
15236            // Past the end of THIS srf's rows → NULL (PG pads).
15237            ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
15238        }
15239        let ext_row = Row::new(core::mem::take(&mut ext_vals));
15240        let mut vals = Vec::with_capacity(projection.len());
15241        for (i, p) in projection.iter().enumerate() {
15242            // v7.39 (round 743) — compiled when possible; the
15243            // interpreter for the rest, with its exact wording.
15244            vals.push(match &plan.compiled[i] {
15245                Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
15246                    .map_err(EngineError::Eval)?,
15247                None => {
15248                    let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
15249                    eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
15250                }
15251            });
15252        }
15253        ext_vals = ext_row.values;
15254        out.push(Row::new(vals));
15255    }
15256    Ok(out)
15257}
15258
15259/// The one-shot spelling, for the callers that expand a single row.
15260/// v7.39 (round 600) — which output column each ORDER BY key names, for a
15261/// query whose target list contains a set-returning function.
15262///
15263/// The keys used to be built from the INPUT row, before the SRF expanded, so
15264/// anything that named the SRF's own output was evaluated as a scalar call:
15265/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
15266/// "function unnest(integer[]) does not exist", and so did the spellings that
15267/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
15268/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
15269/// back in input order. PG sorts AFTER the expansion, so a key that names a
15270/// select-list item reads that item's value out of the expanded row.
15271///
15272/// `None` keeps the key on the input row, which is where an ORDER BY naming
15273/// a column the query does not project has to be evaluated.
15274/// v7.38.19 — the output column an ORDER BY term reads, when reading it
15275/// is provably the same as building a key from the input row.
15276///
15277/// A sort key is a COPY of the sort column, made because the source row
15278/// is gone by the time the sort runs — only the projection survives. On
15279/// `SELECT s_long FROM t ORDER BY s_long` that copy is of data the
15280/// projected row already holds, and on 400,000 rows of 192-character
15281/// text it is 400,000 allocations, 400,000 frees and 77 MB of copying.
15282/// A profile of that cell put the allocator at 2,025 leaf samples of the
15283/// working set, second only to the comparison chain.
15284///
15285/// The condition is narrow on purpose. `srf_order_output_cols` resolves
15286/// an ORDER BY term the way SQL does — a positional ordinal, or a name
15287/// matching the select list — and SQL resolves against the select list
15288/// BEFORE the input columns. The key path resolves against the INPUT
15289/// columns. For `SELECT g AS id … ORDER BY id` on a table that also has
15290/// an `id`, those are different columns, and swapping one for the other
15291/// would change answers rather than timings.
15292///
15293/// So this takes only the case where the two cannot disagree: a bare
15294/// unqualified column name, matching exactly one output item, whose own
15295/// expression is that same column. The projected cell then IS the input
15296/// cell, and the key would have been its copy.
15297/// True when comparing two of this column's VALUES gives the same order
15298/// as comparing the sort KEYS built from them.
15299///
15300/// It does not hold widely. A user ENUM stores its label as text but
15301/// orders by DECLARATION position; an array orders element-wise; a
15302/// domain or composite carries its own rules. For those the two paths
15303/// answer differently, and a sort that skipped the key would silently
15304/// reorder the result. This is the short list where they agree.
15305fn value_order_is_key_order(col: &ColumnSchema) -> bool {
15306    use spg_storage::DataType as T;
15307    col.user_enum_type.is_none()
15308        && col.user_domain_type.is_none()
15309        && col.user_composite_type.is_none()
15310        && col.collation_name.is_none()
15311        && col.collation == spg_storage::Collation::Binary
15312        && matches!(
15313            col.ty,
15314            T::SmallInt | T::Int | T::BigInt | T::Text | T::Varchar(_) | T::Bool | T::Uuid
15315        )
15316}
15317
15318/// The full ORDER BY comparison between two rows, named by index.
15319///
15320/// v7.38.19 — what a permutation sort falls back to when its key ties.
15321fn row_cmp_by_index(
15322    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15323    terms: &[(usize, bool, Option<bool>)],
15324    colls: &[Option<crate::collate::Collated>],
15325    mysql: bool,
15326    ia: u32,
15327    ib: u32,
15328) -> core::cmp::Ordering {
15329    let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
15330    for (i, (col, desc, nf)) in terms.iter().enumerate() {
15331        let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col)) else {
15332            continue;
15333        };
15334        let ord = match (va, vb) {
15335            (Value::Text(x), Value::Text(y)) => match colls.get(i).and_then(Option::as_ref) {
15336                Some(c) => {
15337                    let o = c.compare(x, y);
15338                    if *desc { o.reverse() } else { o }
15339                }
15340                None if !mysql => {
15341                    let o = crate::orderby::str_cmp_prefix_first(x, y);
15342                    if *desc { o.reverse() } else { o }
15343                }
15344                None => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15345            },
15346            _ => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
15347        };
15348        if ord != core::cmp::Ordering::Equal {
15349            return ord;
15350        }
15351    }
15352    core::cmp::Ordering::Equal
15353}
15354
15355/// Whether ordering these rows by BYTES is what the collation in force
15356/// would have answered anyway.
15357///
15358/// v7.38.19 — a collated sort used to be shut out of the keyed path
15359/// entirely, and the cost of that showed up the moment the byte path
15360/// got fast: on the same fixture, the same binary took 92 ms under `C`
15361/// and 371 ms under `en_US`, so declaring a collation had become a
15362/// four-fold tax on a query that sorts md5 hex.
15363///
15364/// It need not be. For several locales `[0-9a-z]` orders exactly as
15365/// bytes do -- `collate::ascii_byte_order` carries that fact, and the
15366/// test beside it re-derives the whole allowlist by sorting a corpus
15367/// twice rather than asserting it. So when the collation is one of
15368/// those AND every value in every sort column is drawn from that
15369/// alphabet, the byte answer IS the collated answer.
15370///
15371/// Both halves are required. A collation outside the list can put `z`
15372/// between `s` and `t`; a value outside the alphabet can be `Ápple`,
15373/// which no locale in the list orders by its bytes. Either one and this
15374/// returns false, and the sort takes the collator's own path.
15375fn byte_order_answers_the_collation(
15376    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15377    terms: &[(usize, bool, Option<bool>)],
15378    colls: &[Option<crate::collate::Collated>],
15379) -> bool {
15380    if colls.iter().all(Option::is_none) {
15381        return true;
15382    }
15383    if !colls
15384        .iter()
15385        .flatten()
15386        .all(crate::collate::Collated::ascii_byte_order)
15387    {
15388        return false;
15389    }
15390    tagged.iter().all(|(_, row)| {
15391        terms.iter().all(|(col, _, _)| match row.values.get(*col) {
15392            // Only TEXT is collation-sensitive; a number or a NULL
15393            // orders the same under every collation there is.
15394            Some(Value::Text(t)) => crate::collate::is_ascii_alnum_lower(t),
15395            _ => true,
15396        })
15397    })
15398}
15399
15400/// An eight-byte key for each row's sort column, paired with the row's
15401/// index — or `None` when the column cannot give one on every row.
15402///
15403/// v7.38.19 — the pair is what the sort array holds instead of the row.
15404/// Two kinds of column can supply it:
15405///
15406///   * an INTEGER, whose whole value fits. Flipping the sign bit maps
15407///     the signed order onto the unsigned one, so the key is EXACT and
15408///     a comparison never has to look at the row at all.
15409///   * TEXT, as the first eight bytes big-endian, zero-padded. That
15410///     orders the same as the string — two that differ inside those
15411///     bytes differ at the same index either way, and one shorter than
15412///     eight pads with zeros exactly where `[u8]`'s own comparison runs
15413///     out — but it is a PREFIX, so equal keys must still ask the full
15414///     comparator.
15415///
15416/// The `None` is the safety of it: a NULL or any other type has no
15417/// faithful eight-byte key, so such a column takes the ordinary path
15418/// rather than being given a made-up one.
15419fn sort_keys_of(
15420    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
15421    col: usize,
15422) -> Option<(Vec<(u64, u32)>, bool)> {
15423    let n = u32::try_from(tagged.len()).ok()?;
15424    let mut out: Vec<(u64, u32)> = Vec::with_capacity(tagged.len());
15425    let exact = match tagged.first()?.1.values.get(col)? {
15426        Value::Text(_) => false,
15427        Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => true,
15428        _ => return None,
15429    };
15430    for (i, row) in (0..n).zip(tagged.iter()) {
15431        let key = match row.1.values.get(col) {
15432            Some(Value::Text(t)) if !exact => {
15433                let mut k = [0u8; 8];
15434                let bytes = t.as_bytes();
15435                let take = bytes.len().min(8);
15436                k[..take].copy_from_slice(&bytes[..take]);
15437                u64::from_be_bytes(k)
15438            }
15439            Some(Value::SmallInt(v)) if exact => (i64::from(*v) as u64) ^ (1 << 63),
15440            Some(Value::Int(v)) if exact => (i64::from(*v) as u64) ^ (1 << 63),
15441            Some(Value::BigInt(v)) if exact => (*v as u64) ^ (1 << 63),
15442            _ => return None,
15443        };
15444        out.push((key, i));
15445    }
15446    Some((out, exact))
15447}
15448
15449/// Whether a PREFIX key is worth sorting a permutation on.
15450///
15451/// v7.38.19 — it is not always, and the panel says so in one cell. The
15452/// `text (26 values)` fixture is two hundred identical characters drawn
15453/// from twenty-six letters, so every eight-byte prefix inside a letter
15454/// is the same and 15,000 rows tie on it. Each tie then pays the prefix
15455/// compare, a two-hundred-byte comparison, AND a random read into a
15456/// 400,000-element array — while sorting the rows in place keeps the
15457/// partition contiguous. Measured: 160 ms sorting rows, 247 ms sorting
15458/// the permutation, on the very fixture built to be degenerate.
15459///
15460/// So the permutation is taken when the key DECIDES, and a sample says
15461/// whether it does. An exact key always decides; a prefix has to earn
15462/// it.
15463fn key_discriminates(keys: &[(u64, u32)]) -> bool {
15464    const SAMPLE: usize = 1024;
15465    let step = (keys.len() / SAMPLE).max(1);
15466    let mut seen: Vec<u64> = keys
15467        .iter()
15468        .step_by(step)
15469        .take(SAMPLE)
15470        .map(|&(k, _)| k)
15471        .collect();
15472    let taken = seen.len();
15473    if taken < 8 {
15474        return true;
15475    }
15476    seen.sort_unstable();
15477    seen.dedup();
15478    seen.len() * 2 >= taken
15479}
15480
15481fn order_by_output_cols_if_identical(
15482    order_by: &[spg_sql::ast::OrderBy],
15483    projection: &[ProjectedItem],
15484    schema_cols: &[ColumnSchema],
15485) -> Option<Vec<usize>> {
15486    if order_by.is_empty() {
15487        return None;
15488    }
15489    let mut out = Vec::with_capacity(order_by.len());
15490    for ob in order_by {
15491        let Expr::Column(c) = &ob.expr else {
15492            return None;
15493        };
15494        if c.qualifier.is_some() {
15495            return None;
15496        }
15497        let mut hit = None;
15498        for (i, p) in projection.iter().enumerate() {
15499            if !p.output_name.eq_ignore_ascii_case(&c.name) {
15500                continue;
15501            }
15502            if hit.is_some() {
15503                return None; // ambiguous — SQL would reject it too
15504            }
15505            // The item must BE that column, not merely be named for it.
15506            let Expr::Column(pc) = &p.expr else {
15507                return None;
15508            };
15509            if !pc.name.eq_ignore_ascii_case(&c.name) {
15510                return None;
15511            }
15512            let sc = schema_cols
15513                .iter()
15514                .find(|s| s.name.eq_ignore_ascii_case(&c.name))?;
15515            if !value_order_is_key_order(sc) {
15516                return None;
15517            }
15518            hit = Some(i);
15519        }
15520        out.push(hit?);
15521    }
15522    Some(out)
15523}
15524
15525fn srf_order_output_cols(
15526    order_by: &[spg_sql::ast::OrderBy],
15527    projection: &[ProjectedItem],
15528) -> Vec<Option<usize>> {
15529    order_by
15530        .iter()
15531        .map(|ob| {
15532            // A positive ordinal is the Nth output column, directly.
15533            // `resolve_positional_order_by` deliberately leaves an ordinal
15534            // pointing at a set-returning item alone — copying the call into
15535            // ORDER BY would have made the key "the whole set" back when keys
15536            // came from the input row. Reading the expanded row's column is
15537            // what it should have meant, and is what this does.
15538            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
15539                && *n >= 1
15540                && let Ok(idx) = usize::try_from(*n - 1)
15541                && idx < projection.len()
15542            {
15543                return Some(idx);
15544            }
15545            // An unqualified name matching exactly one output name. SQL
15546            // resolves ORDER BY against the select list first, so this wins
15547            // over an input column of the same name — which is the whole
15548            // point of `SELECT g AS id … ORDER BY id`.
15549            if let Expr::Column(c) = &ob.expr
15550                && c.qualifier.is_none()
15551            {
15552                let mut hit = None;
15553                for (i, p) in projection.iter().enumerate() {
15554                    if p.output_name.eq_ignore_ascii_case(&c.name) {
15555                        if hit.is_some() {
15556                            hit = None;
15557                            break;
15558                        }
15559                        hit = Some(i);
15560                    }
15561                }
15562                if hit.is_some() {
15563                    return hit;
15564                }
15565            }
15566            // Or the same expression as a select-list item — which is what
15567            // `ORDER BY 1` becomes once `resolve_positional_order_by` has
15568            // run, and what a repeated `ORDER BY unnest(…)` is.
15569            projection.iter().position(|p| p.expr == ob.expr)
15570        })
15571        .collect()
15572}
15573
15574fn expand_srf_row(
15575    engine: &Engine,
15576    projection: &[ProjectedItem],
15577    srf_idxs: &[usize],
15578    row: &Row<'static>,
15579    ctx: &EvalContext<'_>,
15580) -> Result<Vec<Row<'static>>, EngineError> {
15581    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
15582    expand_srf_row_with(engine, &mut plan, projection, row, ctx)
15583}
15584
15585impl Engine {
15586    /// The rows one target-list SRF yields for an input row. `None` from
15587    /// `srf_target_idxs` means the expression is not set-returning at all.
15588    fn srf_values(
15589        &self,
15590        expr: &spg_sql::ast::Expr,
15591        row: &Row<'static>,
15592        ctx: &EvalContext<'_>,
15593    ) -> Result<Vec<Value<'static>>, EngineError> {
15594        if top_level_srf_kind(expr).is_some() {
15595            return top_level_srf_output(expr, row, ctx);
15596        }
15597        // A user set-returning function. Its body runs through the real
15598        // executor, like every function body since round 63.
15599        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
15600            return Err(EngineError::Unsupported(
15601                "expected a SELECT-list SRF call".into(),
15602            ));
15603        };
15604        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
15605        for a in args {
15606            vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
15607        }
15608        let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
15609        // v7.39 (read01 round 68) — in a target list a multi-column function is
15610        // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
15611        // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
15612        // what it is for. A single-column function contributes its bare value.
15613        Ok(rows
15614            .into_iter()
15615            .map(|r| {
15616                if r.values.len() == 1 {
15617                    r.values.into_iter().next().unwrap_or(Value::Null)
15618                } else {
15619                    Value::Composite(
15620                        cols.iter()
15621                            .map(|c| c.name.clone())
15622                            .zip(r.values)
15623                            .collect::<alloc::vec::Vec<_>>(),
15624                    )
15625                }
15626            })
15627            .collect())
15628    }
15629
15630    /// Is THIS node a set-returning call: one of the builtin kinds, or a user
15631    /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
15632    fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
15633        if is_top_level_unnest(e) {
15634            return true;
15635        }
15636        let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
15637            return false;
15638        };
15639        self.active_catalog().functions_named(name).iter().any(|f| {
15640            let r = f.returns.trim().to_ascii_uppercase();
15641            r.starts_with("SETOF") || r.starts_with("TABLE(")
15642        })
15643    }
15644
15645    /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
15646    fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
15647        let mut found = false;
15648        let mut probe = e.clone();
15649        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
15650            if self.is_srf_node(n) {
15651                found = true;
15652                return true;
15653            }
15654            false
15655        });
15656        found
15657    }
15658
15659    /// Which projection items CONTAIN a set-returning call. Before round 78 this
15660    /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
15661    /// ordinary scalar call all the way down to the function dispatcher, which
15662    /// then reported `unnest` as an unknown function.
15663    fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
15664        projection
15665            .iter()
15666            .enumerate()
15667            .filter(|(_, p)| self.expr_contains_srf(&p.expr))
15668            .map(|(i, _)| i)
15669            .collect()
15670    }
15671}
15672
15673impl Engine {
15674    /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
15675    /// no `(f(args)).*` item.
15676    fn lower_record_expansion(
15677        &self,
15678        stmt: &SelectStatement,
15679    ) -> Result<Option<SelectStatement>, EngineError> {
15680        use spg_sql::ast::{Expr, SelectItem};
15681        let is_marker = |it: &SelectItem| {
15682            matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
15683                if name == "__record_expand")
15684        };
15685        if !stmt.items.iter().any(is_marker) {
15686            return Ok(None);
15687        }
15688        let mut out = stmt.clone();
15689        let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
15690        let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
15691        for (n, item) in stmt.items.iter().enumerate() {
15692            if !is_marker(item) {
15693                items.push(item.clone());
15694                continue;
15695            }
15696            let SelectItem::Expr {
15697                expr: Expr::FunctionCall { args, .. },
15698                ..
15699            } = item
15700            else {
15701                unreachable!("checked by is_marker");
15702            };
15703            let Some(Expr::FunctionCall {
15704                name: fname,
15705                args: fargs,
15706            }) = args.first()
15707            else {
15708                return Err(EngineError::Unsupported(
15709                    "(<expr>).* expands a function's record — it needs a function call".into(),
15710                ));
15711            };
15712            let cols = self.setof_declared_columns(fname)?;
15713            let alias = alloc::format!("__rec{n}");
15714            let mut tref = bare_table_ref_named(&alias);
15715            tref.table_fn_call = Some(alloc::boxed::Box::new((
15716                fname.to_ascii_lowercase(),
15717                fargs.clone(),
15718            )));
15719            tref.alias = Some(alias.clone());
15720            lateral_refs.push(tref);
15721            for c in cols {
15722                items.push(SelectItem::Expr {
15723                    expr: Expr::Column(spg_sql::ast::ColumnName {
15724                        qualifier: Some(alias.clone()),
15725                        name: c,
15726                    }),
15727                    alias: None,
15728                });
15729            }
15730        }
15731        out.items = items;
15732        // The function joins the FROM. With no FROM it BECOMES the FROM; with one
15733        // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
15734        // (the arguments may reference the outer row — the round-69 correlation).
15735        for tref in lateral_refs {
15736            match &mut out.from {
15737                None => {
15738                    out.from = Some(spg_sql::ast::FromClause {
15739                        primary: tref,
15740                        joins: alloc::vec::Vec::new(),
15741                    });
15742                }
15743                Some(from) => from.joins.push(spg_sql::ast::FromJoin {
15744                    kind: spg_sql::ast::JoinKind::Cross,
15745                    table: tref,
15746                    on: None,
15747                    using_cols: None,
15748                    natural: false,
15749                }),
15750            }
15751        }
15752        Ok(Some(out))
15753    }
15754
15755    /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
15756    /// v text)` names them; a `SETOF <scalar>` is one column named after the
15757    /// function.
15758    fn setof_declared_columns(
15759        &self,
15760        name: &str,
15761    ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
15762        let cat = self.active_catalog();
15763        let overloads = cat.functions_named(name);
15764        let def = overloads.first().ok_or_else(|| {
15765            EngineError::Unsupported(alloc::format!("function {name} does not exist"))
15766        })?;
15767        let declared = def.returns.trim();
15768        let upper = declared.to_ascii_uppercase();
15769        if upper.starts_with("TABLE(") {
15770            let raw = &declared["TABLE(".len()..declared.len() - 1];
15771            return Ok(raw
15772                .split(',')
15773                .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
15774                .collect());
15775        }
15776        Ok(alloc::vec![name.to_string()])
15777    }
15778}
15779
15780/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
15781/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
15782/// COLUMNS list (data-independent), NESTED children inlined in
15783/// declaration order (PG's flattened output shape).
15784/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
15785/// correlated JSON_TABLE's static schema without evaluating its doc.
15786pub(crate) fn json_table_schema_pub(
15787    cols: &[spg_sql::ast::JsonTableColumn],
15788) -> alloc::vec::Vec<ColumnSchema> {
15789    json_table_schema(cols)
15790}
15791
15792fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
15793    use spg_sql::ast::JsonTableColumn as C;
15794    let mut out = alloc::vec::Vec::new();
15795    for c in cols {
15796        match c {
15797            C::Ordinality { name } => {
15798                out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
15799            }
15800            C::Regular {
15801                name, ty, exists, ..
15802            } => {
15803                let dt = if *exists {
15804                    DataType::Bool
15805                } else {
15806                    crate::conversions::column_type_to_data_type(*ty)
15807                };
15808                out.push(ColumnSchema::new(name.clone(), dt, true));
15809            }
15810            C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
15811        }
15812    }
15813    out
15814}
15815
15816/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
15817/// JSON_TABLE column's declared type (the DEFAULT expr may be a
15818/// string literal like `'none'` that must land as the column type).
15819fn coerce_json_table_default(
15820    v: Value<'static>,
15821    ty: spg_sql::ast::ColumnTypeName,
15822    name: &str,
15823) -> Result<Value<'static>, EngineError> {
15824    if v.is_null() {
15825        return Ok(Value::Null);
15826    }
15827    let dt = crate::conversions::column_type_to_data_type(ty);
15828    crate::conversions::coerce_value(v, dt, name, 0)
15829}
15830
15831/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
15832fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
15833    use crate::json::JsonValue as J;
15834    match v {
15835        Value::Null => J::Null,
15836        Value::Bool(b) => J::Bool(*b),
15837        Value::SmallInt(n) => J::Number(f64::from(*n)),
15838        Value::Int(n) => J::Number(f64::from(*n)),
15839        Value::BigInt(n) => J::Number(*n as f64),
15840        Value::Float(x) => J::Number(*x),
15841        Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
15842        other => J::String(crate::eval::value_to_text(other)),
15843    }
15844}
15845
15846fn bare_table_ref_named(name: &str) -> TableRef {
15847    TableRef {
15848        name: name.to_string(),
15849        alias: None,
15850        only: false,
15851        as_of_segment: None,
15852        unnest_expr: None,
15853        unnest_column_aliases: alloc::vec::Vec::new(),
15854        with_ordinality: false,
15855        generate_series_args: None,
15856        lateral_subquery: None,
15857        jsonb_each_text_arg: None,
15858        table_fn_call: None,
15859        rows_from: None,
15860        json_table: None,
15861        scalar_fn_item: false,
15862    }
15863}
15864
15865impl Engine {
15866    /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
15867    /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
15868    /// entries are the array-able SRFs, already lowered by the parser into their
15869    /// scalar array form.
15870    fn rows_from_rows(
15871        &self,
15872        primary: &TableRef,
15873    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
15874        let entries = primary
15875            .rows_from
15876            .as_ref()
15877            .expect("caller guards rows_from.is_some()");
15878        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
15879        let ctx = self.ev_ctx(&empty, None);
15880        let dummy = Row::new(alloc::vec::Vec::new());
15881        let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
15882        let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
15883        for (name, args) in entries {
15884            let (vals, colname) = if name == "__array" {
15885                // The parser lowered this one to `<array expr>`; its rows are the
15886                // array's elements.
15887                let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
15888                (
15889                    array_value_to_elements(&arr)?,
15890                    alloc::string::String::from("unnest"),
15891                )
15892            } else {
15893                let call = spg_sql::ast::Expr::FunctionCall {
15894                    name: name.clone(),
15895                    args: args.clone(),
15896                };
15897                (self.srf_values(&call, &dummy, &ctx)?, name.clone())
15898            };
15899            let ty = vals
15900                .first()
15901                .and_then(spg_storage::Value::data_type)
15902                .unwrap_or(DataType::Text);
15903            cols.push(ColumnSchema::new(colname, ty, true));
15904            lists.push(vals);
15905        }
15906        let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
15907        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
15908        for k in 0..n {
15909            let mut vals: alloc::vec::Vec<Value<'static>> =
15910                alloc::vec::Vec::with_capacity(lists.len() + 1);
15911            for l in &lists {
15912                vals.push(l.get(k).cloned().unwrap_or(Value::Null));
15913            }
15914            rows.push(Row::new(vals));
15915        }
15916        if primary.with_ordinality {
15917            cols.push(ColumnSchema::new(
15918                "ordinality".to_string(),
15919                DataType::BigInt,
15920                false,
15921            ));
15922            rows = rows
15923                .into_iter()
15924                .enumerate()
15925                .map(|(i, r)| {
15926                    let mut v = r.values;
15927                    v.push(Value::BigInt(i as i64 + 1));
15928                    Row::new(v)
15929                })
15930                .collect();
15931        }
15932        Ok((rows, cols))
15933    }
15934}
15935
15936/// v7.39 (round 232) — PG names the offending set operation in its
15937/// arity / type-mismatch messages ("each UNION query must have the same
15938/// number of columns"). `UNION ALL` is still spelled UNION there.
15939fn set_op_name(kind: UnionKind) -> &'static str {
15940    match kind {
15941        UnionKind::All | UnionKind::Distinct => "UNION",
15942        UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
15943        UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
15944    }
15945}
15946
15947/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
15948/// type: a bare string or NULL literal that no context has typed yet. SPG
15949/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
15950/// be the syntax. A wildcard or a non-literal expression is never unknown.
15951/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
15952/// LABELS as text (the wire render) but the value is an oid-carrying
15953/// dual, so a UNION with a numeric column must not be refused on the
15954/// label (pg_dump: `SELECT classid … UNION ALL SELECT
15955/// 'pg_opfamily'::regclass …`).
15956fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
15957    fn is_regcast(e: &Expr) -> bool {
15958        matches!(
15959            e,
15960            Expr::Cast {
15961                target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
15962                ..
15963            }
15964        )
15965    }
15966    stmt.items
15967        .iter()
15968        .map(|item| match item {
15969            SelectItem::Expr { expr, .. } => is_regcast(expr),
15970            _ => false,
15971        })
15972        .collect()
15973}
15974
15975fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
15976    stmt.items
15977        .iter()
15978        .map(|item| match item {
15979            SelectItem::Expr { expr, .. } => matches!(
15980                expr,
15981                Expr::Literal(spg_sql::ast::Literal::String(_))
15982                    | Expr::Literal(spg_sql::ast::Literal::Null)
15983            ),
15984            _ => false,
15985        })
15986        .collect()
15987}
15988
15989/// v7.39 (round 233) — retype one branch column's cells, reporting the
15990/// conversion failure the way PG does rather than leaving the column
15991/// half-converted. Used when the other branch typed an untyped literal.
15992fn coerce_branch_column(
15993    rows: &mut [Row<'static>],
15994    col_idx: usize,
15995    target: DataType,
15996    col_name: &str,
15997) -> Result<(), EngineError> {
15998    for row in rows.iter_mut() {
15999        let Some(slot) = row.values.get_mut(col_idx) else {
16000            continue;
16001        };
16002        if matches!(slot, Value::Null) {
16003            continue;
16004        }
16005        *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
16006    }
16007    Ok(())
16008}
16009
16010/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
16011/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
16012/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
16013/// reference to q's output columns substituted by the underlying column.
16014///
16015/// Admission is deliberately narrow — anything that changes cardinality,
16016/// order, or scope stays on the materialising path:
16017/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
16018///   FROM with no ordinality or positional column aliases, and no
16019///   subquery anywhere its expressions (an inner scope could reference
16020///   q too — descending is a later knife);
16021/// * inner: one stored table, bare-column projection only, no
16022///   CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
16023/// * every outer column reference must resolve inside q's output list —
16024///   a name that does not is an ERROR today, and flattening would
16025///   silently legalise it against the base table.
16026fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16027    use spg_sql::ast::SelectItem;
16028    let inner = primary.lateral_subquery.as_deref()?;
16029    // Outer shape.
16030    if !stmt.ctes.is_empty()
16031        || !stmt.unions.is_empty()
16032        || stmt.distinct
16033        || !stmt.distinct_on.is_empty()
16034        || !stmt.window_check_exprs.is_empty()
16035        || stmt.locking.is_some()
16036        || primary.with_ordinality
16037        || !primary.unnest_column_aliases.is_empty()
16038    {
16039        return None;
16040    }
16041    // Inner shape.
16042    if !inner.ctes.is_empty()
16043        || !inner.unions.is_empty()
16044        || inner.distinct
16045        || !inner.distinct_on.is_empty()
16046        || inner.group_by.is_some()
16047        || inner.group_by_all
16048        || inner.having.is_some()
16049        || !inner.order_by.is_empty()
16050        || inner.limit.is_some()
16051        || inner.offset.is_some()
16052        || !inner.window_check_exprs.is_empty()
16053        || inner.locking.is_some()
16054    {
16055        return None;
16056    }
16057    let ifrom = inner.from.as_ref()?;
16058    let it = &ifrom.primary;
16059    if !ifrom.joins.is_empty()
16060        || it.name.is_empty()
16061        || it.lateral_subquery.is_some()
16062        || it.unnest_expr.is_some()
16063        || it.generate_series_args.is_some()
16064        || it.as_of_segment.is_some()
16065        || it.jsonb_each_text_arg.is_some()
16066        || it.table_fn_call.is_some()
16067        || it.rows_from.is_some()
16068        || it.json_table.is_some()
16069        || it.with_ordinality
16070        || !it.unnest_column_aliases.is_empty()
16071    {
16072        return None;
16073    }
16074    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16075        return None;
16076    }
16077    // The output map: q's visible name -> the underlying column.
16078    let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
16079    let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
16080        alloc::collections::BTreeMap::new();
16081    for item in &inner.items {
16082        let SelectItem::Expr { expr, alias } = item else {
16083            return None;
16084        };
16085        let Expr::Column(c) = expr else {
16086            return None;
16087        };
16088        if let Some(q) = c.qualifier.as_deref()
16089            && !q.eq_ignore_ascii_case(&inner_alias)
16090        {
16091            return None;
16092        }
16093        let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
16094        // A duplicated output name would make substitution ambiguous.
16095        if map
16096            .insert(out_name.to_ascii_lowercase(), c.clone())
16097            .is_some()
16098        {
16099            return None;
16100        }
16101    }
16102    if map.is_empty() {
16103        return None;
16104    }
16105    let derived_alias = primary
16106        .alias
16107        .clone()
16108        .unwrap_or_else(|| primary.name.clone())
16109        .to_ascii_lowercase();
16110    // Substitute in a clone; bail (None) on the first reference the map
16111    // cannot answer.
16112    let mut out = stmt.clone();
16113    let ok = core::cell::Cell::new(true);
16114    let mut subst = |e: &mut Expr| -> bool {
16115        match e {
16116            Expr::Column(c) => {
16117                match c.qualifier.as_deref() {
16118                    Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
16119                    None => {}
16120                    Some(_) => {
16121                        ok.set(false);
16122                        return true;
16123                    }
16124                }
16125                match map.get(&c.name.to_ascii_lowercase()) {
16126                    Some(target) => *c = target.clone(),
16127                    None => ok.set(false),
16128                }
16129                true
16130            }
16131            // Any subquery could reference q from its own scope;
16132            // descending is a later knife — bail for now.
16133            Expr::ScalarSubquery(_)
16134            | Expr::Exists { .. }
16135            | Expr::InSubquery { .. }
16136            | Expr::RowInSubquery { .. }
16137            | Expr::RowCmpSubquery { .. } => {
16138                ok.set(false);
16139                true
16140            }
16141            _ => false,
16142        }
16143    };
16144    for item in &mut out.items {
16145        match item {
16146            SelectItem::Expr { expr, .. } => {
16147                crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
16148            }
16149            // `SELECT * FROM (…) q` means q's columns, in q's order.
16150            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
16151        }
16152    }
16153    if let Some(w) = &mut out.where_ {
16154        crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
16155    }
16156    if let Some(gs) = &mut out.group_by {
16157        for g in gs {
16158            crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
16159        }
16160    }
16161    if let Some(h) = &mut out.having {
16162        crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
16163    }
16164    for o in &mut out.order_by {
16165        crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
16166    }
16167    for d in &mut out.distinct_on {
16168        crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
16169    }
16170    if !ok.get() {
16171        return None;
16172    }
16173    // FROM becomes the stored table; the filters conjoin.
16174    out.from = Some(spg_sql::ast::FromClause {
16175        primary: it.clone(),
16176        joins: Vec::new(),
16177    });
16178    out.where_ = match (inner.where_.clone(), out.where_.take()) {
16179        (Some(a), Some(b)) => Some(Expr::Binary {
16180            lhs: alloc::boxed::Box::new(a),
16181            op: spg_sql::ast::BinOp::And,
16182            rhs: alloc::boxed::Box::new(b),
16183        }),
16184        (Some(a), None) => Some(a),
16185        (None, b) => b,
16186    };
16187    Some(out)
16188}
16189
16190/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
16191/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
16192/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
16193/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
16194/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
16195/// DISTINCT, an SRF, or an unprovable inner shape stays put.
16196fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
16197    use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
16198    let inner = primary.lateral_subquery.as_deref()?;
16199    // Outer: exactly `SELECT count(*)`, nothing else.
16200    if !stmt.ctes.is_empty()
16201        || !stmt.unions.is_empty()
16202        || stmt.distinct
16203        || !stmt.distinct_on.is_empty()
16204        || stmt.where_.is_some()
16205        || stmt.group_by.is_some()
16206        || stmt.having.is_some()
16207        || !stmt.order_by.is_empty()
16208        || stmt.limit.is_some()
16209        || stmt.offset.is_some()
16210        || stmt.items.len() != 1
16211    {
16212        return None;
16213    }
16214    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16215        return None;
16216    };
16217    let E::FunctionCall { name, args } = expr else {
16218        return None;
16219    };
16220    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16221        return None;
16222    }
16223    // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
16224    let Some(LimitExpr::Literal(k)) = &inner.offset else {
16225        return None;
16226    };
16227    let k = i64::from(*k);
16228    if inner.limit.is_some() || inner.order_by.is_empty() {
16229        return None;
16230    }
16231    let mut counted = inner.clone();
16232    counted.order_by = Vec::new();
16233    counted.offset = None;
16234    // The stripped inner must now be a provable simple shape (its
16235    // items become irrelevant — count(*) reads none of them — but an
16236    // SRF item would change the row count, so the flatten predicate's
16237    // scrutiny still applies).
16238    let base = matview_flatten_probe(&counted)?;
16239    let mut out = stmt.clone();
16240    out.items = alloc::vec![SelectItem::Expr {
16241        expr: E::FunctionCall {
16242            name: String::from("greatest"),
16243            args: alloc::vec![
16244                E::Binary {
16245                    lhs: alloc::boxed::Box::new(E::FunctionCall {
16246                        name: String::from("count_star"),
16247                        args: alloc::vec![],
16248                    }),
16249                    op: spg_sql::ast::BinOp::Sub,
16250                    rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16251                },
16252                E::Literal(spg_sql::ast::Literal::Integer(0)),
16253            ],
16254        },
16255        alias: Some(String::from("count")),
16256    }];
16257    out.from = Some(spg_sql::ast::FromClause {
16258        primary: base,
16259        joins: Vec::new(),
16260    });
16261    out.where_ = counted.where_.clone();
16262    Some(out)
16263}
16264
16265/// The inner-shape probe `try_count_over_offset` shares with the
16266/// flatten: single stored table, no modifiers, no subqueries, no SRF
16267/// items. Returns the base TableRef.
16268fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
16269    use spg_sql::ast::SelectItem;
16270    if !inner.ctes.is_empty()
16271        || !inner.unions.is_empty()
16272        || inner.distinct
16273        || !inner.distinct_on.is_empty()
16274        || inner.group_by.is_some()
16275        || inner.group_by_all
16276        || inner.having.is_some()
16277        || !inner.order_by.is_empty()
16278        || inner.limit.is_some()
16279        || inner.offset.is_some()
16280        || !inner.window_check_exprs.is_empty()
16281        || inner.locking.is_some()
16282    {
16283        return None;
16284    }
16285    let ifrom = inner.from.as_ref()?;
16286    let it = &ifrom.primary;
16287    if !ifrom.joins.is_empty()
16288        || it.name.is_empty()
16289        || it.lateral_subquery.is_some()
16290        || it.unnest_expr.is_some()
16291        || it.generate_series_args.is_some()
16292        || it.as_of_segment.is_some()
16293        || it.jsonb_each_text_arg.is_some()
16294        || it.table_fn_call.is_some()
16295        || it.rows_from.is_some()
16296        || it.json_table.is_some()
16297        || it.with_ordinality
16298    {
16299        return None;
16300    }
16301    for item in &inner.items {
16302        match item {
16303            SelectItem::Expr { expr, .. } => {
16304                if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
16305                    return None;
16306                }
16307            }
16308            SelectItem::Wildcard => {}
16309            SelectItem::QualifiedWildcard(_) => return None,
16310        }
16311    }
16312    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
16313        return None;
16314    }
16315    Some(it.clone())
16316}
16317
16318/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
16319/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
16320/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
16321/// constant-LENGTH array literal unnests to exactly k rows per input
16322/// row (NULL elements are rows too). One SRF item only, elements
16323/// subquery-free, and the stripped inner must pass the same probe the
16324/// count-over-offset rewrite uses.
16325fn try_count_over_const_unnest(
16326    stmt: &SelectStatement,
16327    primary: &TableRef,
16328) -> Option<SelectStatement> {
16329    use spg_sql::ast::{Expr as E, SelectItem};
16330    let inner = primary.lateral_subquery.as_deref()?;
16331    if !stmt.ctes.is_empty()
16332        || !stmt.unions.is_empty()
16333        || stmt.distinct
16334        || !stmt.distinct_on.is_empty()
16335        || stmt.where_.is_some()
16336        || stmt.group_by.is_some()
16337        || stmt.having.is_some()
16338        || !stmt.order_by.is_empty()
16339        || stmt.limit.is_some()
16340        || stmt.offset.is_some()
16341        || stmt.items.len() != 1
16342    {
16343        return None;
16344    }
16345    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
16346        return None;
16347    };
16348    let E::FunctionCall { name, args } = expr else {
16349        return None;
16350    };
16351    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
16352        return None;
16353    }
16354    // Inner: exactly one item, and it is unnest(ARRAY[...]).
16355    if inner.items.len() != 1
16356        || !inner.order_by.is_empty()
16357        || inner.limit.is_some()
16358        || inner.offset.is_some()
16359    {
16360        return None;
16361    }
16362    let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
16363        return None;
16364    };
16365    let E::FunctionCall {
16366        name: fname,
16367        args: fargs,
16368    } = item
16369    else {
16370        return None;
16371    };
16372    if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
16373        return None;
16374    }
16375    let E::Array(elems) = &fargs[0] else {
16376        return None;
16377    };
16378    if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
16379        return None;
16380    }
16381    let k = elems.len() as i64;
16382    // The stripped inner (the SRF item replaced by a plain constant)
16383    // must be the provable simple shape.
16384    let mut counted = inner.clone();
16385    counted.items = alloc::vec![SelectItem::Expr {
16386        expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
16387        alias: None,
16388    }];
16389    let base = matview_flatten_probe(&counted)?;
16390    let mut out = stmt.clone();
16391    out.items = alloc::vec![SelectItem::Expr {
16392        expr: E::Binary {
16393            lhs: alloc::boxed::Box::new(E::FunctionCall {
16394                name: String::from("count_star"),
16395                args: alloc::vec![],
16396            }),
16397            op: spg_sql::ast::BinOp::Mul,
16398            rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
16399        },
16400        alias: Some(String::from("count")),
16401    }];
16402    out.from = Some(spg_sql::ast::FromClause {
16403        primary: base,
16404        joins: Vec::new(),
16405    });
16406    out.where_ = counted.where_.clone();
16407    Some(out)
16408}