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                        )
306                    });
307                    match seek_positions {
308                        Some(mut positions) => {
309                            // Table order, which is the order the scan
310                            // would have produced.
311                            positions.sort_unstable();
312                            for (n, pos) in positions.into_iter().enumerate() {
313                                if n.is_multiple_of(256) {
314                                    cancel.check()?;
315                                }
316                                let Some(row) = table.rows().get(pos) else {
317                                    continue;
318                                };
319                                if passes(row)? {
320                                    filtered.push(row);
321                                }
322                            }
323                        }
324                        None => {
325                            for (i, row) in table.scan_visible(&snap) {
326                                if i.is_multiple_of(256) {
327                                    cancel.check()?;
328                                }
329                                if passes(row)? {
330                                    filtered.push(row);
331                                }
332                            }
333                        }
334                    }
335                }
336            }
337        } else {
338            let deferred = self.build_joined_filtered_rows(
339                from,
340                stmt.where_.as_ref(),
341                cancel,
342                None,
343                &mut ByteBudget::new(self.max_query_bytes),
344            )?;
345            // A join's survivors are row-index tuples over its sources, so
346            // there is no single row to borrow — this branch owns them.
347            owned_rows = deferred.materialise();
348            rows_are_owned = true;
349            schema_cols_owned = deferred.combined_schema;
350            alias_opt = None;
351        }
352        if rows_are_owned {
353            filtered = owned_rows.iter().collect();
354        }
355        let schema_cols = &schema_cols_owned;
356        let ctx = self.ev_ctx(schema_cols, alias_opt);
357        let alias = alias_opt.unwrap_or("");
358        let n_rows = filtered.len();
359        // The window pipeline reads `&[&Row<'static>]`, and `filtered`
360        // already is one whichever branch produced it — the separate
361        // `filtered_refs` this used to build was the collect that made
362        // owning the rows look necessary.
363
364        // 2) Collect unique window function nodes from projection.
365        let mut window_nodes: Vec<Expr> = Vec::new();
366        for item in &stmt.items {
367            if let SelectItem::Expr { expr, .. } = item {
368                collect_window_nodes(expr, &mut window_nodes);
369            }
370        }
371        // v7.39 (round 592) — and from ORDER BY, which may name a window the
372        // select list never mentions. The order-key builder below rewrites
373        // window calls to `__win_N` columns, and a call that was never
374        // collected has no column to become.
375        for o in &stmt.order_by {
376            collect_window_nodes(&o.expr, &mut window_nodes);
377        }
378
379        // 3) For each window, compute per-row value.
380        // Index: same order as window_nodes; for row i, win_vals[w][i].
381        let mut win_vals: Vec<Vec<Value<'static>>> = Vec::with_capacity(window_nodes.len());
382        for wnode in &window_nodes {
383            let Expr::WindowFunction {
384                name,
385                args,
386                partition_by,
387                order_by,
388                frame,
389                null_treatment,
390                filter,
391            } = wnode
392            else {
393                unreachable!("collect_window_nodes pushes only WindowFunction");
394            };
395            // Compute (partition_key, order_key, original_index) for each row.
396            // v7.39 (round 593) — a key that is a plain column sits at the same
397            // position in every row, but was resolved BY NAME for each one. A
398            // per-library profile of `lag(id) OVER (ORDER BY id)` put
399            // `resolve_column` at 5.8% of the query on its own, with
400            // `rehydrate_cell` and the `eval_expr` dispatch behind it. Resolve
401            // once; anything that is not a plain column keeps the resolver.
402            let p_bound: Vec<Option<usize>> = partition_by
403                .iter()
404                .map(|e| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
405                .collect();
406            let o_bound: Vec<Option<usize>> = order_by
407                .iter()
408                .map(|(e, _, _)| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
409                .collect();
410            let arg_bound = args
411                .first()
412                .and_then(|a| crate::orderby::bound_column_position(a, schema_cols, alias_opt));
413            // v7.39 (round 690) — a window's ORDER BY over a column that
414            // declares a collation sorts by it, the same as a top-level
415            // ORDER BY. Resolved from the bound position, so only a bare
416            // column gets one; an expression produces a new value and the
417            // derivation that would give IT a collation is unbuilt.
418            let o_colls: Vec<Option<alloc::string::String>> = o_bound
419                .iter()
420                .map(|p| {
421                    p.and_then(|pos| schema_cols.get(pos))
422                        .and_then(|sc| sc.collation_name.clone())
423                        .filter(|n| crate::collate::is_supported(n))
424                })
425                .collect();
426            let mut indexed: Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)> =
427                Vec::with_capacity(n_rows);
428            // v7.39 (round 731) — single bound INT partition key, no window
429            // ORDER BY: group on the i64 directly. The generic build paid
430            // two heap Vecs per row (pkey + empty okey) plus a canonical
431            // string encode per row just to bucket 500k rows into 100
432            // groups; the whole per-row key apparatus disappears here.
433            // Neither key Vec is read downstream on this path: the hash
434            // grouping replaces partition_key_cmp, and okey is empty by
435            // construction.
436            let int_pkey_fast = order_by.is_empty()
437                && partition_by.len() == 1
438                && p_bound[0].is_some_and(|pos| {
439                    matches!(
440                        schema_cols.get(pos).map(|c| c.ty),
441                        Some(
442                            spg_storage::DataType::Int
443                                | spg_storage::DataType::BigInt
444                                | spg_storage::DataType::SmallInt
445                        )
446                    )
447                });
448            // v7.39 (round 979) — the same idea for a single bound INT
449            // window ORDER BY: sort on the i64 instead of on a heap vector
450            // per row.
451            //
452            // Measured at 400k rows (round 978, ablation, answer checked
453            // byte-for-byte against the general path on a key column that
454            // is a permutation): `row_number() OVER (ORDER BY k)` went
455            // 157.057-157.868 ms to 31.253-31.679, which is 79.8% and puts
456            // it on top of the `OVER ()` baseline — the sort essentially
457            // disappears. Round 977 had already shown the cost was
458            // key-shaped rather than row-shaped: the sort's share was
459            // 132.0 ms on a three-integer table and 132.5 with a 200-byte
460            // column added, and a per-row COPY does scale with width
461            // (round 976 measured that at +36 ns/row/200 bytes).
462            //
463            // Gated to ROW_NUMBER, which is the one function that reads
464            // neither key vector — it numbers the order it is handed.
465            // `rank` and `dense_rank` compare adjacent entries' order keys
466            // in `compute_window_partition`, so leaving those vectors
467            // empty would silently give every row rank 1. A wider version
468            // would carry the i64 in the entry and teach those two to use
469            // it; this one is the part that can be shown correct by
470            // construction.
471            let int_okey_fast = partition_by.is_empty()
472                && order_by.len() == 1
473                && frame.is_none()
474                && filter.is_none()
475                && matches!(null_treatment, spg_sql::ast::NullTreatment::Respect)
476                && name.eq_ignore_ascii_case("row_number")
477                && o_bound[0].is_some_and(|pos| {
478                    matches!(
479                        schema_cols.get(pos).map(|c| c.ty),
480                        Some(
481                            spg_storage::DataType::Int
482                                | spg_storage::DataType::BigInt
483                                | spg_storage::DataType::SmallInt
484                        )
485                    )
486                });
487            // Set when a cell in that column turns out not to be an
488            // integer after all. The declared type says it should be, but
489            // "should" is not a thing to sort 400k rows on, so the general
490            // path takes over and this build is discarded.
491            let mut int_okey_bailed = false;
492            if int_okey_fast {
493                let pos = o_bound[0].expect("gated bound");
494                let desc = order_by[0].1;
495                // PG orders NULLs last ascending and first descending
496                // unless the query says otherwise.
497                let nulls_first = order_by[0].2.unwrap_or(desc);
498                let mut keyed: Vec<(bool, i64, usize)> = Vec::with_capacity(n_rows);
499                for (i, row) in filtered.iter().enumerate() {
500                    match row.values.get(pos) {
501                        Some(Value::Int(n)) => keyed.push((false, i64::from(*n), i)),
502                        Some(Value::BigInt(n)) => keyed.push((false, *n, i)),
503                        Some(Value::SmallInt(n)) => keyed.push((false, i64::from(*n), i)),
504                        Some(Value::Null) | None => keyed.push((true, 0, i)),
505                        Some(_) => {
506                            int_okey_bailed = true;
507                            break;
508                        }
509                    }
510                }
511                if !int_okey_bailed {
512                    // `null_rank` puts NULLs on the side the query asked
513                    // for; the row's original index breaks every tie, so
514                    // equal keys keep the order the scan produced — what
515                    // the stable sort below would have given them.
516                    let null_rank = |is_null: bool| -> u8 { u8::from(is_null != nulls_first) };
517                    keyed.sort_unstable_by(|a, b| {
518                        null_rank(a.0)
519                            .cmp(&null_rank(b.0))
520                            .then_with(|| {
521                                if a.0 {
522                                    core::cmp::Ordering::Equal
523                                } else if desc {
524                                    b.1.cmp(&a.1)
525                                } else {
526                                    a.1.cmp(&b.1)
527                                }
528                            })
529                            .then_with(|| a.2.cmp(&b.2))
530                    });
531                    for (_, _, i) in keyed {
532                        indexed.push((Vec::new(), Vec::new(), i));
533                    }
534                } else {
535                    indexed.clear();
536                }
537            }
538            if int_okey_fast && !int_okey_bailed {
539                // Ordered above; nothing else to build.
540            } else if int_pkey_fast {
541                let pos = p_bound[0].expect("gated bound");
542                let mut slot: hashbrown::HashMap<Option<i64>, usize> = hashbrown::HashMap::new();
543                let mut groups: Vec<Vec<usize>> = Vec::new();
544                for (i, row) in filtered.iter().enumerate() {
545                    let k: Option<i64> = match row.values.get(pos) {
546                        Some(Value::BigInt(n)) => Some(*n),
547                        Some(Value::Int(n)) => Some(i64::from(*n)),
548                        Some(Value::SmallInt(n)) => Some(i64::from(*n)),
549                        _ => None,
550                    };
551                    match slot.get(&k) {
552                        Some(&gi) => groups[gi].push(i),
553                        None => {
554                            slot.insert(k, groups.len());
555                            groups.push(alloc::vec![i]);
556                        }
557                    }
558                }
559                // The downstream partition-boundary scan compares pkeys
560                // of ADJACENT entries, so the key must ride along — one
561                // single-element Vec per row (half the generic build's
562                // allocations, no string encode).
563                for g in groups {
564                    for i in g {
565                        let k: Value<'static> = match filtered[i].values.get(pos) {
566                            Some(v) => v.clone(),
567                            None => Value::Null,
568                        };
569                        indexed.push((alloc::vec![k], Vec::new(), i));
570                    }
571                }
572            } else {
573                for (i, row) in filtered.iter().enumerate() {
574                    let pkey: Vec<Value<'static>> = partition_by
575                        .iter()
576                        .enumerate()
577                        .map(
578                            |(k, p)| match p_bound[k].and_then(|pos| row.values.get(pos)) {
579                                Some(v) => Ok(v.clone()),
580                                None => eval::eval_expr(p, row, &ctx),
581                            },
582                        )
583                        .collect::<Result<_, _>>()?;
584                    // v7.39 (read01 round 54) — a window's ORDER BY over an enum
585                    // column must sort by MEMBER order (enumsortorder), not the
586                    // label's text. Enum values are Text at runtime, so the raw
587                    // value key sorted alphabetically — `row_number() OVER (ORDER
588                    // BY mood)` numbered the rows happy,ok,sad. Substitute the
589                    // member ordinal, the same key the top-level ORDER BY uses.
590                    // (Closes the enum-order knife's recorded window residual.)
591                    let okey: Vec<(Value, bool, Option<bool>)> = order_by
592                        .iter()
593                        .enumerate()
594                        .map(|(k, (e, desc, nf))| -> Result<_, EngineError> {
595                            let v = match o_bound[k].and_then(|pos| row.values.get(pos)) {
596                                Some(v) => v.clone(),
597                                None => eval::eval_expr(e, row, &ctx)?,
598                            };
599                            let v = match crate::orderby::enum_order_ordinal(e, &v, &ctx) {
600                                Some(ord) => Value::Float(ord),
601                                None => v,
602                            };
603                            Ok((v, *desc, *nf))
604                        })
605                        .collect::<Result<_, _>>()?;
606                    indexed.push((pkey, okey, i));
607                }
608            }
609            // Sort by (partition_key, order_key). Partition key uses
610            // a stable encoded form; order key respects ASC/DESC.
611            // v7.39 (round 731) — with NO window ORDER BY the sort's only
612            // job was putting same-partition rows next to each other, and a
613            // 500k-row comparison sort is a spectacular way to hash-group:
614            // the panel's `sum(id) OVER (PARTITION BY g)` spent ~100 ms
615            // here. Group by encoded key instead, preserving row order
616            // inside each group — exactly what the stable sort preserved,
617            // so every function (row_number included) answers the same.
618            if int_okey_fast && !int_okey_bailed {
619                // Already ordered by the i64 key above.
620            } else if int_pkey_fast {
621                // Already grouped above; same-partition rows are adjacent
622                // in original row order.
623            } else if order_by.is_empty() && !partition_by.is_empty() {
624                let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
625                let mut groups: Vec<
626                    Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)>,
627                > = Vec::new();
628                let mut keybuf = String::new();
629                for entry in indexed.drain(..) {
630                    keybuf.clear();
631                    for v in &entry.0 {
632                        crate::aggregate::push_canonical_key(&mut keybuf, v);
633                    }
634                    match slot.get(keybuf.as_str()) {
635                        Some(&gi) => groups[gi].push(entry),
636                        None => {
637                            slot.insert(keybuf.clone(), groups.len());
638                            groups.push(alloc::vec![entry]);
639                        }
640                    }
641                }
642                for g in groups {
643                    indexed.extend(g);
644                }
645            } else {
646                indexed.sort_by(|a, b| {
647                    let p_cmp = partition_key_cmp(&a.0, &b.0);
648                    if p_cmp != core::cmp::Ordering::Equal {
649                        return p_cmp;
650                    }
651                    crate::window::order_key_cmp_in(&a.1, &b.1, &o_colls)
652                });
653            }
654            // Per-partition compute.
655            let mut out_vals: Vec<Value<'static>> = alloc::vec![Value::Null; n_rows];
656            let mut p_start = 0;
657            while p_start < indexed.len() {
658                let mut p_end = p_start + 1;
659                while p_end < indexed.len()
660                    && partition_key_cmp(&indexed[p_start].0, &indexed[p_end].0)
661                        == core::cmp::Ordering::Equal
662                {
663                    p_end += 1;
664                }
665                // Compute the function within this partition slice.
666                compute_window_partition(
667                    name,
668                    args,
669                    arg_bound,
670                    !order_by.is_empty(),
671                    frame.as_ref(),
672                    *null_treatment,
673                    filter.as_deref(),
674                    &indexed[p_start..p_end],
675                    &filtered,
676                    &ctx,
677                    &mut out_vals,
678                )?;
679                p_start = p_end;
680            }
681            win_vals.push(out_vals);
682        }
683
684        // 4) Build extended schema: original columns + synthetic.
685        let mut ext_cols = schema_cols.clone();
686        for i in 0..window_nodes.len() {
687            ext_cols.push(ColumnSchema::new(
688                alloc::format!("__win_{i}"),
689                DataType::Text, // type doesn't matter for projection eval
690                true,
691            ));
692        }
693        // 6) Rewrite the projection: WindowFunction nodes → Column(__win_N).
694        let mut rewritten_items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
695        for item in &stmt.items {
696            let new_item = match item {
697                SelectItem::Wildcard => SelectItem::Wildcard,
698                SelectItem::QualifiedWildcard(q) => SelectItem::QualifiedWildcard(q.clone()),
699                SelectItem::Expr { expr, alias } => {
700                    let mut e = expr.clone();
701                    rewrite_window_to_columns(&mut e, &window_nodes);
702                    // The rewrite swaps the window call for a synthetic
703                    // `__win_N` column, and the projection then reported
704                    // THAT as the column name — `SELECT count(*) OVER ()`
705                    // answered `__win_0`, an internal name, where PG18
706                    // answers `count`. Pin the name while the call the
707                    // column is named for is still in hand.
708                    let alias = if alias.is_none() && e != *expr {
709                        Some(default_output_name(expr, self.backslash_escapes))
710                    } else {
711                        alias.clone()
712                    };
713                    SelectItem::Expr { expr: e, alias }
714                }
715            };
716            rewritten_items.push(new_item);
717        }
718
719        // 7) Project into final rows. JOIN case uses None so the
720        // qualifier check in `resolve_column` falls through to the
721        // composite `alias.col` schema lookup; single-table case
722        // keeps the bare alias so `bare_col` resolution still
723        // works for the projection's per-row column references.
724        // v7.39 (read01 round 54) — build through `ev_ctx`, the canonical
725        // constructor: it threads the catalog (plus render style / tz / GUCs)
726        // that a bare `EvalContext::new` drops. Without the catalog the OUTER
727        // `ORDER BY <enum col>` of a windowed query sorted by TEXT — the
728        // window values were right, the row order silently was not.
729        let ext_ctx = self.ev_ctx(&ext_cols, alias_opt);
730        let projection = build_projection_hiding_tail(
731            &rewritten_items,
732            &ext_cols,
733            alias,
734            self.backslash_escapes,
735            window_nodes.len(),
736        )?;
737        let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(n_rows);
738        // v7.39 (round 592) — the extended row (input columns plus the window
739        // values) used to be materialised for EVERY input row and kept until
740        // the projection had run: the input values cloned into a fresh Vec,
741        // then grown once to take the window columns. A counting allocator put
742        // the window path at 4 allocations a row where a plain derived table
743        // takes 1, and named all four — the input row, the clone, the growth,
744        // and the projected row. Only the last has to exist afterwards, so the
745        // extended row is one buffer refilled per row.
746        let mut ext_row: Row<'static> =
747            Row::new(Vec::with_capacity(schema_cols.len() + window_nodes.len()));
748        for i in 0..n_rows {
749            if i.is_multiple_of(256) {
750                cancel.check()?;
751            }
752            ext_row.values.clear();
753            ext_row.values.extend(filtered[i].values.iter().cloned());
754            for w in 0..window_nodes.len() {
755                ext_row.values.push(win_vals[w][i].clone());
756            }
757            let row = &ext_row;
758            let mut values = Vec::with_capacity(projection.len());
759            for p in &projection {
760                values.push(eval::eval_expr(&p.expr, row, &ext_ctx)?);
761            }
762            let order_keys = if stmt.order_by.is_empty() {
763                Vec::new()
764            } else {
765                let mut keys = Vec::with_capacity(stmt.order_by.len());
766                for o in &stmt.order_by {
767                    let mut e = o.expr.clone();
768                    rewrite_window_to_columns(&mut e, &window_nodes);
769                    let key = eval::eval_expr(&e, row, &ext_ctx)?;
770                    // v7.39 (read01 round 54) — this path builds its order keys
771                    // itself instead of going through `build_order_keys`, so it
772                    // skipped the enum-ordinal substitution: the OUTER
773                    // `ORDER BY <enum col>` of a windowed query sorted by the
774                    // label's TEXT, not by member order. The window values were
775                    // right and only the row order was wrong — silently.
776                    match crate::orderby::enum_order_ordinal(&e, &key, &ext_ctx) {
777                        Some(ord) => keys.push(value_to_order_key(&Value::Float(ord))?),
778                        None => keys.push(value_to_order_key(&key)?),
779                    }
780                }
781                keys
782            };
783            tagged.push((order_keys, Row::new(values)));
784        }
785        // ORDER BY + LIMIT/OFFSET on the projected rows.
786        if !stmt.order_by.is_empty() {
787            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
788            sort_by_keys(&mut tagged, &descs);
789        }
790        let mut out_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
791        // v7.37 D.41 — `SELECT DISTINCT` over a window projection: the window
792        // pipeline builds one output row per input row, so DISTINCT must dedup the
793        // projected rows (PG evaluates window functions before DISTINCT). Applied
794        // after ORDER BY (duplicate rows share sort keys, so order is preserved)
795        // and before LIMIT.
796        if stmt.distinct {
797            out_rows = dedup_rows(out_rows, FoldSpec::dialect(self.backslash_escapes));
798        }
799        apply_offset_and_limit(&mut out_rows, stmt.offset_literal(), stmt.limit_literal());
800        let final_cols: Vec<ColumnSchema> = projection
801            .into_iter()
802            .map(|p| {
803                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
804                c.user_enum_type = p.user_enum_type;
805                c.collation_name = p.collation_name;
806                c.mysql_fsp = p.mysql_fsp;
807                c
808            })
809            .collect();
810        Ok(QueryResult::Rows {
811            columns: final_cols,
812            rows: out_rows,
813        })
814    }
815
816    /// v4.11: materialise each CTE into a temp table inside a
817    /// cloned catalog, then run the body SELECT against a fresh
818    /// engine instance that owns the enriched catalog. The clone
819    /// is moderately expensive — only paid by CTE-bearing queries.
820    /// Subqueries inside CTE bodies / the main body resolve as
821    /// usual; `clock_fn` is propagated so `NOW()` lines up.
822    /// v7.16.2 — mailrs round-10 A.3. Materialise the
823    /// `information_schema.*` / `pg_catalog.*` virtual views
824    /// the SELECT references, then re-execute the SELECT
825    /// against an enriched catalog where those views are real
826    /// tables. Same pattern as `exec_with_ctes`. The temp
827    /// engine carries `meta_views_materialised = true` so its
828    /// own meta-dispatch short-circuits — without that we'd
829    /// infinite-recurse since the temp catalog's view name
830    /// still starts with `__spg_info_` and re-triggers the
831    /// check.
832    pub(crate) fn exec_select_with_meta_views(
833        &self,
834        stmt: &SelectStatement,
835        cancel: CancelToken<'_>,
836    ) -> Result<QueryResult, EngineError> {
837        let catalog = self.meta_view_catalog(stmt)?;
838        let mut temp = Engine::restore(catalog);
839        if let Some(c) = self.clock {
840            temp = temp.with_clock(c);
841        }
842        if let Some(f) = self.salt_fn {
843            temp = temp.with_salt_fn(f);
844        }
845        // v7.39 (round 522) — the temp engine holds the materialised
846        // catalog and, until now, nothing of the SESSION. So every
847        // session-scoped answer changed the moment a system view
848        // appeared in the FROM clause: `SELECT current_user` said
849        // `unmei` and `SELECT current_user FROM pg_class` said `admin`;
850        // `current_setting('work_mem')` fell back to the boot default
851        // after a SET; `application_name` read empty. A privilege check
852        // written against a catalog join was reading a different
853        // identity than the same check written without one.
854        //
855        // Carry what a session can be observed through — its parameters
856        // (which is also where the session user lives), the role store
857        // the privilege builtins read, the dialect, and the rendering
858        // settings a timestamp is spelled with.
859        temp.session_params.clone_from(&self.session_params);
860        temp.users.clone_from(&self.users);
861        temp.backslash_escapes = self.backslash_escapes;
862        temp.mysql_strict = self.mysql_strict;
863        temp.render_style = self.render_style;
864        temp.tz_offset_fn = self.tz_offset_fn;
865        temp.tz_localize_fn = self.tz_localize_fn;
866        temp.tz_abbrev_fn = self.tz_abbrev_fn;
867        temp.meta_views_materialised = true;
868        temp.exec_select_cancel(stmt, cancel)
869    }
870
871    /// v7.39 (round 462) — the catalog a meta-view SELECT resolves
872    /// against: this engine's catalog with every `__spg_*` view the
873    /// statement references materialised into it.
874    ///
875    /// Split out of `exec_select_with_meta_views` so Describe can reach
876    /// the same shapes execution reaches. Describe used to look the FROM
877    /// relation up in the plain catalog, where a system view does not
878    /// exist, and reported "no columns" for every one of them — so an
879    /// extended-protocol client reading `pg_stat_user_tables` got rows
880    /// with no column metadata. Sharing the materialisation means a
881    /// view added here is described correctly the day it is added.
882    pub(crate) fn meta_view_catalog(&self, stmt: &SelectStatement) -> Result<Catalog, EngineError> {
883        let mut needed: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
884        collect_meta_view_names(stmt, &mut needed);
885        let mut catalog = self.active_catalog().clone();
886        for view in &needed {
887            if catalog.get(view).is_some() {
888                continue;
889            }
890            match view.as_str() {
891                "__spg_info_columns" => {
892                    let (schema, rows) = synth_information_schema_columns(
893                        self.active_catalog(),
894                        self.backslash_escapes,
895                    );
896                    materialise_meta_view(&mut catalog, view, schema, rows)?;
897                }
898                "__spg_info_tables" => {
899                    let (schema, rows) = synth_information_schema_tables(self.active_catalog());
900                    materialise_meta_view(&mut catalog, view, schema, rows)?;
901                }
902                "__spg_pg_class" => {
903                    let (schema, rows) = synth_pg_class(
904                        self.active_catalog(),
905                        i64::try_from(self.vacuum_oldest_active()).unwrap_or(i64::MAX),
906                    );
907                    materialise_meta_view(&mut catalog, view, schema, rows)?;
908                }
909                "__spg_pg_attribute" => {
910                    let (schema, rows) = synth_pg_attribute(self.active_catalog());
911                    materialise_meta_view(&mut catalog, view, schema, rows)?;
912                }
913                // v7.17.0 Phase 3.P0-50 — pg_catalog.pg_type for
914                // sqlx / SQLAlchemy / Diesel / pgAdmin lookups.
915                "__spg_pg_type" => {
916                    let (schema, rows) = synth_pg_type(self.active_catalog());
917                    materialise_meta_view(&mut catalog, view, schema, rows)?;
918                }
919                // v7.39 (round 621) — pg_catalog.pg_operator, which did not
920                // exist at all.
921                "__spg_pg_operator" => {
922                    let (schema, rows) = synth_pg_operator(self.active_catalog());
923                    materialise_meta_view(&mut catalog, view, schema, rows)?;
924                }
925                // v7.17.0 Phase 3.P0-51 — pg_catalog.pg_proc for
926                // function-name introspection (ORM / pgAdmin).
927                "__spg_pg_proc" => {
928                    let (schema, rows) = synth_pg_proc(self.active_catalog());
929                    materialise_meta_view(&mut catalog, view, schema, rows)?;
930                }
931                // v7.24 (round-16 D) — pg_catalog.pg_trigger. The
932                // round-16 "why doesn't prod fire the trigger"
933                // question was unanswerable because triggers had NO
934                // introspection surface; tgname/tgenabled plus the
935                // pragmatic relname/timing/events/function columns
936                // make "is it registered and enabled" a one-liner.
937                "__spg_pg_trigger" => {
938                    let (schema, rows) = synth_pg_trigger(self.active_catalog());
939                    materialise_meta_view(&mut catalog, view, schema, rows)?;
940                }
941                // v7.17.0 Phase 3.P0-52 — pg_catalog.pg_namespace
942                // (schema list for admin tools' tree views).
943                "__spg_pg_namespace" => {
944                    let (schema, rows) = synth_pg_namespace(self.active_catalog());
945                    materialise_meta_view(&mut catalog, view, schema, rows)?;
946                }
947                // v7.39 — pg_tables convenience view (was a pgwire
948                // canned response that ignored projections).
949                "__spg_pg_tables" => {
950                    let (schema, rows) =
951                        crate::system_catalog::synth_pg_tables(self.active_catalog());
952                    materialise_meta_view(&mut catalog, view, schema, rows)?;
953                }
954                // v7.37.24 (24.1) — pg_catalog.pg_enum (label list
955                // for ENUM types; sqlx / ORM enum codecs read this).
956                "__spg_pg_enum" => {
957                    let (schema, rows) =
958                        crate::system_catalog::synth_pg_enum(self.active_catalog());
959                    materialise_meta_view(&mut catalog, view, schema, rows)?;
960                }
961                // v7.37.21 (21.13) — pg_catalog.pg_replication_slots
962                // (shape-stable empty until 21.12 persists slot state).
963                // v7.39 (round 277) — session-scoped prepared statements.
964                "__spg_pg_prepared_statements" => {
965                    let (schema, rows) = crate::system_catalog::synth_pg_prepared_statements(
966                        &self.prepared_statements,
967                    );
968                    materialise_meta_view(&mut catalog, view, schema, rows)?;
969                }
970                "__spg_pg_replication_slots" => {
971                    let (schema, rows) =
972                        crate::system_catalog::synth_pg_replication_slots(self.active_catalog());
973                    materialise_meta_view(&mut catalog, view, schema, rows)?;
974                }
975                // v7.37.21 (21.13-b) — pg_catalog.pg_publication
976                // (one row per CREATE PUBLICATION).
977                "__spg_pg_publication" => {
978                    let (schema, rows) = crate::system_catalog::synth_pg_publication(self);
979                    materialise_meta_view(&mut catalog, view, schema, rows)?;
980                }
981                // v7.37.21 (21.13-c) — pg_catalog.pg_subscription
982                // (one row per CREATE SUBSCRIPTION; subconninfo
983                // redacted so dashboards can't leak credentials).
984                "__spg_pg_subscription" => {
985                    let (schema, rows) = crate::system_catalog::synth_pg_subscription(self);
986                    materialise_meta_view(&mut catalog, view, schema, rows)?;
987                }
988                // v7.37.22 (22.x-stat-db) — pg_catalog.pg_stat_database
989                // (one row for SPG's single database; counters are
990                // shape-stable 0 until wiring lands).
991                "__spg_pg_stat_database" => {
992                    let (schema, rows) = crate::system_catalog::synth_pg_stat_database(
993                        self,
994                        self.stat_tup_inserted,
995                        self.stat_tup_updated,
996                        self.stat_tup_deleted,
997                    );
998                    materialise_meta_view(&mut catalog, view, schema, rows)?;
999                }
1000                // v7.37.22 (22.14) — pg_catalog.pg_stat_user_tables
1001                // (per-table churn counters; live_tup = row count).
1002                "__spg_pg_stat_user_tables" => {
1003                    // r192 — DML counters come from the engine-side
1004                    // non-transactional map, not the (tx-shadowed)
1005                    // catalog tables.
1006                    let (schema, rows) = crate::system_catalog::synth_pg_stat_user_tables(
1007                        self.active_catalog(),
1008                        &self.table_write_stats,
1009                    );
1010                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1011                }
1012                // v7.37.22 (22.15) — pg_catalog.pg_stat_user_indexes
1013                // (per-index usage counters; flag unused indexes).
1014                "__spg_pg_stat_user_indexes" => {
1015                    let (schema, rows) =
1016                        crate::system_catalog::synth_pg_stat_user_indexes(self.active_catalog());
1017                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1018                }
1019                // v7.37.22 (22.16) — pg_catalog.pg_stat_bgwriter.
1020                "__spg_pg_stat_bgwriter" => {
1021                    let (schema, rows) =
1022                        crate::system_catalog::synth_pg_stat_bgwriter(self.active_catalog());
1023                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1024                }
1025                // v7.38 (read01 P3.14) — pg_catalog.pg_stat_checkpointer /
1026                // pg_stat_wal shell views (shape-stable, counters pending).
1027                "__spg_pg_stat_checkpointer" => {
1028                    let (schema, rows) =
1029                        crate::system_catalog::synth_pg_stat_checkpointer(self.active_catalog());
1030                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1031                }
1032                "__spg_pg_stat_wal" => {
1033                    let (schema, rows) =
1034                        crate::system_catalog::synth_pg_stat_wal(self.active_catalog());
1035                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1036                }
1037                // v7.38 (read01 P3.15) — pg_catalog.pg_stat_slru /
1038                // pg_stat_subscription_stats shell views.
1039                "__spg_pg_stat_slru" => {
1040                    let (schema, rows) =
1041                        crate::system_catalog::synth_pg_stat_slru(self.active_catalog());
1042                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1043                }
1044                "__spg_pg_stat_subscription_stats" => {
1045                    let (schema, rows) = crate::system_catalog::synth_pg_stat_subscription_stats(
1046                        self.active_catalog(),
1047                    );
1048                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1049                }
1050                // v7.37.22 (22.17) — pg_catalog.pg_stat_archiver.
1051                "__spg_pg_stat_archiver" => {
1052                    let (schema, rows) =
1053                        crate::system_catalog::synth_pg_stat_archiver(self.active_catalog());
1054                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1055                }
1056                // v7.37.21 (21.13-d) — pg_catalog.pg_stat_replication.
1057                "__spg_pg_stat_replication" => {
1058                    let (schema, rows) =
1059                        crate::system_catalog::synth_pg_stat_replication(self.active_catalog());
1060                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1061                }
1062                // v7.37.24 (24.13) — pg_catalog.pg_am.
1063                "__spg_pg_am" => {
1064                    let (schema, rows) = crate::system_catalog::synth_pg_am(self.active_catalog());
1065                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1066                }
1067                // v7.37.22 (22.18) — pg_catalog.pg_stat_io (PG 16+).
1068                "__spg_pg_stat_io" => {
1069                    let (schema, rows) =
1070                        crate::system_catalog::synth_pg_stat_io(self.active_catalog());
1071                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1072                }
1073                // v7.37.22 (22.19) — pg_catalog.pg_stat_user_functions.
1074                "__spg_pg_stat_user_functions" => {
1075                    let (schema, rows) =
1076                        crate::system_catalog::synth_pg_stat_user_functions(self.active_catalog());
1077                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1078                }
1079                // v7.39 (round 287) — pg_catalog.pg_largeobject{,_metadata}.
1080                "__spg_pg_largeobject" => {
1081                    let (schema, rows) =
1082                        crate::system_catalog::synth_pg_largeobject(self.active_catalog());
1083                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1084                }
1085                "__spg_pg_largeobject_metadata" => {
1086                    let (schema, rows) =
1087                        crate::system_catalog::synth_pg_largeobject_metadata(self.active_catalog());
1088                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1089                }
1090                // v7.37.23 (23.7-a) — pg_catalog.pg_statistic_ext.
1091                "__spg_pg_statistic_ext" => {
1092                    let (schema, rows) =
1093                        crate::system_catalog::synth_pg_statistic_ext(self.active_catalog());
1094                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1095                }
1096                // v7.37.24 (24.15) — pg_catalog.pg_statistic.
1097                "__spg_pg_statistic" => {
1098                    let (schema, rows) =
1099                        crate::system_catalog::synth_pg_statistic(self.active_catalog());
1100                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1101                }
1102                // v7.37.22 (22.20) — pg_catalog.pg_stat_progress_vacuum.
1103                "__spg_pg_stat_progress_vacuum" => {
1104                    let (schema, rows) =
1105                        crate::system_catalog::synth_pg_stat_progress_vacuum(self.active_catalog());
1106                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1107                }
1108                // v7.37.22 (22.21) — pg_catalog.pg_stat_progress_create_index.
1109                "__spg_pg_stat_progress_create_index" => {
1110                    let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_create_index(
1111                        self.active_catalog(),
1112                    );
1113                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1114                }
1115                // v7.37.22 (22.22) — pg_catalog.pg_stat_progress_analyze.
1116                "__spg_pg_stat_progress_analyze" => {
1117                    let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_analyze(
1118                        self.active_catalog(),
1119                    );
1120                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1121                }
1122                // v7.37.24 (24.16) — pg_catalog.pg_inherits
1123                // (partition parent → child OID mapping).
1124                "__spg_pg_inherits" => {
1125                    let (schema, rows) =
1126                        crate::system_catalog::synth_pg_inherits(self.active_catalog());
1127                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1128                }
1129                // v7.39 (round 650) — the text-search catalogs, filled
1130                // with what SPG actually has rather than PG's thirty.
1131                "__spg_pg_ts_config_map" => {
1132                    let (schema, rows) =
1133                        crate::system_catalog::synth_pg_ts_config_map(self.active_catalog());
1134                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1135                }
1136                "__spg_pg_ts_config" => {
1137                    let (schema, rows) =
1138                        crate::system_catalog::synth_pg_ts_config(self.active_catalog());
1139                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1140                }
1141                "__spg_pg_ts_dict" => {
1142                    let (schema, rows) =
1143                        crate::system_catalog::synth_pg_ts_dict(self.active_catalog());
1144                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1145                }
1146                "__spg_pg_ts_parser" => {
1147                    let (schema, rows) =
1148                        crate::system_catalog::synth_pg_ts_parser(self.active_catalog());
1149                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1150                }
1151                "__spg_pg_ts_template" => {
1152                    let (schema, rows) =
1153                        crate::system_catalog::synth_pg_ts_template(self.active_catalog());
1154                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1155                }
1156                // v7.37.24 (24.17) — pg_catalog.pg_depend
1157                // (dependency graph; shape-stable empty since
1158                // SPG's drop enforcement is per-kind, not per-object).
1159                "__spg_pg_depend" => {
1160                    let (schema, rows) =
1161                        crate::system_catalog::synth_pg_depend(self.active_catalog());
1162                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1163                }
1164                // 7.38.1 S5.1 — pg_catalog.pg_opclass (pg_dump wall #1).
1165                "__spg_pg_opclass" => {
1166                    let (schema, rows) =
1167                        crate::system_catalog::synth_pg_opclass(self.active_catalog());
1168                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1169                }
1170                "__spg_pg_opfamily" => {
1171                    let (schema, rows) =
1172                        crate::system_catalog::synth_pg_opfamily(self.active_catalog());
1173                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1174                }
1175                "__spg_pg_amop" => {
1176                    let (schema, rows) =
1177                        crate::system_catalog::synth_pg_amop(self.active_catalog());
1178                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1179                }
1180                "__spg_pg_amproc" => {
1181                    let (schema, rows) =
1182                        crate::system_catalog::synth_pg_amproc(self.active_catalog());
1183                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1184                }
1185                // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1186                // ORM reflection + pg_dump read the deparsed default text).
1187                "__spg_pg_attrdef" => {
1188                    let (schema, rows) =
1189                        crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1190                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1191                }
1192                // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1193                "__spg_pg_policy" => {
1194                    let (schema, rows) =
1195                        crate::system_catalog::synth_pg_policy(self.active_catalog());
1196                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1197                }
1198                "__spg_pg_policies" => {
1199                    let (schema, rows) =
1200                        crate::system_catalog::synth_pg_policies(self.active_catalog());
1201                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1202                }
1203                // v7.37.24 (24.14) — pg_catalog.pg_collation.
1204                "__spg_pg_collation" => {
1205                    let (schema, rows) =
1206                        crate::system_catalog::synth_pg_collation(self.active_catalog());
1207                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1208                }
1209                // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1210                "__spg_pg_tablespace" => {
1211                    let (schema, rows) =
1212                        crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1213                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1214                }
1215                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1216                // for pgAdmin / DataGrip "indexes per table" listings.
1217                "__spg_pg_indexes" => {
1218                    let (schema, rows) = synth_pg_indexes(self.active_catalog());
1219                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1220                }
1221                // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1222                // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1223                "__spg_pg_description" => {
1224                    let (schema, rows) =
1225                        crate::system_catalog::synth_pg_description(self.active_catalog());
1226                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1227                }
1228                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1229                // for index introspection by ORM compilers.
1230                "__spg_pg_index" => {
1231                    let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1232                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1233                }
1234                // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1235                // for FK / UNIQUE / PK / CHECK introspection.
1236                "__spg_pg_constraint" => {
1237                    let (schema, rows) = synth_pg_constraint(self.active_catalog());
1238                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1239                }
1240                // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1241                // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1242                "__spg_pg_sequence" => {
1243                    let (schema, rows) = synth_pg_sequence(self.active_catalog());
1244                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1245                }
1246                // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1247                // pg_roles / pg_user. SPG is single-database so
1248                // pg_database surfaces just `postgres`; pg_roles
1249                // / pg_user walk the engine's UserStore.
1250                "__spg_pg_database" => {
1251                    let (schema, rows) = synth_pg_database(self);
1252                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1253                }
1254                "__spg_pg_roles" => {
1255                    let (schema, rows) = synth_pg_roles(self);
1256                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1257                }
1258                // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1259                // same roles, with PG's own `use*` column names. It used to
1260                // publish pg_roles' columns under this name.
1261                "__spg_pg_user" => {
1262                    let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1263                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1264                }
1265                // v7.39 (read01 round 58) — role membership.
1266                "__spg_pg_auth_members" => {
1267                    let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1268                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1269                }
1270                // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1271                // pg_views surfaces every CREATE VIEW result; SPG
1272                // ships one row per declared view from the catalog.
1273                "__spg_pg_views" => {
1274                    let (schema, rows) = synth_pg_views(self.active_catalog());
1275                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1276                }
1277                // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1278                // catalogued query-rewrite RULE.
1279                "__spg_pg_rules" => {
1280                    let (schema, rows) =
1281                        crate::system_catalog::synth_pg_rules(self.active_catalog());
1282                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1283                }
1284                // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1285                // catalogue `pg_get_ruledef(oid)` resolves against.
1286                "__spg_pg_rewrite" => {
1287                    let (schema, rows) =
1288                        crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1289                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1290                }
1291                // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1292                // and PG's own column names.
1293                "__spg_pg_matviews" => {
1294                    let (schema, rows) =
1295                        crate::system_catalog::synth_pg_matviews(self.active_catalog());
1296                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1297                }
1298                // pg_catalog.pg_extension — native capability list
1299                // (mailrs embed round-12).
1300                // v7.39 (round 546) — the catalogs SPG has real content
1301                // for, from the facts it already holds.
1302                "__spg_pg_db_role_setting" => {
1303                    let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1304                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1305                }
1306                "__spg_pg_language" => {
1307                    let (schema, rows) = crate::system_catalog::synth_pg_language();
1308                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1309                }
1310                "__spg_pg_sequences" => {
1311                    let (schema, rows) =
1312                        crate::system_catalog::synth_pg_sequences(self.active_catalog());
1313                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1314                }
1315                "__spg_pg_range" => {
1316                    let (schema, rows) = crate::system_catalog::synth_pg_range();
1317                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1318                }
1319                "__spg_pg_partitioned_table" => {
1320                    let (schema, rows) =
1321                        crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1322                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1323                }
1324                "__spg_pg_authid" => {
1325                    let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1326                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1327                }
1328                "__spg_pg_group" => {
1329                    let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1330                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1331                }
1332                "__spg_pg_shadow" => {
1333                    let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1334                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1335                }
1336                // v7.39 (round 544) — pg_cast, probed from the real
1337                // cast implementation.
1338                "__spg_pg_cast" => {
1339                    let (schema, rows) = crate::system_catalog::synth_pg_cast();
1340                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1341                }
1342                // v7.39 (round 541) — an empty catalog that exists.
1343                "__spg_pg_foreign_table" => {
1344                    let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1345                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1346                }
1347                "__spg_pg_extension" => {
1348                    let (schema, rows) = synth_pg_extension();
1349                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1350                }
1351                // v7.39 (round 502) — the timezone catalogues.
1352                "__spg_pg_timezone_names" => {
1353                    let (schema, rows) = synth_pg_timezone_names(self);
1354                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1355                }
1356                "__spg_pg_timezone_abbrevs" => {
1357                    let (schema, rows) = synth_pg_timezone_abbrevs(self);
1358                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1359                }
1360                // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1361                "__spg_pg_settings" => {
1362                    let (schema, rows) = synth_pg_settings(self);
1363                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1364                }
1365                // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1366                // v7.39 (read01 round 51) — information_schema.role_table_grants
1367                // and .table_privileges. Both report the owner's seven implicit
1368                // table privileges; SPG's single role owns everything.
1369                // v7.39 (read01 round 59) — information_schema.column_privileges.
1370                "__spg_info_column_privileges" => {
1371                    let (schema, rows) =
1372                        crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1373                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1374                }
1375                "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1376                    let grantee = self.current_role().to_string();
1377                    let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1378                        self.active_catalog(),
1379                        &grantee,
1380                    );
1381                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1382                }
1383                "__spg_info_key_column_usage" => {
1384                    let (schema, rows) = synth_info_key_column_usage(self.active_catalog());
1385                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1386                }
1387                // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1388                "__spg_info_referential_constraints" => {
1389                    let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1390                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1391                }
1392                // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1393                "__spg_info_statistics" => {
1394                    let (schema, rows) = synth_info_statistics(self.active_catalog());
1395                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1396                }
1397                // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1398                "__spg_info_routines" => {
1399                    let (schema, rows) = synth_info_routines();
1400                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1401                }
1402                // v7.37.24 (24.3) — information_schema.attributes.
1403                "__spg_info_attributes" => {
1404                    let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1405                        self.active_catalog(),
1406                    );
1407                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1408                }
1409                // v7.37.24 (24.2) — information_schema.domains.
1410                "__spg_info_domains" => {
1411                    let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1412                        self.active_catalog(),
1413                    );
1414                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1415                }
1416                // v7.37.24 (24.9) — information_schema.schemata.
1417                "__spg_info_schemata" => {
1418                    let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1419                        self.active_catalog(),
1420                    );
1421                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1422                }
1423                // v7.37.24 (24.9) — information_schema.views.
1424                "__spg_info_views" => {
1425                    let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1426                        self.active_catalog(),
1427                    );
1428                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1429                }
1430                // v7.37.24 (24.9) — information_schema.table_constraints.
1431                "__spg_info_table_constraints" => {
1432                    let (schema, rows) =
1433                        crate::system_catalog::synth_information_schema_table_constraints(
1434                            self.active_catalog(),
1435                        );
1436                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1437                }
1438                // v7.37.17 — information_schema.constraint_column_usage.
1439                "__spg_info_constraint_column_usage" => {
1440                    let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1441                        self.active_catalog(),
1442                    );
1443                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1444                }
1445                // v7.37.17 — information_schema.triggers.
1446                "__spg_info_triggers" => {
1447                    let (schema, rows) =
1448                        crate::system_catalog::synth_info_triggers(self.active_catalog());
1449                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1450                }
1451                // v7.37.17 — information_schema.check_constraints.
1452                "__spg_info_check_constraints" => {
1453                    let (schema, rows) =
1454                        crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1455                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1456                }
1457                // v7.37.17 — information_schema.sequences.
1458                "__spg_info_sequences" => {
1459                    let (schema, rows) =
1460                        crate::system_catalog::synth_info_sequences(self.active_catalog());
1461                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1462                }
1463                // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1464                "__spg_mysql_user" => {
1465                    let (schema, rows) = synth_mysql_user(self);
1466                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1467                }
1468                "__spg_mysql_db" => {
1469                    let (schema, rows) = synth_mysql_db();
1470                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1471                }
1472                // v7.39 (round 541) — the catalogs PG has that SPG is
1473                // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1474                other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1475                    let (schema, rows) =
1476                        crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1477                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1478                }
1479                _ => {
1480                    return Err(EngineError::Unsupported(alloc::format!(
1481                        "meta view {view:?} is not yet materialisable; \
1482                         v7.16.2 covers information_schema.columns / .tables \
1483                         and pg_catalog.pg_class / pg_attribute; \
1484                         v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1485                         pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1486                         pg_user / pg_views / pg_matviews / pg_settings"
1487                    )));
1488                }
1489            }
1490        }
1491        Ok(catalog)
1492    }
1493
1494    pub(crate) fn exec_with_ctes(
1495        &self,
1496        stmt: &SelectStatement,
1497        cancel: CancelToken<'_>,
1498    ) -> Result<QueryResult, EngineError> {
1499        cancel.check()?;
1500        // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1501        // bodies are supported here. Writable CTEs on a SELECT
1502        // outer require `&mut self` and route through the
1503        // top-level `exec_select_cancel_mut` entry; sentori
1504        // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1505        // INSERT, not a SELECT, so this restriction is harmless
1506        // in practice.
1507        if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1508            // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1509            // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1510            // of a statement, not nested inside a subquery; this path is
1511            // reached exactly when one is nested. The old text described SPG's
1512            // own executor plumbing ("the top-level mutable entry"), which
1513            // means nothing to a client.
1514            return Err(EngineError::Unsupported(
1515                "WITH clause containing a data-modifying statement must be at the top level".into(),
1516            ));
1517        }
1518        let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1519        // Strip CTEs from the body before running on the temp engine
1520        // so we don't recurse forever.
1521        let mut body = stmt.clone();
1522        body.ctes = Vec::new();
1523        let mut temp = Engine::restore(catalog);
1524        if let Some(c) = self.clock {
1525            temp = temp.with_clock(c);
1526        }
1527        if let Some(f) = self.salt_fn {
1528            temp = temp.with_salt_fn(f);
1529        }
1530        temp.exec_select_cancel(&body, cancel)
1531    }
1532
1533    /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1534    /// `&self` SELECT path. Caller guarantees no modifying CTE
1535    /// bodies are present.
1536    pub(crate) fn materialise_ctes_readonly(
1537        &self,
1538        ctes: &[spg_sql::ast::Cte],
1539        cancel: CancelToken<'_>,
1540    ) -> Result<crate::Catalog, EngineError> {
1541        cancel.check()?;
1542        let mut catalog = self.active_catalog().clone();
1543        for cte in ctes {
1544            let body_select = cte.body.as_select().ok_or_else(|| {
1545                EngineError::Unsupported(alloc::format!(
1546                    "data-modifying CTE not supported on this SELECT entry"
1547                ))
1548            })?;
1549            // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1550            // (PG scoping: the WITH name wins for the outer query and later
1551            // CTEs, while THIS body still sees the real table — a
1552            // non-recursive body's self-name is the table, probe P2). This
1553            // materialiser works on a CLONE, so the shadow is simply: run
1554            // the body against the untouched clone, then drop the real
1555            // table from the clone before installing the CTE's temp. A
1556            // RECURSIVE self-reference is the CTE itself (P6), so there the
1557            // drop happens before the iterating materialiser runs.
1558            let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1559                let synthetic = spg_sql::ast::Cte {
1560                    name: cte.name.clone(),
1561                    body: spg_sql::ast::CteBody::Select(body_select.clone()),
1562                    recursive: true,
1563                    column_overrides: cte.column_overrides.clone(),
1564                    search: None,
1565                    cycle: None,
1566                };
1567                if catalog.get(&cte.name).is_some() {
1568                    let _ = catalog.drop_table(&cte.name);
1569                }
1570                self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1571            } else {
1572                let mut cte_engine = Engine::restore(catalog.clone());
1573                if let Some(c) = self.clock {
1574                    cte_engine = cte_engine.with_clock(c);
1575                }
1576                if let Some(f) = self.salt_fn {
1577                    cte_engine = cte_engine.with_salt_fn(f);
1578                }
1579                let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1580                let QueryResult::Rows { columns, rows } = body_result else {
1581                    return Err(EngineError::Unsupported(alloc::format!(
1582                        "CTE {:?} body did not return rows",
1583                        cte.name
1584                    )));
1585                };
1586                (columns, rows)
1587            };
1588            let inferred = infer_column_types(&columns, &rows);
1589            let mut columns = inferred;
1590            if !cte.column_overrides.is_empty() {
1591                if cte.column_overrides.len() != columns.len() {
1592                    return Err(EngineError::Unsupported(alloc::format!(
1593                        "CTE {:?} column list has {} names but body returns {} columns",
1594                        cte.name,
1595                        cte.column_overrides.len(),
1596                        columns.len()
1597                    )));
1598                }
1599                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1600                    col.name.clone_from(name);
1601                }
1602            }
1603            let schema = TableSchema::new(cte.name.clone(), columns);
1604            // v7.39 (round 156) — the body ran against the untouched clone;
1605            // from here on the CTE name resolves to the temp (PG scoping).
1606            if catalog.get(&cte.name).is_some() {
1607                let _ = catalog.drop_table(&cte.name);
1608            }
1609            catalog.create_table(schema).map_err(EngineError::Storage)?;
1610            let table = catalog
1611                .get_mut(&cte.name)
1612                .expect("just-created CTE table must exist");
1613            for row in rows {
1614                table.insert(row).map_err(EngineError::Storage)?;
1615            }
1616        }
1617        Ok(catalog)
1618    }
1619
1620    /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1621    /// Retained for non-DML callers; the DML path (writable CTE on
1622    /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1623    /// `dml.rs` which installs the CTE temps directly on the
1624    /// active catalog so the outer statement's writes hit real
1625    /// tables.
1626    #[allow(dead_code)]
1627    pub(crate) fn materialise_ctes(
1628        &mut self,
1629        ctes: &[spg_sql::ast::Cte],
1630        cancel: CancelToken<'_>,
1631    ) -> Result<crate::Catalog, EngineError> {
1632        cancel.check()?;
1633        // v7.37.43-T4.4 — modifying CTEs need to write through the
1634        // SAME catalog as the outer statement, not a clone (PG's
1635        // writable CTE puts all modifications in one transaction).
1636        // For the read-only case the original logic cloned, but
1637        // since the outer statement also goes through the cloned
1638        // engine and ALL writes must converge, we now drive the
1639        // accumulator off `self.active_catalog().clone()` and
1640        // commit the modifying writes directly to `self`'s active
1641        // catalog so the surface is consistent.
1642        let mut catalog = self.active_catalog().clone();
1643        // v7.39 (round 149) — a modifying CTE body's target must be a
1644        // real relation, never a sibling CTE (PG: relation does not
1645        // exist); checked before any alias lands in the accumulator.
1646        for cte in ctes {
1647            let body_target = match &cte.body {
1648                spg_sql::ast::CteBody::Select(_) => None,
1649                spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1650                spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1651                spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1652                spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1653            };
1654            if let Some(t) = body_target
1655                && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1656                && catalog.get(t).is_none()
1657            {
1658                return Err(EngineError::Storage(
1659                    spg_storage::StorageError::TableNotFound { name: t.into() },
1660                ));
1661            }
1662        }
1663        for cte in ctes {
1664            if catalog.get(&cte.name).is_some() {
1665                return Err(EngineError::Unsupported(alloc::format!(
1666                    "CTE name {:?} shadows an existing table; rename the CTE",
1667                    cte.name
1668                )));
1669            }
1670            let (columns, rows) = match &cte.body {
1671                // v7.39 (round 145) — see the sibling site: only a body that
1672                // truly self-references takes the iterating materialiser.
1673                spg_sql::ast::CteBody::Select(body)
1674                    if cte.recursive && select_refers_to(body, &cte.name) =>
1675                {
1676                    // Recursive CTE — the existing helper takes a
1677                    // SELECT body and the snapshot catalog.
1678                    let synthetic = spg_sql::ast::Cte {
1679                        name: cte.name.clone(),
1680                        body: spg_sql::ast::CteBody::Select(body.clone()),
1681                        recursive: true,
1682                        column_overrides: cte.column_overrides.clone(),
1683                        search: None,
1684                        cycle: None,
1685                    };
1686                    self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1687                }
1688                spg_sql::ast::CteBody::Select(body) => {
1689                    // v7.25 (round-17) — run against the accumulated
1690                    // catalog so later CTEs can reference earlier
1691                    // ones in the same WITH clause.
1692                    let mut cte_engine = Engine::restore(catalog.clone());
1693                    if let Some(c) = self.clock {
1694                        cte_engine = cte_engine.with_clock(c);
1695                    }
1696                    if let Some(f) = self.salt_fn {
1697                        cte_engine = cte_engine.with_salt_fn(f);
1698                    }
1699                    let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1700                    let QueryResult::Rows { columns, rows } = body_result else {
1701                        return Err(EngineError::Unsupported(alloc::format!(
1702                            "CTE {:?} body did not return rows",
1703                            cte.name
1704                        )));
1705                    };
1706                    (columns, rows)
1707                }
1708                spg_sql::ast::CteBody::Insert(body) => {
1709                    self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1710                }
1711                spg_sql::ast::CteBody::Update(body) => {
1712                    self.exec_modifying_cte_update(&cte.name, body, cancel)?
1713                }
1714                spg_sql::ast::CteBody::Delete(body) => {
1715                    self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1716                }
1717                spg_sql::ast::CteBody::Merge(body) => {
1718                    self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1719                }
1720            };
1721            // v4.22: the projection builder labels any non-column
1722            // expression as Text — including literal SELECT 1.
1723            // Promote each column's type to whatever the rows
1724            // actually carry so the CTE storage table accepts them.
1725            let inferred = infer_column_types(&columns, &rows);
1726            let mut columns = inferred;
1727            if !cte.column_overrides.is_empty() {
1728                if cte.column_overrides.len() != columns.len() {
1729                    return Err(EngineError::Unsupported(alloc::format!(
1730                        "CTE {:?} column list has {} names but body returns {} columns",
1731                        cte.name,
1732                        cte.column_overrides.len(),
1733                        columns.len()
1734                    )));
1735                }
1736                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1737                    col.name.clone_from(name);
1738                }
1739            }
1740            let schema = TableSchema::new(cte.name.clone(), columns);
1741            catalog.create_table(schema).map_err(EngineError::Storage)?;
1742            let table = catalog
1743                .get_mut(&cte.name)
1744                .expect("just-created CTE table must exist");
1745            for row in rows {
1746                table.insert(row).map_err(EngineError::Storage)?;
1747            }
1748        }
1749        Ok(catalog)
1750    }
1751
1752    /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1753    /// against `self` (so the mutation lands in the active catalog
1754    /// inside the current transaction) and captures the RETURNING
1755    /// projection — column schema + rows — to materialise as the
1756    /// CTE alias's table. An INSERT without RETURNING produces a
1757    /// 0-row table with a synthetic single-column placeholder
1758    /// (matches PG: the CTE alias is still defined, but referencing
1759    /// it from the outer query without RETURNING raises a
1760    /// column-resolution error at scan time).
1761    fn exec_modifying_cte_insert(
1762        &mut self,
1763        cte_name: &str,
1764        body: &spg_sql::ast::InsertStatement,
1765        _cancel: CancelToken<'_>,
1766    ) -> Result<
1767        (
1768            Vec<spg_storage::ColumnSchema>,
1769            Vec<spg_storage::Row<'static>>,
1770        ),
1771        EngineError,
1772    > {
1773        // round 151 — a WITH-headed body keeps its own ctes; the body
1774        // statement routes through its writable-CTE entry (outer CTEs
1775        // are never copied into bodies, so no recursion risk).
1776        let body = body.clone();
1777        let result = self.exec_insert(body)?;
1778        match result {
1779            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1780            QueryResult::CommandOk { .. } => {
1781                // No RETURNING — emit a sentinel single-column
1782                // schema with zero rows so the alias is defined.
1783                let placeholder = spg_storage::ColumnSchema::new(
1784                    alloc::format!("{cte_name}_returning_absent"),
1785                    spg_storage::DataType::Text,
1786                    true,
1787                );
1788                Ok((alloc::vec![placeholder], Vec::new()))
1789            }
1790        }
1791    }
1792
1793    /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1794    /// as INSERT above.
1795    fn exec_modifying_cte_update(
1796        &mut self,
1797        cte_name: &str,
1798        body: &spg_sql::ast::UpdateStatement,
1799        cancel: CancelToken<'_>,
1800    ) -> Result<
1801        (
1802            Vec<spg_storage::ColumnSchema>,
1803            Vec<spg_storage::Row<'static>>,
1804        ),
1805        EngineError,
1806    > {
1807        let body = body.clone();
1808        let result = self.exec_update_cancel(&body, cancel)?;
1809        match result {
1810            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1811            QueryResult::CommandOk { .. } => {
1812                let placeholder = spg_storage::ColumnSchema::new(
1813                    alloc::format!("{cte_name}_returning_absent"),
1814                    spg_storage::DataType::Text,
1815                    true,
1816                );
1817                Ok((alloc::vec![placeholder], Vec::new()))
1818            }
1819        }
1820    }
1821
1822    /// v7.37.43-T4.4 — execute a DELETE CTE body.
1823    fn exec_modifying_cte_delete(
1824        &mut self,
1825        cte_name: &str,
1826        body: &spg_sql::ast::DeleteStatement,
1827        cancel: CancelToken<'_>,
1828    ) -> Result<
1829        (
1830            Vec<spg_storage::ColumnSchema>,
1831            Vec<spg_storage::Row<'static>>,
1832        ),
1833        EngineError,
1834    > {
1835        let body = body.clone();
1836        let result = self.exec_delete_cancel(&body, cancel)?;
1837        match result {
1838            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1839            QueryResult::CommandOk { .. } => {
1840                let placeholder = spg_storage::ColumnSchema::new(
1841                    alloc::format!("{cte_name}_returning_absent"),
1842                    spg_storage::DataType::Text,
1843                    true,
1844                );
1845                Ok((alloc::vec![placeholder], Vec::new()))
1846            }
1847        }
1848    }
1849
1850    /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1851    fn exec_modifying_cte_merge(
1852        &mut self,
1853        cte_name: &str,
1854        body: &spg_sql::ast::MergeStatement,
1855        cancel: CancelToken<'_>,
1856    ) -> Result<
1857        (
1858            Vec<spg_storage::ColumnSchema>,
1859            Vec<spg_storage::Row<'static>>,
1860        ),
1861        EngineError,
1862    > {
1863        let body = body.clone();
1864        let result = self.exec_merge_cancel(&body, cancel)?;
1865        match result {
1866            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1867            QueryResult::CommandOk { .. } => {
1868                let placeholder = spg_storage::ColumnSchema::new(
1869                    alloc::format!("{cte_name}_returning_absent"),
1870                    spg_storage::DataType::Text,
1871                    true,
1872                );
1873                Ok((alloc::vec![placeholder], Vec::new()))
1874            }
1875        }
1876    }
1877
1878    /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1879    /// UNION (or UNION ALL) of an anchor that does not reference
1880    /// the CTE name, and one or more recursive terms that do. The
1881    /// anchor runs first; each subsequent iteration runs the
1882    /// recursive term against a temp catalog where the CTE name is
1883    /// bound to the *previous* iteration's output. Iteration stops
1884    /// when the recursive term yields no rows; UNION (DISTINCT)
1885    /// deduplicates against the accumulated result, UNION ALL does
1886    /// not. A hard cap on total rows prevents runaway queries.
1887    #[allow(clippy::too_many_lines)]
1888    pub(crate) fn materialise_recursive_cte(
1889        &self,
1890        cte: &spg_sql::ast::Cte,
1891        base_catalog: &Catalog,
1892        cancel: CancelToken<'_>,
1893    ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1894        const MAX_TOTAL_ROWS: usize = 1_000_000;
1895        const MAX_ITERATIONS: usize = 100_000;
1896        cancel.check()?;
1897        // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1898        // a modifying recursive CTE is parser-rejectable but we
1899        // guard here defensively.
1900        let body_select = cte.body.as_select().ok_or_else(|| {
1901            EngineError::Unsupported(alloc::format!(
1902                "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1903                cte.name
1904            ))
1905        })?;
1906        if body_select.unions.is_empty() {
1907            return Err(EngineError::Unsupported(alloc::format!(
1908                "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1909                cte.name
1910            )));
1911        }
1912        // Anchor: the body's leading SELECT, with unions stripped.
1913        let mut anchor = body_select.clone();
1914        let all_union_terms = core::mem::take(&mut anchor.unions);
1915        anchor.ctes = Vec::new();
1916        // v7.37 D.42 — split the UNION members: those that do NOT reference the
1917        // CTE are additional ANCHOR terms, only the ones that do recurse. A
1918        // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1919        // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1920        // treating the non-recursive `SELECT r2` as a recursive term made it
1921        // re-emit its constant row every iteration → runaway loop.
1922        let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1923            .into_iter()
1924            .partition(|(_, t)| !select_refers_to(t, &cte.name));
1925        let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1926        let QueryResult::Rows {
1927            columns: anchor_cols,
1928            rows: mut anchor_rows,
1929        } = anchor_result
1930        else {
1931            return Err(EngineError::Unsupported(alloc::format!(
1932                "WITH RECURSIVE {:?}: anchor did not return rows",
1933                cte.name
1934            )));
1935        };
1936        // Append every non-recursive UNION member's rows to the anchor set.
1937        for (_, term) in &anchor_terms {
1938            let mut term = term.clone();
1939            term.ctes = Vec::new();
1940            if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
1941                anchor_rows.extend(rows);
1942            }
1943        }
1944        // The projection builder labels non-column expressions Text;
1945        // refine column types from the anchor's actual values so the
1946        // intermediate iter-catalog tables accept them.
1947        let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
1948        if !cte.column_overrides.is_empty() {
1949            if cte.column_overrides.len() != columns.len() {
1950                return Err(EngineError::Unsupported(alloc::format!(
1951                    "CTE {:?} column list has {} names but anchor returns {} columns",
1952                    cte.name,
1953                    cte.column_overrides.len(),
1954                    columns.len()
1955                )));
1956            }
1957            for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1958                col.name.clone_from(name);
1959            }
1960        }
1961        let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
1962        let mut working_set: Vec<Row<'static>> = anchor_rows;
1963        let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
1964        // Track at least one "all UNION ALL" flag — if every union
1965        // kind is ALL we skip the dedup step (faster + matches PG).
1966        let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
1967        if !all_union_all {
1968            for r in &all_rows {
1969                seen.insert(encode_row_key(r));
1970            }
1971        }
1972        // v7.39 (round 598) — the engine and its catalog are built ONCE.
1973        // Each iteration used to clone the catalog, create the CTE table,
1974        // and construct a whole `Engine` — which initialises 82 fields — to
1975        // hold that round's working set. A counting allocator put the loop
1976        // at 63 allocations and 104 kB per iteration, or 1 GB for a
1977        // 10,000-row recursive CTE, and none of it varied with how much
1978        // else was in the catalog: the per-round rebuild WAS the cost. The
1979        // table is emptied and refilled instead.
1980        let mut iter_catalog = base_catalog.clone();
1981        let schema = TableSchema::new(cte.name.clone(), columns.clone());
1982        iter_catalog
1983            .create_table(schema)
1984            .map_err(EngineError::Storage)?;
1985        let mut iter_engine = Engine::restore(iter_catalog);
1986        if let Some(c) = self.clock {
1987            iter_engine = iter_engine.with_clock(c);
1988        }
1989        if let Some(f) = self.salt_fn {
1990            iter_engine = iter_engine.with_salt_fn(f);
1991        }
1992        // The recursive terms are cloned once too — the clone stripped the
1993        // CTE list off each of them, per term per iteration.
1994        let recursive_terms: Vec<SelectStatement> = union_terms
1995            .iter()
1996            .map(|(_, t)| {
1997                let mut t = t.clone();
1998                t.ctes = Vec::new();
1999                t
2000            })
2001            .collect();
2002        // v7.39 (round 618) — plan every recursive term once. Taken only if
2003        // ALL of them plan, so a query never runs half on each path.
2004        let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2005            .iter()
2006            .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2007            .collect();
2008        let fast_ctx = term_plans.as_ref().map(|plans| {
2009            let alias = plans[0].alias.clone();
2010            (alias, ())
2011        });
2012        for iter in 0..MAX_ITERATIONS {
2013            cancel.check()?;
2014            if working_set.is_empty() {
2015                break;
2016            }
2017            if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2018                // The worktable IS the working set: no table to empty and
2019                // refill, and no query execution per round.
2020                let mut next_set: Vec<Row<'static>> = Vec::new();
2021                for plan in plans {
2022                    let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2023                    for row in &working_set {
2024                        cancel.check()?;
2025                        if let Some(w) = plan.where_ {
2026                            let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2027                            if !matches!(v, Value::Bool(true)) {
2028                                continue;
2029                            }
2030                        }
2031                        let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2032                        for it in &plan.items {
2033                            vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2034                        }
2035                        let out = Row::new(vals);
2036                        if !all_union_all {
2037                            let key = encode_row_key(&out);
2038                            if !seen.insert(key) {
2039                                continue;
2040                            }
2041                        }
2042                        next_set.push(out);
2043                    }
2044                }
2045                if next_set.is_empty() {
2046                    break;
2047                }
2048                all_rows.extend(next_set.iter().cloned());
2049                working_set = next_set;
2050                if all_rows.len() > MAX_TOTAL_ROWS {
2051                    return Err(EngineError::Unsupported(alloc::format!(
2052                        "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2053                        cte.name
2054                    )));
2055                }
2056                if iter + 1 == MAX_ITERATIONS {
2057                    return Err(EngineError::Unsupported(alloc::format!(
2058                        "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2059                        cte.name
2060                    )));
2061                }
2062                continue;
2063            }
2064            {
2065                // Truncated rather than dropped and recreated: the table's
2066                // own structure is what dropping it throws away, and it is
2067                // identical every round.
2068                let cat = iter_engine.base_catalog_mut();
2069                let table = cat.get_mut(&cte.name).expect("created above");
2070                table.truncate();
2071                for row in &working_set {
2072                    table.insert(row.clone()).map_err(EngineError::Storage)?;
2073                }
2074            }
2075            // Run each recursive term in sequence and collect new rows.
2076            let mut next_set: Vec<Row<'static>> = Vec::new();
2077            for term in &recursive_terms {
2078                let r = iter_engine.exec_select_cancel(term, cancel)?;
2079                let QueryResult::Rows {
2080                    columns: rc,
2081                    rows: rs,
2082                } = r
2083                else {
2084                    return Err(EngineError::Unsupported(alloc::format!(
2085                        "WITH RECURSIVE {:?}: recursive term did not return rows",
2086                        cte.name
2087                    )));
2088                };
2089                if rc.len() != columns.len() {
2090                    return Err(EngineError::Unsupported(alloc::format!(
2091                        "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2092                        cte.name,
2093                        rc.len(),
2094                        columns.len()
2095                    )));
2096                }
2097                for row in rs {
2098                    if !all_union_all {
2099                        let key = encode_row_key(&row);
2100                        if !seen.insert(key) {
2101                            continue;
2102                        }
2103                    }
2104                    next_set.push(row);
2105                }
2106            }
2107            if next_set.is_empty() {
2108                break;
2109            }
2110            all_rows.extend(next_set.iter().cloned());
2111            working_set = next_set;
2112            if all_rows.len() > MAX_TOTAL_ROWS {
2113                return Err(EngineError::Unsupported(alloc::format!(
2114                    "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2115                    cte.name
2116                )));
2117            }
2118            if iter + 1 == MAX_ITERATIONS {
2119                return Err(EngineError::Unsupported(alloc::format!(
2120                    "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2121                    cte.name
2122                )));
2123            }
2124        }
2125        Ok((columns, all_rows))
2126    }
2127
2128    pub(crate) fn resolve_select_subqueries(
2129        &self,
2130        stmt: &mut SelectStatement,
2131        cancel: CancelToken<'_>,
2132    ) -> Result<(), EngineError> {
2133        for item in &mut stmt.items {
2134            if let SelectItem::Expr { expr, alias } = item {
2135                // An UNCORRELATED subquery is replaced by its value right
2136                // here, and the shape the column was named for goes with
2137                // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2138                // boolean literal, so SPG answered `?column?` where PG18
2139                // answers `exists`. Only a subquery at the TOP of the item
2140                // loses its name this way — one nested inside a call still
2141                // reports the call.
2142                if alias.is_none()
2143                    && matches!(
2144                        expr,
2145                        Expr::ScalarSubquery(_)
2146                            | Expr::Exists { .. }
2147                            | Expr::InSubquery { .. }
2148                            | Expr::RowInSubquery { .. }
2149                            | Expr::RowCmpSubquery { .. }
2150                    )
2151                {
2152                    *alias = Some(default_output_name(expr, self.backslash_escapes));
2153                }
2154                self.resolve_expr_subqueries(expr, cancel)?;
2155            }
2156        }
2157        if let Some(w) = &mut stmt.where_ {
2158            self.resolve_expr_subqueries(w, cancel)?;
2159        }
2160        // v7.24.1 — JOIN ON conditions can carry subqueries too;
2161        // they were never walked, so even an UNCORRELATED subquery
2162        // in ON hit "subquery reached row eval".
2163        if let Some(from) = &mut stmt.from {
2164            for j in &mut from.joins {
2165                if let Some(on) = &mut j.on {
2166                    self.resolve_expr_subqueries(on, cancel)?;
2167                }
2168            }
2169        }
2170        if let Some(gs) = &mut stmt.group_by {
2171            for g in gs {
2172                self.resolve_expr_subqueries(g, cancel)?;
2173            }
2174        }
2175        if let Some(h) = &mut stmt.having {
2176            self.resolve_expr_subqueries(h, cancel)?;
2177        }
2178        for o in &mut stmt.order_by {
2179            self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2180        }
2181        for (_, peer) in &mut stmt.unions {
2182            self.resolve_select_subqueries(peer, cancel)?;
2183        }
2184        Ok(())
2185    }
2186
2187    #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2188    pub(crate) fn resolve_expr_subqueries(
2189        &self,
2190        e: &mut Expr,
2191        cancel: CancelToken<'_>,
2192    ) -> Result<(), EngineError> {
2193        // Replace-on-this-node cases first.
2194        if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2195            *e = replacement;
2196            return Ok(());
2197        }
2198        match e {
2199            Expr::NamedArg { expr, .. } => self.resolve_expr_subqueries(expr, cancel)?,
2200            Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2201            Expr::AggregateOrdered { call, order_by, .. } => {
2202                self.resolve_expr_subqueries(call, cancel)?;
2203                for o in order_by.iter_mut() {
2204                    self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2205                }
2206            }
2207            Expr::Binary { lhs, rhs, .. } => {
2208                self.resolve_expr_subqueries(lhs, cancel)?;
2209                self.resolve_expr_subqueries(rhs, cancel)?;
2210            }
2211            Expr::Unary { expr, .. }
2212            | Expr::Cast { expr, .. }
2213            | Expr::IsNull { expr, .. }
2214            | Expr::BoolTest { expr, .. }
2215            | Expr::FieldAccess { base: expr, .. } => {
2216                self.resolve_expr_subqueries(expr, cancel)?;
2217            }
2218            Expr::FunctionCall { args, .. } => {
2219                for a in args {
2220                    self.resolve_expr_subqueries(a, cancel)?;
2221                }
2222            }
2223            Expr::Like { expr, pattern, .. } => {
2224                self.resolve_expr_subqueries(expr, cancel)?;
2225                self.resolve_expr_subqueries(pattern, cancel)?;
2226            }
2227            Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2228            // v4.12 window functions — recurse into args + ORDER BY
2229            // + PARTITION BY in case they carry inner subqueries.
2230            Expr::WindowFunction {
2231                args,
2232                partition_by,
2233                order_by,
2234                ..
2235            } => {
2236                for a in args {
2237                    self.resolve_expr_subqueries(a, cancel)?;
2238                }
2239                for p in partition_by {
2240                    self.resolve_expr_subqueries(p, cancel)?;
2241                }
2242                for (e, _, _) in order_by {
2243                    self.resolve_expr_subqueries(e, cancel)?;
2244                }
2245            }
2246            // Subquery nodes are handled in subquery_replacement
2247            // (which returned None — defensive no-op); Literal /
2248            // Column are leaves.
2249            Expr::ScalarSubquery(_)
2250            | Expr::Exists { .. }
2251            | Expr::InSubquery { .. }
2252            | Expr::RowInSubquery { .. }
2253            | Expr::RowCmpSubquery { .. }
2254            | Expr::Literal(_)
2255            | Expr::Placeholder(_)
2256            | Expr::Column(_) => {}
2257            // v7.30.2 — list elements can carry scalar subqueries
2258            // (`x IN (1, (SELECT …))`).
2259            Expr::InList { expr, list, .. } => {
2260                self.resolve_expr_subqueries(expr, cancel)?;
2261                for item in list {
2262                    self.resolve_expr_subqueries(item, cancel)?;
2263                }
2264            }
2265            // v7.10.10 — recurse children.
2266            Expr::Array(items) => {
2267                for elem in items {
2268                    self.resolve_expr_subqueries(elem, cancel)?;
2269                }
2270            }
2271            Expr::ArraySubscript { target, index } => {
2272                self.resolve_expr_subqueries(target, cancel)?;
2273                self.resolve_expr_subqueries(index, cancel)?;
2274            }
2275            Expr::ArraySlice { target, lo, hi } => {
2276                self.resolve_expr_subqueries(target, cancel)?;
2277                if let Some(l) = lo {
2278                    self.resolve_expr_subqueries(l, cancel)?;
2279                }
2280                if let Some(h) = hi {
2281                    self.resolve_expr_subqueries(h, cancel)?;
2282                }
2283            }
2284            Expr::AnyAll { expr, array, .. } => {
2285                self.resolve_expr_subqueries(expr, cancel)?;
2286                // Quantified subquery — an uncorrelated one
2287                // materialises up front; a correlated one stays for
2288                // the per-row resolver.
2289                if let Expr::ScalarSubquery(inner) = array.as_mut() {
2290                    if !crate::subquery::select_is_correlated(inner) {
2291                        let s = (**inner).clone();
2292                        **array = self.materialize_quantified_rows(&s, cancel)?;
2293                    }
2294                } else {
2295                    self.resolve_expr_subqueries(array, cancel)?;
2296                }
2297            }
2298            Expr::Case {
2299                operand,
2300                branches,
2301                else_branch,
2302            } => {
2303                if let Some(o) = operand {
2304                    self.resolve_expr_subqueries(o, cancel)?;
2305                }
2306                for (w, t) in branches {
2307                    self.resolve_expr_subqueries(w, cancel)?;
2308                    self.resolve_expr_subqueries(t, cancel)?;
2309                }
2310                if let Some(e) = else_branch {
2311                    self.resolve_expr_subqueries(e, cancel)?;
2312                }
2313            }
2314        }
2315        Ok(())
2316    }
2317}
2318
2319impl Engine {
2320    /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2321    /// `SelectItem::Wildcard` to all schema columns and
2322    /// `SelectItem::Expr` via the regular eval path.
2323    pub(crate) fn project_row_simple(
2324        &self,
2325        row: &Row<'static>,
2326        items: &[SelectItem],
2327        schema_cols: &[ColumnSchema],
2328        alias: &str,
2329    ) -> Result<Row<'static>, EngineError> {
2330        let ctx = self.ev_ctx(schema_cols, Some(alias));
2331        let cancel = CancelToken::none();
2332        let mut out_vals = Vec::new();
2333        for item in items {
2334            match item {
2335                // In a single-table projection (AS OF SEGMENT / RETURNING) a
2336                // qualified `t.*` covers exactly the same columns as a bare `*`.
2337                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2338                    out_vals.extend(row.values.iter().cloned());
2339                }
2340                SelectItem::Expr { expr, .. } => {
2341                    let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2342                    out_vals.push(v);
2343                }
2344            }
2345        }
2346        Ok(Row::new(out_vals))
2347    }
2348
2349    /// v6.10.2 — derive the output `ColumnSchema` list for an
2350    /// AS OF SEGMENT projection. Wildcards take the full schema;
2351    /// expressions take the alias if present or a synthetic
2352    /// `?column?` (PG convention) otherwise.
2353    pub(crate) fn derive_output_columns(
2354        &self,
2355        items: &[SelectItem],
2356        schema_cols: &[ColumnSchema],
2357        table_alias: &str,
2358    ) -> Vec<ColumnSchema> {
2359        let mut out = Vec::new();
2360        for item in items {
2361            match item {
2362                // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2363                // a single-table projection.
2364                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2365                    out.extend(schema_cols.iter().cloned());
2366                }
2367                SelectItem::Expr { expr, alias } => {
2368                    // Bare column references inherit the schema
2369                    // column's name + type — PG names `RETURNING id`
2370                    // "id" and types it BIGINT, and the sqlx embed
2371                    // path type-checks RowDescription against the
2372                    // Rust target (mailrs embed round-12).
2373                    if let Expr::Column(col) = expr
2374                        && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2375                    {
2376                        let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2377                        let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2378                        // v7.39 (read01 round 54) — carry the enum identity:
2379                        // it lives outside the DataType lattice, so a derived
2380                        // table built from this schema otherwise forgets it and
2381                        // the OUTER `ORDER BY <enum col>` silently sorts by the
2382                        // label's TEXT instead of member order.
2383                        c.user_enum_type = sc.user_enum_type.clone();
2384                        out.push(c);
2385                        continue;
2386                    }
2387                    let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2388                    // v7.30.4 (mailrs round-27, P0) — type the
2389                    // expression with the same inference the SELECT
2390                    // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2391                    // The old Text default broke every typed decode
2392                    // of `RETURNING uidnext - 1 AS uid`: four days
2393                    // of inbound mail indexed nowhere. Inference
2394                    // failure keeps the old Text fallback rather
2395                    // than inventing new error paths here.
2396                    // v7.39 (round 258) — take the enum identity from the
2397                    // same projection build, not just the type: a constant
2398                    // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2399                    // VALUES row lowers to) is an EXPRESSION, so it landed
2400                    // here and the derived table forgot the enum.
2401                    let (ty, nullable) = build_projection(
2402                        core::slice::from_ref(item),
2403                        schema_cols,
2404                        table_alias,
2405                        self.backslash_escapes,
2406                    )
2407                    .ok()
2408                    .and_then(|p| p.into_iter().next())
2409                    .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2410                    out.push(ColumnSchema::new(name, ty, nullable));
2411                }
2412            }
2413        }
2414        out
2415    }
2416
2417    /// v4.5: SELECT with cooperative cancellation. The token is
2418    /// honoured between UNION peers and inside the bare-SELECT row
2419    /// loop; HNSW kNN graph walks and the aggregate executor don't
2420    /// honour it yet (deferred — those paths bound their work
2421    /// internally by `LIMIT k` and `GROUP BY` cardinality).
2422    /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2423    /// its (lowercased) name, or None if the name isn't a virtual view.
2424    /// Callers decide whether to return it directly (`SELECT *`) or stage
2425    /// it as a temp table for the full query pipeline.
2426    fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2427        Some(match name {
2428            "spg_statistic" => self.exec_spg_statistic(),
2429            "spg_stat_replication" => self.exec_spg_stat_replication(),
2430            "spg_stat_segment" => self.exec_spg_stat_segment(),
2431            "spg_memory_stats" => self.exec_spg_memory_stats(),
2432            "spg_stat_query" => self.exec_spg_stat_query(),
2433            "pg_stat_statements" => self.exec_pg_stat_statements(),
2434            "spg_stat_activity" => self.exec_spg_stat_activity(),
2435            "pg_stat_activity" => self.exec_pg_stat_activity(),
2436            "pg_locks" => self.exec_pg_locks(),
2437            "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2438            "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2439            "spg_partition_health" => self.exec_spg_partition_health(),
2440            "spg_audit_chain" => self.exec_spg_audit_chain(),
2441            "spg_audit_verify" => self.exec_spg_audit_verify(),
2442            "spg_table_ddl" => self.exec_spg_table_ddl(),
2443            "spg_role_ddl" => self.exec_spg_role_ddl(),
2444            "spg_database_ddl" => self.exec_spg_database_ddl(),
2445            _ => return None,
2446        })
2447    }
2448
2449    /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2450    /// describes against: this engine's catalog with the view staged as a
2451    /// table, exactly as `exec_select_cancel_as` stages it for a
2452    /// non-bare query.
2453    ///
2454    /// These views never reach the catalog — each is a fixed row set built
2455    /// inside its own `exec_*` — so Describe reported no columns for all
2456    /// seventeen of them. Rows are deliberately not inserted: Describe
2457    /// only needs the shape, and `infer_column_types` reads the rows we
2458    /// already have in hand.
2459    pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2460        let from = stmt.from.as_ref()?;
2461        if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2462            return None;
2463        }
2464        let lower = from.primary.name.to_ascii_lowercase();
2465        let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2466            return None;
2467        };
2468        let mut catalog = self.active_catalog().clone();
2469        let cols = infer_column_types(&columns, &rows);
2470        catalog
2471            .create_table(TableSchema::new(from.primary.name.clone(), cols))
2472            .ok()?;
2473        Some(catalog)
2474    }
2475
2476    pub(crate) fn exec_select_cancel(
2477        &self,
2478        stmt: &SelectStatement,
2479        cancel: CancelToken<'_>,
2480    ) -> Result<QueryResult, EngineError> {
2481        self.exec_select_cancel_as(stmt, cancel, None)
2482    }
2483
2484    /// v7.39 (round 334, V55) — the same read core, authorised as
2485    /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2486    /// function's OWNER: that is the entire point of the form, and without
2487    /// it every definer function failed with "permission denied" on the
2488    /// very table it exists to expose.
2489    /// v7.39 (round 559) — see the call site. `None` for anything but
2490    /// the bare shape, so every other query keeps its old path.
2491    fn try_bare_count_star(
2492        &self,
2493        stmt: &SelectStatement,
2494        as_role: Option<&str>,
2495    ) -> Result<Option<QueryResult>, EngineError> {
2496        use spg_sql::ast::SelectItem;
2497        if as_role.is_some()
2498            || !stmt.ctes.is_empty()
2499            || !stmt.unions.is_empty()
2500            || stmt.where_.is_some()
2501            || stmt.group_by.is_some()
2502            || stmt.having.is_some()
2503            || stmt.distinct
2504            || !stmt.order_by.is_empty()
2505            || stmt.limit.is_some()
2506            || stmt.offset.is_some()
2507            || stmt.items.len() != 1
2508        {
2509            return Ok(None);
2510        }
2511        let Some(from) = &stmt.from else {
2512            return Ok(None);
2513        };
2514        if !from.joins.is_empty()
2515            || stmt.locking.is_some()
2516            || from.primary.lateral_subquery.is_some()
2517            || from.primary.unnest_expr.is_some()
2518            || from.primary.generate_series_args.is_some()
2519            || from.primary.name.is_empty()
2520            || from.primary.name.starts_with("__spg_")
2521        {
2522            return Ok(None);
2523        }
2524        // A partition PARENT holds no rows of its own — they live in the
2525        // children — so its header count is 0 and the ordinary path has
2526        // to fan out. Caught by the partition conformance cases.
2527        //
2528        // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2529        // of them, which is worse: its header count is a real number,
2530        // just not the answer. `SELECT count(*) FROM par` returned 1
2531        // where PG returns 2, because this shortcut fired before the
2532        // fan-out could. The question is "does anything descend from
2533        // this", not "was it declared a partition parent".
2534        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2535            return Ok(None);
2536        }
2537        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2538            return Ok(None);
2539        };
2540        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2541            return Ok(None);
2542        };
2543        if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2544            return Ok(None);
2545        }
2546        // A row-security policy filters rows, so the header count is not
2547        // the answer; the ordinary path applies the policy.
2548        let Some(table) = self.active_catalog().get(&from.primary.name) else {
2549            return Ok(None);
2550        };
2551        if table.schema().row_security {
2552            return Ok(None);
2553        }
2554        // Rows frozen to the cold tier are not in `headers`, so the
2555        // header count would miss them. Caught by the cold-tier e2e.
2556        if table.has_cold_rows_fast() {
2557            return Ok(None);
2558        }
2559        let n = table.count_visible(&self.current_snapshot());
2560        let col = alias.clone().unwrap_or_else(|| String::from("count"));
2561        Ok(Some(QueryResult::Rows {
2562            columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2563            rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2564                i64::try_from(n).unwrap_or(i64::MAX)
2565            )])],
2566        }))
2567    }
2568
2569    /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2570    /// that col>` served from the index, never reading a row.
2571    ///
2572    /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2573    /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2574    /// count (2x at 1k). PG needs its visibility map for this — a heap
2575    /// tuple carries its own visibility, so an index entry alone cannot
2576    /// say whether the row is live, and PG reads the heap for any page
2577    /// the map does not mark all-visible. SPG keeps a header array
2578    /// beside the rows, so the locator answers it directly and there is
2579    /// no map to be stale.
2580    /// v7.39 (round 564) — the shape test, once, for both the
2581    /// materialising scan and the streaming one.
2582    ///
2583    /// Two callers asking the same question in two places is how a fact
2584    /// starts drifting; the answer here is the single copy. Returns the
2585    /// table, the alias the predicate is written against, the projected
2586    /// column's position, and the name the single output column takes.
2587    pub(crate) fn index_only_shape<'s>(
2588        &'s self,
2589        stmt: &'s SelectStatement,
2590    ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2591        use spg_sql::ast::SelectItem;
2592        if !stmt.ctes.is_empty()
2593            || !stmt.unions.is_empty()
2594            || stmt.group_by.is_some()
2595            || stmt.having.is_some()
2596            || stmt.distinct
2597            || stmt.locking.is_some()
2598            || !stmt.order_by.is_empty()
2599            || stmt.limit.is_some()
2600            || stmt.offset.is_some()
2601            || stmt.items.len() != 1
2602        {
2603            return None;
2604        }
2605        let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2606            return None;
2607        };
2608        if !from.joins.is_empty()
2609            || from.primary.lateral_subquery.is_some()
2610            || from.primary.unnest_expr.is_some()
2611            || from.primary.generate_series_args.is_some()
2612            || from.primary.name.is_empty()
2613            || from.primary.name.starts_with("__spg_")
2614        {
2615            return None;
2616        }
2617        // v7.39 (round 645) — see the note on the sibling shortcut above:
2618        // an inheritance parent's own header count is not the answer.
2619        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2620            return None;
2621        }
2622        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2623            return None;
2624        };
2625        let spg_sql::ast::Expr::Column(c) = expr else {
2626            return None;
2627        };
2628        let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2629        if let Some(q) = c.qualifier.as_deref()
2630            && !q.eq_ignore_ascii_case(alias_name)
2631        {
2632            return None;
2633        }
2634        let table = self.active_catalog().get(&from.primary.name)?;
2635        if table.schema().row_security {
2636            return None;
2637        }
2638        let cols = &table.schema().columns;
2639        let pos = cols
2640            .iter()
2641            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2642        let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2643        Some((table, alias_name, pos, out))
2644    }
2645
2646    /// v7.39 (round 565) — would this statement be answered out of the
2647    /// index alone?
2648    ///
2649    /// EXPLAIN has to name the node the executor will actually run, and
2650    /// the only honest way to know is to ask the same two questions the
2651    /// executor asks: the statement's shape, and everything decidable
2652    /// about the scan before it walks. Neither is re-stated here.
2653    pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2654        let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2655            return false;
2656        };
2657        let Some(where_) = stmt.where_.as_ref() else {
2658            return false;
2659        };
2660        crate::index_access::index_only_precheck(
2661            where_,
2662            &table.schema().columns,
2663            table,
2664            alias_name,
2665            pos,
2666        )
2667        .is_some()
2668    }
2669
2670    fn try_index_only_scan(
2671        &self,
2672        stmt: &SelectStatement,
2673    ) -> Result<Option<QueryResult>, EngineError> {
2674        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2675            return Ok(None);
2676        };
2677        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2678        // are not materialised here, and a partition parent's own
2679        // heap/indexes are empty (its rows live in the children).
2680        if !stmt.ctes.is_empty() {
2681            return Ok(None);
2682        }
2683        if let Some(from) = &stmt.from
2684            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2685        {
2686            return Ok(None);
2687        }
2688        let where_ = stmt.where_.as_ref().expect("shape checked it");
2689        let cols = &table.schema().columns;
2690        let Some(values) = crate::index_access::try_index_only_range(
2691            where_,
2692            cols,
2693            table,
2694            alias_name,
2695            &self.current_snapshot(),
2696            pos,
2697        ) else {
2698            return Ok(None);
2699        };
2700        let schema = alloc::vec![ColumnSchema::new(
2701            out_name,
2702            cols[pos].ty,
2703            cols[pos].nullable
2704        )];
2705        Ok(Some(QueryResult::Rows {
2706            columns: schema,
2707            rows: values
2708                .into_iter()
2709                .map(|v| Row::new(alloc::vec![v]))
2710                .collect(),
2711        }))
2712    }
2713
2714    /// v7.39 (round 564) — the same scan, emitting each value instead of
2715    /// building a `Vec<Row>` for the encoder to walk once and drop.
2716    ///
2717    /// A profile of the server serving a 50k-row range put 10.2% of the
2718    /// connection thread's CPU on BUILDING that vector and another 9.7%
2719    /// on dropping it — a fifth of the query, spent allocating and
2720    /// freeing one single-element `Vec` per output row so that the wire
2721    /// encoder could borrow each value for a few nanoseconds. The
2722    /// streaming interface it then hands them to takes `&[Value]`
2723    /// already.
2724    ///
2725    /// Returns `None` when the shape does not apply, so the caller falls
2726    /// back before anything has been emitted.
2727    pub(crate) fn try_index_only_stream<F>(
2728        &self,
2729        stmt: &SelectStatement,
2730        emit: &mut F,
2731    ) -> Result<Option<usize>, EngineError>
2732    where
2733        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2734    {
2735        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2736            return Ok(None);
2737        };
2738        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2739        // are not materialised here, and a partition parent's own
2740        // heap/indexes are empty (its rows live in the children).
2741        if !stmt.ctes.is_empty() {
2742            return Ok(None);
2743        }
2744        if let Some(from) = &stmt.from
2745            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2746        {
2747            return Ok(None);
2748        }
2749        let where_ = stmt.where_.as_ref().expect("shape checked it");
2750        let cols = &table.schema().columns;
2751        let schema = alloc::vec![ColumnSchema::new(
2752            out_name,
2753            cols[pos].ty,
2754            cols[pos].nullable
2755        )];
2756        let snapshot = self.current_snapshot();
2757        // The header goes out only once the walk has agreed to run — a
2758        // shape rejection after it would leave the client with a
2759        // RowDescription for a result that never comes.
2760        let mut wrote_header = false;
2761        let counted = crate::index_access::index_only_range_each(
2762            where_,
2763            cols,
2764            table,
2765            alias_name,
2766            &snapshot,
2767            pos,
2768            &mut |v: spg_storage::Value<'_>| {
2769                if !wrote_header {
2770                    emit(crate::StreamItem::Header(&schema))?;
2771                    wrote_header = true;
2772                }
2773                emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2774            },
2775        );
2776        match counted {
2777            None => Ok(None),
2778            Some(Err(e)) => Err(e),
2779            Some(Ok(n)) => {
2780                if !wrote_header {
2781                    emit(crate::StreamItem::Header(&schema))?;
2782                }
2783                Ok(Some(n))
2784            }
2785        }
2786    }
2787
2788    /// `DISTINCT ON`'s de-duplication, which runs after the inner
2789    /// SELECT has produced its rows.
2790    ///
2791    /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2792    /// reason round 848 established: a debug build gives every branch's
2793    /// locals a slot in the frame whichever branch runs, and this one is
2794    /// eighty lines of hashing, key slicing and survivor sorting that a
2795    /// statement without `DISTINCT ON` never touches. Round 867
2796    /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2797    /// reaches none of it — the segment that had been blamed on
2798    /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2799    #[inline(never)]
2800    fn apply_distinct_on(
2801        &self,
2802        result: QueryResult,
2803        don_hidden: usize,
2804        don_limit: &(
2805            Option<spg_sql::ast::LimitExpr>,
2806            Option<spg_sql::ast::LimitExpr>,
2807        ),
2808        don_top1: usize,
2809        orig_order_by: &[spg_sql::ast::OrderBy],
2810    ) -> Result<QueryResult, EngineError> {
2811        let QueryResult::Rows { columns, rows } = result else {
2812            return Ok(result);
2813        };
2814        // The keys are the hidden trailing columns appended above.
2815        // v7.39 (round 729) — top-1 mode: the trailing columns are the
2816        // DON keys plus the ORDER tail; keep each group's best in one
2817        // hash pass, then sort the SURVIVORS with the original spec.
2818        let mut kept: alloc::vec::Vec<Row<'static>>;
2819        let key_start;
2820        if don_top1 > 0 {
2821            let tail = don_top1 - 1;
2822            key_start = columns.len().saturating_sub(don_hidden + tail);
2823            let ord_start = key_start + don_hidden;
2824            let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2825                .iter()
2826                .map(|o| (o.desc, o.nulls_first))
2827                .collect();
2828            let mysql = self.backslash_escapes;
2829            let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2830                for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2831                    let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2832                    let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2833                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2834                        core::cmp::Ordering::Less => return true,
2835                        core::cmp::Ordering::Greater => return false,
2836                        core::cmp::Ordering::Equal => {}
2837                    }
2838                }
2839                false
2840            };
2841            let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2842            let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2843            let mut keybuf = String::new();
2844            for row in rows {
2845                keybuf.clear();
2846                for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2847                    aggregate::push_canonical_key(&mut keybuf, v);
2848                }
2849                match slot.get(keybuf.as_str()) {
2850                    Some(&i) => {
2851                        if better(&row, &best[i]) {
2852                            best[i] = row;
2853                        }
2854                    }
2855                    None => {
2856                        slot.insert(keybuf.clone(), best.len());
2857                        best.push(row);
2858                    }
2859                }
2860            }
2861            // Survivors sort with the FULL original spec (keys are still
2862            // aboard as hidden columns).
2863            let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2864                .iter()
2865                .map(|o| (o.desc, o.nulls_first))
2866                .collect();
2867            best.sort_by(|a, b| {
2868                for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2869                    let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2870                    let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2871                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2872                        core::cmp::Ordering::Equal => {}
2873                        o => return o,
2874                    }
2875                }
2876                core::cmp::Ordering::Equal
2877            });
2878            for r in &mut best {
2879                r.values.truncate(key_start);
2880            }
2881            kept = best;
2882        } else {
2883            key_start = columns.len().saturating_sub(don_hidden);
2884            let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2885            kept = alloc::vec::Vec::new();
2886            for mut row in rows {
2887                let key: alloc::vec::Vec<Value<'static>> =
2888                    row.values.get(key_start..).unwrap_or(&[]).to_vec();
2889                if seen.iter().any(|k| k == &key) {
2890                    continue;
2891                }
2892                seen.push(key);
2893                row.values.truncate(key_start);
2894                kept.push(row);
2895            }
2896        }
2897        let mut columns = columns;
2898        columns.truncate(key_start);
2899        // PG limits what DISTINCT ON left, not what fed it.
2900        let kept = apply_deferred_limit(kept, don_limit);
2901        Ok(QueryResult::Rows {
2902            columns,
2903            rows: kept,
2904        })
2905    }
2906
2907    pub(crate) fn exec_select_cancel_as(
2908        &self,
2909        stmt: &SelectStatement,
2910        cancel: CancelToken<'_>,
2911        as_role: Option<&str>,
2912    ) -> Result<QueryResult, EngineError> {
2913        // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2914        // <all columns>` is legal PG (the wildcard expands to grouped
2915        // columns); SPG refused the whole shape. Expand the wildcard
2916        // into explicit column refs up front — the aggregate layer's
2917        // existing "must appear in the GROUP BY clause" validation
2918        // then answers PG's sentence for any non-grouped column.
2919        if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2920            return self.exec_select_cancel_as(&expanded, cancel, as_role);
2921        }
2922        // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2923        // a row.
2924        //
2925        // The aggregate layer already short-circuits this to
2926        // `rows.len()`, so the O(1) part was never the problem — the
2927        // cost is UPSTREAM, materialising every visible row so that
2928        // layer can take its length. Measured over pgwire on 500k rows:
2929        // PG18 8.2 ms with two parallel workers, 10.3 ms with
2930        // parallelism off, SPG 16.5 ms — 1.6x slower than a
2931        // single-threaded PG on the commonest aggregate there is, and no
2932        // ledger entry recorded it.
2933        //
2934        // Counting visible HEADERS needs no row at all. PG cannot do
2935        // this: its visibility lives in the heap tuples themselves, so
2936        // it has to read them (that is why its own count(*) is a full
2937        // scan, parallel or not).
2938        // v7.39 (read01 round 57) — the table-privilege gate on the common
2939        // read core. A superuser session returns from it immediately.
2940        // v7.39 (round 529) — resolve an ORDER BY that names an output
2941        // ALIAS. The statement-level pass never reached a SELECT nested in
2942        // a FROM clause, a CTE or a scalar subquery, so the same query
2943        // worked on its own and failed the moment anything wrapped it —
2944        // which is what generated SQL does constantly.
2945        let aliased;
2946        let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
2947            let mut s = stmt.clone();
2948            crate::orderby::resolve_order_by_position(&mut s);
2949            aliased = s;
2950            &aliased
2951        } else {
2952            stmt
2953        };
2954        // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
2955        //
2956        // Its keys were evaluated against the PROJECTED row, so a key that
2957        // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
2958        // ORDER BY g, v DESC`, the canonical "latest row per group" — could
2959        // not be read at all and the query failed. PG evaluates them on the
2960        // input. They are projected as hidden columns here and stripped
2961        // again below, the same way the grouping-set ordering columns
2962        // already travel.
2963        //
2964        // And the dedup ran AFTER the inner statement's LIMIT, so
2965        // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
2966        // PG answers two: the limit had already taken two rows of the same
2967        // group before anything deduplicated them. A paginated DISTINCT ON
2968        // returned short pages, with no error. The limit is deferred to
2969        // after the dedup, which is PG's order.
2970        let don_stmt;
2971        // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
2972        // order spec (the rewritten stmt's is emptied).
2973        let orig_order_by = stmt.order_by.clone();
2974        let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
2975            (stmt, 0, (None, None), 0usize)
2976        } else {
2977            let mut s = stmt.clone();
2978            let hidden = s.distinct_on.len();
2979            for (i, e) in stmt.distinct_on.iter().enumerate() {
2980                s.items.push(SelectItem::Expr {
2981                    expr: e.clone(),
2982                    alias: Some(alloc::format!("__distinct_on_{i}")),
2983                });
2984            }
2985            // v7.39 (round 729) — group-top-1 short circuit. When the
2986            // DISTINCT ON keys are exactly the ORDER BY's leading keys,
2987            // the answer is "per group, the row that wins the remaining
2988            // order" — a single O(n) hash pass. The old path sorted the
2989            // ENTIRE input first (500k rows, ~180 ms on the panel cell)
2990            // to keep 100. The inner query runs UNSORTED with every
2991            // order key appended as a hidden column; the dedup below
2992            // keeps each group's best, then sorts the SURVIVORS.
2993            // Declared-collation order keys stay on the sorting path
2994            // (the value comparator here is collation-blind).
2995            let prefix_matches = s.order_by.len() >= hidden
2996                && stmt
2997                    .distinct_on
2998                    .iter()
2999                    .zip(s.order_by.iter())
3000                    .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3001            let colls_plain =
3002                crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3003                    .map(|cs| cs.iter().all(Option::is_none))
3004                    .unwrap_or(false);
3005            let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3006                let tail = s.order_by.len() - hidden;
3007                for (j, o) in s.order_by[hidden..].iter().enumerate() {
3008                    s.items.push(SelectItem::Expr {
3009                        expr: o.expr.clone(),
3010                        alias: Some(alloc::format!("__don_ord_{j}")),
3011                    });
3012                }
3013                // Carry the tail's direction flags through the aliases'
3014                // ORDER; the survivors re-sort below with the full spec.
3015                s.order_by = Vec::new();
3016                tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3017            } else {
3018                0
3019            };
3020            // Only a folded literal is deferred; a placeholder or an
3021            // expression keeps the path it has today rather than being
3022            // resolved a second way here.
3023            let deferrable = matches!(
3024                (&s.limit, &s.offset),
3025                (
3026                    None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3027                    None | Some(spg_sql::ast::LimitExpr::Literal(_))
3028                )
3029            );
3030            let deferred = if deferrable {
3031                (s.limit.take(), s.offset.take())
3032            } else {
3033                (None, None)
3034            };
3035            don_stmt = s;
3036            (&don_stmt, hidden, deferred, top1_tail)
3037        };
3038        self.acl_check_select_as(stmt, as_role)?;
3039        validate_aggregate_placement(stmt)?;
3040        // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3041        // privilege gate above. Placed before it at first, and the
3042        // security-definer e2e caught it immediately: a SECURITY INVOKER
3043        // function whose body is `SELECT count(*) FROM t` answered
3044        // instead of being refused, because the fast path never reached
3045        // the check.
3046        if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3047            return Ok(r);
3048        }
3049        // v7.39 (round 560) — an index-only range scan. Same placement
3050        // reasoning as the count above: after the privilege gate.
3051        if let Some(r) = self.try_index_only_scan(stmt)? {
3052            return Ok(r);
3053        }
3054        validate_locking_clause(stmt)?;
3055        let result = self.exec_select_cancel_inner(stmt, cancel)?;
3056        // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3057        // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3058        // They carry the per-branch mask through the UNION-ALL sort and must not
3059        // appear in the output. Stripped per SELECT level (grouping-set queries
3060        // are often wrapped in a derived subquery), before DISTINCT ON.
3061        let result = strip_synthetic_order_cols(result);
3062        // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3063        // rows arrive here already ORDER BY'd; keep the FIRST row of
3064        // each group the expressions define (PG semantics). The
3065        // expressions evaluate against the projected schema — an
3066        // expression that isn't in the select list errors honestly.
3067        if stmt.distinct_on.is_empty() {
3068            return Ok(result);
3069        }
3070        self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3071    }
3072
3073    /// The UNION chain: execute the head as a bare block, then fold each
3074    /// peer in with left-associative dedup.
3075    ///
3076    /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3077    /// reason round 848 established. A statement with no unions returns
3078    /// one line above the call — and every nested subquery on a deep
3079    /// path is such a statement, so each level of the recursion carried
3080    /// 170 lines of locals it could not reach. Round 867 measured that
3081    /// frame at 34,800 bytes, the largest single one on the descent,
3082    /// after two earlier attributions had blamed its caller and then its
3083    /// callee: the gap between two marks is the frame of everything
3084    /// BETWEEN them, and this function had no mark of its own.
3085    #[inline(never)]
3086    fn exec_union_chain(
3087        &self,
3088        stmt_ref: &SelectStatement,
3089        stmt: &SelectStatement,
3090        cancel: CancelToken<'_>,
3091    ) -> Result<QueryResult, EngineError> {
3092        // UNION path: clone-strip the head into a bare block (its own
3093        // DISTINCT and any inner ORDER BY are dropped by parser rule —
3094        // the wrapper SelectStatement carries them), execute, then chain
3095        // peers with left-associative dedup semantics.
3096        // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3097        // output columns; a position past their count is PG's 42P10.
3098        crate::orderby::check_order_by_positions(stmt_ref)?;
3099        let mut head_unknown = branch_unknown_mask(stmt_ref);
3100        let head_regcast = branch_regcast_mask(stmt_ref);
3101        let mut head = stmt_ref.clone();
3102        head.unions = Vec::new();
3103        head.order_by = Vec::new();
3104        head.limit = None;
3105        let QueryResult::Rows {
3106            mut columns,
3107            mut rows,
3108        } = self.exec_bare_select_cancel(&head, cancel)?
3109        else {
3110            unreachable!("bare SELECT cannot return CommandOk")
3111        };
3112        for (kind, peer) in &stmt_ref.unions {
3113            // v7.37.17 (17.6 siblings) — a peer carrying its own
3114            // unions is a nested INTERSECT group (the parser's
3115            // precedence regrouping); recurse through the
3116            // union-aware wrapper for it.
3117            let peer_result = if peer.unions.is_empty() {
3118                self.exec_bare_select_cancel(peer, cancel)?
3119            } else {
3120                self.exec_select_cancel(peer, cancel)?
3121            };
3122            let QueryResult::Rows {
3123                columns: peer_cols,
3124                rows: mut peer_rows,
3125            } = peer_result
3126            else {
3127                unreachable!("bare SELECT cannot return CommandOk")
3128            };
3129            if peer_cols.len() != columns.len() {
3130                // v7.39 (round 232) — PG's wording, which clients match on.
3131                return Err(EngineError::Unsupported(alloc::format!(
3132                    "each {} query must have the same number of columns",
3133                    set_op_name(*kind)
3134                )));
3135            }
3136            // v7.39 (round 232+233) — PG resolves each result column to one
3137            // type before it merges anything, and refuses the query when the
3138            // two branches have no common type. SPG's unifier
3139            // (`unify_union_columns`) is value-driven and deliberately
3140            // conservative — "a column where any cell fails to coerce is left
3141            // exactly as it was" — so a mismatch produced a column holding
3142            // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3143            // back with integers and text interleaved) instead of an error.
3144            //
3145            // The check has to read the branch ASTs, not just their schemas:
3146            // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3147            // as TEXT and is indistinguishable from a real text column by
3148            // schema alone — yet PG treats the two completely differently
3149            // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3150            // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3151            let peer_unknown = branch_unknown_mask(peer);
3152            let peer_regcast = branch_regcast_mask(peer);
3153            for i in 0..columns.len() {
3154                let hu = head_unknown.get(i).copied().unwrap_or(false);
3155                let pu = peer_unknown.get(i).copied().unwrap_or(false);
3156                let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3157                let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3158                    || head_regcast.get(i).copied().unwrap_or(false);
3159                match (hu, pu) {
3160                    // Both sides carry a real type: they must share a category.
3161                    (false, false) => {
3162                        if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3163                            return Err(EngineError::Unsupported(alloc::format!(
3164                                "{} types {} and {} cannot be matched",
3165                                set_op_name(*kind),
3166                                crate::conversions::pg_type_name_for_error(ht),
3167                                crate::conversions::pg_type_name_for_error(pt),
3168                            )));
3169                        }
3170                    }
3171                    // One side is an untyped literal: it takes the other's
3172                    // type, and failing to convert is the error PG reports.
3173                    (true, false) => {
3174                        coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3175                        columns[i].ty = pt;
3176                        head_unknown[i] = false;
3177                    }
3178                    (false, true) => {
3179                        coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3180                    }
3181                    // Both untyped — nothing to resolve against yet.
3182                    (true, true) => {}
3183                }
3184            }
3185            // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3186            // nullable (PG semantics). Previously the result kept only the head's
3187            // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3188            // non-null `1`) wrongly reported the column NOT NULL, which let
3189            // `count(col)`'s NOT-NULL fast-path count the NULL row.
3190            for (i, pc) in peer_cols.iter().enumerate() {
3191                if pc.nullable {
3192                    columns[i].nullable = true;
3193                }
3194            }
3195            // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3196            // text by the session collation (CI + accent + PAD SPACE), like
3197            // GROUP BY. PG stays byte-exact.
3198            let mysql = self.backslash_escapes;
3199            // v7.38.13 — RESIDUAL, recorded rather than faked: a set
3200            // operation over a byte-wise column has the same folding hole
3201            // DISTINCT had, and this site has no output columns in scope
3202            // to build a mask from. Behaviour here is unchanged.
3203            let fold = FoldSpec::dialect(mysql);
3204            match kind {
3205                UnionKind::All => rows.extend(peer_rows),
3206                UnionKind::Distinct => {
3207                    rows.extend(peer_rows);
3208                    rows = dedup_rows(rows, fold);
3209                }
3210                // v7.37.17 (17.6 siblings) — PG set semantics.
3211                // v7.39 (round 591) — all four ask the same question of the
3212                // right side, and all four used to answer it by scanning it
3213                // once per left row. `PeerIndex` buckets it by the hash
3214                // DISTINCT already uses, so the answer is a lookup.
3215                // INTERSECT: distinct rows present on both sides.
3216                UnionKind::Intersect => {
3217                    let idx = PeerIndex::build(&peer_rows, fold);
3218                    rows = dedup_rows(rows, fold)
3219                        .into_iter()
3220                        .filter(|r| idx.contains(r))
3221                        .collect();
3222                }
3223                // INTERSECT ALL: multiset intersection — each row
3224                // keeps min(left count, right count) occurrences.
3225                UnionKind::IntersectAll => {
3226                    let mut idx = PeerIndex::build(&peer_rows, fold);
3227                    let mut kept: Vec<Row<'static>> = Vec::new();
3228                    for r in rows {
3229                        if idx.take_one(&r) {
3230                            kept.push(r);
3231                        }
3232                    }
3233                    rows = kept;
3234                }
3235                // EXCEPT: distinct left rows absent from the right.
3236                UnionKind::Except => {
3237                    let idx = PeerIndex::build(&peer_rows, fold);
3238                    rows = dedup_rows(rows, fold)
3239                        .into_iter()
3240                        .filter(|r| !idx.contains(r))
3241                        .collect();
3242                }
3243                // EXCEPT ALL: multiset subtraction — each right
3244                // occurrence cancels one left occurrence.
3245                UnionKind::ExceptAll => {
3246                    let mut idx = PeerIndex::build(&peer_rows, fold);
3247                    let mut kept: Vec<Row<'static>> = Vec::new();
3248                    for r in rows {
3249                        if !idx.take_one(&r) {
3250                            kept.push(r);
3251                        }
3252                    }
3253                    rows = kept;
3254                }
3255            }
3256        }
3257        // PG resolves a UNION / VALUES result column to one common type
3258        // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3259        // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3260        // built each branch independently, leaving mixed-type columns
3261        // that broke ORDER BY, comparisons, and value-based window
3262        // frames. Unify + coerce before the combined ORDER BY sees them.
3263        unify_union_columns(&mut columns, &mut rows);
3264        // ORDER BY at the top of a UNION applies to the combined result.
3265        // Eval against the projected schema (NOT the source table).
3266        if !stmt.order_by.is_empty() {
3267            // v7.39 (read01 round 54) — the combined-result ctx must carry the
3268            // catalog, and the projected columns must keep their enum identity
3269            // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3270            // by TEXT instead of member order — silently wrong rows, not an
3271            // error. (Same shape as the enum-order knife's GROUP BY fix.)
3272            let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3273            // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3274            // survive to here when the head projects a Wildcard (the
3275            // group-tail wrapper shape): map them onto the Nth
3276            // projected column so the combined sort works.
3277            let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3278                .order_by
3279                .iter()
3280                .map(|o| {
3281                    let mut o = o.clone();
3282                    if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3283                        && *n >= 1
3284                        && let Ok(idx) = usize::try_from(*n - 1)
3285                        && idx < columns.len()
3286                    {
3287                        o.expr = Expr::Column(spg_sql::ast::ColumnName {
3288                            qualifier: None,
3289                            name: columns[idx].name.clone(),
3290                        });
3291                    }
3292                    o
3293                })
3294                .collect();
3295            let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3296            let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3297            for r in rows {
3298                let keys = build_order_keys(&resolved_order, &r, &synth_ctx)?;
3299                tagged.push((keys, r));
3300            }
3301            sort_by_keys(&mut tagged, &descs);
3302            rows = tagged.into_iter().map(|(_, r)| r).collect();
3303        }
3304        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3305        Ok(QueryResult::Rows { columns, rows })
3306    }
3307
3308    fn exec_select_cancel_inner(
3309        &self,
3310        stmt: &SelectStatement,
3311        cancel: CancelToken<'_>,
3312    ) -> Result<QueryResult, EngineError> {
3313        cancel.check()?;
3314        // v7.38 P0 元机制 A — first observable point inside the
3315        // planner / executor. Tests use this to inject a delay or
3316        // a cancellation race before any row is produced. Release
3317        // build expands to `let _ = (...);` — zero cost.
3318        crate::injection_point!("planner_first_row_fetch", &stmt.from);
3319        // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3320        // PG analyses every definition, referenced or not, so `SELECT i FROM
3321        // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3322        // succeeded here (the parser used to drop the unreferenced defs
3323        // whole). The check is the CREATE VIEW check's shape (round 700): a
3324        // LIMIT-0 run of the same FROM with the definitions' key
3325        // expressions as the projection — it cannot disagree with what a
3326        // referencing window would have done, because it resolves the same
3327        // names the same way. Zero cost for the ordinary statement: the
3328        // list is empty unless a WINDOW clause left unreferenced defs.
3329        if !stmt.window_check_exprs.is_empty() {
3330            let mut probe = stmt.clone();
3331            probe.items = stmt
3332                .window_check_exprs
3333                .iter()
3334                .map(|e| spg_sql::ast::SelectItem::Expr {
3335                    expr: e.clone(),
3336                    alias: None,
3337                })
3338                .collect();
3339            probe.window_check_exprs = Vec::new();
3340            probe.distinct = false;
3341            probe.distinct_on = Vec::new();
3342            probe.group_by = None;
3343            probe.group_by_all = false;
3344            probe.having = None;
3345            probe.unions = Vec::new();
3346            probe.order_by = Vec::new();
3347            probe.locking = None;
3348            probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3349            probe.offset = None;
3350            probe.limit_with_ties = false;
3351            self.exec_select_cancel_inner(&probe, cancel)?;
3352        }
3353        // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3354        // takes the catalog, so the parser leaves a marker and the rewrite lands
3355        // here: the call moves into a LATERAL FROM item and the item becomes one
3356        // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3357        // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3358        // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3359        // second one.
3360        if let Some(lowered) = self.lower_record_expansion(stmt)? {
3361            return self.exec_select_cancel_inner(&lowered, cancel);
3362        }
3363        // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3364        // FROM / JOIN graph references any catalogued view name,
3365        // re-parse the view body and prepend it as a synthetic
3366        // CTE. Recurses on views-in-views via the regular CTE
3367        // dispatch below. Fast-path: skip the walker entirely when
3368        // the catalog has no views (the typical OLTP load).
3369        if !self.active_catalog().views_all().is_empty() {
3370            if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3371                return self.exec_select_cancel(&rewritten, cancel);
3372            }
3373        }
3374        // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3375        // gets rewritten to a UNION-ALL over the children that overlap
3376        // the WHERE-derived key range. Uses the same CTE-injection
3377        // trick as VIEW expansion above so downstream resolution
3378        // doesn't need a partition-aware code path.
3379        if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3380            return self.exec_select_cancel(&rewritten, cancel);
3381        }
3382        // v7.16.2 — information_schema / pg_catalog virtual
3383        // views (mailrs round-10 A.3). If the SELECT touches a
3384        // synthetic meta-table name (`__spg_info_*` /
3385        // `__spg_pg_*` — produced by the parser for
3386        // `information_schema.X` / `pg_catalog.X`), clone the
3387        // catalog, materialise the requested view as a real
3388        // temporary table, and re-execute against an enriched
3389        // engine. Same pattern as `exec_with_ctes` for CTEs.
3390        if !self.meta_views_materialised && select_references_meta_view(stmt) {
3391            return self.exec_select_with_meta_views(stmt, cancel);
3392        }
3393        // v6.10.2 — cold-tier time-travel short-circuit. When the
3394        // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3395        // dedicated cold-segment scan instead of the regular
3396        // hot+index path. The scope is intentionally narrow for
3397        // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3398        // optionally with a single-column-equality WHERE. JOINs /
3399        // aggregates / ORDER BY / subqueries on top of a time-
3400        // travelled scan are STABILITY § "Out of v6.10".
3401        if let Some(from) = &stmt.from
3402            && let Some(seg_id) = from.primary.as_of_segment
3403        {
3404            return self.exec_select_as_of_segment(stmt, from, seg_id);
3405        }
3406        // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3407        // pre-CTE because they don't read from the catalog and
3408        // shouldn't participate in regular FROM resolution.
3409        // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3410        // short-circuits. A meta-view FROM materialises to a fixed row
3411        // set. For a bare `SELECT *` we return it directly; otherwise we
3412        // stage it as a temp table and run the normal pipeline, so
3413        // projection / WHERE / ORDER BY / aggregates work over these views
3414        // (they were `SELECT *`-only before). A real table shadowing the
3415        // name wins (checked first), which also stops the staged re-run
3416        // from recursing back into meta-view detection.
3417        if let Some(from) = &stmt.from
3418            && from.joins.is_empty()
3419            && self.active_catalog().get(&from.primary.name).is_none()
3420        {
3421            let lower = from.primary.name.to_ascii_lowercase();
3422            if let Some(result) = self.meta_view_result(&lower) {
3423                let bare = stmt.where_.is_none()
3424                    && stmt.group_by.is_none()
3425                    && stmt.having.is_none()
3426                    && stmt.unions.is_empty()
3427                    && stmt.order_by.is_empty()
3428                    && stmt.limit.is_none()
3429                    && stmt.offset.is_none()
3430                    && !stmt.distinct
3431                    && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3432                if bare {
3433                    return Ok(result);
3434                }
3435                if let QueryResult::Rows { columns, rows } = result {
3436                    let mut catalog = self.active_catalog().clone();
3437                    let cols = infer_column_types(&columns, &rows);
3438                    let schema = TableSchema::new(from.primary.name.clone(), cols);
3439                    catalog.create_table(schema).map_err(EngineError::Storage)?;
3440                    let t = catalog
3441                        .get_mut(&from.primary.name)
3442                        .expect("just-created meta-view table must exist");
3443                    for row in rows {
3444                        t.insert(row).map_err(EngineError::Storage)?;
3445                    }
3446                    let mut eng = Engine::restore(catalog);
3447                    if let Some(c) = self.clock {
3448                        eng = eng.with_clock(c);
3449                    }
3450                    if let Some(f) = self.salt_fn {
3451                        eng = eng.with_salt_fn(f);
3452                    }
3453                    // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3454                    // connection identity so `WHERE pid = pg_backend_pid()`
3455                    // matches inside the staged meta-view run.
3456                    if let Some(f) = self.backend_pid_fn {
3457                        eng.set_backend_pid_fn(f);
3458                    }
3459                    return eng.exec_select_cancel(stmt, cancel);
3460                }
3461                return Ok(result);
3462            }
3463        }
3464        // v4.11: CTEs materialise into a temporary enriched catalog
3465        // *before* anything else — the body SELECT can then refer
3466        // to CTE names via the regular FROM-clause resolution.
3467        // Uncorrelated only: each CTE body runs once against the
3468        // current catalog, not against later CTEs' results (left-
3469        // to-right materialisation would relax this, but we keep
3470        // it simple for v4.11 MVP).
3471        if !stmt.ctes.is_empty() {
3472            return self.exec_with_ctes(stmt, cancel);
3473        }
3474        // v4.10: subqueries (uncorrelated) are resolved here, before
3475        // the executor sees the row loop. We clone the statement so
3476        // we can mutate without disturbing the caller's AST — most
3477        // queries pass through with no subquery nodes and the clone
3478        // is cheap; with subqueries the materialisation cost
3479        // dominates anyway.
3480        let mut stmt_owned;
3481        let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3482            stmt_owned = stmt.clone();
3483            // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3484            // aggregate-wrapped correlated scalar subquery whose
3485            // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3486            // executor streams one join instead of splicing a per-row
3487            // subplan. Runs before the per-row/batch resolver, which then
3488            // only sees the subqueries the pull-up left behind.
3489            self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3490            // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3491            // the "per-key latest" scalar subquery shape (inbox / feed
3492            // / timeline applications) becomes a CTE + LEFT JOIN
3493            // against a GROUP BY pre-aggregation that reuses the v7.33
3494            // first_ordered argmax executor. Runs AFTER unique-key
3495            // pull-up (so the unique-key fast path still wins for
3496            // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3497            // Phase 1 (this commit) is skeleton only — no-op pass.
3498            self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3499            // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3500            // sublink pull-up to semi/anti-join, before the resolver gets
3501            // a chance to walk per-row.
3502            self.pull_up_exists_sublinks(&mut stmt_owned);
3503            // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3504            // exec_with_ctes so they materialise once before the body
3505            // SELECT runs. exec_with_ctes strips ctes from the body
3506            // clone, then re-enters select.
3507            if !stmt_owned.ctes.is_empty() {
3508                return self.exec_with_ctes(&stmt_owned, cancel);
3509            }
3510            // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3511            //   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3512            // BEFORE `resolve_select_subqueries` materialises the inner
3513            // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3514            // INSUBQ benchmark). Run the inner once, collect the result
3515            // values into a `HashSet<i64>` directly, then probe A.pk per
3516            // value and tally. Returns `Some` when the shape matches.
3517            if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3518                return Ok(out);
3519            }
3520            self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3521            &stmt_owned
3522        } else {
3523            stmt
3524        };
3525        if stmt_ref.unions.is_empty() {
3526            return self.exec_bare_select_cancel(stmt_ref, cancel);
3527        }
3528        self.exec_union_chain(stmt_ref, stmt, cancel)
3529    }
3530
3531    #[allow(clippy::too_many_lines)]
3532    #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3533    /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3534    /// Synthesises a single-column virtual table whose column type
3535    /// is TEXT and whose rows are the array elements. Routes
3536    /// through the regular projection / WHERE / ORDER BY / LIMIT
3537    /// machinery so set-returning UNNEST composes naturally with
3538    /// the rest of the SELECT surface.
3539    fn exec_select_unnest(
3540        &self,
3541        stmt: &SelectStatement,
3542        primary: &TableRef,
3543        cancel: CancelToken<'_>,
3544    ) -> Result<QueryResult, EngineError> {
3545        let expr = primary
3546            .unnest_expr
3547            .as_deref()
3548            .expect("caller guards unnest_expr.is_some()");
3549        // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3550        // N value columns instead of one; the shared builder does
3551        // the work and the tail below (WHERE / agg / projection)
3552        // runs against the wider schema.
3553        let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3554            match unnest_zip_args(expr) {
3555                Some(args) => Some(unnest_zip_rows(args)?),
3556                None => None,
3557            };
3558        // Evaluate the array expression once. Empty schema / empty
3559        // row — uncorrelated UNNEST cannot reference outer columns.
3560        // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3561        // introspection family (enum_range / enum_first / enum_last) resolves
3562        // its labels from the argument's STATIC enum type against the
3563        // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3564        // fell through to the generic arm, got NULL, and expanded to zero rows
3565        // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3566        // carry the catalog) worked.
3567        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3568        let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3569        let dummy_row = Row::new(alloc::vec::Vec::new());
3570        // v7.11.13 — unnest dispatches per array element type so
3571        // INT[] / BIGINT[] surface their PG types in projection.
3572        // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3573        // columns (PG: lexeme | positions | weights); everything else
3574        // keeps the alias / "unnest" defaults below.
3575        let mut composite_names: Option<&[&str]> = None;
3576        let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3577            if let Some(m) = multi {
3578                m
3579            } else {
3580                // v7.39 (round 236) — flatten a multidimensional array into
3581                // its row-major elements (PG) before the 1-D-only match.
3582                let unnest_src = {
3583                    let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3584                    crate::eval::values::flatten_2d(&v).unwrap_or(v)
3585                };
3586                let mut return_multi: Option<(
3587                    alloc::vec::Vec<DataType>,
3588                    alloc::vec::Vec<Row<'static>>,
3589                )> = None;
3590                let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3591                {
3592                    Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3593                    Value::TextArray(items) => {
3594                        let rows = items
3595                            .into_iter()
3596                            .map(|item| {
3597                                Row::new(alloc::vec![match item {
3598                                    Some(s) => Value::text(s),
3599                                    None => Value::Null,
3600                                }])
3601                            })
3602                            .collect();
3603                        (DataType::Text, rows)
3604                    }
3605                    Value::IntArray(items) => {
3606                        let rows = items
3607                            .into_iter()
3608                            .map(|item| {
3609                                Row::new(alloc::vec![match item {
3610                                    Some(n) => Value::Int(n),
3611                                    None => Value::Null,
3612                                }])
3613                            })
3614                            .collect();
3615                        (DataType::Int, rows)
3616                    }
3617                    Value::BigIntArray(items) => {
3618                        let rows = items
3619                            .into_iter()
3620                            .map(|item| {
3621                                Row::new(alloc::vec![match item {
3622                                    Some(n) => Value::BigInt(n),
3623                                    None => Value::Null,
3624                                }])
3625                            })
3626                            .collect();
3627                        (DataType::BigInt, rows)
3628                    }
3629                    Value::Multirange { kind, ranges } => {
3630                        let rows = ranges
3631                            .iter()
3632                            .map(|sp| {
3633                                Row::new(alloc::vec![Value::Range {
3634                                    kind,
3635                                    lower: sp.lower.clone(),
3636                                    upper: sp.upper.clone(),
3637                                    lower_inc: sp.lower_inc,
3638                                    upper_inc: sp.upper_inc,
3639                                    empty: false,
3640                                }])
3641                            })
3642                            .collect();
3643                        (DataType::Range(kind), rows)
3644                    }
3645                    // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3646                    // one row per lexeme, PG18-measured columns
3647                    // lexeme | positions | weights (`a | {1,3} |
3648                    // {D,D}`); a position-less lexeme (a stripped
3649                    // vector) reads NULL in both array columns.
3650                    Value::TsVector(lexemes) => {
3651                        composite_names = Some(&["lexeme", "positions", "weights"]);
3652                        let rows = lexemes
3653                            .iter()
3654                            .map(|l| {
3655                                let (pos, wts) = if l.positions.is_empty() {
3656                                    (Value::Null, Value::Null)
3657                                } else {
3658                                    let letter = match l.weight {
3659                                        3 => "A",
3660                                        2 => "B",
3661                                        1 => "C",
3662                                        _ => "D",
3663                                    };
3664                                    (
3665                                        Value::SmallIntArray(
3666                                            l.positions
3667                                                .iter()
3668                                                .map(|p| {
3669                                                    Some(i16::try_from(*p).unwrap_or(i16::MAX))
3670                                                })
3671                                                .collect(),
3672                                        ),
3673                                        Value::TextArray(
3674                                            l.positions
3675                                                .iter()
3676                                                .map(|_| Some(letter.into()))
3677                                                .collect(),
3678                                        ),
3679                                    )
3680                                };
3681                                Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3682                            })
3683                            .collect();
3684                        return_multi = Some((
3685                            alloc::vec![
3686                                DataType::Text,
3687                                DataType::SmallIntArray,
3688                                DataType::TextArray
3689                            ],
3690                            rows,
3691                        ));
3692                        (DataType::Text, alloc::vec::Vec::new())
3693                    }
3694                    other => {
3695                        // v7.39 (round 622, S05a) — see table_access.rs:
3696                        // the same sentence, and it is a type mismatch.
3697                        return Err(EngineError::Eval(EvalError::TypeMismatch {
3698                            detail: alloc::format!(
3699                                "unnest() expects an array argument, got {}",
3700                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3701                            ),
3702                        }));
3703                    }
3704                };
3705                if let Some(m) = return_multi {
3706                    m
3707                } else {
3708                    (alloc::vec![elem_dtype], rows)
3709                }
3710            };
3711        let alias = primary
3712            .alias
3713            .clone()
3714            .unwrap_or_else(|| "unnest".to_string());
3715        // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3716        // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3717        // entries map positionally over the value columns. Without
3718        // the column list, a single column falls back to the table
3719        // alias (pre-v7.13.2 behaviour); multi-arg columns default
3720        // to PG's `unnest`.
3721        let n_vals = dtypes.len();
3722        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3723            .iter()
3724            .enumerate()
3725            .map(|(i, dt)| {
3726                let name = primary
3727                    .unnest_column_aliases
3728                    .get(i)
3729                    .cloned()
3730                    .unwrap_or_else(|| {
3731                        if let Some(names) = composite_names {
3732                            names
3733                                .get(i)
3734                                .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3735                        } else if n_vals == 1 {
3736                            alias.clone()
3737                        } else {
3738                            "unnest".to_string()
3739                        }
3740                    });
3741                ColumnSchema::new(name, *dt, true)
3742            })
3743            .collect();
3744        // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3745        // parser desugared a base-type-returning function here (see
3746        // TableRef::scalar_fn_item); the marker rides the column so it survives
3747        // every EvalContext an inner stage rebuilds.
3748        if primary.scalar_fn_item && schema_cols.len() == 1 {
3749            schema_cols[0].scalar_row_source = true;
3750        }
3751        // WITH ORDINALITY — trailing BIGINT counting rows from 1
3752        // in element order. The alias entry after the value
3753        // columns renames it (PG default: `ordinality`).
3754        let rows = if primary.with_ordinality {
3755            let ord_name = primary
3756                .unnest_column_aliases
3757                .get(n_vals)
3758                .cloned()
3759                .unwrap_or_else(|| "ordinality".to_string());
3760            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3761            rows.into_iter()
3762                .enumerate()
3763                .map(|(i, row)| {
3764                    let mut vals = row.values.clone();
3765                    vals.push(Value::BigInt(i as i64 + 1));
3766                    Row::new(vals)
3767                })
3768                .collect()
3769        } else {
3770            rows
3771        };
3772        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3773        // `EvalContext::new` drops it and every catalog-dependent cast
3774        // (regclass / enum / composite / domain) silently degrades.
3775        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3776        // Apply WHERE.
3777        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3778            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3779            for row in rows {
3780                cancel.check()?;
3781                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3782                if matches!(v, Value::Bool(true)) {
3783                    out.push(row);
3784                }
3785            }
3786            out
3787        } else {
3788            rows
3789        };
3790        // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3791        // unnest source. Same routing the relational scan path
3792        // already takes — without it `SELECT COUNT(*) FROM
3793        // unnest(ARRAY[…])` either errored at projection time or
3794        // returned the wrong shape.
3795        if aggregate::uses_aggregate(stmt) {
3796            // v7.29 — a per-query memo so correlated scalar
3797            // subqueries batch-evaluate once (group map) instead of
3798            // executing per group.
3799            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3800            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3801                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3802                    .map_err(|err| match err {
3803                        EngineError::Eval(ev) => ev,
3804                        other => eval::EvalError::TypeMismatch {
3805                            detail: alloc::format!("{other}"),
3806                        },
3807                    })
3808            };
3809            // v7.39 (round 656) — hand the rows over as they are rather than
3810            // collecting a second vector of `RowRef` wrappers. Note this is
3811            // a set-returning-function path, NOT the relational scan: the
3812            // measured O(rows) cost lived in `run_single_table_aggregate`,
3813            // and converting these four first was a miss that cost a full
3814            // round — every test stayed green and the number did not move.
3815            let agg = aggregate::run(
3816                stmt,
3817                crate::join::AggRows::Owned(&filtered),
3818                &schema_cols,
3819                Some(&alias),
3820                Some(&agg_correlated),
3821                self.parallel_runner.0.as_deref(),
3822                Some(self.active_catalog()),
3823                Some(self),
3824            )?;
3825            return self.finish_agg_result(agg, stmt, cancel);
3826        }
3827        // Projection.
3828        let projection =
3829            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
3830        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3831            alloc::vec::Vec::with_capacity(filtered.len());
3832        // v7.19 P5 — Set-Returning-Function in projection
3833        // position (PG `SELECT unnest(arr) FROM t` shape). When a
3834        // SELECT item evaluates to a top-level unnest(arr) call,
3835        // expand it: for each input row, evaluate the array, emit
3836        // one output row per element, broadcasting non-SRF
3837        // projections from the same input row. Multi-SRF + LCM
3838        // padding stays a documented carve-out; mailrs uses
3839        // single-SRF for redirect_uris.
3840        // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3841        // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3842        let srf_idxs = self.srf_target_idxs(&projection);
3843        // v7.39 (round 621) — which input row each output row came from. An
3844        // SRF turns one input row into many, and the ORDER BY below used to
3845        // index the EXPANDED rows by the INPUT row's position: the result was
3846        // silently truncated to the input row count and left unsorted, so
3847        // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3848        // answered three of its six rows, in no order. Without the ORDER BY
3849        // the same query was already right.
3850        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3851        if !srf_idxs.is_empty() {
3852            let (rows, src) =
3853                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3854            projected_rows = rows;
3855            src_of_row = src;
3856        } else {
3857            // v7.24 (round-16 B) — select-list subqueries resolve
3858            // per row (correlated-aware; plain exprs take the fast
3859            // path inside).
3860            let mut proj_memo = memoize::MemoizeCache::default();
3861            for row in &filtered {
3862                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3863                for p in &projection {
3864                    vals.push(self.eval_expr_with_correlated(
3865                        &p.expr,
3866                        row,
3867                        &scan_ctx,
3868                        cancel,
3869                        Some(&mut proj_memo),
3870                    )?);
3871                }
3872                projected_rows.push(Row::new(vals));
3873            }
3874        }
3875        // ORDER BY / LIMIT — apply on the projected rows (cheap;
3876        // unnest result sets are small by design).
3877        let columns: alloc::vec::Vec<ColumnSchema> = projection
3878            .iter()
3879            // v7.39 (read01 round 54) — keep the column's enum identity through
3880            // the projection (it lives outside the DataType lattice), or a
3881            // derived table / UNION / windowed result forgets it and any outer
3882            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
3883            .map(|p| {
3884                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
3885                c.user_enum_type = p.user_enum_type.clone();
3886                c.mysql_fsp = p.mysql_fsp;
3887                c
3888            })
3889            .collect();
3890        // Re-evaluate ORDER BY against the source schema (pre-projection
3891        // so col refs by name still resolve through `scan_ctx`).
3892        // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
3893        // column. Evaluated as an expression it is just the constant N: the same
3894        // key for every row, so the sort ran and changed nothing.
3895        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
3896        if !order_by.is_empty() {
3897            // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
3898            // A key that names a select-list item reads it out of the expanded
3899            // row (PG sorts AFTER the expansion); one that names a source
3900            // column the query does not project is evaluated on the input row
3901            // it came from, which is what `srf_order_output_cols` decides.
3902            let out_cols = if srf_idxs.is_empty() {
3903                alloc::vec![None; order_by.len()]
3904            } else {
3905                srf_order_output_cols(&order_by, &projection)
3906            };
3907            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
3908                .iter()
3909                .enumerate()
3910                .map(|(k, out)| -> Result<_, EngineError> {
3911                    let src = src_of_row.get(k).copied().unwrap_or(k);
3912                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
3913                        .iter()
3914                        .zip(out_cols.iter())
3915                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
3916                        .collect();
3917                    Ok((k, keys?))
3918                })
3919                .collect::<Result<_, _>>()?;
3920            indexed.sort_by(|a, b| {
3921                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
3922                    let o = &order_by[idx];
3923                    let cmp = order_by_value_cmp_in(
3924                        o.desc,
3925                        o.nulls_first,
3926                        ka,
3927                        kb,
3928                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
3929                    );
3930                    if cmp != core::cmp::Ordering::Equal {
3931                        return cmp;
3932                    }
3933                }
3934                core::cmp::Ordering::Equal
3935            });
3936            projected_rows = indexed
3937                .into_iter()
3938                .map(|(i, _)| projected_rows[i].clone())
3939                .collect();
3940        }
3941        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
3942        if stmt.distinct {
3943            projected_rows = dedup_rows(projected_rows, FoldSpec::dialect(scan_ctx.mysql_dialect));
3944        }
3945        // LIMIT / OFFSET — apply at the tail.
3946        if let Some(offset) = stmt.offset_literal() {
3947            let off = (offset as usize).min(projected_rows.len());
3948            projected_rows.drain(..off);
3949        }
3950        if let Some(limit) = stmt.limit_literal() {
3951            projected_rows.truncate(limit as usize);
3952        }
3953        Ok(QueryResult::Rows {
3954            columns,
3955            rows: projected_rows,
3956        })
3957    }
3958
3959    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
3960    /// step])` set-returning source. Mirrors `exec_select_unnest`'s
3961    /// shape: evaluate the arg list once against an empty row,
3962    /// materialise the row stream by stepping start → stop, then
3963    /// route through the standard WHERE / projection / ORDER BY /
3964    /// LIMIT pipeline. Two arg-type combos in v7.17:
3965    ///   * integer / integer [/ integer] — SmallInt, Int, BigInt
3966    ///     (widened to BigInt internally; step defaults to 1)
3967    ///   * timestamp / timestamp / interval — date-range
3968    ///     iteration (mailrs's daily-report pattern)
3969    fn exec_select_generate_series(
3970        &self,
3971        stmt: &SelectStatement,
3972        primary: &TableRef,
3973        cancel: CancelToken<'_>,
3974    ) -> Result<QueryResult, EngineError> {
3975        let args = primary
3976            .generate_series_args
3977            .as_ref()
3978            .expect("caller guards generate_series_args.is_some()");
3979        let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
3980        let alias = primary
3981            .alias
3982            .clone()
3983            .unwrap_or_else(|| "generate_series".to_string());
3984        // `AS t(n)` — the first column-alias entry renames the
3985        // series column (PG semantics); bare alias keeps the
3986        // pre-existing behaviour of naming the column after it.
3987        let col_name = primary
3988            .unnest_column_aliases
3989            .first()
3990            .cloned()
3991            .unwrap_or_else(|| alias.clone());
3992        let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
3993        let mut schema_cols = alloc::vec![col_schema.clone()];
3994        // WITH ORDINALITY — trailing BIGINT counting rows from 1;
3995        // the second column-alias entry renames it.
3996        let rows = if primary.with_ordinality {
3997            let ord_name = primary
3998                .unnest_column_aliases
3999                .get(1)
4000                .cloned()
4001                .unwrap_or_else(|| "ordinality".to_string());
4002            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4003            rows.into_iter()
4004                .enumerate()
4005                .map(|(i, row)| {
4006                    let mut vals = row.values.clone();
4007                    vals.push(Value::BigInt(i as i64 + 1));
4008                    Row::new(vals)
4009                })
4010                .collect()
4011        } else {
4012            rows
4013        };
4014        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4015        // `EvalContext::new` drops it and every catalog-dependent cast
4016        // (regclass / enum / composite / domain) silently degrades.
4017        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4018        // WHERE.
4019        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4020            let mut out = alloc::vec::Vec::with_capacity(rows.len());
4021            for row in rows {
4022                cancel.check()?;
4023                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4024                if matches!(v, Value::Bool(true)) {
4025                    out.push(row);
4026                }
4027            }
4028            out
4029        } else {
4030            rows
4031        };
4032        // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4033        // returning sources. When the SELECT projection contains
4034        // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4035        // …) we route the filtered row stream through the same
4036        // aggregate executor the relational scan path uses, so
4037        // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4038        // a single 100 row instead of erroring at projection
4039        // time. GROUP BY / HAVING / ORDER BY over the aggregate
4040        // output all ride through `aggregate::run`.
4041        if aggregate::uses_aggregate(stmt) {
4042            // v7.29 — a per-query memo so correlated scalar
4043            // subqueries batch-evaluate once (group map) instead of
4044            // executing per group.
4045            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4046            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4047                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4048                    .map_err(|err| match err {
4049                        EngineError::Eval(ev) => ev,
4050                        other => eval::EvalError::TypeMismatch {
4051                            detail: alloc::format!("{other}"),
4052                        },
4053                    })
4054            };
4055            // v7.39 (round 656) — hand the rows over as they are rather than
4056            // collecting a second vector of `RowRef` wrappers. Note this is
4057            // a set-returning-function path, NOT the relational scan: the
4058            // measured O(rows) cost lived in `run_single_table_aggregate`,
4059            // and converting these four first was a miss that cost a full
4060            // round — every test stayed green and the number did not move.
4061            let agg = aggregate::run(
4062                stmt,
4063                crate::join::AggRows::Owned(&filtered),
4064                &schema_cols,
4065                Some(&alias),
4066                Some(&agg_correlated),
4067                self.parallel_runner.0.as_deref(),
4068                Some(self.active_catalog()),
4069                Some(self),
4070            )?;
4071            return self.finish_agg_result(agg, stmt, cancel);
4072        }
4073        // Projection.
4074        let projection =
4075            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
4076        // v7.39 (round 621) — and here, for the same reason.
4077        let srf_idxs = self.srf_target_idxs(&projection);
4078        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4079        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4080            alloc::vec::Vec::with_capacity(filtered.len());
4081        let mut proj_memo = memoize::MemoizeCache::default();
4082        if !srf_idxs.is_empty() {
4083            let (rows, src) =
4084                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4085            projected_rows = rows;
4086            src_of_row = src;
4087        } else {
4088            for row in &filtered {
4089                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4090                for p in &projection {
4091                    // v7.24 (round-16 B) — correlated-aware.
4092                    vals.push(self.eval_expr_with_correlated(
4093                        &p.expr,
4094                        row,
4095                        &scan_ctx,
4096                        cancel,
4097                        Some(&mut proj_memo),
4098                    )?);
4099                }
4100                projected_rows.push(Row::new(vals));
4101            }
4102        }
4103        let columns: alloc::vec::Vec<ColumnSchema> = projection
4104            .iter()
4105            // v7.39 (read01 round 54) — keep the column's enum identity through
4106            // the projection (it lives outside the DataType lattice), or a
4107            // derived table / UNION / windowed result forgets it and any outer
4108            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4109            .map(|p| {
4110                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
4111                c.user_enum_type = p.user_enum_type.clone();
4112                c.mysql_fsp = p.mysql_fsp;
4113                c
4114            })
4115            .collect();
4116        // ORDER BY against the source schema.
4117        // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4118        // more of them than there were inputs), and a positional key means the
4119        // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4120        // and what the other two synthetic-source tails already did.
4121        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4122        if !order_by.is_empty() {
4123            let out_cols = if srf_idxs.is_empty() {
4124                alloc::vec![None; order_by.len()]
4125            } else {
4126                srf_order_output_cols(&order_by, &projection)
4127            };
4128            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4129                .iter()
4130                .enumerate()
4131                .map(|(k, out)| -> Result<_, EngineError> {
4132                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4133                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4134                        .iter()
4135                        .zip(out_cols.iter())
4136                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4137                        .collect();
4138                    Ok((k, keys?))
4139                })
4140                .collect::<Result<_, _>>()?;
4141            indexed.sort_by(|a, b| {
4142                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4143                    let o = &stmt.order_by[idx];
4144                    let cmp = order_by_value_cmp_in(
4145                        o.desc,
4146                        o.nulls_first,
4147                        ka,
4148                        kb,
4149                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4150                    );
4151                    if cmp != core::cmp::Ordering::Equal {
4152                        return cmp;
4153                    }
4154                }
4155                core::cmp::Ordering::Equal
4156            });
4157            projected_rows = indexed
4158                .into_iter()
4159                .map(|(i, _)| projected_rows[i].clone())
4160                .collect();
4161        }
4162        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4163        if stmt.distinct {
4164            projected_rows = dedup_rows(projected_rows, FoldSpec::dialect(scan_ctx.mysql_dialect));
4165        }
4166        if let Some(offset) = stmt.offset_literal() {
4167            let off = (offset as usize).min(projected_rows.len());
4168            projected_rows.drain(..off);
4169        }
4170        if let Some(limit) = stmt.limit_literal() {
4171            projected_rows.truncate(limit as usize);
4172        }
4173        Ok(QueryResult::Rows {
4174            columns,
4175            rows: projected_rows,
4176        })
4177    }
4178
4179    /// The FROM shapes that are not an ordinary table scan — joins, the
4180    /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4181    ///
4182    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4183    /// reason round 848 established in the parser: a debug build gives
4184    /// EVERY branch's locals a slot in the frame, whichever branch runs.
4185    /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4186    /// stacks several of them; a plain scan reaches none of these
4187    /// branches. Moving them out took the frame to 52,336.
4188    ///
4189    /// `Ok(None)` means "not one of these shapes, carry on".
4190    #[inline(never)]
4191    fn try_from_shape_paths(
4192        &self,
4193        stmt: &SelectStatement,
4194        from: &spg_sql::ast::FromClause,
4195        cancel: CancelToken<'_>,
4196    ) -> Result<Option<QueryResult>, EngineError> {
4197        if !from.joins.is_empty() {
4198            // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4199            // elimination: when a LEFT JOIN's right side is referenced
4200            // ONLY in the ON equality and the right-side join key is
4201            // UNIQUE/PK, the join preserves outer cardinality exactly
4202            // and contributes no values used downstream. Drop the
4203            // entire join. PG does this on the
4204            // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4205            // — A's row count is what survives, B never has to be
4206            // touched.
4207            if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4208                return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4209            }
4210            // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4211            // the v7.32 joinfold rewrite that turns inner JOINs into a
4212            // single-table scan when the catalogue can prove key-only
4213            // dependency. Tests use this to assert "without joinfold,
4214            // the join still executes correctly" (joinfold is a
4215            // semantically-equivalent rewrite, not a correctness fix).
4216            if !self.env_cfg().disable_joinfold {
4217                if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4218                    return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4219                }
4220            }
4221            return self.exec_joined_select(stmt, from, cancel).map(Some);
4222        }
4223        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4224        // single-column table at SELECT entry by evaluating the
4225        // expression once against the empty row (UNNEST is
4226        // uncorrelated in v7.11; correlated / LATERAL unnest is a
4227        // v7.12 carve-out). Build a virtual `Table` in a heap-only
4228        // catalog, then route to the regular scan path.
4229        if from.primary.unnest_expr.is_some() {
4230            return self
4231                .exec_select_unnest(stmt, &from.primary, cancel)
4232                .map(Some);
4233        }
4234        // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4235        // returning function. Same dispatch shape as unnest but
4236        // emits a two-column (key TEXT, value TEXT) row stream.
4237        if from.primary.jsonb_each_text_arg.is_some() {
4238            return self
4239                .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4240                .map(Some);
4241        }
4242        // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4243        // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4244        // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4245        // array form. Each function runs; the results zip in LOCKSTEP with the
4246        // shorter padded to NULL — the SAME rule the target-list SRFs follow
4247        // (round 67), which is why `srf_values` is what evaluates each entry.
4248        if from.primary.rows_from.is_some() {
4249            let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4250            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4251                if let Some(col) = schema_cols.get_mut(i) {
4252                    col.name = new_name.clone();
4253                }
4254            }
4255            let alias = from
4256                .primary
4257                .alias
4258                .clone()
4259                .unwrap_or_else(|| from.primary.name.clone());
4260            return self
4261                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4262                .map(Some);
4263        }
4264        // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4265        // COLUMNS (...))`. Materialise the row stream + schema by
4266        // walking the row path, then run the regular pipeline over it.
4267        if let Some(jt) = &from.primary.json_table {
4268            let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4269            let alias = from
4270                .primary
4271                .alias
4272                .clone()
4273                .unwrap_or_else(|| from.primary.name.clone());
4274            return self
4275                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4276                .map(Some);
4277        }
4278        if from.primary.table_fn_call.is_some() {
4279            let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4280            // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4281            // (from 1, in output order) AFTER the function's own columns. The
4282            // alias list names it like any other, which is why it is appended
4283            // BEFORE the renaming pass below.
4284            let rows = if from.primary.with_ordinality {
4285                schema_cols.push(ColumnSchema::new(
4286                    "ordinality".to_string(),
4287                    DataType::BigInt,
4288                    false,
4289                ));
4290                rows.into_iter()
4291                    .enumerate()
4292                    .map(|(i, r)| {
4293                        let mut vals = r.values;
4294                        vals.push(Value::BigInt(i as i64 + 1));
4295                        Row::new(vals)
4296                    })
4297                    .collect()
4298            } else {
4299                rows
4300            };
4301            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4302                if let Some(col) = schema_cols.get_mut(i) {
4303                    col.name = new_name.clone();
4304                }
4305            }
4306            let alias = from
4307                .primary
4308                .alias
4309                .clone()
4310                .unwrap_or_else(|| from.primary.name.clone());
4311            return self
4312                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4313                .map(Some);
4314        }
4315        // v7.37.17 (17.6 siblings) — plain derived table in primary
4316        // position: `FROM ( SELECT … ) alias` (no joins). The inner
4317        // SELECT materialises once (it is uncorrelated by
4318        // construction), then the outer projection / WHERE /
4319        // aggregate / ORDER BY pipeline runs over the synthetic
4320        // table. Joined derived tables keep riding the LATERAL
4321        // machinery in join.rs.
4322        if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4323            // v7.39 (round 727) — flatten first. A simple derived table
4324            // (bare-column projection over one stored table, nothing that
4325            // changes cardinality or order) used to force the inner
4326            // SELECT through the SERIAL row-at-a-time projection pipeline
4327            // just to materialise a synthetic table the outer query then
4328            // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4329            // measured 18.6 ms against PG's 5 — and bare count over the
4330            // same filter WITHOUT the wrapper is 2 ms here, because it
4331            // rides the fused parallel lane. Rewriting to the unwrapped
4332            // form is PG's subquery pull-up; the whole tree gets the
4333            // fast lanes back.
4334            if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4335                return self.exec_select_cancel(&flat, cancel).map(Some);
4336            }
4337            // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4338            // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4339            // ORDER BY never changes the row count, and OFFSET drops
4340            // exactly k. The materialising path sorted 500k rows to
4341            // count 10k (57 ms); PG runs its parallel sort anyway
4342            // (28 ms). The rewrite skips the sort entirely on both
4343            // counts — a plan PG itself does not have.
4344            if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4345                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4346            }
4347            // v7.39 (round 743) — `count(*) OVER a derived whose only
4348            // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4349            // a constant-length array unnests to exactly k rows per
4350            // input row, NULL elements included. PG expands the set to
4351            // count it (6.6 ms on the panel cell); the identity doesn't.
4352            if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4353                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4354            }
4355            return self
4356                .exec_select_derived(stmt, &from.primary, cancel)
4357                .map(Some);
4358        }
4359        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4360        // [, step])` set-returning source. Dispatch mirrors UNNEST:
4361        // materialise the row stream from a single eval pass, then
4362        // run the regular projection / WHERE / ORDER BY / LIMIT
4363        // pipeline over the synthetic single-column table.
4364        if from.primary.generate_series_args.is_some() {
4365            return self
4366                .exec_select_generate_series(stmt, &from.primary, cancel)
4367                .map(Some);
4368        }
4369        Ok(None)
4370    }
4371
4372    /// Pick an index seek for this WHERE, if any of the four apply:
4373    /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4374    ///
4375    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4376    /// frame reason on `try_from_shape_paths`: in a debug build a
4377    /// closure's locals belong to the enclosing frame, and this one is
4378    /// four seek attempts wide on a function that nests.
4379    #[inline(never)]
4380    fn pick_indexed_rows<'r>(
4381        &'r self,
4382        stmt: &SelectStatement,
4383        table: &'r spg_storage::Table,
4384        schema_cols: &[spg_storage::ColumnSchema],
4385        alias: &str,
4386        ctx: &crate::eval::EvalContext<'_>,
4387        seek_snapshot: &crate::Snapshot,
4388    ) -> Option<Vec<Cow<'r, Row<'static>>>> {
4389        stmt.where_.as_ref().and_then(|w| {
4390            // BTree / col=literal seek first — covers the v7.11.3 multi-
4391            // column AND case and the leading-column equality lookup.
4392            try_index_seek(
4393                w,
4394                schema_cols,
4395                self.active_catalog(),
4396                table,
4397                alias,
4398                seek_snapshot,
4399            )
4400            .or_else(|| {
4401                // v7.12.3 — GIN-accelerated `WHERE col @@
4402                // tsquery` when the column has a `USING gin`
4403                // index. Returns an over-approximate candidate
4404                // set; the WHERE re-eval loop below verifies
4405                // the full `@@` predicate per row.
4406                try_gin_seek(
4407                    w,
4408                    schema_cols,
4409                    self.active_catalog(),
4410                    table,
4411                    alias,
4412                    ctx,
4413                    seek_snapshot,
4414                )
4415            })
4416            .or_else(|| {
4417                // v7.15.0 — trigram-GIN-accelerated
4418                // `WHERE col LIKE / ILIKE '<pat>'` when the
4419                // column has a `gin_trgm_ops` GIN index.
4420                // Over-approximate candidate set; the WHERE
4421                // re-eval verifies the LIKE per row.
4422                try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4423            })
4424            .or_else(|| {
4425                // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4426                // accelerated `WHERE col @> <jsonb_literal>`
4427                // when the column has a `USING gin` index. The
4428                // posting-list intersection returns an over-
4429                // approximate candidate set; the WHERE re-eval
4430                // verifies the full `@>` predicate per row.
4431                try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4432            })
4433        })
4434    }
4435
4436    /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4437    /// the two `count(*)` short-circuits. Out-of-line for the frame
4438    /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4439    /// of them, and in a debug build their locals sit in the frame
4440    /// regardless.
4441    #[inline(never)]
4442    fn try_seek_fast_paths(
4443        &self,
4444        stmt: &SelectStatement,
4445        table: &spg_storage::Table,
4446        schema_cols: &[spg_storage::ColumnSchema],
4447        alias: &str,
4448        seek_snapshot: &crate::Snapshot,
4449        cancel: CancelToken<'_>,
4450    ) -> Result<Option<QueryResult>, EngineError> {
4451        if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4452            // NSW kNN dispatches against the hot-tier vector index only
4453            // (vector cells aren't promoted to cold segments), so wrap
4454            // the returned row indices as `Cow::Borrowed` for the
4455            // unified `materialise_in_order` shape.
4456            let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4457                .into_iter()
4458                .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4459                .collect();
4460            return materialise_in_order(
4461                stmt,
4462                schema_cols,
4463                alias,
4464                &ordered,
4465                self.backslash_escapes,
4466            )
4467            .map(Some);
4468        }
4469
4470        // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4471        // the scan via the BTree iterator in the requested direction
4472        // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4473        // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4474        // the load-bearing consumer; this skips the materialise-every-
4475        // row + partial-sort tail entirely. Walker output is already
4476        // in ORDER BY order so `materialise_in_order` (no extra sort)
4477        // is the natural sink.
4478        if let Some(walked) = try_pk_walk_top_n(
4479            stmt,
4480            self.active_catalog(),
4481            table,
4482            schema_cols,
4483            alias,
4484            self,
4485            cancel,
4486        ) {
4487            return materialise_in_order(stmt, schema_cols, alias, &walked, self.backslash_escapes)
4488                .map(Some);
4489        }
4490
4491        // Index seek: if WHERE is `col = literal` (or commuted) and the
4492        // referenced column has an index, dispatch each locator through
4493        // the catalog (hot tier → borrow, cold tier → page-read +
4494        // decode) and iterate just those rows. Otherwise fall back to a
4495        // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4496        // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4497        // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4498        // we don't pay the row materialisation cost twice. Returns
4499        // a bare `Rows{count}` if the shape matches.
4500        if aggregate::uses_aggregate(stmt)
4501            && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4502        {
4503            return Ok(Some(out));
4504        }
4505        // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4506        // locators directly, skipping row materialisation + WHERE re-eval.
4507        if aggregate::uses_aggregate(stmt)
4508            && let Some(out) = self.try_count_star_indexed_range_fast(
4509                stmt,
4510                table,
4511                schema_cols,
4512                alias,
4513                seek_snapshot,
4514            )
4515        {
4516            return Ok(Some(out));
4517        }
4518        Ok(None)
4519    }
4520
4521    /// The two rewrites that must happen before the FROM clause is even
4522    /// looked at: a meta-view reference needs the catalog views
4523    /// materialised, and a windowed projection belongs to the window
4524    /// executor. Out-of-line for the frame reason on
4525    /// `try_from_shape_paths`.
4526    #[inline(never)]
4527    fn try_pre_from_paths(
4528        &self,
4529        stmt: &SelectStatement,
4530        cancel: CancelToken<'_>,
4531    ) -> Result<Option<QueryResult>, EngineError> {
4532        if !self.meta_views_materialised && select_references_meta_view(stmt) {
4533            return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4534        }
4535        // v4.12: window-function path. When the projection contains
4536        // any `name(args) OVER (...)` we route to the dedicated
4537        // executor — partition + sort + per-row window value before
4538        // the regular projection.
4539        if select_has_window(stmt) {
4540            // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4541            // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4542            // needs the aggregation done first, then windows over the grouped
4543            // rows. Rewrite to an aggregate derived subquery + outer window query
4544            // (which the window-over-derived path, D.13, executes). Only fires on
4545            // the currently-erroring agg+window+GROUP BY shape, so it can't
4546            // regress working window-only or aggregate-only queries.
4547            if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4548                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4549            }
4550            return self.exec_select_with_window(stmt, cancel).map(Some);
4551        }
4552        Ok(None)
4553    }
4554
4555    /// A projection naming `ctid` or another system column: the schema
4556    /// has to be widened with them before the scan. Out-of-line for the
4557    /// frame reason on `try_from_shape_paths`.
4558    #[inline(never)]
4559    fn try_ctid_projection(
4560        &self,
4561        stmt: &SelectStatement,
4562        primary: &spg_sql::ast::TableRef,
4563        table: &spg_storage::Table,
4564        schema_cols: &[spg_storage::ColumnSchema],
4565        alias: &str,
4566        cancel: CancelToken<'_>,
4567    ) -> Result<Option<QueryResult>, EngineError> {
4568        if references_ctid(stmt) {
4569            let snapshot = self.current_snapshot();
4570            let mut ext_cols = schema_cols.to_vec();
4571            for name in SYSTEM_COLUMNS {
4572                ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4573            }
4574            let table_oid =
4575                crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4576                    .unwrap_or(0);
4577            let headers = table.headers();
4578            let rows: Vec<Row<'static>> = table
4579                .scan_visible(&snapshot)
4580                .map(|(i, r)| {
4581                    let mut vals = r.values.clone();
4582                    // One block, offsets from 1, as PG numbers them.
4583                    vals.push(Value::Tid(0, i as u32 + 1));
4584                    let h = headers.get(i);
4585                    vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4586                    vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4587                    // SPG keeps no per-statement command ids; PG shows 0 for
4588                    // every row a reader can see, which is every row here.
4589                    vals.push(Value::Cid(0));
4590                    vals.push(Value::Cid(0));
4591                    vals.push(Value::BigInt(table_oid));
4592                    Row::new(vals)
4593                })
4594                .collect();
4595            return self
4596                .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4597                .map(Some);
4598        }
4599        Ok(None)
4600    }
4601
4602    /// A sequence read as a one-row relation (`SELECT last_value FROM
4603    /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4604    /// the frame reason on `try_from_shape_paths`.
4605    #[inline(never)]
4606    fn try_sequence_relation(
4607        &self,
4608        stmt: &SelectStatement,
4609        primary: &spg_sql::ast::TableRef,
4610        cancel: CancelToken<'_>,
4611    ) -> Result<Option<QueryResult>, EngineError> {
4612        if self.active_catalog().get(&primary.name).is_none()
4613            && let Some(seq) = self.active_catalog().sequence(&primary.name)
4614        {
4615            let rows = alloc::vec![Row::new(alloc::vec![
4616                Value::BigInt(seq.last_value),
4617                Value::BigInt(0),
4618                Value::Bool(seq.is_called),
4619            ])];
4620            let schema_cols = alloc::vec![
4621                ColumnSchema::new("last_value", DataType::BigInt, false),
4622                ColumnSchema::new("log_cnt", DataType::BigInt, false),
4623                ColumnSchema::new("is_called", DataType::Bool, false),
4624            ];
4625            let alias = primary
4626                .alias
4627                .clone()
4628                .unwrap_or_else(|| primary.name.clone());
4629            return self
4630                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4631                .map(Some);
4632        }
4633        Ok(None)
4634    }
4635
4636    pub(crate) fn exec_bare_select_cancel(
4637        &self,
4638        stmt: &SelectStatement,
4639        cancel: CancelToken<'_>,
4640    ) -> Result<QueryResult, EngineError> {
4641        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4642        // is meaningless without an ORDER BY; PG raises a hard
4643        // error and SPG mirrors the surface so the same DDL/app
4644        // path behaves identically on cutover.
4645        check_with_ties_requires_order_by(stmt)?;
4646        // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4647        // PG rejects window calls there outright. Checked here rather than
4648        // on the window path: `HAVING row_number() OVER () = 1` has no
4649        // window in its projection at all.
4650        crate::window::reject_window_in_row_clauses(stmt)?;
4651        // v7.39 (round 232) — the ORDER BY legality rules (positional
4652        // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4653        // check: before anything scans.
4654        crate::orderby::check_order_by_legality(stmt)?;
4655        // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4656        // equivalent statement the regular executor handles (merged join
4657        // columns collapse to a single unqualified output column; NATURAL
4658        // gets its common-column ON synthesised). The rewrite clears the
4659        // flags, so this re-entrant call is a no-op on the second pass.
4660        if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4661            return self.exec_bare_select_cancel(&rewritten, cancel);
4662        }
4663        // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4664        // exactly the group keys, IS a DISTINCT and was paying for the
4665        // aggregate executor to find that out. Same placement and shape
4666        // as the desugar above; the rewrite clears `group_by`, so the
4667        // re-entry is a no-op on the second pass. See `baregroup` for
4668        // what the gate rules out.
4669        if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4670            return self.exec_bare_select_cancel(&rewritten, cancel);
4671        }
4672        // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4673        // operand in a security-barrier subquery, then re-enter (the wrapped
4674        // operands are no longer bare RLS tables, so this is a no-op on the
4675        // second pass).
4676        if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4677            return self.exec_bare_select_cancel(&rewritten, cancel);
4678        }
4679        // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4680        // AND the RLS USING predicate into a single-table SELECT's WHERE.
4681        // Superuser sessions and non-RLS tables get `None` (no clone, no
4682        // change). Applied inline (shadowing `stmt`) rather than via re-entry
4683        // so it can't re-inject on a recursive pass.
4684        let rls_stmt;
4685        let stmt = match self.rls_select_predicate(stmt)? {
4686            Some(pred) => {
4687                let mut s = stmt.clone();
4688                s.where_ = Some(match s.where_.take() {
4689                    Some(existing) => spg_sql::ast::Expr::Binary {
4690                        lhs: alloc::boxed::Box::new(existing),
4691                        op: spg_sql::ast::BinOp::And,
4692                        rhs: alloc::boxed::Box::new(pred),
4693                    },
4694                    None => pred,
4695                });
4696                rls_stmt = s;
4697                &rls_stmt
4698            }
4699            None => stmt,
4700        };
4701        // v7.16.2 — same meta-view dispatch as
4702        // `exec_select_cancel`, applied here too because
4703        // `subquery_replacement` enters this function directly
4704        // for Exists / ScalarSubquery / InSubquery resolution
4705        // (bypassing the top-level entry to avoid double
4706        // subquery walking). Without this dispatch the subquery
4707        // hits `__spg_info_columns` and reports TableNotFound.
4708        if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4709            return Ok(done);
4710        }
4711        // Constant SELECT (no FROM) — evaluate each item once against an
4712        // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4713        // `SELECT '7'::INT`. Column references will surface as
4714        // ColumnNotFound on eval since the schema is empty.
4715        let Some(from) = &stmt.from else {
4716            return self.exec_constant_select(stmt);
4717        };
4718        // Multi-table FROM (one or more joined peers) goes through the
4719        // nested-loop join executor. Single-table FROM stays on the
4720        // existing scan + index-seek path.
4721        if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4722            return Ok(done);
4723        }
4724        // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4725        // tested — eight ORDER BY shapes byte-identical spilled against
4726        // in-memory, with 103 runs opened to prove the spill ran — and it
4727        // loses on wall clock, which is a hard stop whatever the memory
4728        // buys. Measured round 865, same psql client both sides, same
4729        // machine, row counts verified, and both sides confirmed to be
4730        // doing an external merge rather than an indexed walk:
4731        //
4732        //   PG18        178.7 - 187.0 ms   Sort Method: external merge, 85 MB
4733        //   SPG spilled 269.7 - 299.6 ms   33 spill files at peak
4734        //
4735        // Non-overlapping, about 1.55x. Re-enable by restoring the call
4736        // below once that closes; nothing else has to change, which is
4737        // the point of it being a separate path.
4738        //
4739        //   if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4740        //       return Ok(done);
4741        //   }
4742        //
4743        // v7.37 (round 882) — this walk stays unhooked, but its streaming
4744        // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4745        // bail in `try_exec_joined_streaming`. Collecting the answer was
4746        // most of what this one cost: handing rows over as the merge
4747        // produces them holds peak to the budget plus one row, and the
4748        // wall clock lands inside PG18's range rather than 1.55x outside
4749        // it. Numbers in `extsort.rs`'s header.
4750        let primary = &from.primary;
4751        // v7.39 (round 244) — a sequence is selectable as a one-row relation
4752        // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4753        // read it). Synthesize PG's three columns.
4754        if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4755            return Ok(done);
4756        }
4757        let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4758            StorageError::TableNotFound {
4759                name: primary.name.clone(),
4760            }
4761        })?;
4762        let schema_cols = &table.schema().columns;
4763        // The qualifier accepted on column refs is the alias (if any) else the
4764        // bare table name.
4765        let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4766        // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4767        // system columns at all: `SELECT ctid FROM t` answered "column
4768        // \"ctid\" does not exist", which takes out the dedup idiom every
4769        // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4770        // GROUP BY key)`.
4771        //
4772        // The value comes from the row's position, which the scan already
4773        // yields; the column is appended to the schema and the rows only
4774        // when the statement asks for it, so nothing else pays for it. That
4775        // also routes the query down the general path, past the index fast
4776        // paths below — they hand back rows without positions, and a ctid
4777        // that was sometimes right would be worse than none.
4778        if let Some(done) =
4779            self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4780        {
4781            return Ok(done);
4782        }
4783        let ctx = self.ev_ctx(schema_cols, Some(alias));
4784
4785        // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4786        // WHERE and an NSW index on `col` skips the full scan. The
4787        // walk returns rows already in ascending-distance order, so
4788        // ORDER BY / LIMIT are honoured implicitly.
4789        // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4790        // and thread it into every index-seek fast path below. No-op
4791        // today (every hot header is committed-alive).
4792        let seek_snapshot = self.current_snapshot();
4793        if let Some(done) =
4794            self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4795        {
4796            return Ok(done);
4797        }
4798        // full scan over the hot tier (cold-tier rows are only reached
4799        // via index seek in v5.1 — full table scans against cold-tier
4800        // data ship in v5.2 with the freezer's per-segment scan API).
4801        let indexed_rows =
4802            self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4803
4804        // Aggregate path: filter rows first, then hand off to the
4805        // aggregate executor which does its own projection + ORDER BY.
4806        if aggregate::uses_aggregate(stmt) {
4807            return self.run_single_table_aggregate(
4808                stmt,
4809                table,
4810                schema_cols,
4811                alias,
4812                indexed_rows,
4813                cancel,
4814            );
4815        }
4816        self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4817    }
4818
4819    /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4820    /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4821    /// uncorrelated FROM-primary case is the simpler shape, used by
4822    /// e2e pins. Materialises the (key, value) pair stream into a
4823    /// synthetic two-column TEXT table, then routes through the
4824    /// regular projection / WHERE / ORDER BY pipeline.
4825    /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4826    /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4827    /// item into (rows, schema). `outer_doc` is `Some` only when this
4828    /// is a NESTED level being expanded against a parent row item's
4829    /// already-parsed sub-document; the top-level call parses the doc
4830    /// expr itself. Row/column paths reuse the existing jsonpath
4831    /// evaluator (`json::json_table_path`); coercion reuses
4832    /// `coerce_value` on the JSON scalar text, so a json string
4833    /// coerces to DATE by its content, matching PG.
4834    #[allow(clippy::type_complexity)]
4835    pub(crate) fn json_table_rows(
4836        &self,
4837        jt: &spg_sql::ast::JsonTable,
4838        outer_doc: Option<&crate::json::JsonValue>,
4839    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4840        // Column schema is static (independent of data): flatten the
4841        // COLUMNS tree in declaration order (NESTED contributes its
4842        // children inline, the PG output shape).
4843        let schema = json_table_schema(&jt.columns);
4844
4845        // PASSING variables → a single JsonValue object the jsonpath
4846        // engine reads `$name` from.
4847        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4848        let ctx = EvalContext::new(&empty_schema, None);
4849        let dummy = Row::new(alloc::vec::Vec::new());
4850        let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4851            None
4852        } else {
4853            let mut entries = alloc::vec::Vec::new();
4854            for (name, e) in &jt.passing {
4855                let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
4856                entries.push((name.clone(), value_to_json_value(&v)));
4857            }
4858            Some(crate::json::JsonValue::Object(entries))
4859        };
4860
4861        // The document root: a NESTED level gets it from the parent;
4862        // the top level parses its doc expr.
4863        let root_owned;
4864        let root: &crate::json::JsonValue = match outer_doc {
4865            Some(d) => d,
4866            None => {
4867                let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
4868                let src = match &doc_val {
4869                    Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
4870                    Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
4871                    other => {
4872                        return Err(EngineError::Unsupported(alloc::format!(
4873                            "JSON_TABLE document must be json/text, got {}",
4874                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
4875                        )));
4876                    }
4877                };
4878                root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
4879                &root_owned
4880            }
4881        };
4882
4883        let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
4884            .map_err(EngineError::Eval)?;
4885        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
4886        for (idx, item) in items.iter().enumerate() {
4887            self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
4888        }
4889        Ok((rows, schema))
4890    }
4891
4892    /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
4893    /// Regular columns produce one value each; a NESTED column expands
4894    /// as an outer join (each nested match → one row sharing the
4895    /// parent cells; no nested match → one row with the nested cells
4896    /// NULL). Sibling NESTED at one level cross by concatenation of
4897    /// their independent expansions (PG's UNION-of-outer shape).
4898    fn json_table_emit_item(
4899        &self,
4900        jt: &spg_sql::ast::JsonTable,
4901        item: &crate::json::JsonValue,
4902        ordinality: usize,
4903        vars: Option<&crate::json::JsonValue>,
4904        out: &mut alloc::vec::Vec<Row<'static>>,
4905    ) -> Result<(), EngineError> {
4906        use spg_sql::ast::JsonTableColumn as C;
4907        // Parent cells (regular + ordinality), left-to-right; NESTED
4908        // columns contribute a run of child cells appended after.
4909        let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
4910        let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
4911            alloc::vec::Vec::new();
4912        let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4913        for col in &jt.columns {
4914            match col {
4915                C::Ordinality { .. } => {
4916                    parent_cells.push(Value::BigInt(ordinality as i64 + 1));
4917                }
4918                C::Regular { .. } => {
4919                    parent_cells.push(self.json_table_column_value(col, item, vars)?);
4920                }
4921                C::Nested { path, columns } => {
4922                    // Recurse: a nested JSON_TABLE over `item` filtered
4923                    // by `path`, with the same PASSING vars.
4924                    let sub = spg_sql::ast::JsonTable {
4925                        doc: jt.doc.clone(), // unused (outer_doc provided)
4926                        row_path: path.clone(),
4927                        columns: columns.clone(),
4928                        passing: alloc::vec::Vec::new(),
4929                    };
4930                    let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
4931                    nested_widths.push(nschema.len());
4932                    nested_runs.push(nrows);
4933                }
4934            }
4935        }
4936        if nested_runs.is_empty() {
4937            out.push(Row::new(parent_cells));
4938            return Ok(());
4939        }
4940        // PG sibling-NESTED semantics: each sibling expands
4941        // INDEPENDENTLY and the results CONCATENATE — a row from
4942        // sibling s fills only s's cells, every other sibling's cells
4943        // NULL. An empty sibling contributes ZERO rows (not a NULL
4944        // row). Only when EVERY sibling is empty does the parent still
4945        // emit one all-NULL row (the outer-join guarantee that a parent
4946        // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
4947        // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
4948        let before = out.len();
4949        for (s_idx, run) in nested_runs.iter().enumerate() {
4950            for nrow in run {
4951                let mut cells = parent_cells.clone();
4952                for (o_idx, w) in nested_widths.iter().enumerate() {
4953                    if o_idx == s_idx {
4954                        cells.extend(nrow.values.iter().cloned());
4955                    } else {
4956                        for _ in 0..*w {
4957                            cells.push(Value::Null);
4958                        }
4959                    }
4960                }
4961                out.push(Row::new(cells));
4962            }
4963        }
4964        if out.len() == before {
4965            // Every sibling empty → one all-NULL nested row.
4966            let mut cells = parent_cells.clone();
4967            for w in &nested_widths {
4968                for _ in 0..*w {
4969                    cells.push(Value::Null);
4970                }
4971            }
4972            out.push(Row::new(cells));
4973        }
4974        Ok(())
4975    }
4976
4977    /// v7.39 (round 205) — evaluate one Regular column against a row
4978    /// item: EXISTS → bool; else path → at most one value, coerced to
4979    /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
4980    fn json_table_column_value(
4981        &self,
4982        col: &spg_sql::ast::JsonTableColumn,
4983        item: &crate::json::JsonValue,
4984        vars: Option<&crate::json::JsonValue>,
4985    ) -> Result<Value<'static>, EngineError> {
4986        use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
4987        let C::Regular {
4988            name,
4989            ty,
4990            path,
4991            exists,
4992            format_json,
4993            wrapper,
4994            on_empty,
4995            on_error,
4996        } = col
4997        else {
4998            unreachable!("caller guards Regular");
4999        };
5000        let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5001        if *exists {
5002            return Ok(Value::Bool(!matches.is_empty()));
5003        }
5004        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5005        let ctx = EvalContext::new(&empty_schema, None);
5006        let dummy = Row::new(alloc::vec::Vec::new());
5007        let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5008            match b {
5009                B::Null => Ok(Some(Value::Null)),
5010                B::Error => Ok(None),
5011                B::Default(e) => Ok(Some(
5012                    eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5013                )),
5014            }
5015        };
5016        // Empty match set → ON EMPTY.
5017        if matches.is_empty() {
5018            return match default_of(on_empty)? {
5019                Some(v) => coerce_json_table_default(v, *ty, name),
5020                None => Err(EngineError::Unsupported(alloc::format!(
5021                    "no SQL/JSON item found for JSON_TABLE column {name:?}"
5022                ))),
5023            };
5024        }
5025        let first = &matches[0];
5026        // FORMAT JSON: return the PG-canonical json representation.
5027        // WITH WRAPPER wraps the whole match SET in an array (even a
5028        // single scalar → `[5]`); without it, the single match's json.
5029        if *format_json {
5030            let text = if *wrapper {
5031                crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5032            } else {
5033                first.canonical_json_text()
5034            };
5035            return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5036        }
5037        if first.is_json_null() {
5038            return Ok(Value::Null);
5039        }
5040        // Coerce the scalar text to the declared type; on failure → ON
5041        // ERROR (default NULL, DEFAULT expr, or raise).
5042        let dt = crate::conversions::column_type_to_data_type(*ty);
5043        let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5044        match crate::conversions::coerce_value(scalar, dt, name, 0) {
5045            Ok(v) => Ok(v),
5046            Err(e) => match default_of(on_error)? {
5047                Some(v) => coerce_json_table_default(v, *ty, name),
5048                None => Err(e),
5049            },
5050        }
5051    }
5052
5053    /// table function into (rows, default schema). Dispatch by name.
5054    pub(crate) fn table_fn_rows(
5055        &self,
5056        primary: &TableRef,
5057    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5058        let (fn_name, args) = primary
5059            .table_fn_call
5060            .as_deref()
5061            .expect("caller guards table_fn_call.is_some()");
5062        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5063        let ctx = EvalContext::new(&empty_schema, None);
5064        let dummy_row = Row::new(alloc::vec::Vec::new());
5065        let arg0: Option<Value<'static>> = match args.first() {
5066            Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5067            None => None,
5068        };
5069        match fn_name.as_str() {
5070            // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5071            // `…_recordset` (+ json_ variants). The row shape is the BASE
5072            // argument's declared type — a table's or a composite type's
5073            // column list — which only the catalog knows, so the parser hands
5074            // the raw arguments here rather than desugaring blind.
5075            "jsonb_populate_record"
5076            | "json_populate_record"
5077            | "jsonb_populate_recordset"
5078            | "json_populate_recordset" => {
5079                let type_name = match args.first() {
5080                    Some(Expr::Cast {
5081                        target: spg_sql::ast::CastTarget::Named(n),
5082                        ..
5083                    }) => n.clone(),
5084                    _ => {
5085                        return Err(EngineError::Unsupported(alloc::format!(
5086                            "{fn_name}(): first argument must name a row type, \
5087                             e.g. NULL::mytable"
5088                        )));
5089                    }
5090                };
5091                let cat = self.active_catalog();
5092                let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5093                    t.schema().columns.clone()
5094                } else if let Some(c) = cat.composite_types().get(&type_name) {
5095                    c.fields
5096                        .iter()
5097                        .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5098                        .collect()
5099                } else {
5100                    return Err(EngineError::Unsupported(alloc::format!(
5101                        "type \"{type_name}\" does not exist"
5102                    )));
5103                };
5104                let json_arg = match args.get(1) {
5105                    Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5106                    None => Value::Null,
5107                };
5108                // The set form iterates the JSON array; the scalar form is
5109                // the one-element case of the same walk.
5110                let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5111                    crate::json::array_element_rows(&json_arg, false, fn_name)
5112                        .map_err(EngineError::Eval)?
5113                        .into_iter()
5114                        .map(|s| s.map_or(Value::Null, Value::json))
5115                        .collect()
5116                } else if matches!(json_arg, Value::Null) {
5117                    alloc::vec::Vec::new()
5118                } else {
5119                    alloc::vec![json_arg]
5120                };
5121                let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5122                for doc in &docs {
5123                    let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5124                    for c in &cols {
5125                        // `->>` semantics: a missing key is NULL, present keys
5126                        // arrive as text and cast to the declared column type.
5127                        let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5128                            .map_err(EngineError::Eval)?;
5129                        let v = if matches!(raw, Value::Null) {
5130                            Value::Null
5131                        } else {
5132                            crate::conversions::coerce_value(raw, c.ty, "", 0)
5133                                .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5134                        };
5135                        vals.push(v);
5136                    }
5137                    rows.push(Row::new(vals));
5138                }
5139                Ok((rows, cols))
5140            }
5141            // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5142            // a text[] of 'name=value' reloptions/fdw options → one
5143            // (option_name, option_value) row per element. NULL or an
5144            // empty array yields zero rows (PG); an element without
5145            // '=' carries a NULL option_value, matching PG's split.
5146            "pg_options_to_table" => {
5147                let schema = alloc::vec![
5148                    ColumnSchema::new("option_name", DataType::Text, true),
5149                    ColumnSchema::new("option_value", DataType::Text, true),
5150                ];
5151                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5152                if let Some(Value::TextArray(items)) = arg0 {
5153                    for item in items.into_iter().flatten() {
5154                        let (name, value) = match item.split_once('=') {
5155                            Some((n, v)) => (Value::text(n), Value::text(v)),
5156                            None => (Value::text(item.as_str()), Value::Null),
5157                        };
5158                        rows.push(Row::new(alloc::vec![name, value]));
5159                    }
5160                }
5161                Ok((rows, schema))
5162            }
5163            // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5164            // PG18's per-sequence state SRF, (last_value, is_called).
5165            // pg_dump reads it joined to pg_sequence for every dumped
5166            // sequence's setval line. The oid resolves through the
5167            // same relation_oid mapping seqrelid publishes.
5168            "pg_get_sequence_data" => {
5169                let schema = alloc::vec![
5170                    ColumnSchema::new("last_value", DataType::BigInt, false),
5171                    ColumnSchema::new("is_called", DataType::Bool, false),
5172                ];
5173                let want = match arg0 {
5174                    Some(Value::Int(n)) => i64::from(n),
5175                    Some(Value::BigInt(n)) => n,
5176                    _ => {
5177                        return Err(EngineError::Unsupported(
5178                            "pg_get_sequence_data(): argument must be a sequence oid".into(),
5179                        ));
5180                    }
5181                };
5182                let cat = self.active_catalog();
5183                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5184                for (name, def) in cat.sequences_all() {
5185                    if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5186                        rows.push(Row::new(alloc::vec![
5187                            Value::BigInt(def.last_value),
5188                            Value::Bool(def.is_called),
5189                        ]));
5190                        break;
5191                    }
5192                }
5193                Ok((rows, schema))
5194            }
5195            "pg_partition_tree" => {
5196                let cols = alloc::vec![
5197                    ColumnSchema::new("relid".to_string(), DataType::Text, true),
5198                    ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5199                    ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5200                    ColumnSchema::new("level".to_string(), DataType::Int, true),
5201                ];
5202                let Some(Value::Text(name)) = &arg0 else {
5203                    // NULL (or missing) argument → zero rows (PG).
5204                    return Ok((alloc::vec::Vec::new(), cols));
5205                };
5206                let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5207                if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5208                    return Err(EngineError::Unsupported(alloc::format!(
5209                        "relation \"{name}\" does not exist"
5210                    )));
5211                }
5212                let rows = entries
5213                    .into_iter()
5214                    .map(|(relid, parent, isleaf, level)| {
5215                        Row::new(alloc::vec![
5216                            Value::text(relid),
5217                            parent.map_or(Value::Null, Value::text),
5218                            Value::Bool(isleaf),
5219                            #[allow(clippy::cast_possible_truncation)]
5220                            Value::Int(level as i32),
5221                        ])
5222                    })
5223                    .collect();
5224                Ok((rows, cols))
5225            }
5226            "pg_partition_ancestors" => {
5227                let cols =
5228                    alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5229                let Some(Value::Text(name)) = &arg0 else {
5230                    return Ok((alloc::vec::Vec::new(), cols));
5231                };
5232                let cat = self.active_catalog();
5233                if cat.get(name.as_ref()).is_none() {
5234                    return Err(EngineError::Unsupported(alloc::format!(
5235                        "relation \"{name}\" does not exist"
5236                    )));
5237                }
5238                // A relation outside any partition tree yields no rows (PG).
5239                let in_tree = cat
5240                    .get(name.as_ref())
5241                    .is_some_and(|t| t.schema().partition_role.is_some());
5242                let rows = if in_tree {
5243                    crate::partition_walks::ancestors_of(cat, name.as_ref())
5244                        .into_iter()
5245                        .map(|n| Row::new(alloc::vec![Value::text(n)]))
5246                        .collect()
5247                } else {
5248                    alloc::vec::Vec::new()
5249                };
5250                Ok((rows, cols))
5251            }
5252            // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5253            // saw, what each token was called, which dictionary took it
5254            // and what came out. It is a projection of the same tokenizer
5255            // and the same map the indexer uses, so it cannot describe a
5256            // pipeline other than the one that runs.
5257            "ts_debug" => {
5258                use crate::fts::{TokenType, TsDict};
5259                let cols = alloc::vec![
5260                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5261                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5262                    ColumnSchema::new("token".to_string(), DataType::Text, false),
5263                    ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5264                    ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5265                    ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5266                ];
5267                // PG's one-arg form uses the session configuration; the
5268                // two-arg form names one.
5269                let (cfg_name, text) = match (&arg0, args.get(1)) {
5270                    (Some(Value::Text(c)), Some(t)) => {
5271                        let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5272                        (c.to_string(), crate::eval::value_to_text(&v))
5273                    }
5274                    (Some(v), None) => (
5275                        alloc::string::String::from("english"),
5276                        crate::eval::value_to_text(v),
5277                    ),
5278                    _ => return Ok((alloc::vec::Vec::new(), cols)),
5279                };
5280                let english = match cfg_name
5281                    .trim()
5282                    .trim_start_matches("pg_catalog.")
5283                    .to_ascii_lowercase()
5284                    .as_str()
5285                {
5286                    "english" => true,
5287                    "simple" => false,
5288                    other => {
5289                        return Err(EngineError::Unsupported(alloc::format!(
5290                            "text search configuration \"{other}\" does not exist"
5291                        )));
5292                    }
5293                };
5294                let rows = crate::fts::tokenize_typed(&text)
5295                    .into_iter()
5296                    .map(|tok| {
5297                        let dict = tok.ty.dictionary(english);
5298                        let dname = dict.map(|d| match d {
5299                            TsDict::Simple => "simple",
5300                            TsDict::EnglishStem => "english_stem",
5301                        });
5302                        let folded = tok.text.to_lowercase();
5303                        let lexemes = dict.map(|d| match d {
5304                            TsDict::Simple => alloc::vec![Some(folded.clone())],
5305                            TsDict::EnglishStem => {
5306                                if crate::fts::is_english_stopword(&folded) {
5307                                    alloc::vec::Vec::new()
5308                                } else {
5309                                    alloc::vec![Some(crate::fts::porter_stem(&folded))]
5310                                }
5311                            }
5312                        });
5313                        Row::new(alloc::vec![
5314                            Value::text(tok.ty.alias()),
5315                            Value::text(tok.ty.description()),
5316                            Value::text(tok.text),
5317                            Value::TextArray(
5318                                dname
5319                                    .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5320                                    .unwrap_or_default(),
5321                            ),
5322                            dname.map_or(Value::Null, Value::text),
5323                            lexemes.map_or(Value::Null, Value::TextArray),
5324                        ])
5325                    })
5326                    .collect();
5327                let _ = TokenType::AsciiWord;
5328                Ok((rows, cols))
5329            }
5330            // v7.39 (round 651) — `ts_token_type('default')`, the list the
5331            // parser actually produces. It is a projection of the
5332            // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5333            // read, so the three cannot disagree about what a token is.
5334            "ts_token_type" => {
5335                use crate::fts::TokenType as T;
5336                let cols = alloc::vec![
5337                    ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5338                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5339                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5340                ];
5341                // PG takes the parser by name or oid; SPG has the one.
5342                if let Some(Value::Text(p)) = &arg0
5343                    && !p.eq_ignore_ascii_case("default")
5344                    && !p.eq_ignore_ascii_case("pg_catalog.default")
5345                {
5346                    return Err(EngineError::Unsupported(alloc::format!(
5347                        "text search parser \"{p}\" does not exist"
5348                    )));
5349                }
5350                const TYPES: &[T] = &[
5351                    T::AsciiWord,
5352                    T::Word,
5353                    T::NumWord,
5354                    T::Email,
5355                    T::Url,
5356                    T::Host,
5357                    T::SFloat,
5358                    T::Version,
5359                    T::HwordNumPart,
5360                    T::HwordPart,
5361                    T::HwordAsciiPart,
5362                    T::Blank,
5363                    T::Tag,
5364                    T::Protocol,
5365                    T::NumHword,
5366                    T::AsciiHword,
5367                    T::Hword,
5368                    T::UrlPath,
5369                    T::File,
5370                    T::Float,
5371                    T::Int,
5372                    T::Uint,
5373                    T::Entity,
5374                ];
5375                let rows = TYPES
5376                    .iter()
5377                    .map(|t| {
5378                        Row::new(alloc::vec![
5379                            Value::Int(*t as i32),
5380                            Value::text(t.alias()),
5381                            Value::text(t.description()),
5382                        ])
5383                    })
5384                    .collect();
5385                Ok((rows, cols))
5386            }
5387            // v7.39 (read01 round 65) — a set-returning USER function in FROM
5388            // (`FROM rows_of(2)`). Its body runs through the real executor, like
5389            // every other function body since round 63.
5390            other => {
5391                if !self.active_catalog().functions_named(other).is_empty() {
5392                    return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5393                }
5394                Err(EngineError::Unsupported(alloc::format!(
5395                    "table function {other}() is not supported in FROM"
5396                )))
5397            }
5398        }
5399    }
5400
5401    /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5402    /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5403    /// are bound into it as literals and it goes through the read path, so the
5404    /// rows it yields are exactly the rows a hand-written query would see.
5405    ///
5406    /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5407    /// text)` names them, and a `SETOF <scalar>` yields a single column named
5408    /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5409    /// shows.
5410    fn exec_setof_user_function(
5411        &self,
5412        name: &str,
5413        args: &[spg_sql::ast::Expr],
5414        // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5415        // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5416        alias: Option<&str>,
5417    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5418        // The call's arguments belong to the ENCLOSING query, so they are
5419        // evaluated here and the body sees values.
5420        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5421        let arg_ctx = self.ev_ctx(&empty, None);
5422        let dummy = Row::new(alloc::vec::Vec::new());
5423        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5424        for a in args {
5425            vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5426        }
5427        self.setof_rows_of(name, &vals, alias)
5428    }
5429
5430    /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5431    /// arguments. Shared by the FROM position and the target-list expansion, so
5432    /// a function cannot behave differently depending on where it is called.
5433    pub(crate) fn setof_rows_of(
5434        &self,
5435        name: &str,
5436        arg_values: &[Value<'static>],
5437        alias: Option<&str>,
5438    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5439        let cat = self.active_catalog();
5440        let overloads = cat.functions_named(name);
5441        let def = overloads
5442            .iter()
5443            .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5444            .ok_or_else(|| {
5445                EngineError::Unsupported(alloc::format!(
5446                    "function {name} does not exist with {} argument(s)",
5447                    arg_values.len()
5448                ))
5449            })?;
5450        let declared = def.returns.trim().to_string();
5451        let upper = declared.to_ascii_uppercase();
5452        if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5453            return Err(EngineError::Unsupported(alloc::format!(
5454                "function {name}() does not return a set — it cannot be used in FROM"
5455            )));
5456        }
5457
5458        let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5459        // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5460        // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5461        if def.language.eq_ignore_ascii_case("plpgsql") {
5462            let out_rows = self
5463                .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5464                .map_err(EngineError::Eval)?;
5465            let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5466            let rows = out_rows.into_iter().map(Row::new).collect();
5467            return Ok((rows, cols));
5468        }
5469        let body = def.body.trim().trim_end_matches(';');
5470        let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5471            EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5472        })?;
5473        let spg_sql::ast::Statement::Select(body_select) = stmt else {
5474            return Err(EngineError::Unsupported(alloc::format!(
5475                "function {name}(): a set-returning body must be a SELECT"
5476            )));
5477        };
5478        let arg_names = spg_storage::function_arg_names(&def.args_repr);
5479        let bound = crate::eval::bind_user_fn_args(
5480            self.active_catalog(),
5481            &body_select,
5482            &arg_names,
5483            arg_values,
5484        )
5485        .map_err(EngineError::Eval)?;
5486        let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5487        let QueryResult::Rows { columns, rows } = out else {
5488            return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5489        };
5490        // Name the columns from the DECLARED shape — the same rule the plpgsql
5491        // path above uses, so a body's language cannot change the row shape.
5492        let cols = setof_column_shape_from(&declared, name, alias, &columns);
5493        Ok((rows, cols))
5494    }
5495
5496    fn exec_select_jsonb_each_text(
5497        &self,
5498        stmt: &SelectStatement,
5499        primary: &TableRef,
5500        cancel: CancelToken<'_>,
5501    ) -> Result<QueryResult, EngineError> {
5502        let (each_fn, arg_expr) = primary
5503            .jsonb_each_text_arg
5504            .as_ref()
5505            .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5506            .expect("caller guards jsonb_each_text_arg.is_some()");
5507        // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5508        // forms keep JSON rendering in the value column (JSON null
5509        // stays jsonb 'null', strings keep their quotes).
5510        let as_text = each_fn.ends_with("_text");
5511        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5512        let ctx = EvalContext::new(&empty_schema, None);
5513        let dummy_row = Row::new(alloc::vec::Vec::new());
5514        let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5515        let pairs =
5516            crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5517        let rows: alloc::vec::Vec<Row<'static>> = pairs
5518            .into_iter()
5519            .map(|(k, v)| {
5520                let key_val = Value::text(k);
5521                let value_val = match v {
5522                    Some(s) if as_text => Value::text(s),
5523                    Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5524                    None => Value::Null,
5525                };
5526                Row::new(alloc::vec![key_val, value_val])
5527            })
5528            .collect();
5529        let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5530        let value_dtype = if as_text {
5531            spg_storage::DataType::Text
5532        } else {
5533            spg_storage::DataType::Json
5534        };
5535        let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5536        let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5537        let mut schema_cols = alloc::vec![key_col, value_col];
5538        // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5539        // LATERAL-position form of the same call already honours it.
5540        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5541            if let Some(col) = schema_cols.get_mut(i) {
5542                col.name = new_name.clone();
5543            }
5544        }
5545        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5546        // `EvalContext::new` drops it and every catalog-dependent cast
5547        // (regclass / enum / composite / domain) silently degrades.
5548        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5549        // WHERE.
5550        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5551            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5552            for row in rows {
5553                cancel.check()?;
5554                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5555                if matches!(v, Value::Bool(true)) {
5556                    out.push(row);
5557                }
5558            }
5559            out
5560        } else {
5561            rows
5562        };
5563        // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5564        if aggregate::uses_aggregate(stmt) {
5565            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5566            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5567                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5568                    .map_err(|err| match err {
5569                        EngineError::Eval(ev) => ev,
5570                        other => eval::EvalError::TypeMismatch {
5571                            detail: alloc::format!("{other}"),
5572                        },
5573                    })
5574            };
5575            // v7.39 (round 656) — hand the rows over as they are rather than
5576            // collecting a second vector of `RowRef` wrappers. Note this is
5577            // a set-returning-function path, NOT the relational scan: the
5578            // measured O(rows) cost lived in `run_single_table_aggregate`,
5579            // and converting these four first was a miss that cost a full
5580            // round — every test stayed green and the number did not move.
5581            let agg = aggregate::run(
5582                stmt,
5583                crate::join::AggRows::Owned(&filtered),
5584                &schema_cols,
5585                Some(&alias),
5586                Some(&agg_correlated),
5587                self.parallel_runner.0.as_deref(),
5588                Some(self.active_catalog()),
5589                Some(self),
5590            )?;
5591            return self.finish_agg_result(agg, stmt, cancel);
5592        }
5593        // Projection.
5594        let projection =
5595            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
5596        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5597            alloc::vec::Vec::with_capacity(filtered.len());
5598        for row in &filtered {
5599            let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5600            for p in &projection {
5601                let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5602                vals.push(v);
5603            }
5604            projected_rows.push(Row::new(vals));
5605        }
5606        let columns: alloc::vec::Vec<ColumnSchema> = projection
5607            .iter()
5608            // v7.39 (read01 round 54) — keep the column's enum identity through
5609            // the projection (it lives outside the DataType lattice), or a
5610            // derived table / UNION / windowed result forgets it and any outer
5611            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5612            .map(|p| {
5613                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5614                c.user_enum_type = p.user_enum_type.clone();
5615                c.mysql_fsp = p.mysql_fsp;
5616                c
5617            })
5618            .collect();
5619        // ORDER BY.
5620        if !stmt.order_by.is_empty() {
5621            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5622                .iter()
5623                .enumerate()
5624                .map(|(i, r)| -> Result<_, EngineError> {
5625                    let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5626                        .order_by
5627                        .iter()
5628                        .map(|ob| {
5629                            eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5630                        })
5631                        .collect();
5632                    Ok((i, keys?))
5633                })
5634                .collect::<Result<_, _>>()?;
5635            indexed.sort_by(|a, b| {
5636                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5637                    let o = &stmt.order_by[idx];
5638                    let cmp = order_by_value_cmp_in(
5639                        o.desc,
5640                        o.nulls_first,
5641                        ka,
5642                        kb,
5643                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5644                    );
5645                    if cmp != core::cmp::Ordering::Equal {
5646                        return cmp;
5647                    }
5648                }
5649                core::cmp::Ordering::Equal
5650            });
5651            projected_rows = indexed
5652                .into_iter()
5653                .map(|(i, _)| projected_rows[i].clone())
5654                .collect();
5655        }
5656        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5657        if stmt.distinct {
5658            projected_rows = dedup_rows(projected_rows, FoldSpec::dialect(scan_ctx.mysql_dialect));
5659        }
5660        if let Some(offset) = stmt.offset_literal() {
5661            let off = (offset as usize).min(projected_rows.len());
5662            projected_rows.drain(..off);
5663        }
5664        if let Some(limit) = stmt.limit_literal() {
5665            projected_rows.truncate(limit as usize);
5666        }
5667        Ok(QueryResult::Rows {
5668            columns,
5669            rows: projected_rows,
5670        })
5671    }
5672
5673    /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5674    /// ( SELECT … ) alias` in primary position. The inner SELECT
5675    /// materialises once through the regular bare-select executor
5676    /// (UNION tails included), then the outer WHERE / aggregate /
5677    /// projection / ORDER BY / LIMIT pipeline runs over the
5678    /// synthetic table — the same post-materialisation shape as
5679    /// exec_select_jsonb_each_text, generalised to N columns.
5680    fn exec_select_derived(
5681        &self,
5682        stmt: &SelectStatement,
5683        primary: &TableRef,
5684        cancel: CancelToken<'_>,
5685    ) -> Result<QueryResult, EngineError> {
5686        let inner = primary
5687            .lateral_subquery
5688            .as_deref()
5689            .expect("caller guards lateral_subquery.is_some()");
5690        // exec_select_cancel is the union-aware wrapper — the inner
5691        // SELECT may carry UNION tails on stmt.unions.
5692        let QueryResult::Rows {
5693            columns: inner_cols,
5694            rows,
5695        } = self.exec_select_cancel(inner, cancel)?
5696        else {
5697            return Err(EngineError::Unsupported(
5698                "derived table subquery must return rows".into(),
5699            ));
5700        };
5701        let alias = primary
5702            .alias
5703            .clone()
5704            .unwrap_or_else(|| primary.name.clone());
5705        // `AS t(a, b)` renames the materialised columns positionally
5706        // (extra inner columns keep their own names, PG behaviour).
5707        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5708        // v7.39 (read01 round 78) — a column-alias list longer than the item is
5709        // the error PG reports; SPG used to let the extra names through and then
5710        // fail two layers downstream with "column not found: <the extra name>".
5711        let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5712        if primary.unnest_column_aliases.len() > n_out {
5713            return Err(EngineError::Unsupported(alloc::format!(
5714                "table \"{alias}\" has {n_out} columns available but {} columns specified",
5715                primary.unnest_column_aliases.len()
5716            )));
5717        }
5718        if primary.scalar_fn_item && schema_cols.len() == 1 {
5719            schema_cols[0].scalar_row_source = true;
5720        }
5721        // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5722        // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5723        // The column-alias list, if given, names it like any other column.
5724        let mut rows = rows;
5725        if primary.with_ordinality {
5726            schema_cols.push(ColumnSchema::new(
5727                "ordinality".to_string(),
5728                DataType::BigInt,
5729                false,
5730            ));
5731            rows = rows
5732                .into_iter()
5733                .enumerate()
5734                .map(|(i, r)| {
5735                    let mut v = r.values;
5736                    #[allow(clippy::cast_possible_wrap)]
5737                    v.push(Value::BigInt(i as i64 + 1));
5738                    Row::new(v)
5739                })
5740                .collect();
5741        }
5742        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5743            if let Some(col) = schema_cols.get_mut(i) {
5744                col.name = new_name.clone();
5745            }
5746        }
5747        self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5748    }
5749
5750    /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5751    /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5752    /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5753    /// derived-table executor and the FROM-position table functions.
5754    fn exec_select_over_rows(
5755        &self,
5756        stmt: &SelectStatement,
5757        rows: alloc::vec::Vec<Row<'static>>,
5758        schema_cols: alloc::vec::Vec<ColumnSchema>,
5759        alias: &str,
5760        cancel: CancelToken<'_>,
5761    ) -> Result<QueryResult, EngineError> {
5762        let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5763        // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5764        // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5765        // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5766        // (the same path the aggregate branch uses); the old plain eval_expr let
5767        // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5768        let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5769        // WHERE.
5770        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5771            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5772            for row in rows {
5773                cancel.check()?;
5774                let v = self.eval_expr_with_correlated(
5775                    w,
5776                    &row,
5777                    &scan_ctx,
5778                    cancel,
5779                    Some(&mut corr_memo.borrow_mut()),
5780                )?;
5781                if matches!(v, Value::Bool(true)) {
5782                    out.push(row);
5783                }
5784            }
5785            out
5786        } else {
5787            rows
5788        };
5789        // Aggregate dispatch.
5790        if aggregate::uses_aggregate(stmt) {
5791            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5792            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5793                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5794                    .map_err(|err| match err {
5795                        EngineError::Eval(ev) => ev,
5796                        other => eval::EvalError::TypeMismatch {
5797                            detail: alloc::format!("{other}"),
5798                        },
5799                    })
5800            };
5801            // v7.39 (round 656) — hand the rows over as they are rather than
5802            // collecting a second vector of `RowRef` wrappers. Note this is
5803            // a set-returning-function path, NOT the relational scan: the
5804            // measured O(rows) cost lived in `run_single_table_aggregate`,
5805            // and converting these four first was a miss that cost a full
5806            // round — every test stayed green and the number did not move.
5807            let agg = aggregate::run(
5808                stmt,
5809                crate::join::AggRows::Owned(&filtered),
5810                &schema_cols,
5811                Some(alias),
5812                Some(&agg_correlated),
5813                self.parallel_runner.0.as_deref(),
5814                Some(self.active_catalog()),
5815                Some(self),
5816            )?;
5817            return self.finish_agg_result(agg, stmt, cancel);
5818        }
5819        // Projection.
5820        let projection =
5821            build_projection(&stmt.items, &schema_cols, alias, self.backslash_escapes)?;
5822        // v7.39 (round 621) — a target-list SRF expands here too. This tail
5823        // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5824        // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5825        // answered `function unnest(integer[]) does not exist` for a query PG
5826        // answers.
5827        let srf_idxs = self.srf_target_idxs(&projection);
5828        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5829        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5830            alloc::vec::Vec::with_capacity(filtered.len());
5831        if !srf_idxs.is_empty() {
5832            let (rows, src) =
5833                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5834            projected_rows = rows;
5835            src_of_row = src;
5836        } else {
5837            for row in &filtered {
5838                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5839                for p in &projection {
5840                    let v = self.eval_expr_with_correlated(
5841                        &p.expr,
5842                        row,
5843                        &scan_ctx,
5844                        cancel,
5845                        Some(&mut corr_memo.borrow_mut()),
5846                    )?;
5847                    vals.push(v);
5848                }
5849                projected_rows.push(Row::new(vals));
5850            }
5851        }
5852        let columns: alloc::vec::Vec<ColumnSchema> = projection
5853            .iter()
5854            // v7.39 (read01 round 54) — keep the column's enum identity through
5855            // the projection (it lives outside the DataType lattice), or a
5856            // derived table / UNION / windowed result forgets it and any outer
5857            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5858            .map(|p| {
5859                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5860                c.user_enum_type = p.user_enum_type.clone();
5861                c.mysql_fsp = p.mysql_fsp;
5862                c
5863            })
5864            .collect();
5865        // ORDER BY over the source rows (same shape as the other
5866        // synthetic-table executors).
5867        // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
5868        // OUTPUT column. Evaluated as an expression, as it was here, the literal
5869        // `1` is just the constant 1: the same sort key for every row, so the
5870        // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
5871        // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
5872        // landing on this executor) came back in input order.
5873        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
5874        if !order_by.is_empty() {
5875            // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
5876            // SRF makes more of them than there were inputs.
5877            let out_cols = if srf_idxs.is_empty() {
5878                alloc::vec![None; order_by.len()]
5879            } else {
5880                srf_order_output_cols(&order_by, &projection)
5881            };
5882            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
5883                .iter()
5884                .enumerate()
5885                .map(|(k, out)| -> Result<_, EngineError> {
5886                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
5887                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
5888                        .iter()
5889                        .zip(out_cols.iter())
5890                        .map(|(ob, oc)| {
5891                            // v7.39 (read01 round 54) — this path builds its
5892                            // sort keys itself instead of going through
5893                            // `build_order_keys`, so it skipped the enum-ordinal
5894                            // substitution: an OUTER `ORDER BY <enum col>` over
5895                            // a DERIVED TABLE sorted by the label TEXT, not by
5896                            // member order. Silently wrong rows, not an error.
5897                            let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
5898                            Ok(
5899                                match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
5900                                    Some(ord) => Value::Float(ord),
5901                                    None => v,
5902                                },
5903                            )
5904                        })
5905                        .collect();
5906                    Ok((k, keys?))
5907                })
5908                .collect::<Result<_, _>>()?;
5909            indexed.sort_by(|a, b| {
5910                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5911                    let o = &stmt.order_by[idx];
5912                    let cmp = order_by_value_cmp_in(
5913                        o.desc,
5914                        o.nulls_first,
5915                        ka,
5916                        kb,
5917                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5918                    );
5919                    if cmp != core::cmp::Ordering::Equal {
5920                        return cmp;
5921                    }
5922                }
5923                core::cmp::Ordering::Equal
5924            });
5925            projected_rows = indexed
5926                .into_iter()
5927                .map(|(i, _)| projected_rows[i].clone())
5928                .collect();
5929        }
5930        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5931        if stmt.distinct {
5932            projected_rows = dedup_rows(projected_rows, FoldSpec::dialect(scan_ctx.mysql_dialect));
5933        }
5934        if let Some(offset) = stmt.offset_literal() {
5935            let off = (offset as usize).min(projected_rows.len());
5936            projected_rows.drain(..off);
5937        }
5938        if let Some(limit) = stmt.limit_literal() {
5939            projected_rows.truncate(limit as usize);
5940        }
5941        Ok(QueryResult::Rows {
5942            columns,
5943            rows: projected_rows,
5944        })
5945    }
5946
5947    /// Constant `SELECT` with no FROM: evaluate each projection item
5948    /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
5949    fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
5950        let empty_schema: Vec<ColumnSchema> = Vec::new();
5951        let ctx = self.ev_ctx(&empty_schema, None);
5952        // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
5953        // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
5954        // `SELECT string_agg('x',',')` → x). Before this it fell through to the
5955        // scalar projection, where the aggregate name looked like an unknown
5956        // function. The WHERE filters that one row, so `… WHERE false` leaves
5957        // the aggregate zero input rows (`count(*)` → 0).
5958        if aggregate::uses_aggregate(stmt) {
5959            let dummy = Row::new(Vec::new());
5960            let passes = match &stmt.where_ {
5961                Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
5962                None => true,
5963            };
5964            let rows: Vec<RowRef<'_>> = if passes {
5965                alloc::vec![RowRef::Owned(&dummy)]
5966            } else {
5967                Vec::new()
5968            };
5969            let agg = aggregate::run(
5970                stmt,
5971                crate::join::AggRows::Refs(&rows),
5972                &empty_schema,
5973                None,
5974                None,
5975                self.parallel_runner.0.as_deref(),
5976                Some(self.active_catalog()),
5977                Some(self),
5978            )?;
5979            return self.finish_agg_result(agg, stmt, CancelToken::none());
5980        }
5981        let projection = build_projection(&stmt.items, &empty_schema, "", self.backslash_escapes)?;
5982        // `SELECT … WHERE cond` with no FROM — the one conceptual
5983        // row survives only when the condition is true (previously
5984        // the WHERE was silently ignored: `SELECT 1 WHERE false`
5985        // returned a row).
5986        let dummy_row = Row::new(Vec::new());
5987        if let Some(w) = &stmt.where_ {
5988            let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
5989            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
5990                let columns: Vec<ColumnSchema> = projection
5991                    .into_iter()
5992                    .map(|p| {
5993                        let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5994                        c.user_enum_type = p.user_enum_type;
5995                        c.collation_name = p.collation_name;
5996                        c.mysql_fsp = p.mysql_fsp;
5997                        c
5998                    })
5999                    .collect();
6000                return Ok(QueryResult::Rows {
6001                    columns,
6002                    rows: Vec::new(),
6003                });
6004            }
6005        }
6006        // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6007        // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6008        // desugar to unnest) expands here: one output row per SRF row, sibling
6009        // scalar columns repeated. unnest / array_elements / path_query reach a
6010        // real FROM via the parser rewrite and never land here.
6011        // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6012        let srf_idxs = self.srf_target_idxs(&projection);
6013        if !srf_idxs.is_empty() {
6014            let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6015            let columns: Vec<ColumnSchema> = projection
6016                .into_iter()
6017                .map(|p| {
6018                    let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
6019                    c.user_enum_type = p.user_enum_type;
6020                    c.collation_name = p.collation_name;
6021                    c.mysql_fsp = p.mysql_fsp;
6022                    c
6023                })
6024                .collect();
6025            // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6026            // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6027            // to. This returned straight out of the expansion, so
6028            // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6029            // input order — the sort was not wrong, it never ran. (There is
6030            // exactly one conceptual input row here, which is why the ordinary
6031            // scan pipeline is not on this path at all.)
6032            if !stmt.order_by.is_empty() {
6033                let synth_ctx =
6034                    EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6035                let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6036                    .order_by
6037                    .iter()
6038                    .map(|o| {
6039                        let mut o = o.clone();
6040                        if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6041                            && *n >= 1
6042                            && let Ok(idx) = usize::try_from(*n - 1)
6043                            && idx < columns.len()
6044                        {
6045                            o.expr = Expr::Column(spg_sql::ast::ColumnName {
6046                                qualifier: None,
6047                                name: columns[idx].name.clone(),
6048                            });
6049                        }
6050                        o
6051                    })
6052                    .collect();
6053                let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6054                let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6055                for r in rows {
6056                    let keys = build_order_keys(&resolved, &r, &synth_ctx)?;
6057                    tagged.push((keys, r));
6058                }
6059                sort_by_keys(&mut tagged, &descs);
6060                rows = tagged.into_iter().map(|(_, r)| r).collect();
6061            }
6062            apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6063            return Ok(QueryResult::Rows { columns, rows });
6064        }
6065        let mut values = Vec::with_capacity(projection.len());
6066        for p in &projection {
6067            values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6068        }
6069        let columns: Vec<ColumnSchema> = projection
6070            .into_iter()
6071            .map(|p| {
6072                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
6073                c.user_enum_type = p.user_enum_type;
6074                c.collation_name = p.collation_name;
6075                c.mysql_fsp = p.mysql_fsp;
6076                c
6077            })
6078            .collect();
6079        // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6080        // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6081        // returns none. (The SRF and aggregate arms above already applied
6082        // them; this tail was the one that didn't.)
6083        let mut rows = alloc::vec![Row::new(values)];
6084        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6085        Ok(QueryResult::Rows { columns, rows })
6086    }
6087
6088    /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6089    /// circuit. Catches
6090    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6091    /// BEFORE `resolve_select_subqueries` materialises the inner result
6092    /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6093    /// values into a `HashSet<i64>` directly, then probes A.pk per
6094    /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6095    /// (~150 µs / query at INSUBQ benchmark scale).
6096    pub(crate) fn try_count_star_pk_in_subquery_fast(
6097        &self,
6098        stmt: &SelectStatement,
6099        cancel: CancelToken<'_>,
6100    ) -> Result<Option<QueryResult>, EngineError> {
6101        use spg_sql::ast::SelectItem;
6102        if stmt.distinct
6103            || stmt.limit_with_ties
6104            || stmt.group_by.is_some()
6105            || stmt.having.is_some()
6106            || !stmt.unions.is_empty()
6107            || !stmt.order_by.is_empty()
6108            || stmt.limit.is_some()
6109            || stmt.offset.is_some()
6110            || stmt.items.len() != 1
6111        {
6112            return Ok(None);
6113        }
6114        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6115            return Ok(None);
6116        };
6117        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6118            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6119        if !is_count_star {
6120            return Ok(None);
6121        }
6122        let Some(from) = stmt.from.as_ref() else {
6123            return Ok(None);
6124        };
6125        if !from.joins.is_empty()
6126            || from.primary.lateral_subquery.is_some()
6127            || from.primary.unnest_expr.is_some()
6128            || from.primary.generate_series_args.is_some()
6129            || from.primary.table_fn_call.is_some()
6130            || from.primary.as_of_segment.is_some()
6131        {
6132            return Ok(None);
6133        }
6134        let Some(where_expr) = stmt.where_.as_ref() else {
6135            return Ok(None);
6136        };
6137        // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6138        // negated=false; no other predicates.
6139        let Expr::InSubquery {
6140            expr: col_expr,
6141            subquery,
6142            negated: false,
6143        } = where_expr
6144        else {
6145            return Ok(None);
6146        };
6147        let Expr::Column(c) = col_expr.as_ref() else {
6148            return Ok(None);
6149        };
6150        let outer_alias = from
6151            .primary
6152            .alias
6153            .as_deref()
6154            .unwrap_or(from.primary.name.as_str());
6155        if let Some(q) = c.qualifier.as_deref()
6156            && !q.eq_ignore_ascii_case(outer_alias)
6157        {
6158            return Ok(None);
6159        }
6160        // Outer column must be a single-column PK on integer family.
6161        let catalog = self.active_catalog();
6162        let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6163            return Ok(None);
6164        };
6165        let outer_schema = outer_table.schema();
6166        let Some(outer_pos) = outer_schema
6167            .columns
6168            .iter()
6169            .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6170        else {
6171            return Ok(None);
6172        };
6173        if !matches!(
6174            outer_schema.columns[outer_pos].ty,
6175            spg_storage::DataType::BigInt
6176                | spg_storage::DataType::Int
6177                | spg_storage::DataType::SmallInt
6178        ) {
6179            return Ok(None);
6180        }
6181        if !outer_schema
6182            .uniqueness_constraints
6183            .iter()
6184            .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6185        {
6186            return Ok(None);
6187        }
6188        let Some(idx) = outer_table.index_on(outer_pos) else {
6189            return Ok(None);
6190        };
6191        // Inner must be uncorrelated. The cheap-correlation pre-check
6192        // exists upstream; here we just attempt the bare exec.
6193        if crate::subquery::select_is_correlated(subquery) {
6194            return Ok(None);
6195        }
6196        let mut inner = (**subquery).clone();
6197        self.resolve_select_subqueries(&mut inner, cancel)?;
6198        let r = match self.exec_bare_select_cancel(&inner, cancel) {
6199            Ok(r) => r,
6200            Err(_) => return Ok(None),
6201        };
6202        let QueryResult::Rows { columns, rows, .. } = r else {
6203            return Ok(None);
6204        };
6205        if columns.len() != 1 {
6206            return Ok(None);
6207        }
6208        // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6209        // subquery projects a column known to be UNIQUE/PK on its table
6210        // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6211        // in `tbl.uniqueness_constraints`), survivor values are
6212        // guaranteed distinct and the per-survivor `HashSet::insert`
6213        // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6214        //
6215        // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6216        // projection that is a bare Column ref, table-column lookup in
6217        // catalog confirms the column appears as a unique constraint's
6218        // sole member. UNIQUE NOT NULL is required — a nullable unique
6219        // column may have multiple NULLs, but NULLs are already skipped
6220        // above (`Value::Null => continue`), so a UNIQUE-only column is
6221        // still safe to dedup-skip.
6222        let inner_unique = (|| -> bool {
6223            if inner.distinct
6224                || inner.group_by.is_some()
6225                || !inner.unions.is_empty()
6226                || inner.having.is_some()
6227                || inner.items.len() != 1
6228            {
6229                return false;
6230            }
6231            let Some(inner_from) = inner.from.as_ref() else {
6232                return false;
6233            };
6234            if !inner_from.joins.is_empty()
6235                || inner_from.primary.lateral_subquery.is_some()
6236                || inner_from.primary.unnest_expr.is_some()
6237                || inner_from.primary.generate_series_args.is_some()
6238                || inner_from.primary.table_fn_call.is_some()
6239            {
6240                return false;
6241            }
6242            let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6243                return false;
6244            };
6245            let Expr::Column(pc) = proj else {
6246                return false;
6247            };
6248            let inner_alias = inner_from
6249                .primary
6250                .alias
6251                .as_deref()
6252                .unwrap_or(inner_from.primary.name.as_str());
6253            if let Some(q) = pc.qualifier.as_deref()
6254                && !q.eq_ignore_ascii_case(inner_alias)
6255            {
6256                return false;
6257            }
6258            let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6259                return false;
6260            };
6261            let isch = inner_table.schema();
6262            let Some(ipos) = isch
6263                .columns
6264                .iter()
6265                .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6266            else {
6267                return false;
6268            };
6269            isch.uniqueness_constraints
6270                .iter()
6271                .any(|u| u.columns.as_slice() == [ipos])
6272        })();
6273        // Collect inner i64 values directly into a HashSet, then probe.
6274        let mut count: i64 = 0;
6275        let mut probed = if inner_unique {
6276            hashbrown::HashSet::<i64>::new()
6277        } else {
6278            hashbrown::HashSet::<i64>::with_capacity(rows.len())
6279        };
6280        for row in &rows {
6281            let v = row.values.first().cloned().unwrap_or(Value::Null);
6282            let n = match v {
6283                Value::BigInt(n) => n,
6284                Value::Int(n) => i64::from(n),
6285                Value::SmallInt(n) => i64::from(n),
6286                Value::Null => continue,
6287                _ => return Ok(None),
6288            };
6289            // De-duplicate inner key set so a duplicate inner value
6290            // doesn't double-count the same outer row. Skipped when
6291            // the inner projection is statically unique.
6292            if !inner_unique && !probed.insert(n) {
6293                continue;
6294            }
6295            // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6296            // the `IndexKey::from_value` enum-dispatch and the per-call
6297            // `IndexKey` wrapper construction. The outer column is
6298            // already gated to integer-family above, so an i64 key
6299            // always corresponds to a valid PK lookup.
6300            if !idx.lookup_eq_i64(n).is_empty() {
6301                count += 1;
6302            }
6303        }
6304        let columns_out = alloc::vec![ColumnSchema::new(
6305            "count".to_string(),
6306            spg_storage::DataType::BigInt,
6307            false,
6308        )];
6309        let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6310        Ok(Some(QueryResult::Rows {
6311            columns: columns_out,
6312            rows: rows_out,
6313        }))
6314    }
6315
6316    /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6317    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6318    /// (the post-subquery-replacement shape of the INSUBQ probe
6319    /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6320    /// The general aggregate path materialises every seeked row into
6321    /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6322    /// For COUNT(*) we only care how many keys hit; iterate the list
6323    /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6324    /// row materialisation, the aggregate state machine, and the per-
6325    /// row WHERE re-eval (the seek already filtered by the same list).
6326    /// Returns `None` when the shape doesn't match.
6327    fn try_count_star_pk_in_list_fast(
6328        &self,
6329        stmt: &SelectStatement,
6330        table: &spg_storage::Table,
6331        schema_cols: &[ColumnSchema],
6332        alias: &str,
6333    ) -> Option<QueryResult> {
6334        use spg_sql::ast::{ColumnName, SelectItem};
6335        // Gates on the SELECT shape.
6336        if stmt.distinct
6337            || stmt.limit_with_ties
6338            || stmt.group_by.is_some()
6339            || stmt.having.is_some()
6340            || !stmt.unions.is_empty()
6341            || !stmt.order_by.is_empty()
6342            || stmt.limit.is_some()
6343            || stmt.offset.is_some()
6344            || stmt.items.len() != 1
6345        {
6346            return None;
6347        }
6348        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6349            return None;
6350        };
6351        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6352            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6353        if !is_count_star {
6354            return None;
6355        }
6356        // WHERE must be `<col> IN (literal list)` with no other
6357        // conjuncts (the seek result is a true subset of the row
6358        // population for this predicate).
6359        let where_expr = stmt.where_.as_ref()?;
6360        let Expr::InList {
6361            expr: col_expr,
6362            list,
6363            negated: false,
6364        } = where_expr
6365        else {
6366            return None;
6367        };
6368        let Expr::Column(c) = col_expr.as_ref() else {
6369            return None;
6370        };
6371        if let Some(q) = c.qualifier.as_deref()
6372            && !q.eq_ignore_ascii_case(alias)
6373        {
6374            return None;
6375        }
6376        let col_pos = schema_cols
6377            .iter()
6378            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6379        // The column must be a single-column PK on an integer family
6380        // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6381        // so the antiset stays collision-free under `HashSet<i64>`.
6382        let schema = table.schema();
6383        if !matches!(
6384            schema.columns[col_pos].ty,
6385            spg_storage::DataType::BigInt
6386                | spg_storage::DataType::Int
6387                | spg_storage::DataType::SmallInt
6388        ) {
6389            return None;
6390        }
6391        if !schema
6392            .uniqueness_constraints
6393            .iter()
6394            .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6395        {
6396            return None;
6397        }
6398        let idx = table.index_on(col_pos)?;
6399        // Tally non-empty seek results across all literal values.
6400        let mut count: i64 = 0;
6401        for lit in list {
6402            let Expr::Literal(l) = lit else {
6403                return None;
6404            };
6405            // r1039 — through the shared resolver, so a literal spelled
6406            // in another type ('5' against an integer PK) is read as the
6407            // column's before it becomes a key. This tally answers from
6408            // the index alone, so a key in the wrong space would return a
6409            // COUNT of zero rather than fall back to a scan.
6410            let col = schema.columns.get(col_pos)?;
6411            let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6412            let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6413            if !idx.lookup_eq(&key).is_empty() {
6414                count += 1;
6415            }
6416        }
6417        let columns = alloc::vec![ColumnSchema::new(
6418            "count".to_string(),
6419            spg_storage::DataType::BigInt,
6420            false,
6421        )];
6422        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6423        let _ = ColumnName {
6424            qualifier: None,
6425            name: String::new(),
6426        };
6427        Some(QueryResult::Rows { columns, rows })
6428    }
6429
6430    /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6431    /// BETWEEN a AND b` on an indexed column. The index range walk yields
6432    /// exactly the matching (visible) rows, so we count locators directly —
6433    /// skipping the row materialisation, the aggregate state machine, and the
6434    /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6435    /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6436    /// when the shape doesn't match.
6437    fn try_count_star_indexed_range_fast(
6438        &self,
6439        stmt: &SelectStatement,
6440        table: &spg_storage::Table,
6441        schema_cols: &[ColumnSchema],
6442        alias: &str,
6443        snapshot: &spg_storage::snapshot::Snapshot,
6444    ) -> Option<QueryResult> {
6445        use spg_sql::ast::SelectItem;
6446        if stmt.distinct
6447            || stmt.limit_with_ties
6448            || stmt.group_by.is_some()
6449            || stmt.having.is_some()
6450            || !stmt.unions.is_empty()
6451            || !stmt.order_by.is_empty()
6452            || stmt.limit.is_some()
6453            || stmt.offset.is_some()
6454            || stmt.items.len() != 1
6455        {
6456            return None;
6457        }
6458        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6459            return None;
6460        };
6461        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6462            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6463        if !is_count_star {
6464            return None;
6465        }
6466        let where_expr = stmt.where_.as_ref()?;
6467        let count =
6468            crate::index_access::try_range_count(where_expr, schema_cols, table, alias, snapshot)?;
6469        let columns = alloc::vec![ColumnSchema::new(
6470            "count".to_string(),
6471            spg_storage::DataType::BigInt,
6472            false,
6473        )];
6474        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6475        Some(QueryResult::Rows { columns, rows })
6476    }
6477
6478    /// Single-table aggregate path: filter the (optionally index-seeked)
6479    /// rows, then hand off to the aggregate executor which does its own
6480    /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6481    fn run_single_table_aggregate<'a>(
6482        &self,
6483        stmt: &SelectStatement,
6484        table: &'a spg_storage::Table,
6485        schema_cols: &'a [ColumnSchema],
6486        alias: &str,
6487        indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6488        cancel: CancelToken<'_>,
6489    ) -> Result<QueryResult, EngineError> {
6490        // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6491        // REPEATABLE (see run_single_table_scan). Aggregates
6492        // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6493        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6494        let ctx = self
6495            .ev_ctx(schema_cols, Some(alias))
6496            .with_sample_rng(&sample_cell);
6497        // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6498        // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6499        // and every abandoned buffer on the way stays resident: RSS is a
6500        // high-water mark, so the intermediates are paid for even though
6501        // they are freed. Round 656 measured the scan at 17 bytes/row
6502        // where the survivor list itself only needs 8.
6503        let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6504            Vec::with_capacity(table.rows().len())
6505        } else {
6506            // With a WHERE, the row count is an UPPER bound and reserving it
6507            // is the worse trade: `… WHERE id = 5` over 50M rows would take
6508            // 400 MB of pointers to hold one survivor. Let it grow.
6509            Vec::new()
6510        };
6511        // v6.2.6 — Memoize: per-query LRU cache for correlated
6512        // scalar subqueries. Fresh per row-loop entry so each
6513        // SELECT execution gets an isolated cache.
6514        let mut memo = memoize::MemoizeCache::new();
6515        // v7.37 (perf) — single-table aggregate's WHERE filter
6516        // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6517        // correlated`) per row, even for subquery-free WHEREs that
6518        // the single-table SCAN path has compiled since v7.32
6519        // (perf knife D). The asymmetry meant a fold-to-filter
6520        // rewrite (joinfold) that swapped a JOIN for a single-table
6521        // aggregate over a compiled WHERE saw the tree-walker
6522        // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6523        // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6524        // step. Compile once if eligible; fall back to the walker
6525        // for subquery-bearing or non-compilable WHEREs.
6526        let compiled_where: Option<eval::CompiledExpr> = stmt
6527            .where_
6528            .as_ref()
6529            .filter(|w| eval::fully_compilable(w))
6530            .map(|w| {
6531                // v7.38.8 — the scan filter runs the cheap half of its
6532                // conjunction first. Called from HERE and not from
6533                // `eval::compiled`, deliberately: the row loop lives in
6534                // that file, and adding a function to it cost this
6535                // query 11 % through layout alone while doing no work
6536                // for it. See `crate::qualorder`.
6537                match crate::qualorder::reordered(w) {
6538                    Some(r) => eval::compile_expr(&r, &ctx),
6539                    None => eval::compile_expr(w, &ctx),
6540                }
6541            });
6542        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6543        let mut row_passes_where = |row: &Row<'static>,
6544                                    eval_stack: &mut Vec<Value<'static>>,
6545                                    memo: &mut memoize::MemoizeCache|
6546         -> Result<bool, EngineError> {
6547            match (&compiled_where, &stmt.where_) {
6548                (Some(cw), _) => {
6549                    // v7.39 (round 479) — the predicate wants a bool, not a
6550                    // Value. The owned entry ended in `Value::into_owned`
6551                    // and the caller then dropped it, once per row; round
6552                    // 478's profile put that pair above the comparison
6553                    // itself.
6554                    Ok(eval::compiled::eval_compiled_pred(
6555                        cw,
6556                        row,
6557                        &ctx,
6558                        eval_stack,
6559                        ctx.mysql_dialect,
6560                    )
6561                    .map_err(EngineError::Eval)?)
6562                }
6563                (None, Some(w)) => {
6564                    let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6565                    Ok(crate::eval::predicate_is_true(
6566                        &cond,
6567                        "WHERE",
6568                        ctx.mysql_dialect,
6569                    )?)
6570                }
6571                (None, None) => Ok(true),
6572            }
6573        };
6574        if let Some(rows) = &indexed_rows {
6575            for cow in rows {
6576                let row = cow.as_ref();
6577                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6578                    continue;
6579                }
6580                filtered.push(row);
6581            }
6582        }
6583        // v7.36 (cold-tier coverage) — single-table aggregate's
6584        // non-indexed full scan was hot-only and silently lost cold
6585        // rows on COUNT/SUM/etc. Materialise cold rows once into
6586        // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6587        // shape stays unchanged; the cold rows live until the end of
6588        // the aggregate run.
6589        let cold_rows_storage = if indexed_rows.is_none() {
6590            self.iter_cold_rows_of_table(table)
6591        } else {
6592            Vec::new()
6593        };
6594        if indexed_rows.is_none() {
6595            // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6596            // single-table aggregate full-scan path. Mirrors the gate on
6597            // `run_single_table_scan`: this is a user-query result path,
6598            // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6599            // reader's snapshot cannot see (e.g. tombstoned versions),
6600            // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6601            // under the default gate-off: every hot row is frozen or
6602            // committed-and-alive, so `is_row_visible` returns true.
6603            // Cold-tier rows are frozen (visible) by definition — left
6604            // ungated, matching the plain-scan path.
6605            let scan_snapshot = self.current_snapshot();
6606            // v7.39 (pg_stat knife B) — this full-scan branch walks
6607            // headers directly (serial and sharded alike); count the
6608            // sequential scan here.
6609            table.note_seq_scan();
6610            // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6611            // filter dominate the pre-aggregate wall time on big
6612            // scans (P1's ground truth: accumulation is only ~17%).
6613            // Shard THAT work when the host injected an executor and
6614            // the WHERE is compiled (the compiled evaluator is pure
6615            // over &row; the tree-walker fallback can hit correlated
6616            // subqueries and stays serial). Shards return surviving
6617            // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6618            // 'static bound — and the main thread only dereferences.
6619            let n = table.row_count();
6620            let par = self.parallel_runner.0.as_deref().filter(|_| {
6621                n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6622            });
6623            // v7.38.11 — ask the BRIN summary first. When it prunes,
6624            // the work left is a few thousand rows and sharding it
6625            // costs more than it saves, so the serial pruned loop below
6626            // takes it; the shard machinery is left exactly as it was
6627            // rather than taught about slots.
6628            let brin_slots = stmt
6629                .where_
6630                .as_ref()
6631                .and_then(|w| crate::brin::candidate_slots(w, table));
6632            let brin_prunes = brin_slots
6633                .as_ref()
6634                .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6635            if let Some(r) = par
6636                && !brin_prunes
6637            {
6638                let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6639                let chunk = n.div_ceil(n_shards);
6640                type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6641                let cw = &compiled_where;
6642                let snap_ref = &scan_snapshot;
6643                let results = r.run_shards(n_shards, &|s| {
6644                    let lo = s * chunk;
6645                    let hi = ((s + 1) * chunk).min(n);
6646                    let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6647                    // EvalContext carries Cells (sampler / row counters)
6648                    // and is !Sync — each shard builds its own from the
6649                    // same Sync inputs. The compiled WHERE is gated to
6650                    // the pure-scalar whitelist, which reads none of the
6651                    // session state the engine-built ctx would add
6652                    // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6653                    // sampled scans never take this branch).
6654                    let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6655                    let mut stack: Vec<Value<'static>> = Vec::new();
6656                    let out: ShardOut = (|| {
6657                        for i in lo..hi {
6658                            if !table.is_row_visible(i, snap_ref) {
6659                                continue;
6660                            }
6661                            let row = &table.rows()[i];
6662                            // v7.39 (round 480) — the parallel full-scan
6663                            // shard is the path the aggregate benchmark
6664                            // actually takes, and it was still on the OWNED
6665                            // entry: round 480's profile attributed 68.7 %
6666                            // of `drop_glue<Value>` to this closure, which
6667                            // is why round 479's fix to the indexed path
6668                            // barely moved the total.
6669                            //
6670                            // The `matches!(…, Value::Bool(true))` form was
6671                            // also a narrower reading than the rest of the
6672                            // engine uses — `predicate_is_true` is what
6673                            // handles NULL and MySQL truthiness — so the
6674                            // bool entry fixes the shape as well as the cost.
6675                            let pass = match cw {
6676                                Some(c) => eval::compiled::eval_compiled_pred(
6677                                    c,
6678                                    row,
6679                                    &shard_ctx,
6680                                    &mut stack,
6681                                    shard_ctx.mysql_dialect,
6682                                )
6683                                .map_err(EngineError::Eval)?,
6684                                None => true,
6685                            };
6686                            if pass {
6687                                keep.push(i);
6688                            }
6689                        }
6690                        Ok(keep)
6691                    })();
6692                    alloc::boxed::Box::new(out)
6693                });
6694                // v7.39 (round 567) — `rows()` is a 32-way trie, so
6695                // indexing it is four dependent loads and a scan that
6696                // reads every row paid them every row. A profile of
6697                // `SELECT sum(id)` over 500k rows put 37.8% of the
6698                // connection thread's CPU on THIS ONE LINE. The cursor
6699                // holds the leaf, making that one descent per 32.
6700                let mut rows_cur = table.rows().run_cursor();
6701                for boxed in results {
6702                    let shard = boxed
6703                        .downcast::<ShardOut>()
6704                        .expect("runner echoes the closure's box");
6705                    for i in (*shard)? {
6706                        if let Some(row) = rows_cur.get(i) {
6707                            filtered.push(row);
6708                        }
6709                    }
6710                }
6711            } else {
6712                let mut rows_cur = table.rows().run_cursor();
6713                // v7.38.11 — the slots the BRIN summary could not rule
6714                // out. The predicate still runs on every row that
6715                // survives: the summary decides what to SKIP, never
6716                // what to return.
6717                let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6718                for range in ranges {
6719                    for i in range {
6720                        if !table.is_row_visible(i, &scan_snapshot) {
6721                            continue;
6722                        }
6723                        let Some(row) = rows_cur.get(i) else { continue };
6724                        if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6725                            continue;
6726                        }
6727                        filtered.push(row);
6728                    }
6729                }
6730            }
6731            for row in &cold_rows_storage {
6732                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6733                    continue;
6734                }
6735                filtered.push(row);
6736            }
6737        }
6738        // v7.29 — a per-query memo so correlated scalar
6739        // subqueries batch-evaluate once (group map) instead of
6740        // executing per group.
6741        let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6742        let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6743            self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6744                .map_err(|err| match err {
6745                    EngineError::Eval(ev) => ev,
6746                    other => eval::EvalError::TypeMismatch {
6747                        detail: alloc::format!("{other}"),
6748                    },
6749                })
6750        };
6751        // v7.39 (round 656) — the plain relational scan. This collect() was
6752        // the measured defect: one 64-byte `RowRef` per surviving row to
6753        // wrap an 8-byte pointer `filtered` already holds. Scalar
6754        // aggregates measured ~81 bytes/row of working memory because of
6755        // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6756        // one number. `AggRows::Ptrs` reads the pointers directly.
6757        let agg = aggregate::run(
6758            stmt,
6759            crate::join::AggRows::Ptrs(&filtered),
6760            schema_cols,
6761            Some(alias),
6762            Some(&agg_correlated),
6763            self.parallel_runner.0.as_deref(),
6764            Some(self.active_catalog()),
6765            Some(self),
6766        )?;
6767        self.finish_agg_result(agg, stmt, cancel)
6768    }
6769
6770    /// Single-table scan + projection path: WHERE filter (compiled when
6771    /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6772    /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6773    fn run_single_table_scan<'a>(
6774        &self,
6775        stmt: &SelectStatement,
6776        table: &'a spg_storage::Table,
6777        schema_cols: &'a [ColumnSchema],
6778        alias: &str,
6779        indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6780        cancel: CancelToken<'_>,
6781    ) -> Result<QueryResult, EngineError> {
6782        // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6783        // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6784        // deterministic `__tsm_fract(seed)` draws share one scan-local
6785        // state (isolated from the global random() PRNG); a fresh cell per
6786        // scan makes a repeat / rescan reproduce the same sample. Unused
6787        // and cheap when the query carries no sample.
6788        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6789        let ctx = self
6790            .ev_ctx(schema_cols, Some(alias))
6791            .with_sample_rng(&sample_cell);
6792        let projection = build_projection(&stmt.items, schema_cols, alias, self.backslash_escapes)?;
6793        // v7.19 P5 — single-table SELECT path for SRF
6794        // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6795        // unnest in the projection list. When present, the
6796        // per-row processor emits one output row per array
6797        // element (broadcasting non-SRF projections from the
6798        // same input row). Empty / NULL arrays emit zero rows
6799        // for that input — PG semantics.
6800        // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
6801        let srf_idxs = self.srf_target_idxs(&projection);
6802        let srf_position = srf_idxs.first().copied();
6803        // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
6804        let mut srf_plan = if srf_position.is_some() {
6805            Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
6806        } else {
6807            None
6808        };
6809
6810        // Materialise the filter pass into `(order_key, projected_row)`
6811        // tuples. The order key is `None` when there's no ORDER BY clause.
6812        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
6813        // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
6814        // output row to the per-query byte budget as it is built, so a
6815        // fat single-table scan / sort REJECTS with QueryBytesExceeded
6816        // at ~the ceiling instead of materialising the whole table and
6817        // only noticing at the final enforce_row_limit check. Without
6818        // this, N concurrent fat scans peak at N×table and OOM the host.
6819        // `max_query_bytes = None` (the embedded default) = no ceiling,
6820        // so existing unbudgeted behaviour is byte-identical.
6821        let mut budget = ByteBudget::new(self.max_query_bytes);
6822        // v6.2.6 — Memoize per-row WHERE eval shares one cache.
6823        let mut memo = memoize::MemoizeCache::new();
6824        // v7.32 (perf knife D) — subquery-free WHERE compiles once;
6825        // the row loop then runs a flat step program instead of a
6826        // tree interpretation per row.
6827        let compiled_where: Option<eval::CompiledExpr> = stmt
6828            .where_
6829            .as_ref()
6830            .filter(|w| eval::fully_compilable(w))
6831            .map(|w| {
6832                // v7.38.8 — the scan filter runs the cheap half of its
6833                // conjunction first. Called from HERE and not from
6834                // `eval::compiled`, deliberately: the row loop lives in
6835                // that file, and adding a function to it cost this
6836                // query 11 % through layout alone while doing no work
6837                // for it. See `crate::qualorder`.
6838                match crate::qualorder::reordered(w) {
6839                    Some(r) => eval::compile_expr(&r, &ctx),
6840                    None => eval::compile_expr(w, &ctx),
6841                }
6842            });
6843        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6844        // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
6845        // SELECT-item scalar subquery for the PK-probe fast path. The
6846        // analysis (gate checks + catalog lookups) takes ~500 ns; doing
6847        // it once per query instead of once per row × 100 rows saves
6848        // ~50 µs and lets the per-row evaluation reduce to a single
6849        // index probe + outer-column read.
6850        let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
6851            .iter()
6852            .map(|p| {
6853                if let Expr::ScalarSubquery(inner) = &p.expr {
6854                    self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
6855                } else {
6856                    None
6857                }
6858            })
6859            .collect();
6860        let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
6861        // v7.39 (round 487) — a projection item that is a bare column
6862        // reference binds its position ONCE per query.
6863        //
6864        // Per row it used to walk `eval_expr_with_correlated` (a memo
6865        // lookup for "does this have a subquery", then an un-memoised
6866        // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
6867        // then `resolve_column`, which finds the column by scanning the
6868        // schema and comparing NAMES. On `SELECT g FROM h` that chain was
6869        // 19 % of self time for what is ultimately one cell read.
6870        //
6871        // `compile_column_pos` is the Step VM's resolver, already
6872        // `pub(crate)` and already reused by the aggregate's bind-once
6873        // path: it mirrors `resolve_column`'s happy layers and returns
6874        // None for anything that would reach an error, an ambiguity, or a
6875        // miss, so those still go the interpreter's way and keep its
6876        // exact message. A composite column is excluded for the same
6877        // reason `compile_into` excludes it — it must be rehydrated from
6878        // stored JSON, which is not a cell read.
6879        let proj_direct = bind_direct_columns(&projection, &ctx);
6880        let any_proj_direct = proj_direct.iter().any(Option::is_some);
6881        // v7.39 (round 605) — a projection item that cannot depend on the row
6882        // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
6883        // allocations a row against one for a plain column, `'abc' || 'def'`
6884        // six and `upper('abc')` five, all of them producing the same value
6885        // 50,000 times. An item that fails to evaluate is left alone, so its
6886        // error still comes from the row loop in the interpreter's wording.
6887        let proj_const: Vec<Option<Value<'static>>> = projection
6888            .iter()
6889            .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
6890            .collect();
6891        let any_proj_const = proj_const.iter().any(Option::is_some);
6892        crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
6893        // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
6894        // projection. Statement prep (`resolve_order_by_position`) can only map
6895        // `ORDER BY 1` onto the first SELECT item when that item is an
6896        // expression; a `*` is not one, so the literal survived to here and was
6897        // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
6898        // at all. The parser rewrites `SELECT unnest(a) x` into
6899        // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
6900        // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
6901        // back in input order. The projection is built by now, so the Nth output
6902        // column is known — resolve against it.
6903        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6904        // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
6905        // EXPANDED rows, so a key naming a select-list item reads that item.
6906        let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
6907            srf_order_output_cols(&order_by, &projection)
6908        } else {
6909            Vec::new()
6910        };
6911        let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
6912        // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
6913        // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
6914        // Hoisted above the closure so the projection-eval path can
6915        // gate `memo` passing on it: the SELECT-item correlated-scalar
6916        // batch path scans the FULL inner table once (~5 ms for 12.5 k
6917        // rows) and is only a win when N outer rows is large; for small
6918        // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
6919        let early_cap: Option<usize> = if order_by.is_empty()
6920            && !stmt.distinct
6921            && !stmt.limit_with_ties
6922            && srf_position.is_none()
6923            && stmt.where_.is_none()
6924        {
6925            stmt.limit_literal()
6926                .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
6927        } else {
6928            None
6929        };
6930        // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
6931        // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
6932        // full-sort by the test gate) keep only the running top-`keep`
6933        // rows in memory instead of materialising every projected row,
6934        // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
6935        // space, not O(rows). `None` = accumulate everything (the prior
6936        // behaviour). The final `partial_sort_tagged(keep)` below still
6937        // runs and produces the identical rows.
6938        // v7.39 (round 683) — the declared collation for each ORDER BY
6939        // position, resolved once and carried beside `descs` for the same
6940        // reason `descs` is carried: it is per key position, not per row.
6941        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
6942        let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
6943            && !stmt.distinct
6944            && !stmt.limit_with_ties
6945            && srf_position.is_none()
6946            && !self.env_cfg().disable_topk
6947        {
6948            stmt.limit_literal().and_then(|l| {
6949                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
6950                (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
6951            })
6952        } else {
6953            None
6954        };
6955        // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
6956        // kept rows in `tagged`. Probing on the PROJECTED row as soon as
6957        // it is built means a duplicate costs neither a build_order_keys
6958        // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
6959        // a tagged slot, and the sort below runs over u survivors, not
6960        // n input rows — PG's hash-distinct-then-sort plan shape.
6961        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
6962            hashbrown::HashMap::new();
6963        let distinct_hb = hashbrown::DefaultHashBuilder::default();
6964        // v7.38.13 — which output positions must NOT fold. Built once per
6965        // scan from the projection, which carries the source column's
6966        // byte-wise-ness; see `FoldSpec`.
6967        let distinct_mask = fold_mask(&projection);
6968        // v7.39 (round 485) — one projection buffer for the whole scan
6969        // rather than a fresh `Vec` per input row. A row that survives
6970        // the DISTINCT probe takes the buffer with it (`mem::take`) and
6971        // the next row allocates a new one; a row that duplicates an
6972        // earlier one leaves the buffer — and its capacity — in place.
6973        // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
6974        // projected rows are duplicates, so that is 49 900 allocate /
6975        // free pairs the scan no longer performs. Shapes where every row
6976        // survives (plain projection, `DISTINCT` over a unique column)
6977        // allocate exactly as often as before.
6978        let mut proj_buf: Vec<Value<'static>> = Vec::new();
6979        // v7.39 (round 571) — buffers handed back by the top-N trim.
6980        // Round 485 made the scan share ONE projection buffer, but a
6981        // surviving row takes it (`mem::take`) and without DISTINCT
6982        // almost every row survives, so the next one starts from zero
6983        // capacity and allocates. The trim drops `keep` rows at a time
6984        // and their buffers come back here instead of being freed.
6985        let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
6986        let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
6987        // v7.39 (round 581) — the worst row the accumulator is currently
6988        // keeping. Anything that loses to it cannot reach the answer, so
6989        // it is dropped before its projection is ever built.
6990        let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
6991        // v7.39 (round 582) — resolve each ORDER BY column once, not
6992        // once per row. See `order_by_bound_positions`.
6993        let order_bound =
6994            crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
6995        // v7.39 (round 581) — and it stops asking when the answer is
6996        // always "keep".
6997        //
6998        // The check earns its place only on rows it rejects. Over
6999        // ascending ids, `ORDER BY id DESC` never rejects one — every
7000        // row beats the current worst — so the comparison is pure
7001        // overhead there, measured at +5.5% in three batches out of
7002        // three. After a window of rows it looks at what it has
7003        // actually rejected and switches itself off if the shape is not
7004        // paying. The answers do not depend on it either way.
7005        const BOUNDARY_WINDOW: u32 = 8192;
7006        let mut boundary_checks: u32 = 0;
7007        let mut boundary_rejects: u32 = 0;
7008        let mut boundary_check_on = true;
7009        // Inline the per-row work in a closure so the indexed and full-
7010        // scan branches share the body.
7011        let mut process_row = |row: &Row<'static>, loop_idx: usize| -> Result<(), EngineError> {
7012            if loop_idx.is_multiple_of(256) {
7013                cancel.check()?;
7014            }
7015            if let Some(cw) = &compiled_where {
7016                let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7017                    .map_err(EngineError::Eval)?;
7018                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7019                    return Ok(());
7020                }
7021            } else if let Some(where_expr) = &stmt.where_ {
7022                let cond =
7023                    self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7024                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7025                    return Ok(());
7026                }
7027            }
7028            // Under DISTINCT the keys are built AFTER the dup probe
7029            // (survivors only); the non-distinct order is unchanged.
7030            // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7031            // row further down, and building them here would evaluate the
7032            // ORDER BY against the INPUT row: a key naming the SRF's own
7033            // output became a scalar call to it, which is where
7034            // "function unnest(integer[]) does not exist" came from.
7035            let order_keys = if order_by.is_empty() || stmt.distinct || srf_position.is_some() {
7036                Vec::new()
7037            } else {
7038                let mut buf = key_pool.pop().unwrap_or_default();
7039                crate::orderby::build_order_keys_bound(
7040                    &order_by,
7041                    &order_bound,
7042                    &order_colls,
7043                    row,
7044                    &ctx,
7045                    &mut buf,
7046                )?;
7047                // v7.39 (round 581) — reject before projecting.
7048                //
7049                // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7050                // 50 distinct `g` decides nearly every row on the FIRST
7051                // key, and PG answers it FASTER than the single-key form
7052                // (7.4 ms against 10.4) because a rejected row costs it
7053                // one comparison. SPG built both keys AND the projected
7054                // row for all 500k before throwing them away. The keys
7055                // are needed to compare; the projection is not.
7056                if boundary_check_on
7057                    && let Some((_, descs)) = &topk_stream
7058                    && let Some(b) = &topk_boundary
7059                {
7060                    boundary_checks += 1;
7061                    let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7062                        == core::cmp::Ordering::Greater;
7063                    if loses {
7064                        boundary_rejects += 1;
7065                    }
7066                    if boundary_checks == BOUNDARY_WINDOW {
7067                        // Keep asking only if it has been rejecting at
7068                        // least a quarter of what it saw.
7069                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7070                    }
7071                    if loses {
7072                        buf.clear();
7073                        key_pool.push(buf);
7074                        return Ok(());
7075                    }
7076                }
7077                buf
7078            };
7079            if srf_position.is_some() {
7080                let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7081                for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7082                    if stmt.distinct {
7083                        let bucket = seen_distinct
7084                            .entry(norm_hash_row(
7085                                &out,
7086                                &distinct_hb,
7087                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7088                            ))
7089                            .or_default();
7090                        if bucket.iter().any(|i| {
7091                            row_eq_norm(
7092                                &tagged[i].1,
7093                                &out,
7094                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7095                            )
7096                        }) {
7097                            continue;
7098                        }
7099                        bucket.push(tagged.len());
7100                    }
7101                    budget.charge(approx_row_bytes(&out))?;
7102                    // The keys come from THIS expanded row: a key naming a
7103                    // select-list item reads its value, anything else is
7104                    // still evaluated against the input row.
7105                    let keys = if order_by.is_empty() {
7106                        Vec::new()
7107                    } else {
7108                        let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7109                        for (k, ob) in order_by.iter().enumerate() {
7110                            kv.push(match srf_order_cols.get(k).copied().flatten() {
7111                                Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7112                                None => eval::eval_expr(&ob.expr, row, &ctx)
7113                                    .map_err(EngineError::Eval)?,
7114                            });
7115                        }
7116                        // Packed by the same code every other ORDER BY uses,
7117                        // so DESC / NULLS FIRST / the MySQL rule are not
7118                        // restated here.
7119                        let key_row = Row::new(kv);
7120                        let mut buf = Vec::new();
7121                        crate::orderby::build_order_keys_bound(
7122                            &order_by,
7123                            &srf_key_bound,
7124                            &order_colls,
7125                            &key_row,
7126                            &ctx,
7127                            &mut buf,
7128                        )?;
7129                        buf
7130                    };
7131                    tagged.push((keys, out));
7132                }
7133            } else {
7134                let values = &mut proj_buf;
7135                values.clear();
7136                values.reserve(projection.len());
7137                for (i, p) in projection.iter().enumerate() {
7138                    // v7.37.x (docker-fair SCALARSQ attack) — pre-
7139                    // analysed PK-probe fast path. The per-row work is
7140                    // a read of outer.col from the row plus an index
7141                    // probe — no Expr clone, no walker, no
7142                    // `eval_expr_with_correlated` framework.
7143                    if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7144                        values.push(self.probe_with_pk_fast_path(fp, row));
7145                        continue;
7146                    }
7147                    // v7.39 (round 605) — the same value every row.
7148                    if any_proj_const && let Some(v) = &proj_const[i] {
7149                        values.push(v.clone());
7150                        continue;
7151                    }
7152                    // v7.39 (round 487) — bound column: read the cell.
7153                    // This is `rehydrate_cell`'s body for a non-composite
7154                    // column, which is what the whole chain below reduces
7155                    // to once the name has been resolved.
7156                    if any_proj_direct && let Some(pos) = proj_direct[i] {
7157                        crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7158                        values.push(row.values[pos].clone().into_owned());
7159                        continue;
7160                    }
7161                    // v7.24 (round-16 B) — correlated-aware.
7162                    // v7.37.x (docker-fair SCALARSQ attack) — share the
7163                    // per-row memo with projection. Required for the
7164                    // batch-evaluated correlated-scalar path to fire on
7165                    // SELECT-item scalar subqueries; otherwise each row
7166                    // re-executes the inner.
7167                    //
7168                    // Skip the memo when the outer row count is small
7169                    // (early-limited): the batch path scans the FULL
7170                    // inner table to build a GroupMap (~5 ms for a
7171                    // 12.5 k-row inner), while per-row execution with a
7172                    // PK index seek is ~5 µs per call — much cheaper for
7173                    // N ≤ ~1000 outer rows.
7174                    let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7175                    let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7176                    values.push(
7177                        self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7178                    );
7179                }
7180                crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7181                if stmt.distinct {
7182                    let bucket = seen_distinct
7183                        .entry(norm_hash_values(
7184                            &proj_buf,
7185                            &distinct_hb,
7186                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7187                        ))
7188                        .or_default();
7189                    if bucket.iter().any(|i| {
7190                        values_eq_norm(
7191                            &tagged[i].1.values,
7192                            &proj_buf,
7193                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7194                        )
7195                    }) {
7196                        crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7197                        return Ok(());
7198                    }
7199                    bucket.push(tagged.len());
7200                }
7201                let out = Row::new(core::mem::replace(
7202                    &mut proj_buf,
7203                    proj_pool.pop().unwrap_or_default(),
7204                ));
7205                let order_keys = if stmt.distinct && !order_by.is_empty() {
7206                    // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7207                    // the bound-cell path precisely so an ORDER BY key that
7208                    // names a column is READ instead of evaluated, and the
7209                    // non-DISTINCT branch above has passed it ever since;
7210                    // this branch never did, so `SELECT DISTINCT k .. ORDER
7211                    // BY k` resolved "k" by string for every surviving row.
7212                    let mut buf = key_pool.pop().unwrap_or_default();
7213                    crate::orderby::build_order_keys_bound(
7214                        &order_by,
7215                        &order_bound,
7216                        &order_colls,
7217                        row,
7218                        &ctx,
7219                        &mut buf,
7220                    )?;
7221                    buf
7222                } else {
7223                    order_keys
7224                };
7225                budget.charge(approx_row_bytes(&out))?;
7226                tagged.push((order_keys, out));
7227            }
7228            // Streaming top-N: bound the accumulator to O(keep) rows.
7229            if let Some((k, descs)) = &topk_stream {
7230                crate::orderby::topk_trim_recycling(
7231                    &mut tagged,
7232                    *k,
7233                    descs,
7234                    &mut proj_pool,
7235                    &mut key_pool,
7236                    &mut topk_boundary,
7237                );
7238            }
7239            Ok(())
7240        };
7241        // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7242        // load-bearing full-scan path. This is the primary single-table
7243        // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7244        // in-place writers retain dead/old versions, an ungated scan
7245        // here would return them, so the gate must land BEFORE the
7246        // writers flip (see the plan's activation-order rule). A no-op
7247        // today: every hot row is frozen or committed-and-alive under
7248        // the reader's snapshot, so `is_row_visible` returns true for
7249        // all of them (verified by the full e2e suite staying green).
7250        let scan_snapshot = self.current_snapshot();
7251        let mut emitted: usize = 0;
7252        if let Some(rows) = &indexed_rows {
7253            for (loop_idx, cow) in rows.iter().enumerate() {
7254                if let Some(cap) = early_cap
7255                    && emitted >= cap
7256                {
7257                    break;
7258                }
7259                process_row(cow.as_ref(), loop_idx)?;
7260                emitted = emitted.saturating_add(1);
7261            }
7262        } else {
7263            // v7.39 (round 570) — the row store is a 32-way trie, so
7264            // indexing it is four dependent loads. Round 567 measured
7265            // -18% on the aggregate scan from holding the leaf between
7266            // rows; this is the same loop for the projecting scan.
7267            let mut rows_cur = table.rows().run_cursor();
7268            // v7.38.11 — see the aggregate scan above: a BRIN index on a
7269            // column this WHERE bounds says which slots cannot match.
7270            let brin_slots = stmt
7271                .where_
7272                .as_ref()
7273                .and_then(|w| crate::brin::candidate_slots(w, table))
7274                .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7275            for i in brin_slots.into_iter().flatten() {
7276                if let Some(cap) = early_cap
7277                    && emitted >= cap
7278                {
7279                    break;
7280                }
7281                // Skip rows this snapshot cannot see (invisible rows do
7282                // not count toward the LIMIT).
7283                if !table.is_row_visible(i, &scan_snapshot) {
7284                    continue;
7285                }
7286                let Some(row) = rows_cur.get(i) else { continue };
7287                process_row(row, i)?;
7288                emitted = emitted.saturating_add(1);
7289            }
7290            // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7291            // rows into the same loop. The full-scan path here is the
7292            // load-bearing single-table SELECT executor, and pre-
7293            // 7.35.1 it only walked `table.rows()` (hot), so any
7294            // `SELECT … FROM t` against a table with cold segments
7295            // silently returned a subset.
7296            let cold_rows = self.iter_cold_rows_of_table(table);
7297            for (offset, row) in cold_rows.iter().enumerate() {
7298                if let Some(cap) = early_cap
7299                    && emitted >= cap
7300                {
7301                    break;
7302                }
7303                process_row(row, table.row_count() + offset)?;
7304                emitted = emitted.saturating_add(1);
7305            }
7306        }
7307
7308        // (DISTINCT already de-duped STREAMING inside process_row, so the
7309        // sort below only sees the u survivors and the partial-sort
7310        // budget applies to DISTINCT too.)
7311        if !order_by.is_empty() {
7312            // Partial-sort fast path: when LIMIT is small relative to
7313            // the row count, select_nth_unstable + sort just the
7314            // prefix is O(n + k log k) instead of O(n log n).
7315            // WITH TIES needs the full sort so the tie extension can
7316            // scan past `limit` to find rows that share the last-kept
7317            // row's key.
7318            let keep = if stmt.limit_with_ties
7319                // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7320                // forces the full-sort fallback by suppressing the
7321                // partial-sort `keep` budget. See
7322                // `xtests/sigil/test-mode-gucs.md`.
7323                || self.env_cfg().disable_topk
7324            {
7325                None
7326            } else {
7327                stmt.limit_literal()
7328                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7329            };
7330            let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7331            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7332        }
7333
7334        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7335        // past the truncated tail through every row that shares the
7336        // last-kept row's ORDER BY key. The tie check uses the
7337        // already-computed `(order_keys, row)` pairs so it matches
7338        // the sort comparator exactly. DISTINCT + WITH TIES falls
7339        // through to the no-ties path (PG also disallows their
7340        // combination; SPG silently drops the tie extension here so
7341        // the customer doesn't see a hard error mid-query — the
7342        // user-visible result is still correct, just narrower).
7343        let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7344            apply_offset_and_limit_tagged(
7345                &mut tagged,
7346                stmt.offset_literal(),
7347                stmt.limit_literal(),
7348                true,
7349            );
7350            tagged.into_iter().map(|(_, r)| r).collect()
7351        } else {
7352            // DISTINCT already de-duped pre-sort above.
7353            let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7354            apply_offset_and_limit(
7355                &mut output_rows,
7356                stmt.offset_literal(),
7357                stmt.limit_literal(),
7358            );
7359            output_rows
7360        };
7361
7362        let columns: Vec<ColumnSchema> = projection
7363            .into_iter()
7364            .map(|p| {
7365                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
7366                c.user_enum_type = p.user_enum_type;
7367                c.collation_name = p.collation_name;
7368                c.mysql_fsp = p.mysql_fsp;
7369                c
7370            })
7371            .collect();
7372
7373        Ok(QueryResult::Rows {
7374            columns,
7375            rows: output_rows,
7376        })
7377    }
7378
7379    /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7380    /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7381    /// select items for the surviving rows only — PG's Result-above-
7382    /// Limit shape, where SubPlan loops equal the OUTPUT row count
7383    /// (50) instead of the group count (24k).
7384    fn finish_agg_result(
7385        &self,
7386        mut agg: aggregate::AggResult,
7387        stmt: &SelectStatement,
7388        cancel: CancelToken<'_>,
7389    ) -> Result<QueryResult, EngineError> {
7390        apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7391        if !agg.deferred.is_empty() {
7392            apply_offset_and_limit(
7393                &mut agg.synth_rows,
7394                stmt.offset_literal(),
7395                stmt.limit_literal(),
7396            );
7397            let ctx = EvalContext::new(&agg.synth_schema, None);
7398            let mut memo = memoize::MemoizeCache::default();
7399            // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7400            // Deferred subqueries are referenced only by surviving
7401            // select-list rows (≤ LIMIT), so their correlation keys are
7402            // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7403            // each batchable subquery's group map over just those keys
7404            // via per-key index seek; the per-row splice loop below then
7405            // reuses the seeded map. A join-shaped or un-indexed inner
7406            // falls through to the all-keys batch inside the call (built
7407            // eagerly here instead of lazily on row 0 — same cost), so
7408            // it still pays the full scan, never the 715 ms per-row
7409            // direct eval; its index-nested-loop probe is the next
7410            // knife. Genuinely non-batchable shapes return None and are
7411            // left unseeded for the loop's per-row resolver, as before.
7412            for (_, expr) in &agg.deferred {
7413                let mut subs: Vec<&SelectStatement> = Vec::new();
7414                collect_scalar_subqueries(expr, &mut subs);
7415                for sub in subs {
7416                    let repr = alloc::format!("{sub}");
7417                    if memo.group_maps.contains_key(&repr) {
7418                        continue;
7419                    }
7420                    if let Some(gm) = self.try_batch_correlated_scalar(
7421                        sub,
7422                        Some((&agg.synth_rows, &ctx)),
7423                        cancel,
7424                    )? {
7425                        memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7426                    }
7427                }
7428            }
7429            for (ri, srow) in agg.synth_rows.iter().enumerate() {
7430                cancel.check()?;
7431                for (col, expr) in &agg.deferred {
7432                    let v =
7433                        self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7434                    if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7435                        *cell = v;
7436                    }
7437                }
7438            }
7439        }
7440        Ok(QueryResult::Rows {
7441            columns: agg.columns,
7442            rows: agg.rows,
7443        })
7444    }
7445
7446    /// v7.37 — streaming projection for the joined-non-aggregate
7447    /// shape (multi-table FROM, all projection items bound, no
7448    /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
7449    /// UNION). Walks the deferred join survivors and emits
7450    /// `&[&Value]` borrowed straight out of the source tables — no
7451    /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
7452    /// on the mailrs `PROJ` shape (about 4 ms saved).
7453    ///
7454    /// Returns `Ok(None)` when the shape doesn't qualify; the caller
7455    /// then falls back to the materialising path.
7456    /// v7.37 (round 831) — stream a joinless SELECT straight off the
7457    /// stored table, one row at a time, without ever building a row set.
7458    ///
7459    /// Returns `Ok(None)` for anything this cannot serve, and the caller
7460    /// falls through to the deferred-join path exactly as before: a
7461    /// missing table, or a cold tier whose hydration the fallback handles.
7462    /// Sort a single-table scan through the external sorter, so the
7463    /// answer's size is bounded by `work_mem` and not by the input.
7464    ///
7465    /// Sorting held every row twice — the scan's `Vec<Row>` and the
7466    /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
7467    /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
7468    /// enough ORDER BY took the server down, which is a liveness
7469    /// problem before it is a performance one.
7470    ///
7471    /// A SEPARATE walk rather than a change to `run_single_table_scan`,
7472    /// following what round 831 did for the joinless shape. That
7473    /// function is 552 lines whose projection loop is entangled with
7474    /// DISTINCT (which indexes back into the tagged vector) and with
7475    /// streaming top-N (whose boundary moves as the scan runs); both
7476    /// assume the projection has already happened when a row is
7477    /// pushed, which is exactly what spilling has to defer. Two earlier
7478    /// attempts tried to rework that loop and were reverted. Here the
7479    /// existing path is untouched and this one only claims shapes it
7480    /// can serve, so a decline costs nothing.
7481    ///
7482    /// Records are SOURCE rows, not projected ones: `finish` re-derives
7483    /// keys from what it decodes, and an ORDER BY key need not be in
7484    /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
7485    fn try_spill_sorted_scan(
7486        &self,
7487        stmt: &SelectStatement,
7488        from: &FromClause,
7489        cancel: CancelToken<'_>,
7490    ) -> Result<Option<QueryResult>, EngineError> {
7491        // Shapes this walk does not serve. Each one either needs the
7492        // whole tagged vector addressable (DISTINCT probes back into
7493        // it, WITH TIES re-reads its tail) or is already bounded
7494        // without spilling (a LIMIT makes the partial sort O(keep)).
7495        if !self.can_spill()
7496            || stmt.order_by.is_empty()
7497            || stmt.distinct
7498            || stmt.limit_with_ties
7499            || stmt.limit_literal().is_some()
7500            || !from.joins.is_empty()
7501            || from.primary.lateral_subquery.is_some()
7502            || from.primary.unnest_expr.is_some()
7503            || from.primary.generate_series_args.is_some()
7504            || select_has_window(stmt)
7505        {
7506            return Ok(None);
7507        }
7508        // A parent's rows are its children's. These walks scan the named
7509        // relation alone, so a partitioned or inherited parent comes back
7510        // short — and silently: the corpus caught `SELECT id FROM pr
7511        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
7512        // parent's own rows instead of the partitions'. `ONLY` is exactly
7513        // the case that does not fan out, so it stays, which is the test
7514        // the FROM-clause fan-out itself makes.
7515        if !from.primary.only
7516            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7517        {
7518            return Ok(None);
7519        }
7520        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7521            return Ok(None);
7522        };
7523        // Cold-tier rows live outside `rows()`; this walk would drop
7524        // them silently, the same reason round 831's walk declines.
7525        if table.has_cold_rows_fast() {
7526            return Ok(None);
7527        }
7528
7529        let alias = from
7530            .primary
7531            .alias
7532            .as_deref()
7533            .unwrap_or(from.primary.name.as_str());
7534        let cols = table.schema().columns.clone();
7535        let sess = self.dml_session();
7536        let ctx = EvalContext::new(&cols, Some(alias))
7537            .with_catalog(self.active_catalog())
7538            .with_session(&sess);
7539        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7540        let order_by = stmt.order_by.clone();
7541        // The same one-shot resolution the general path does (round
7542        // 582): each ORDER BY column is bound once, not once per row.
7543        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
7544        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7545        // Resolved BEFORE the scan, because it now decides what the sort
7546        // STORES and not just what it decodes (round 995).
7547        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
7548
7549        let mut sorter = crate::extsort::ExternalSorter::new(
7550            self.temp_run_factory,
7551            self.session_work_mem_bytes(),
7552            cols.clone(),
7553            &descs,
7554        )
7555        .with_stats(&self.spill_stats)
7556        .with_pruned(&needed);
7557        let snapshot = self.current_snapshot();
7558        // One key buffer for the whole scan: `push` drains it and leaves
7559        // the capacity behind.
7560        let mut keys: Vec<OrderKey> = Vec::new();
7561        // r1024 — compile the predicate once for the scan.
7562        //
7563        // These two sorted-spill scans are the paths a single-table SELECT
7564        // with an ORDER BY takes, and they were the last row-returning ones
7565        // still walking the expression tree per row. r1023 did the
7566        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
7567        // exactly this shape.
7568        //
7569        // Found from the profile's CALL TREE rather than its leaves. The
7570        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
7571        // 261, `mod_op` 178 — and two attempts at reasoning out which
7572        // function asked for it were both wrong. The tree names the caller
7573        // chain, and it named this one.
7574        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7575            .where_
7576            .as_ref()
7577            .filter(|w| crate::eval::fully_compilable(w))
7578            .map(|w| crate::eval::compile_expr(w, &ctx));
7579        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7580        for (i, row) in table.scan_visible_from(0, &snapshot) {
7581            if i.is_multiple_of(256) {
7582                cancel.check()?;
7583            }
7584            if let Some(c) = &compiled_where {
7585                if !crate::eval::compiled::eval_compiled_pred(
7586                    c,
7587                    row,
7588                    &ctx,
7589                    &mut eval_stack,
7590                    ctx.mysql_dialect,
7591                )? {
7592                    continue;
7593                }
7594            } else if let Some(w) = &stmt.where_ {
7595                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
7596                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7597                    continue;
7598                }
7599            }
7600            keys.clear();
7601            // `&[]`: this sorter compares with `cmp_multi_key_in(.., &[])`
7602            // (extsort.rs:213/543/1221), so the key must stay folded or the
7603            // two would disagree. See `build_order_keys_bound`.
7604            crate::orderby::build_order_keys_bound(
7605                &order_by,
7606                &order_bound,
7607                &[],
7608                row,
7609                &ctx,
7610                &mut keys,
7611            )?;
7612            sorter.push(&mut keys, row)?;
7613        }
7614
7615        let key_ctx = &ctx;
7616        let rows = sorter.finish(
7617            |src, buf| {
7618                crate::orderby::build_order_keys_bound(
7619                    &order_by,
7620                    &order_bound,
7621                    &[],
7622                    src,
7623                    key_ctx,
7624                    buf,
7625                )
7626            },
7627            |src| {
7628                let mut values = Vec::with_capacity(projection.len());
7629                for p in &projection {
7630                    values.push(
7631                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
7632                    );
7633                }
7634                Ok(Row::new(values))
7635            },
7636        )?;
7637
7638        let columns: Vec<ColumnSchema> = projection
7639            .iter()
7640            .map(|p| {
7641                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7642                c.user_enum_type = p.user_enum_type.clone();
7643                c.mysql_fsp = p.mysql_fsp;
7644                c
7645            })
7646            .collect();
7647        Ok(Some(QueryResult::Rows { columns, rows }))
7648    }
7649
7650    /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
7651    /// handing each row to the consumer instead of collecting the answer.
7652    ///
7653    /// That walk bounds the SORT and then returns `QueryResult::Rows`,
7654    /// which holds every output row. Measured at `work_mem = 4 MB` over
7655    /// 200-byte rows, RSS above the server's own baseline while the
7656    /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
7657    /// at 400k — linear — while the spill underneath worked correctly
7658    /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
7659    /// removes each file, so a count taken afterwards reads 0 whatever
7660    /// happened, and an earlier reading of "no spill at all" was that
7661    /// blind witness). The growth is the collected result, not the sort.
7662    ///
7663    /// Emitting makes peak the budget, one buffer per run and a single
7664    /// row — the state a merge already holds at every step. It also
7665    /// frees each projected row as the next is built rather than
7666    /// accumulating them, which is where the time is: a profile of the
7667    /// collecting walk put the allocator at 586 samples, more than every
7668    /// sort comparison combined (420), against 19 for `push` itself.
7669    /// v7.37 (round 923) — which of a sort record's columns the output half
7670    /// reads. The record is the SOURCE row (round 836), so a narrow projection
7671    /// decoded every column: skipping one 200-byte text halves a decode
7672    /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
7673    ///
7674    /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
7675    /// column reads NULL. Answers only when every projection item is a bare
7676    /// column reference AND every ORDER BY key is a bound column; anything
7677    /// else returns empty, decoding everything as before.
7678    /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
7679    /// drops references from expression kinds it does not enumerate.
7680    ///
7681    /// ORDER BY columns are included — the merge re-derives keys from the
7682    /// decoded row on the spilled path, so pruning one would sort NULLs.
7683    pub(crate) fn sort_record_columns_needed(
7684        items: &[SelectItem],
7685        order_bound: &[Option<usize>],
7686        arity: usize,
7687        ctx: &EvalContext,
7688    ) -> Vec<bool> {
7689        let all_bare = items.iter().all(|i| {
7690            matches!(
7691                i,
7692                SelectItem::Expr {
7693                    expr: Expr::Column(_),
7694                    ..
7695                }
7696            )
7697        });
7698        if !all_bare || order_bound.iter().any(Option::is_none) {
7699            return Vec::new();
7700        }
7701        let mut mask = alloc::vec![false; arity];
7702        for item in items {
7703            if let SelectItem::Expr {
7704                expr: Expr::Column(c),
7705                ..
7706            } = item
7707            {
7708                match crate::eval::find_column_pos(c, ctx) {
7709                    Some(p) if p < arity => mask[p] = true,
7710                    _ => return Vec::new(),
7711                }
7712            }
7713        }
7714        for p in order_bound.iter().flatten() {
7715            if *p < arity {
7716                mask[*p] = true;
7717            } else {
7718                return Vec::new();
7719            }
7720        }
7721        mask
7722    }
7723
7724    /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
7725    /// of sorting.
7726    ///
7727    /// PG serves such an ordering from the index and never sorts. We sorted:
7728    /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
7729    /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
7730    /// the sorter's own round trip — `ExternalSorter::finish_each` →
7731    /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
7732    /// Every row is encoded into the sorter's arena and decoded back out,
7733    /// for an order the index already holds.
7734    ///
7735    /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
7736    /// because it was built for top-N. This is the unbounded sibling.
7737    ///
7738    /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
7739    /// from a btree, so walking one would silently drop those rows. That is
7740    /// exactly the defect r1020 fixed on the top-N path, where it had
7741    /// shipped.
7742    /// r1044 — the index this statement's ORDER BY can be WALKED on,
7743    /// instead of sorted, or `None`.
7744    ///
7745    /// Extracted so `EXPLAIN` can ask the same question the executor
7746    /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
7747    /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
7748    /// while the executor walked the primary key — 34.9 ms against
7749    /// 147.0 for the same query ordered by an unindexed column, so the
7750    /// walk was plainly running. Round 551 fixed a different case of
7751    /// this and wrote the reason down: EXPLAIN is the first thing any
7752    /// performance question opens, and an instrument that misnames the
7753    /// access path is worse than one that says nothing.
7754    ///
7755    /// The gate is here once. Two copies of it is how the plan and the
7756    /// executor come to disagree again.
7757    pub(crate) fn index_order_walk_target(
7758        &self,
7759        stmt: &SelectStatement,
7760        from: &FromClause,
7761    ) -> Option<(String, usize)> {
7762        if stmt.order_by.len() != 1
7763            || !stmt.distinct_on.is_empty()
7764            || stmt.limit_with_ties
7765            || stmt.limit.is_some()
7766            || stmt.offset.is_some()
7767            || stmt.having.is_some()
7768            || stmt.group_by.is_some()
7769            || !stmt.unions.is_empty()
7770            || !from.joins.is_empty()
7771            || from.primary.lateral_subquery.is_some()
7772            || from.primary.unnest_expr.is_some()
7773            || from.primary.as_of_segment.is_some()
7774            || from.primary.generate_series_args.is_some()
7775            || select_has_window(stmt)
7776            || aggregate::uses_aggregate(stmt)
7777        {
7778            return None;
7779        }
7780        if stmt
7781            .items
7782            .iter()
7783            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7784        {
7785            return None;
7786        }
7787        let table = self.active_catalog().get(&from.primary.name)?;
7788        if table.has_cold_rows_fast() {
7789            return None;
7790        }
7791        if !from.primary.only
7792            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7793        {
7794            return None;
7795        }
7796        let alias = from
7797            .primary
7798            .alias
7799            .as_deref()
7800            .unwrap_or(from.primary.name.as_str());
7801        let cols = &table.schema().columns;
7802        let order = &stmt.order_by[0];
7803        let Expr::Column(oc) = &order.expr else {
7804            return None;
7805        };
7806        if let Some(q) = &oc.qualifier
7807            && !q.eq_ignore_ascii_case(alias)
7808        {
7809            return None;
7810        }
7811        let order_pos = cols
7812            .iter()
7813            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
7814        // r1047 — DISTINCT joins the walk when the projection IS the
7815        // order column, and only then. The index's keys are canonical
7816        // (r1039: representation equality is value equality — the
7817        // property every seek already depends on), so one key is one
7818        // distinct value and the walk can emit the first passing row of
7819        // each key group instead of hashing every row. On the release
7820        // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
7821        // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
7822        // with an ablation floor of 14.8, because the hash must
7823        // normalize and probe ALL the rows; the walk visits each key
7824        // once. A wider projection makes DISTINCT about the whole tuple,
7825        // not the key, so anything else still declines.
7826        if stmt.distinct {
7827            let only_the_order_column = stmt.items.len() == 1
7828                && match &stmt.items[0] {
7829                    SelectItem::Expr {
7830                        expr: Expr::Column(c),
7831                        ..
7832                    } => {
7833                        c.name.eq_ignore_ascii_case(&oc.name)
7834                            && match &c.qualifier {
7835                                Some(q) => q.eq_ignore_ascii_case(alias),
7836                                None => true,
7837                            }
7838                    }
7839                    _ => false,
7840                };
7841            if !only_the_order_column {
7842                return None;
7843            }
7844        }
7845        // r1046 — a nullable key no longer refuses the walk; it changes
7846        // what the walk has to do. A NULL key is not in the btree, so
7847        // walking alone would silently drop those rows — the r1020
7848        // defect, which shipped once. The walk emits them separately, at
7849        // the end SQL puts them.
7850        //
7851        // Refusing was costing every nullable indexed column a 3.4x:
7852        // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
7853        // 72.0 ms with the column nullable and 20.2 with the same data
7854        // under NOT NULL. `NOT NULL` is not the default, so that was the
7855        // common case paying for the uncommon one.
7856        let index = table.index_on(order_pos)?;
7857        if !matches!(index.kind, spg_storage::IndexKind::BTree(_))
7858            || index.expression.is_some()
7859            || index.partial_predicate.is_some()
7860        {
7861            return None;
7862        }
7863        Some((index.name.clone(), order_pos))
7864    }
7865
7866    fn try_index_order_stream<F>(
7867        &self,
7868        stmt: &SelectStatement,
7869        from: &FromClause,
7870        cancel: CancelToken<'_>,
7871        emit: &mut F,
7872    ) -> Result<Option<usize>, EngineError>
7873    where
7874        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7875    {
7876        // r1044 — the shape gate lives in `index_order_walk_target`, so
7877        // `EXPLAIN` answers the same question. What stays here is the
7878        // part that RAISES (an illegal ORDER BY has to keep erroring
7879        // from where it did) and the bindings the walk needs.
7880        crate::orderby::check_order_by_legality(stmt)?;
7881        crate::orderby::check_order_by_positions(stmt)?;
7882        crate::window::reject_window_in_row_clauses(stmt)?;
7883        let Some((_, order_pos)) = self.index_order_walk_target(stmt, from) else {
7884            return Ok(None);
7885        };
7886        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7887            return Ok(None);
7888        };
7889        let alias = from
7890            .primary
7891            .alias
7892            .as_deref()
7893            .unwrap_or(from.primary.name.as_str());
7894        let cols = table.schema().columns.clone();
7895        let order = &stmt.order_by[0];
7896        let Some(index) = table.index_on(order_pos) else {
7897            return Ok(None);
7898        };
7899
7900        let sess = self.dml_session();
7901        let ctx = EvalContext::new(&cols, Some(alias))
7902            .with_catalog(self.active_catalog())
7903            .with_session(&sess);
7904        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7905        let columns: Vec<ColumnSchema> = projection
7906            .iter()
7907            .map(|p| {
7908                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7909                c.user_enum_type = p.user_enum_type.clone();
7910                c.mysql_fsp = p.mysql_fsp;
7911                c
7912            })
7913            .collect();
7914        emit(crate::StreamItem::Header(&columns))?;
7915        let bound_pos: Vec<Option<usize>> = projection
7916            .iter()
7917            .map(|p| match &p.expr {
7918                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
7919                    Ok(Some(pos)) => Some(pos),
7920                    _ => None,
7921                },
7922                _ => None,
7923            })
7924            .collect();
7925
7926        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7927            .where_
7928            .as_ref()
7929            .filter(|w| crate::eval::fully_compilable(w))
7930            .map(|w| crate::eval::compile_expr(w, &ctx));
7931        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7932        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
7933        let snapshot = self.current_snapshot();
7934
7935        // A btree holds one locator per row VERSION, so a row whose key was
7936        // updated can sit under two keys and a dead one can sit beside its
7937        // replacement. The visibility gate drops the dead; `seen` drops a
7938        // live row that the walk reaches twice, which would otherwise be a
7939        // duplicated output row rather than a slow one.
7940        let mut emitted_rows = alloc::vec![false; table.rows().len()];
7941
7942        // r1046 — the rows the index cannot hold.
7943        //
7944        // A NULL key is not in the btree, so the walk below never reaches
7945        // those rows; they are emitted here, at the end SQL puts them.
7946        // PG's default is NULLS LAST ascending and NULLS FIRST
7947        // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
7948        // the same rule `order_by_value_cmp_raw` applies to the sort this
7949        // replaces, so the two orders agree.
7950        //
7951        // Finding them costs one pass over the column. That pass is why
7952        // this is still worth doing: the sort it replaces encodes and
7953        // decodes every row, and the walk plus the pass measured 72.0 ms
7954        // down to about 22 on 400,000 rows.
7955        let nulls_first = order.nulls_first.unwrap_or(order.desc);
7956        // r1047 — under DISTINCT the walk emits the FIRST passing row of
7957        // each key group and skips the rest; the gate admits DISTINCT
7958        // only when the projection is the order column itself, so one
7959        // canonical key is one output row. NULL is one distinct value,
7960        // so the NULL pass stops at its first emit too.
7961        let distinct = stmt.distinct;
7962        let mut count = 0usize;
7963        let mut visited = 0usize;
7964        let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
7965                                  eval_stack: &mut Vec<Value<'static>>,
7966                                  values: &mut Vec<Value<'static>>,
7967                                  visited: &mut usize,
7968                                  emit: &mut F|
7969         -> Result<usize, EngineError> {
7970            if !cols[order_pos].nullable {
7971                return Ok(0);
7972            }
7973            let mut n = 0usize;
7974            for (ri, row) in table.rows().iter().enumerate() {
7975                if !matches!(row.values.get(order_pos), Some(Value::Null)) {
7976                    continue;
7977                }
7978                if emitted_rows.get(ri).copied().unwrap_or(true) {
7979                    continue;
7980                }
7981                if !table.is_row_visible(ri, &snapshot) {
7982                    continue;
7983                }
7984                *visited += 1;
7985                if visited.is_multiple_of(256) {
7986                    cancel.check()?;
7987                }
7988                emitted_rows[ri] = true;
7989                if Self::stream_project_row(
7990                    row,
7991                    stmt.where_.as_ref(),
7992                    compiled_where.as_ref(),
7993                    eval_stack,
7994                    &projection,
7995                    &bound_pos,
7996                    &ctx,
7997                    values,
7998                    emit,
7999                )? {
8000                    n += 1;
8001                    if distinct {
8002                        break;
8003                    }
8004                }
8005            }
8006            Ok(n)
8007        };
8008
8009        if nulls_first {
8010            count += emit_null_rows(
8011                &mut emitted_rows,
8012                &mut eval_stack,
8013                &mut values,
8014                &mut visited,
8015                emit,
8016            )?;
8017        }
8018
8019        let walker: alloc::boxed::Box<
8020            dyn Iterator<Item = (&spg_storage::IndexKey, &spg_storage::PostingList)>,
8021        > = if order.desc {
8022            alloc::boxed::Box::new(index.iter_desc())
8023        } else {
8024            alloc::boxed::Box::new(index.iter_asc())
8025        };
8026        for (_key, locators) in walker {
8027            for loc in locators {
8028                let spg_storage::RowLocator::Hot(ri) = *loc else {
8029                    continue;
8030                };
8031                if emitted_rows.get(ri).copied().unwrap_or(true) {
8032                    continue;
8033                }
8034                if !table.is_row_visible(ri, &snapshot) {
8035                    continue;
8036                }
8037                let Some(row) = table.rows().get(ri) else {
8038                    continue;
8039                };
8040                visited += 1;
8041                if visited.is_multiple_of(256) {
8042                    cancel.check()?;
8043                }
8044                emitted_rows[ri] = true;
8045                if Self::stream_project_row(
8046                    row,
8047                    stmt.where_.as_ref(),
8048                    compiled_where.as_ref(),
8049                    &mut eval_stack,
8050                    &projection,
8051                    &bound_pos,
8052                    &ctx,
8053                    &mut values,
8054                    emit,
8055                )? {
8056                    count += 1;
8057                    // One row per key group: the rest are the same value.
8058                    if distinct {
8059                        break;
8060                    }
8061                }
8062            }
8063        }
8064
8065        if !nulls_first {
8066            count += emit_null_rows(
8067                &mut emitted_rows,
8068                &mut eval_stack,
8069                &mut values,
8070                &mut visited,
8071                emit,
8072            )?;
8073        }
8074        Ok(Some(count))
8075    }
8076
8077    /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
8078    /// building an `OrderKey` vector per row.
8079    ///
8080    /// The row-returning sorted scan allocates twice per row: one
8081    /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
8082    /// projection. Counted over 400 k rows (r1030,
8083    /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
8084    /// allocations and 208 MB of traffic for an answer of four hundred
8085    /// thousand integers.
8086    ///
8087    /// The key half is pure ceremony on this shape.
8088    /// `sort_tagged_by_inline_int_key` already sorts indices rather than
8089    /// rows, so the per-row vector is built, has one integer taken out of
8090    /// it, and is then dragged through the permutation — it exists to carry
8091    /// a number the row's column already held. This lane carries the number
8092    /// instead, in a fixed-size array that lives inside the buffer element
8093    /// and allocates nothing. Same idea as the predicate VM's integer lane.
8094    ///
8095    /// Declines to `None` for anything it does not cover, and every caller
8096    /// falls through to the general path, so the gate list is the
8097    /// specification.
8098    ///
8099    /// Ties: equal keys keep scan order, as the stable sort on the general
8100    /// path does. Rows that tie on every ORDER BY term are entitled to any
8101    /// order among themselves either way — see `STABILITY.md`.
8102    fn try_int_key_sorted_stream<F>(
8103        &self,
8104        stmt: &SelectStatement,
8105        from: &FromClause,
8106        cancel: CancelToken<'_>,
8107        emit: &mut F,
8108    ) -> Result<Option<usize>, EngineError>
8109    where
8110        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8111    {
8112        /// Sort terms this lane carries inline. Four covers every ORDER BY
8113        /// in the endpoint sweep and in the dogfood corpus; wider ones fall
8114        /// through rather than growing the buffer element for everybody.
8115        const MAX_KEYS: usize = 4;
8116
8117        if stmt.order_by.is_empty()
8118            || stmt.order_by.len() > MAX_KEYS
8119            || stmt.distinct
8120            || stmt.limit_with_ties
8121            || stmt.limit.is_some()
8122            || stmt.offset.is_some()
8123            || stmt.having.is_some()
8124            || stmt.group_by.is_some()
8125            || !stmt.unions.is_empty()
8126            || !from.joins.is_empty()
8127            || from.primary.lateral_subquery.is_some()
8128            || from.primary.unnest_expr.is_some()
8129            || from.primary.as_of_segment.is_some()
8130            || from.primary.generate_series_args.is_some()
8131            || select_has_window(stmt)
8132            || aggregate::uses_aggregate(stmt)
8133        {
8134            return Ok(None);
8135        }
8136        if stmt
8137            .items
8138            .iter()
8139            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8140        {
8141            return Ok(None);
8142        }
8143        crate::orderby::check_order_by_legality(stmt)?;
8144        crate::orderby::check_order_by_positions(stmt)?;
8145        crate::window::reject_window_in_row_clauses(stmt)?;
8146        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8147            return Ok(None);
8148        };
8149        if table.has_cold_rows_fast() {
8150            return Ok(None);
8151        }
8152        if !from.primary.only
8153            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8154        {
8155            return Ok(None);
8156        }
8157        let alias = from
8158            .primary
8159            .alias
8160            .as_deref()
8161            .unwrap_or(from.primary.name.as_str());
8162        let cols = table.schema().columns.clone();
8163
8164        // Every ORDER BY term must be a NOT NULL integer column of this
8165        // table. NOT NULL is what lets the key be a bare integer: with
8166        // NULLs the lane would have to carry their ordering too, and
8167        // getting that subtly wrong is the r1020 defect.
8168        let mut key_pos = [0usize; MAX_KEYS];
8169        let mut descs = [false; MAX_KEYS];
8170        // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
8171        // which the AST records as `None`; `unwrap_or(desc)` is how the
8172        // rest of the engine resolves it.
8173        let mut nulls_first = [false; MAX_KEYS];
8174        let n_keys = stmt.order_by.len();
8175        for (slot, order) in stmt.order_by.iter().enumerate() {
8176            let Expr::Column(oc) = &order.expr else {
8177                return Ok(None);
8178            };
8179            if let Some(q) = &oc.qualifier
8180                && !q.eq_ignore_ascii_case(alias)
8181            {
8182                return Ok(None);
8183            }
8184            let Some(pos) = cols
8185                .iter()
8186                .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
8187            else {
8188                return Ok(None);
8189            };
8190            if !matches!(
8191                cols[pos].ty,
8192                spg_storage::DataType::SmallInt
8193                    | spg_storage::DataType::Int
8194                    | spg_storage::DataType::BigInt
8195            ) {
8196                return Ok(None);
8197            }
8198            key_pos[slot] = pos;
8199            descs[slot] = order.desc;
8200            nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
8201        }
8202
8203        let sess = self.dml_session();
8204        let ctx = EvalContext::new(&cols, Some(alias))
8205            .with_catalog(self.active_catalog())
8206            .with_session(&sess);
8207        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8208        let columns: Vec<ColumnSchema> = projection
8209            .iter()
8210            .map(|p| {
8211                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8212                c.user_enum_type = p.user_enum_type.clone();
8213                c.mysql_fsp = p.mysql_fsp;
8214                c
8215            })
8216            .collect();
8217        let bound_pos: Vec<Option<usize>> = projection
8218            .iter()
8219            .map(|p| match &p.expr {
8220                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8221                    Ok(Some(pos)) => Some(pos),
8222                    _ => None,
8223                },
8224                _ => None,
8225            })
8226            .collect();
8227        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8228            .where_
8229            .as_ref()
8230            .filter(|w| crate::eval::fully_compilable(w))
8231            .map(|w| crate::eval::compile_expr(w, &ctx));
8232
8233        // The same first-observable point the materialising planner fires,
8234        // placed after the gates so it fires exactly once: this lane runs
8235        // BEFORE that planner and would otherwise be a hole in the
8236        // panic-isolation and cancellation-race coverage rather than a
8237        // faster path through it.
8238        crate::injection_point!("planner_first_row_fetch", &stmt.from);
8239
8240        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8241        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8242        let mut budget = ByteBudget::new(self.max_query_bytes);
8243        let snapshot = self.current_snapshot();
8244        // Keys, a NULL bit per key slot, and the row. The bitmask keeps
8245        // the element small: a nullable key still costs one bit rather
8246        // than a second array.
8247        let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
8248
8249        for (ri, row) in table.rows().iter().enumerate() {
8250            if ri.is_multiple_of(256) {
8251                cancel.check()?;
8252            }
8253            if !table.is_row_visible(ri, &snapshot) {
8254                continue;
8255            }
8256            // The key comes from the STORED row, before projection: an
8257            // ORDER BY column need not appear in the select list.
8258            let mut keys = [0i64; MAX_KEYS];
8259            let mut nulls = 0u8;
8260            let mut keyed = true;
8261            for slot in 0..n_keys {
8262                match row.values.get(key_pos[slot]) {
8263                    Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
8264                    Some(Value::Int(v)) => keys[slot] = i64::from(*v),
8265                    Some(Value::BigInt(v)) => keys[slot] = *v,
8266                    Some(Value::Null) | None => nulls |= 1 << slot,
8267                    // An integer column holding something else is a row
8268                    // this lane cannot order; hand the whole query back
8269                    // rather than guess at it.
8270                    _ => {
8271                        keyed = false;
8272                        break;
8273                    }
8274                }
8275            }
8276            if !keyed {
8277                return Ok(None);
8278            }
8279            if !Self::stream_filter_project(
8280                row,
8281                stmt.where_.as_ref(),
8282                compiled_where.as_ref(),
8283                &mut eval_stack,
8284                &projection,
8285                &bound_pos,
8286                &ctx,
8287                &mut values,
8288            )? {
8289                continue;
8290            }
8291            budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
8292            sorted.push((keys, nulls, core::mem::take(&mut values)));
8293            values.reserve(projection.len());
8294        }
8295
8296        sorted.sort_by(|a, b| {
8297            use core::cmp::Ordering;
8298            for slot in 0..n_keys {
8299                let bit = 1u8 << slot;
8300                let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
8301                    (true, true) => Ordering::Equal,
8302                    // Where the NULLs go is already decided — `nulls_first`
8303                    // resolved DESC's default when it was read. Reversing
8304                    // this for DESC as well would apply the direction
8305                    // twice and put them at the wrong end.
8306                    (true, false) => {
8307                        if nulls_first[slot] {
8308                            Ordering::Less
8309                        } else {
8310                            Ordering::Greater
8311                        }
8312                    }
8313                    (false, true) => {
8314                        if nulls_first[slot] {
8315                            Ordering::Greater
8316                        } else {
8317                            Ordering::Less
8318                        }
8319                    }
8320                    (false, false) => {
8321                        let o = a.0[slot].cmp(&b.0[slot]);
8322                        if descs[slot] { o.reverse() } else { o }
8323                    }
8324                };
8325                if ord != Ordering::Equal {
8326                    return ord;
8327                }
8328            }
8329            Ordering::Equal
8330        });
8331
8332        emit(crate::StreamItem::Header(&columns))?;
8333        let count = sorted.len();
8334        for (_, _, vals) in &sorted {
8335            emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
8336        }
8337        Ok(Some(count))
8338    }
8339
8340    fn try_spill_sorted_stream<F>(
8341        &self,
8342        stmt: &SelectStatement,
8343        from: &FromClause,
8344        cancel: CancelToken<'_>,
8345        emit: &mut F,
8346    ) -> Result<Option<usize>, EngineError>
8347    where
8348        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8349    {
8350        // The shapes `try_spill_sorted_scan` declines, plus the ones the
8351        // streaming executor does not carry (a LIMIT is already bounded
8352        // by a partial sort; the rest need the answer addressable).
8353        if !self.can_spill()
8354            || stmt.order_by.is_empty()
8355            || stmt.distinct
8356            || stmt.limit_with_ties
8357            || stmt.limit.is_some()
8358            || stmt.offset.is_some()
8359            || stmt.having.is_some()
8360            || stmt.group_by.is_some()
8361            || !stmt.unions.is_empty()
8362            || !from.joins.is_empty()
8363            || from.primary.lateral_subquery.is_some()
8364            || from.primary.unnest_expr.is_some()
8365            || from.primary.as_of_segment.is_some()
8366            || from.primary.generate_series_args.is_some()
8367            || select_has_window(stmt)
8368            || aggregate::uses_aggregate(stmt)
8369        {
8370            return Ok(None);
8371        }
8372        if stmt
8373            .items
8374            .iter()
8375            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8376        {
8377            return Ok(None);
8378        }
8379        // Everything `exec_bare_select_cancel` does before it scans runs
8380        // BELOW this path, so a statement claimed here skips it. Three of
8381        // those were missed on the way in and each was caught by a
8382        // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
8383        // ORDER BY 2` sorted happily instead of raising 42P10), the
8384        // cancellation check by another, the partition fan-out by the
8385        // differential corpus. What is reconciled, item by item: with-ties
8386        // needs ORDER BY (gated above), USING/NATURAL and RLS join
8387        // rewrites (joins gated above), the single-table RLS predicate
8388        // (the dispatcher declines a policy-subject table before this is
8389        // reached), the meta-view dispatch (those names are not in the
8390        // catalog, so the lookup below declines). These three are calls,
8391        // so the message and SQLSTATE are the ones the fall-back gives —
8392        // `select_has_window` above reads the select list and ORDER BY but
8393        // not WHERE, which is the case the third one covers.
8394        crate::orderby::check_order_by_legality(stmt)?;
8395        crate::orderby::check_order_by_positions(stmt)?;
8396        crate::window::reject_window_in_row_clauses(stmt)?;
8397        // A parent's rows are its children's. These walks scan the named
8398        // relation alone, so a partitioned or inherited parent comes back
8399        // short — and silently: the corpus caught `SELECT id FROM pr
8400        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8401        // parent's own rows instead of the partitions'. `ONLY` is exactly
8402        // the case that does not fan out, so it stays, which is the test
8403        // the FROM-clause fan-out itself makes.
8404        if !from.primary.only
8405            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8406        {
8407            return Ok(None);
8408        }
8409        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8410            return Ok(None);
8411        };
8412        // Cold-tier rows live outside `rows()`; this walk would drop
8413        // them silently, the same reason round 831's walk declines.
8414        if table.has_cold_rows_fast() {
8415            return Ok(None);
8416        }
8417
8418        let alias = from
8419            .primary
8420            .alias
8421            .as_deref()
8422            .unwrap_or(from.primary.name.as_str());
8423        let cols = table.schema().columns.clone();
8424        let sess = self.dml_session();
8425        let ctx = EvalContext::new(&cols, Some(alias))
8426            .with_catalog(self.active_catalog())
8427            .with_session(&sess);
8428        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8429        let order_by = stmt.order_by.clone();
8430        // The same one-shot resolution the general path does (round
8431        // 582): each ORDER BY column is bound once, not once per row.
8432        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8433        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8434        // Resolved BEFORE the scan, because it now decides what the sort
8435        // STORES and not just what it decodes (round 995).
8436        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8437
8438        let mut sorter = crate::extsort::ExternalSorter::new(
8439            self.temp_run_factory,
8440            self.session_work_mem_bytes(),
8441            cols.clone(),
8442            &descs,
8443        )
8444        .with_stats(&self.spill_stats)
8445        .with_pruned(&needed);
8446        let snapshot = self.current_snapshot();
8447        // One key buffer for the whole scan: `push` drains it and leaves
8448        // the capacity behind.
8449        let mut keys: Vec<OrderKey> = Vec::new();
8450        // r1024 — compile the predicate once for the scan.
8451        //
8452        // These two sorted-spill scans are the paths a single-table SELECT
8453        // with an ORDER BY takes, and they were the last row-returning ones
8454        // still walking the expression tree per row. r1023 did the
8455        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8456        // exactly this shape.
8457        //
8458        // Found from the profile's CALL TREE rather than its leaves. The
8459        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8460        // 261, `mod_op` 178 — and two attempts at reasoning out which
8461        // function asked for it were both wrong. The tree names the caller
8462        // chain, and it named this one.
8463        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8464            .where_
8465            .as_ref()
8466            .filter(|w| crate::eval::fully_compilable(w))
8467            .map(|w| crate::eval::compile_expr(w, &ctx));
8468        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8469        for (i, row) in table.scan_visible_from(0, &snapshot) {
8470            if i.is_multiple_of(256) {
8471                cancel.check()?;
8472            }
8473            if let Some(c) = &compiled_where {
8474                if !crate::eval::compiled::eval_compiled_pred(
8475                    c,
8476                    row,
8477                    &ctx,
8478                    &mut eval_stack,
8479                    ctx.mysql_dialect,
8480                )? {
8481                    continue;
8482                }
8483            } else if let Some(w) = &stmt.where_ {
8484                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8485                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8486                    continue;
8487                }
8488            }
8489            keys.clear();
8490            // `&[]`: this sorter compares with `cmp_multi_key_in(.., &[])`
8491            // (extsort.rs:213/543/1221), so the key must stay folded or the
8492            // two would disagree. See `build_order_keys_bound`.
8493            crate::orderby::build_order_keys_bound(
8494                &order_by,
8495                &order_bound,
8496                &[],
8497                row,
8498                &ctx,
8499                &mut keys,
8500            )?;
8501            sorter.push(&mut keys, row)?;
8502        }
8503
8504        let columns: Vec<ColumnSchema> = projection
8505            .iter()
8506            .map(|p| {
8507                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8508                c.user_enum_type = p.user_enum_type.clone();
8509                c.mysql_fsp = p.mysql_fsp;
8510                c
8511            })
8512            .collect();
8513        emit(crate::StreamItem::Header(&columns))?;
8514
8515        let key_ctx = &ctx;
8516        let mut emitted_since_check = 0usize;
8517        let n = sorter.finish_each(
8518            |src, buf| {
8519                crate::orderby::build_order_keys_bound(
8520                    &order_by,
8521                    &order_bound,
8522                    &[],
8523                    src,
8524                    key_ctx,
8525                    buf,
8526                )
8527            },
8528            |src, values| {
8529                for p in &projection {
8530                    values.push(
8531                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8532                    );
8533                }
8534                Ok(())
8535            },
8536            |cells| {
8537                // The merge is the long half of a big sort, and the scan's
8538                // check above stops running once it ends: a cancelled
8539                // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
8540                // anyway. Same stride as the scan.
8541                emitted_since_check += 1;
8542                if emitted_since_check >= 256 {
8543                    emitted_since_check = 0;
8544                    cancel.check()?;
8545                }
8546                emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
8547            },
8548        )?;
8549        Ok(Some(n))
8550    }
8551
8552    /// One row of the single-table streaming walk: the WHERE test, the
8553    /// projection, the emit. Returns whether a row was emitted.
8554    ///
8555    /// v7.39 (round 970) — factored out because the walk now has two ways
8556    /// to reach a row, the sequential scan and an index seek's candidate
8557    /// positions, and both must do IDENTICALLY this. A copy in each is how
8558    /// two paths for one job drift; this file already carries the cost of
8559    /// that lesson twice (rounds 823 and 961, both resolvers).
8560    ///
8561    /// `#[inline]` so the scan loop keeps the shape round 957 measured it
8562    /// in — a shared hot path pays for a new abstraction whether or not it
8563    /// uses it, and this one is on the scan.
8564    #[inline]
8565    #[allow(clippy::too_many_arguments)]
8566    fn stream_filter_project(
8567        row: &spg_storage::Row<'static>,
8568        where_: Option<&Expr>,
8569        // r1023 — the same WHERE, compiled once by the caller. `None` means
8570        // the expression did not qualify and `where_` is evaluated as before.
8571        compiled_where: Option<&crate::eval::CompiledExpr>,
8572        eval_stack: &mut Vec<Value<'static>>,
8573        projection: &[ProjectedItem],
8574        bound_pos: &[Option<usize>],
8575        ctx: &crate::eval::EvalContext<'_>,
8576        values: &mut Vec<Value<'static>>,
8577    ) -> Result<bool, EngineError> {
8578        // r1023 — this scan ran its predicate through the TREE INTERPRETER,
8579        // once per row, and it was the only row-returning path that did.
8580        // The aggregate path, `table_access`, and the PK walker all compile
8581        // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
8582        // server's live samples were `eval_expr` 99, `apply_binary` 81,
8583        // `mod_op` 29 — the interpreter, not delivery.
8584        //
8585        // The arithmetic accounted for it exactly. Over the wire, the same
8586        // filter costs 6.375 ms returning rows and 0.679 ms counting them;
8587        // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
8588        // which is what an interpreted predicate costs against the compiled
8589        // lane's 11.7. It was named "delivery after a filter" before this
8590        // profile, and it was never delivery.
8591        if let Some(c) = compiled_where {
8592            if !crate::eval::compiled::eval_compiled_pred(
8593                c,
8594                row,
8595                ctx,
8596                eval_stack,
8597                ctx.mysql_dialect,
8598            )? {
8599                return Ok(false);
8600            }
8601        } else if let Some(w) = where_ {
8602            let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
8603            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8604                return Ok(false);
8605            }
8606        }
8607        values.clear();
8608        for (p, bound) in projection.iter().zip(bound_pos) {
8609            values.push(match bound {
8610                Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
8611                None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
8612            });
8613        }
8614        Ok(true)
8615    }
8616
8617    /// The same filter and projection, then emit. Split from
8618    /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
8619    /// before it can emit them — a sort — runs the identical predicate and
8620    /// projection rather than a second copy of them.
8621    #[allow(clippy::too_many_arguments)]
8622    fn stream_project_row<F>(
8623        row: &spg_storage::Row<'static>,
8624        where_: Option<&Expr>,
8625        compiled_where: Option<&crate::eval::CompiledExpr>,
8626        eval_stack: &mut Vec<Value<'static>>,
8627        projection: &[ProjectedItem],
8628        bound_pos: &[Option<usize>],
8629        ctx: &crate::eval::EvalContext<'_>,
8630        values: &mut Vec<Value<'static>>,
8631        emit: &mut F,
8632    ) -> Result<bool, EngineError>
8633    where
8634        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8635    {
8636        if !Self::stream_filter_project(
8637            row,
8638            where_,
8639            compiled_where,
8640            eval_stack,
8641            projection,
8642            bound_pos,
8643            ctx,
8644            values,
8645        )? {
8646            return Ok(false);
8647        }
8648        emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
8649        Ok(true)
8650    }
8651
8652    fn try_stream_single_table<F>(
8653        &self,
8654        stmt: &SelectStatement,
8655        from: &FromClause,
8656        cancel: CancelToken<'_>,
8657        emit: &mut F,
8658    ) -> Result<Option<usize>, EngineError>
8659    where
8660        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8661    {
8662        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8663            return Ok(None);
8664        };
8665        // Cold-tier rows live outside `rows()`; the materialising fallback
8666        // covers both tiers and this walk would silently drop them.
8667        if table.has_cold_rows_fast() {
8668            return Ok(None);
8669        }
8670        let alias = from
8671            .primary
8672            .alias
8673            .as_deref()
8674            .unwrap_or(from.primary.name.as_str());
8675        let cols = table.schema().columns.clone();
8676        let sess = self.dml_session();
8677        let ctx = EvalContext::new(&cols, Some(alias))
8678            .with_catalog(self.active_catalog())
8679            .with_session(&sess);
8680        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8681
8682        let columns: Vec<ColumnSchema> = projection
8683            .iter()
8684            .map(|p| {
8685                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8686                c.user_enum_type = p.user_enum_type.clone();
8687                c.mysql_fsp = p.mysql_fsp;
8688                c
8689            })
8690            .collect();
8691        emit(crate::StreamItem::Header(&columns))?;
8692
8693        // v7.37 (round 957) — resolve each bare-column projection ONCE
8694        // instead of once per row. `find_column_pos`-style resolution is a
8695        // linear walk of the schema comparing column-name strings, and the
8696        // row loop below ran it for every cell of every row: measured at
8697        // 400k rows, binding it out of the loop took `SELECT pad` from
8698        // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
8699        //
8700        // ORDER BY has bound its keys this way since round 582
8701        // (`order_by_bound_positions`); the projection never did.
8702        //
8703        // `locate_column` is the same resolution `resolve_column` performs,
8704        // returning the site instead of the value, so the two cannot drift
8705        // apart the way a second hand-written resolver would. Anything it
8706        // declines — an expression, a whole-row reference, a name that does
8707        // not resolve — binds to `None` and takes the general path below,
8708        // errors included, so an empty table still reports nothing rather
8709        // than raising at bind time.
8710        let bound_pos: Vec<Option<usize>> = projection
8711            .iter()
8712            .map(|p| match &p.expr {
8713                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8714                    Ok(Some(pos)) => Some(pos),
8715                    _ => None,
8716                },
8717                _ => None,
8718            })
8719            .collect();
8720
8721        // One snapshot for the whole scan, as the materialising path takes.
8722        let snapshot = self.current_snapshot();
8723
8724        // v7.39 (round 970) — ask the indices BEFORE walking the table.
8725        //
8726        // This walk had no index step at all, and it is preferred over the
8727        // materialising path, which does have one (`pick_indexed_rows` ->
8728        // `try_index_seek`). So a primary-key point lookup — the commonest
8729        // statement there is — read every row: measured on 500k rows,
8730        // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
8731        // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
8732        // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
8733        //
8734        // The control that named it: `... OFFSET 0` — semantically the same
8735        // query — answered in 0.159 ms, because OFFSET is one of the shape
8736        // gates that declines this walk and sends the statement to the path
8737        // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
8738        // no semantics in common; what they share is making this function
8739        // stand down.
8740        //
8741        // The seek only NARROWS: every candidate still goes through the
8742        // full WHERE below, exactly as the mutation paths use it, so a
8743        // partial index match cannot change an answer. Positions come back
8744        // already visibility-filtered and already capped at a quarter of the
8745        // table (round 490), so a seek can never cost more than the scan it
8746        // replaces, and `None` means "walk the table" as before.
8747        //
8748        // Sorted because the scan would have produced table order and the
8749        // index produces key order. Without an ORDER BY neither is promised,
8750        // but a walk that silently reorders its answer when an index happens
8751        // to exist is a difference nobody asked for.
8752        let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
8753            crate::index_access::try_index_seek_positions(w, &cols, table, alias, &snapshot)
8754        });
8755
8756        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8757        // r1023 — compile the predicate once for the whole scan. Same gate
8758        // every other path uses: `fully_compilable` or keep the interpreter,
8759        // so a shape the VM cannot take answers exactly as it did before.
8760        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8761            .where_
8762            .as_ref()
8763            .filter(|w| crate::eval::fully_compilable(w))
8764            .map(|w| crate::eval::compile_expr(w, &ctx));
8765        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8766        let mut count: usize = 0;
8767        match seek_positions {
8768            Some(mut positions) => {
8769                positions.sort_unstable();
8770                for (n, pos) in positions.into_iter().enumerate() {
8771                    if n.is_multiple_of(256) {
8772                        cancel.check()?;
8773                    }
8774                    let Some(row) = table.rows().get(pos) else {
8775                        continue;
8776                    };
8777                    if Self::stream_project_row(
8778                        row,
8779                        stmt.where_.as_ref(),
8780                        compiled_where.as_ref(),
8781                        &mut eval_stack,
8782                        &projection,
8783                        &bound_pos,
8784                        &ctx,
8785                        &mut values,
8786                        emit,
8787                    )? {
8788                        count += 1;
8789                    }
8790                }
8791            }
8792            None => {
8793                // v7.38.11 — the streaming scan is the path a client
8794                // reaches over the wire, so it is the one that has to
8795                // ask the BRIN summary which slots can be skipped. The
8796                // predicate still runs on every row that survives.
8797                let slots = stmt
8798                    .where_
8799                    .as_ref()
8800                    .and_then(|w| crate::brin::candidate_slots(w, table))
8801                    .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
8802                for (i, row) in table.scan_visible_slots(slots, &snapshot) {
8803                    if i.is_multiple_of(256) {
8804                        cancel.check()?;
8805                    }
8806                    if Self::stream_project_row(
8807                        row,
8808                        stmt.where_.as_ref(),
8809                        compiled_where.as_ref(),
8810                        &mut eval_stack,
8811                        &projection,
8812                        &bound_pos,
8813                        &ctx,
8814                        &mut values,
8815                        emit,
8816                    )? {
8817                        count += 1;
8818                    }
8819                }
8820            }
8821        }
8822        Ok(Some(count))
8823    }
8824
8825    pub(crate) fn try_exec_joined_streaming<F>(
8826        &self,
8827        stmt: &SelectStatement,
8828        cancel: CancelToken<'_>,
8829        emit: &mut F,
8830    ) -> Result<Option<usize>, EngineError>
8831    where
8832        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8833    {
8834        // Shape gates — keep the streamable surface narrow on
8835        // purpose. The fall-back path still handles everything else.
8836        let Some(from) = &stmt.from else {
8837            return Ok(None);
8838        };
8839        // v7.37 (round 830) — decline anything a row-security policy binds
8840        // for this session. Policies are injected in
8841        // `exec_bare_select_cancel`, below this path, so a statement claimed
8842        // here would read the table unfiltered: measured, `SELECT val FROM
8843        // sec` returned all three rows to a session whose policy allows two,
8844        // while `SELECT upper(val) FROM sec` — declined by the shape gates
8845        // and so materialised — returned the correct two.
8846        //
8847        // Declining sends it to the path that enforces. Teaching this one to
8848        // inject the predicate itself would keep the streaming benefit for
8849        // RLS tables and is the better end state; it is not what a
8850        // correctness fix should carry, and the fall-back is exactly as
8851        // correct, only slower.
8852        if self.select_reads_policy_subject_table(stmt) {
8853            return Ok(None);
8854        }
8855        // r1058 — a WITH list this path never materialises: the CTE
8856        // name would be resolved as a physical relation and error
8857        // ("relation \"big\" does not exist" over the extended
8858        // protocol, caught by the perm-runner's wire legs). The
8859        // materialising fallback owns CTE execution.
8860        if !stmt.ctes.is_empty() {
8861            return Ok(None);
8862        }
8863        // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
8864        // tables` and kin) exist only as synth arms on the
8865        // materialising path; claiming one here errored "relation
8866        // does not exist" over the extended protocol for a query the
8867        // simple protocol answered. Prefix test only — a genuinely
8868        // missing relation must keep erroring in-path.
8869        if from.primary.name.starts_with("__spg_")
8870            || from
8871                .joins
8872                .iter()
8873                .any(|j| j.table.name.starts_with("__spg_"))
8874        {
8875            return Ok(None);
8876        }
8877        // r1058 — decline partitioned / inheritance parents, same
8878        // shape of bug as the RLS decline above: this path scans the
8879        // named table's own (empty) heap, so `SELECT id, region FROM
8880        // cust` on a partition parent streamed ZERO rows over the wire
8881        // while COUNT(*) — an aggregate, materialised below — said 3.
8882        // Caught by the perm-runner's server permutations; the
8883        // materialising fallback expands children correctly.
8884        if crate::partition::has_children(self.active_catalog(), &from.primary.name)
8885            || from
8886                .joins
8887                .iter()
8888                .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
8889        {
8890            return Ok(None);
8891        }
8892        // v7.39 (round 790) — single-table SELECTs stream too. This
8893        // gate said "joins only" because the path was written for
8894        // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
8895        // fell to the materialising fallback, which builds the whole
8896        // `Vec<Row<'static>>` and only then iterates it. Measured on
8897        // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
8898        // reached through a one-row JOIN — 2.6x, purely for lacking a
8899        // join. The deferred-join structure handles one source as the
8900        // degenerate stride-1 case, so the walk below is unchanged.
8901        let _single_table = from.joins.is_empty();
8902        // An ORDER BY that the bounded sort can serve streams; everything
8903        // else still falls to the materialising fallback below.
8904        // r1025 — an ordering the index already holds needs no sort at all.
8905        // Tried before the spill sort, which is the path it replaces.
8906        if !stmt.order_by.is_empty()
8907            && from.joins.is_empty()
8908            && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
8909        {
8910            return Ok(Some(n));
8911        }
8912        if !stmt.order_by.is_empty()
8913            && from.joins.is_empty()
8914            && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
8915        {
8916            return Ok(Some(n));
8917        }
8918        // r1031 — integer keys carried inline instead of an `OrderKey`
8919        // vector per row. Tried AFTER the spill sort on purpose: this lane
8920        // buffers the whole answer, so anything the spill path would take
8921        // must keep taking it rather than be turned back into an in-memory
8922        // sort that answers with a budget error.
8923        if !stmt.order_by.is_empty()
8924            && from.joins.is_empty()
8925            && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
8926        {
8927            return Ok(Some(n));
8928        }
8929        if !stmt.order_by.is_empty()
8930            || stmt.limit.is_some()
8931            || stmt.offset.is_some()
8932            || stmt.having.is_some()
8933            || stmt.group_by.is_some()
8934            || stmt.distinct
8935            || !stmt.unions.is_empty()
8936            || stmt.limit_with_ties
8937        {
8938            return Ok(None);
8939        }
8940        if aggregate::uses_aggregate(stmt) {
8941            return Ok(None);
8942        }
8943        // No window / SRF on the streaming path.
8944        if select_has_window(stmt) {
8945            return Ok(None);
8946        }
8947        if stmt
8948            .items
8949            .iter()
8950            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8951        {
8952            return Ok(None);
8953        }
8954        // v7.37 (round 831) — a joinless FROM over a plain stored table
8955        // never needs the deferred structure, and building one costs the
8956        // whole table. `materialise_table_ref_filtered` clones every row
8957        // into a `Vec<Row<'static>>` before anything is filtered or
8958        // projected, so peak cost tracks the TABLE, not the result:
8959        // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
8960        // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
8961        // projection saving nothing, while an arithmetic projection — which
8962        // the shape gates decline, so it materialises through the ordinary
8963        // executor — cost +21 MB.
8964        //
8965        // Scanning in batches and releasing each one is what `cursor_fill`
8966        // already does for a lazy cursor, and it is the same walk: resume
8967        // from a slot, take visible rows, evaluate, hand them over, drop
8968        // them. Round 800's finding stands and is why this reads rows OUT
8969        // rather than seeding the join by index — touching the stored
8970        // `PersistentVec` in place makes the whole table resident, which is
8971        // worse than the copy. Each batch is copied, then freed.
8972        if from.joins.is_empty()
8973            && from.primary.unnest_expr.is_none()
8974            && from.primary.lateral_subquery.is_none()
8975            && from.primary.as_of_segment.is_none()
8976            && from.primary.generate_series_args.is_none()
8977            && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
8978        {
8979            return Ok(Some(n));
8980        }
8981        // Build the deferred join under the regular byte budget.
8982        let mut budget = ByteBudget::new(self.max_query_bytes);
8983        let deferred = {
8984            let mut needed = alloc::collections::BTreeSet::new();
8985            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
8986            self.build_joined_filtered_rows(
8987                from,
8988                stmt.where_.as_ref(),
8989                cancel,
8990                if prunable { Some(&needed) } else { None },
8991                &mut budget,
8992            )?
8993        };
8994        let combined_schema = &deferred.combined_schema;
8995        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
8996        // `::regclass` / enum cast in a joined projection or HAVING needs it.
8997        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
8998        // the same predicate the unjoined shape carries.
8999        let joined_sess = self.dml_session();
9000        let ctx = EvalContext::new(combined_schema, None)
9001            .with_catalog(self.active_catalog())
9002            .with_session(&joined_sess);
9003        let projection =
9004            build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
9005        // Every projection item must be a bound qualified column —
9006        // anything that needs `eval_expr_with_correlated` keeps the
9007        // materialising path.
9008        let bound_pos = |e: &Expr| -> Option<usize> {
9009            match e {
9010                // v7.39 (round 822) — an UNQUALIFIED column resolves here
9011                // too. The `qualifier.is_some()` guard this replaces meant
9012                // `SELECT pad FROM big` — the commonest projection there is
9013                // — never reached the streaming walk: it fell out at this
9014                // gate and re-ran on the materialising path, after the
9015                // deferred join structure had already been built and paid
9016                // for. Measured (round 821, statement_timeout=120 over 400k
9017                // rows): `big.pad` and `b.pad` streamed and cancelled at
9018                // ~65k rows in 0.14 s, while bare `pad` ran to completion in
9019                // 0.80 s with the timeout never consulted. `find_column_pos`
9020                // has always handled the unqualified case (it falls through
9021                // to a by-name match), so the guard narrowed the gate for no
9022                // reason it recorded.
9023                Expr::Column(c) => eval::find_column_pos(c, &ctx),
9024                _ => None,
9025            }
9026        };
9027        let proj_decomposed: Vec<(usize, usize)> = {
9028            let mut out = Vec::with_capacity(projection.len());
9029            for p in &projection {
9030                let Some(abs) = bound_pos(&p.expr) else {
9031                    return Ok(None);
9032                };
9033                let Some(k) = deferred
9034                    .offsets
9035                    .partition_point(|&o| o <= abs)
9036                    .checked_sub(1)
9037                else {
9038                    return Ok(None);
9039                };
9040                out.push((k, abs - deferred.offsets[k]));
9041            }
9042            out
9043        };
9044        // Emit columns once.
9045        let columns: Vec<ColumnSchema> = projection
9046            .iter()
9047            // v7.39 (read01 round 54) — keep the column's enum identity through
9048            // the projection (it lives outside the DataType lattice), or a
9049            // derived table / UNION / windowed result forgets it and any outer
9050            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
9051            .map(|p| {
9052                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
9053                c.user_enum_type = p.user_enum_type.clone();
9054                c.mysql_fsp = p.mysql_fsp;
9055                c
9056            })
9057            .collect();
9058        emit(crate::StreamItem::Header(&columns))?;
9059        let sources_ref = &deferred.sources;
9060        let stride = deferred.stride;
9061        let survivors_ref = &deferred.survivors;
9062        let n_surv = if stride == 0 {
9063            0
9064        } else {
9065            survivors_ref.len() / stride
9066        };
9067        // Reused per-row cell-ref scratch — pushes are zero-alloc
9068        // after the first row.
9069        let null_value = Value::Null;
9070        let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
9071        let mut count: usize = 0;
9072        for surv_i in 0..n_surv {
9073            if surv_i.is_multiple_of(256) {
9074                cancel.check()?;
9075            }
9076            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9077            cell_refs.clear();
9078            for &(k, col_in_src) in &proj_decomposed {
9079                let ri = tuple[k];
9080                let v: &Value = if ri == usize::MAX {
9081                    &null_value
9082                } else {
9083                    sources_ref[k]
9084                        .get(ri)
9085                        .and_then(|r| r.values.get(col_in_src))
9086                        .unwrap_or(&null_value)
9087                };
9088                cell_refs.push(v);
9089            }
9090            emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
9091            count += 1;
9092        }
9093        Ok(Some(count))
9094    }
9095
9096    fn exec_joined_select(
9097        &self,
9098        stmt: &SelectStatement,
9099        from: &FromClause,
9100        cancel: CancelToken<'_>,
9101    ) -> Result<QueryResult, EngineError> {
9102        // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
9103        // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
9104        // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
9105        // FROM B WHERE B.k = A.k)` into
9106        //   SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
9107        //   WHERE B.k IS NULL
9108        // The general join executor builds a hash, probes every outer
9109        // tuple, materialises (left_padded_with_null) for every miss,
9110        // then runs the aggregate over the result set. For COUNT(*) we
9111        // only need the count — skip the tuple materialisation. Build
9112        // a HashSet of B's unique join values, scan A's PK index, and
9113        // increment the counter on each miss. PG's Merge Anti-Join
9114        // does roughly this; ours becomes a simple HashSet probe.
9115        if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
9116            return Ok(out);
9117        }
9118        // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
9119        // When ORDER BY is on an indexed primary column, walking the
9120        // btree in the requested direction lets the streamer break
9121        // after `LIMIT + OFFSET` survivors without ever materialising
9122        // the rest of the join — the 80 ms `mailrs_prod_not_exists`
9123        // plateau is exactly this shape.
9124        if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
9125            return Ok(out);
9126        }
9127        // v7.30.3 (mailrs round-26) — the bounded single-join path
9128        // first; peak memory scales with LIMIT instead of the table.
9129        if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
9130            return Ok(out);
9131        }
9132        // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
9133        // WHERE materialisation to the shared helper so the LATERAL
9134        // / UNNEST / regular-catalog paths route through one place.
9135        // (`build_joined_filtered_rows` carries LATERAL support as
9136        // of Phase 3.P0-41.) Downstream we still handle aggregate /
9137        // projection / ORDER BY / DISTINCT / LIMIT inline because
9138        // those depend on the SelectStatement's items list.
9139        let mut budget = ByteBudget::new(self.max_query_bytes);
9140        let deferred = {
9141            let mut needed = alloc::collections::BTreeSet::new();
9142            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
9143            self.build_joined_filtered_rows(
9144                from,
9145                stmt.where_.as_ref(),
9146                cancel,
9147                if prunable { Some(&needed) } else { None },
9148                &mut budget,
9149            )?
9150        };
9151        let combined_schema = &deferred.combined_schema;
9152        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
9153        // `::regclass` / enum cast in a joined projection or HAVING needs it.
9154        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
9155        // the same predicate the unjoined shape carries.
9156        let joined_sess = self.dml_session();
9157        let ctx = EvalContext::new(combined_schema, None)
9158            .with_catalog(self.active_catalog())
9159            .with_session(&joined_sess);
9160        // Aggregate path: handle GROUP BY / aggregate calls over the
9161        // joined+filtered rows.
9162        if aggregate::uses_aggregate(stmt) {
9163            // v7.32 (P4 borrow channel, increment 2) — borrow each
9164            // surviving join tuple as a RowRef::Tuple; the aggregate
9165            // engine reads source cells by reference (bound fast path =
9166            // zero clone) instead of consuming materialised combined
9167            // Rows. This is where the +211k materialise_tuple_vals
9168            // clones disappear for the join+aggregate shape.
9169            let refs = deferred.row_refs();
9170            // v7.29 — a per-query memo so correlated scalar
9171            // subqueries batch-evaluate once (group map) instead of
9172            // executing per group.
9173            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
9174            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
9175                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
9176                    .map_err(|err| match err {
9177                        EngineError::Eval(ev) => ev,
9178                        other => eval::EvalError::TypeMismatch {
9179                            detail: alloc::format!("{other}"),
9180                        },
9181                    })
9182            };
9183            let agg = aggregate::run(
9184                stmt,
9185                crate::join::AggRows::Refs(&refs),
9186                combined_schema,
9187                None,
9188                Some(&agg_correlated),
9189                self.parallel_runner.0.as_deref(),
9190                Some(self.active_catalog()),
9191                Some(self),
9192            )?;
9193            return self.finish_agg_result(agg, stmt, cancel);
9194        }
9195
9196        let projection =
9197            build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
9198        // v7.39 (round 734) — a set-returning projection over a JOIN.
9199        // This executor's projection loop treats every item as a scalar,
9200        // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
9201        // "function unnest(integer[]) does not exist" where PG expands
9202        // it. The row-set executor already carries the full SRF pipeline
9203        // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
9204        // sharding): materialise the joined survivors and hand over. The
9205        // WHERE is cleared — the join already applied it, and combined
9206        // columns resolve identically in both executors.
9207        if !self.srf_target_idxs(&projection).is_empty() {
9208            let refs = deferred.row_refs();
9209            let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
9210            let mut s2 = stmt.clone();
9211            s2.where_ = None;
9212            let schema = combined_schema.clone();
9213            return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
9214        }
9215        // v7.33 (P4 borrow channel, increment 3) — project directly off
9216        // the deferred row-index tuples instead of materialising an
9217        // intermediate combined Row per survivor. A bound qualified
9218        // column is read by reference (`RowRef::get` → `tuple_value`) and
9219        // cloned ONCE into the output row; the old `materialise()` (a full
9220        // combined Row plus a source→intermediate clone per referenced
9221        // cell, for every survivor) is gone. A row materialises on demand
9222        // only when a projection or ORDER BY expression needs the eval
9223        // path (subquery / function / arithmetic / unqualified column).
9224        // Same bind-once classification the aggregate input fast path uses
9225        // (`accumulate_groups`), reading the same `tuple_value` mapping the
9226        // differential gate already covers.
9227        let refs = deferred.row_refs();
9228        let bound_pos = |e: &Expr| -> Option<usize> {
9229            match e {
9230                Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
9231                _ => None,
9232            }
9233        };
9234        let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
9235        let all_proj_bound = proj_pos.iter().all(Option::is_some);
9236        // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
9237        // pre-decompose each bound projection position into
9238        // `(source_k, col_in_source)` so the per-row column read
9239        // skips the per-cell `tuple_value` partition_point + slice
9240        // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
9241        // calls) that walk dominated; this version reaches into
9242        // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
9243        let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
9244            .iter()
9245            .map(|p| {
9246                p.and_then(|abs| {
9247                    let k = deferred
9248                        .offsets
9249                        .partition_point(|&o| o <= abs)
9250                        .checked_sub(1)?;
9251                    Some((k, abs - deferred.offsets[k]))
9252                })
9253            })
9254            .collect();
9255        // v7.39 (round 962) — which projection items are whole-row
9256        // references, and to which join source. The test is
9257        // `locate_column` declining the name, which is the SAME resolver
9258        // the evaluation path uses, so this cannot drift from it: a real
9259        // column carrying an alias's name resolves to a position and is
9260        // not reported here. The source index comes from the alias
9261        // prefix, the way the combined schema names its columns.
9262        let whole_row_src: Vec<Option<usize>> = projection
9263            .iter()
9264            .map(|p| {
9265                let Expr::Column(c) = &p.expr else {
9266                    return None;
9267                };
9268                if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
9269                    return None;
9270                }
9271                let prefix = alloc::format!("{name}.", name = c.name);
9272                let abs = deferred
9273                    .combined_schema
9274                    .iter()
9275                    .position(|s| s.name.starts_with(&prefix))?;
9276                deferred
9277                    .offsets
9278                    .partition_point(|&o| o <= abs)
9279                    .checked_sub(1)
9280            })
9281            .collect();
9282        // ORDER BY (when present) still evaluates against a materialised
9283        // Row — keep the order-key encoder correct rather than fork it.
9284        let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
9285        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
9286        let mut proj_memo = memoize::MemoizeCache::default();
9287        let sources_ref = &deferred.sources;
9288        let stride = deferred.stride;
9289        let survivors_ref = &deferred.survivors;
9290        let n_surv = survivors_ref.len() / stride.max(1);
9291        // v7.38 (read01 B8) — streaming top-N budget (see the sibling
9292        // single-table path). Bounds this JOIN projection's accumulator
9293        // to O(keep) for `ORDER BY … LIMIT k`.
9294        let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
9295            && !stmt.distinct
9296            && !stmt.limit_with_ties
9297            && !self.env_cfg().disable_topk
9298        {
9299            stmt.limit_literal().and_then(|l| {
9300                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
9301                (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
9302            })
9303        } else {
9304            None
9305        };
9306        // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
9307        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9308            hashbrown::HashMap::new();
9309        let distinct_hb = hashbrown::DefaultHashBuilder::default();
9310        // v7.38.13 — which output positions must NOT fold. Built once per
9311        // scan from the projection, which carries the source column's
9312        // byte-wise-ness; see `FoldSpec`.
9313        let distinct_mask = fold_mask(&projection);
9314        for surv_i in 0..n_surv {
9315            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9316            let row = &refs[surv_i];
9317            let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
9318                Some(row.as_row())
9319            } else {
9320                None
9321            };
9322            let mut values = Vec::with_capacity(projection.len());
9323            for (i, p) in projection.iter().enumerate() {
9324                if let Some((k, col_in_src)) = proj_decomposed[i] {
9325                    // v7.36 — direct (source_k, col) lookup, no
9326                    // partition_point. tuple[k] is the row index in
9327                    // sources[k]; LEFT-NULL slots are `usize::MAX`.
9328                    let ri = tuple[k];
9329                    let v: Value<'static> = if ri == usize::MAX {
9330                        Value::Null
9331                    } else {
9332                        sources_ref[k]
9333                            .get(ri)
9334                            .and_then(|r| r.values.get(col_in_src))
9335                            .cloned()
9336                            .map(Value::into_owned)
9337                            .unwrap_or(Value::Null)
9338                    };
9339                    values.push(v);
9340                } else if let Some(pos) = proj_pos[i] {
9341                    // Bound but couldn't decompose (shouldn't normally
9342                    // happen — keep as a safe path).
9343                    values.push(
9344                        row.get(pos)
9345                            .cloned()
9346                            .map(Value::into_owned)
9347                            .unwrap_or(Value::Null),
9348                    );
9349                } else if let Some(k) = whole_row_src[i]
9350                    && tuple[k] == usize::MAX
9351                {
9352                    // v7.39 (round 962) — a whole-row reference to a side
9353                    // an OUTER join null-extended is NULL, not a
9354                    // composite whose fields are all NULL. PG18.4 answers
9355                    // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
9356                    // an empty cell; round 961 answered `(,)`.
9357                    //
9358                    // The evaluator below cannot tell the two apart: it
9359                    // reads the MATERIALISED combined row, where a
9360                    // null-extended side is indistinguishable from a real
9361                    // row whose every column is NULL — and that row is
9362                    // `(,)` in PG too, so guessing by "all fields NULL"
9363                    // would trade one wrong answer for another. The
9364                    // tuple, which is still in hand here, does know:
9365                    // `usize::MAX` is the sentinel the join writes for
9366                    // exactly this.
9367                    values.push(Value::Null);
9368                } else {
9369                    // Eval path — `materialised` is Some whenever any
9370                    // projection item is non-bound (need_eval_row true).
9371                    // v7.24 (round-16 B) — select-list subqueries under a
9372                    // JOIN go through the correlated-aware evaluator too.
9373                    let mrow = materialised.as_deref().expect("materialised for eval");
9374                    values.push(self.eval_expr_with_correlated(
9375                        &p.expr,
9376                        mrow,
9377                        &ctx,
9378                        cancel,
9379                        Some(&mut proj_memo),
9380                    )?);
9381                }
9382            }
9383            let out_row = Row::new(values);
9384            // v7.37.16 — streaming DISTINCT (see the scan-path twin):
9385            // probe on the projected row; duplicates skip the
9386            // build_order_keys eval and never enter `tagged`.
9387            if stmt.distinct {
9388                let bucket = seen_distinct
9389                    .entry(norm_hash_row(
9390                        &out_row,
9391                        &distinct_hb,
9392                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9393                    ))
9394                    .or_default();
9395                if bucket.iter().any(|i| {
9396                    row_eq_norm(
9397                        &tagged[i].1,
9398                        &out_row,
9399                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9400                    )
9401                }) {
9402                    continue;
9403                }
9404                bucket.push(tagged.len());
9405            }
9406            let order_keys = if stmt.order_by.is_empty() {
9407                Vec::new()
9408            } else {
9409                let mrow = materialised.as_deref().expect("materialised for order by");
9410                build_order_keys(&stmt.order_by, mrow, &ctx)?
9411            };
9412            budget.charge(approx_row_bytes(&out_row))?;
9413            tagged.push((order_keys, out_row));
9414            if let Some((k, descs)) = &topk_stream {
9415                topk_trim(&mut tagged, *k, descs);
9416            }
9417        }
9418        if !stmt.order_by.is_empty() {
9419            // v7.38 元机制 D acceptor — see other call site above.
9420            let keep = if self.env_cfg().disable_topk {
9421                None
9422            } else {
9423                stmt.limit_literal()
9424                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
9425            };
9426            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
9427            // v7.39 (round 688) — the join's ORDER BY resolves its keys
9428            // against `ctx`, which is built from `build_combined_schema`, so
9429            // this is where a declared collation reaches the sort. There was
9430            // exactly ONE resolver call in the engine before this — the
9431            // single-table scan's — which is why every other shape sorted by
9432            // bytes no matter what the schemas carried.
9433            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
9434            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
9435        }
9436        let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
9437        apply_offset_and_limit(
9438            &mut output_rows,
9439            stmt.offset_literal(),
9440            stmt.limit_literal(),
9441        );
9442        let columns: Vec<ColumnSchema> = projection
9443            .into_iter()
9444            .map(|p| {
9445                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
9446                c.user_enum_type = p.user_enum_type;
9447                c.collation_name = p.collation_name;
9448                c.mysql_fsp = p.mysql_fsp;
9449                c
9450            })
9451            .collect();
9452        Ok(QueryResult::Rows {
9453            columns,
9454            rows: output_rows,
9455        })
9456    }
9457}
9458
9459impl Engine {
9460    /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
9461    /// by id, decodes each row body against the table's current
9462    /// schema, applies the SELECT's projection + optional WHERE +
9463    /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
9464    /// / ORDER BY are unsupported on this path (STABILITY carve-
9465    /// out); operators wanting them should restore the segment
9466    /// into a regular table first.
9467    fn exec_select_as_of_segment(
9468        &self,
9469        stmt: &SelectStatement,
9470        from: &spg_sql::ast::FromClause,
9471        segment_id: u32,
9472    ) -> Result<QueryResult, EngineError> {
9473        // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
9474        // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
9475        if !from.joins.is_empty()
9476            || stmt.group_by.is_some()
9477            || stmt.having.is_some()
9478            || !stmt.unions.is_empty()
9479            || !stmt.order_by.is_empty()
9480            || stmt.offset.is_some()
9481            || stmt.distinct
9482            || aggregate::uses_aggregate(stmt)
9483        {
9484            return Err(EngineError::Unsupported(
9485                "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
9486                 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
9487                    .into(),
9488            ));
9489        }
9490        let table = self
9491            .active_catalog()
9492            .get(&from.primary.name)
9493            .ok_or_else(|| StorageError::TableNotFound {
9494                name: from.primary.name.clone(),
9495            })?;
9496        let schema = table.schema().clone();
9497        let schema_cols = &schema.columns;
9498        let alias = from
9499            .primary
9500            .alias
9501            .as_deref()
9502            .unwrap_or(from.primary.name.as_str());
9503        let ctx = self.ev_ctx(schema_cols, Some(alias));
9504        let seg = self
9505            .active_catalog()
9506            .cold_segment(segment_id)
9507            .ok_or_else(|| {
9508                EngineError::Unsupported(alloc::format!(
9509                    "AS OF SEGMENT: cold segment {segment_id} not registered"
9510                ))
9511            })?;
9512        let mut out_rows: Vec<Row<'static>> = Vec::new();
9513        let mut limit_remaining: Option<usize> =
9514            stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
9515        for (_key, body) in seg.scan() {
9516            let (row, _consumed) =
9517                spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
9518                    .map_err(EngineError::Storage)?;
9519            if let Some(where_expr) = &stmt.where_ {
9520                let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
9521                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9522                    continue;
9523                }
9524            }
9525            // Projection.
9526            let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
9527            out_rows.push(projected);
9528            if let Some(rem) = limit_remaining.as_mut() {
9529                if *rem == 0 {
9530                    out_rows.pop();
9531                    break;
9532                }
9533                *rem -= 1;
9534            }
9535        }
9536        // Output column schema: derive from SELECT items.
9537        let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
9538        Ok(QueryResult::Rows {
9539            columns,
9540            rows: out_rows,
9541        })
9542    }
9543
9544    /// v6.10.2 — simple-path WHERE eval that doesn't go through
9545    /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
9546    /// scan paths predicate against a snapshot frozen segment, no
9547    /// cross-row state.
9548    fn eval_expr_simple(
9549        &self,
9550        expr: &Expr,
9551        row: &Row<'static>,
9552        ctx: &EvalContext,
9553    ) -> Result<Value<'static>, EngineError> {
9554        let cancel = CancelToken::none();
9555        self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
9556    }
9557}
9558
9559// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
9560
9561/// One row-producing projection: an expression to evaluate, the resulting
9562/// column's user-visible name, its inferred type, and nullability.
9563#[derive(Debug, Clone)]
9564pub(crate) struct ProjectedItem {
9565    pub(crate) expr: Expr,
9566    pub(crate) output_name: String,
9567    pub(crate) ty: DataType,
9568    pub(crate) nullable: bool,
9569    /// v7.39 (read01 round 54) — a projected enum column keeps its enum
9570    /// identity. Enum-ness lives outside the DataType lattice (the value is a
9571    /// Text), so a projection that dropped this made the RESULT schema forget
9572    /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
9573    /// that schema, silently fell back to TEXT order instead of member order.
9574    pub(crate) user_enum_type: Option<String>,
9575    /// v7.39 (round 425) — a projected MySQL temporal column keeps its
9576    /// declared fractional-seconds precision, so the renderer can pad to
9577    /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
9578    /// a whole second). Like `user_enum_type` this lives outside the
9579    /// DataType lattice, so a projection that dropped it made the RESULT
9580    /// schema forget how wide the fraction should print.
9581    pub(crate) mysql_fsp: Option<u8>,
9582    /// v7.39 (round 688) — and its declared collation, the third thing to
9583    /// live outside the DataType lattice and the third to be lost the same
9584    /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
9585    /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
9586    /// projection rebuilt the output column and the ORDER BY resolves
9587    /// against THAT schema.
9588    pub(crate) collation_name: Option<String>,
9589    /// v7.38.13 — and whether this position must NOT fold when DISTINCT
9590    /// de-dups it. The fourth thing to live outside the DataType lattice
9591    /// and the fourth to be lost the same way: a column declared
9592    /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
9593    /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
9594    /// returns two.
9595    ///
9596    /// A BOOL rather than the `Collation` enum on purpose. The enum's
9597    /// storage default is `Binary`, but the FOLD default under MySQL is
9598    /// case-insensitive — carrying the enum would silently mean
9599    /// "exempt" for every projected expression that is not a column.
9600    /// This field states the question it answers.
9601    pub(crate) fold_exempt: bool,
9602}
9603
9604/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
9605/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
9606/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
9607/// the spec's "two NULLs are not distinct"; the second is a tolerated
9608/// quirk for v1 (no NaN literals are reachable from the SQL surface).
9609/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
9610fn expr_is_aggregate_call(e: &Expr) -> bool {
9611    match e {
9612        Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
9613        Expr::AggregateOrdered { .. } => true,
9614        _ => false,
9615    }
9616}
9617
9618/// Collect distinct top-level aggregate call expressions (dedup by value). Does
9619/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
9620/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
9621/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
9622/// than today — never a regression on a working query).
9623fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
9624    if expr_is_aggregate_call(e) {
9625        if !out.iter().any(|x| x == e) {
9626            out.push(e.clone());
9627        }
9628        return;
9629    }
9630    match e {
9631        Expr::Binary { lhs, rhs, .. } => {
9632            collect_agg_exprs(lhs, out);
9633            collect_agg_exprs(rhs, out);
9634        }
9635        Expr::Unary { expr, .. }
9636        | Expr::Cast { expr, .. }
9637        | Expr::IsNull { expr, .. }
9638        | Expr::BoolTest { expr, .. }
9639        | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
9640        Expr::FunctionCall { args, .. } => {
9641            for a in args {
9642                collect_agg_exprs(a, out);
9643            }
9644        }
9645        Expr::Like { expr, pattern, .. } => {
9646            collect_agg_exprs(expr, out);
9647            collect_agg_exprs(pattern, out);
9648        }
9649        Expr::Extract { source, .. } => collect_agg_exprs(source, out),
9650        Expr::WindowFunction {
9651            args,
9652            partition_by,
9653            order_by,
9654            ..
9655        } => {
9656            for a in args {
9657                collect_agg_exprs(a, out);
9658            }
9659            for p in partition_by {
9660                collect_agg_exprs(p, out);
9661            }
9662            for (o, _, _) in order_by {
9663                collect_agg_exprs(o, out);
9664            }
9665        }
9666        _ => {}
9667    }
9668}
9669
9670/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
9671fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
9672    if expr_is_aggregate_call(e) {
9673        if let Some(idx) = aggs.iter().position(|x| x == e) {
9674            *e = Expr::Column(ColumnName {
9675                qualifier: None,
9676                name: alloc::format!("__agg{idx}"),
9677            });
9678        }
9679        return;
9680    }
9681    match e {
9682        Expr::Binary { lhs, rhs, .. } => {
9683            replace_agg_exprs(lhs, aggs);
9684            replace_agg_exprs(rhs, aggs);
9685        }
9686        Expr::Unary { expr, .. }
9687        | Expr::Cast { expr, .. }
9688        | Expr::IsNull { expr, .. }
9689        | Expr::BoolTest { expr, .. }
9690        | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
9691        Expr::FunctionCall { args, .. } => {
9692            for a in args {
9693                replace_agg_exprs(a, aggs);
9694            }
9695        }
9696        Expr::Like { expr, pattern, .. } => {
9697            replace_agg_exprs(expr, aggs);
9698            replace_agg_exprs(pattern, aggs);
9699        }
9700        Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
9701        Expr::WindowFunction {
9702            args,
9703            partition_by,
9704            order_by,
9705            ..
9706        } => {
9707            for a in args {
9708                replace_agg_exprs(a, aggs);
9709            }
9710            for p in partition_by {
9711                replace_agg_exprs(p, aggs);
9712            }
9713            for (o, _, _) in order_by {
9714                replace_agg_exprs(o, aggs);
9715            }
9716        }
9717        _ => {}
9718    }
9719}
9720
9721/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
9722/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
9723/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
9724/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
9725/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
9726/// Returns None outside the bounded subset (leaves current behaviour). Only fires
9727/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
9728/// window-only / aggregate-only queries.
9729fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
9730    if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
9731        return None;
9732    }
9733    // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
9734    if !stmt.unions.is_empty() {
9735        return None;
9736    }
9737    let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
9738    if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
9739        return None;
9740    }
9741    stmt.from.as_ref()?;
9742    // Collect the aggregate calls to hoist from projection + outer ORDER BY.
9743    let mut aggs: Vec<Expr> = Vec::new();
9744    for item in &stmt.items {
9745        if let SelectItem::Expr { expr, .. } = item {
9746            collect_agg_exprs(expr, &mut aggs);
9747        }
9748    }
9749    for ob in &stmt.order_by {
9750        collect_agg_exprs(&ob.expr, &mut aggs);
9751    }
9752    // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
9753    let mut inner_items: Vec<SelectItem> = Vec::new();
9754    for g in &group_cols {
9755        inner_items.push(SelectItem::Expr {
9756            expr: g.clone(),
9757            alias: None,
9758        });
9759    }
9760    for (i, a) in aggs.iter().enumerate() {
9761        inner_items.push(SelectItem::Expr {
9762            expr: a.clone(),
9763            alias: Some(alloc::format!("__agg{i}")),
9764        });
9765    }
9766    let inner = SelectStatement {
9767        items: inner_items,
9768        distinct: false,
9769        distinct_on: Vec::new(),
9770        unions: Vec::new(),
9771        order_by: Vec::new(),
9772        limit: None,
9773        offset: None,
9774        limit_with_ties: false,
9775        window_check_exprs: Vec::new(),
9776        ..stmt.clone()
9777    };
9778    let derived = TableRef {
9779        name: "__aggwin".into(),
9780        alias: Some("__aggwin".into()),
9781        only: false,
9782        as_of_segment: None,
9783        unnest_expr: None,
9784        unnest_column_aliases: Vec::new(),
9785        with_ordinality: false,
9786        generate_series_args: None,
9787        lateral_subquery: Some(alloc::boxed::Box::new(inner)),
9788        jsonb_each_text_arg: None,
9789        table_fn_call: None,
9790        rows_from: None,
9791        json_table: None,
9792        scalar_fn_item: false,
9793    };
9794    // Outer window query over the derived rows: aggregates → __aggN column refs.
9795    let mut outer_items = stmt.items.clone();
9796    for item in &mut outer_items {
9797        if let SelectItem::Expr { expr, alias } = item {
9798            // Preserve PG's column label for a bare aggregate projection.
9799            if alias.is_none()
9800                && let Expr::FunctionCall { name, .. } = expr
9801                && crate::aggregate::is_aggregate_name(name)
9802            {
9803                *alias = Some(name.to_ascii_lowercase());
9804            }
9805            replace_agg_exprs(expr, &aggs);
9806        }
9807    }
9808    let mut outer_order = stmt.order_by.clone();
9809    for ob in &mut outer_order {
9810        replace_agg_exprs(&mut ob.expr, &aggs);
9811    }
9812    let mut outer_distinct_on = stmt.distinct_on.clone();
9813    for e in &mut outer_distinct_on {
9814        replace_agg_exprs(e, &aggs);
9815    }
9816    Some(SelectStatement {
9817        locking: None,
9818        ctes: Vec::new(),
9819        distinct: stmt.distinct,
9820        distinct_on: outer_distinct_on,
9821        items: outer_items,
9822        from: Some(FromClause {
9823            primary: derived,
9824            joins: Vec::new(),
9825        }),
9826        where_: None,
9827        group_by: None,
9828        group_by_all: false,
9829        having: None,
9830        unions: Vec::new(),
9831        order_by: outer_order,
9832        limit: stmt.limit.clone(),
9833        offset: stmt.offset.clone(),
9834        limit_with_ties: stmt.limit_with_ties,
9835        window_check_exprs: Vec::new(),
9836    })
9837}
9838
9839/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
9840/// membership.
9841///
9842/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
9843/// there?", and all four answered by scanning the whole right side once per
9844/// left row. The cost was (left rows x right rows), which is why
9845/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
9846/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
9847/// row that does not pays for all of it. Over 100k left rows, raising the
9848/// right side from 100 to 10,000 took 35 ms to 2848.
9849///
9850/// This is the shape round 485 already solved for DISTINCT, and it reuses
9851/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
9852/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
9853/// every bucket with the exact comparator, so a collision costs time and
9854/// never an answer.
9855struct PeerIndex<'r> {
9856    bh: hashbrown::DefaultHashBuilder,
9857    buckets: hashbrown::HashMap<u64, Vec<usize>>,
9858    rows: &'r [Row<'static>],
9859    fold: FoldSpec<'r>,
9860}
9861
9862impl<'r> PeerIndex<'r> {
9863    fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
9864        // ONE hasher for the whole pass: the default builder is seeded per
9865        // instance, so a fresh one per row would put equal rows in different
9866        // buckets.
9867        let bh = hashbrown::DefaultHashBuilder::default();
9868        let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
9869            hashbrown::HashMap::with_capacity(rows.len());
9870        for (i, r) in rows.iter().enumerate() {
9871            buckets
9872                .entry(norm_hash_row(r, &bh, fold))
9873                .or_default()
9874                .push(i);
9875        }
9876        Self {
9877            bh,
9878            buckets,
9879            rows,
9880            fold,
9881        }
9882    }
9883
9884    fn contains(&self, r: &Row<'static>) -> bool {
9885        let h = norm_hash_row(r, &self.bh, self.fold);
9886        self.buckets
9887            .get(&h)
9888            .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
9889    }
9890
9891    /// Remove ONE occurrence, so the multiset forms cancel row for row the
9892    /// way the pool they replaced did.
9893    fn take_one(&mut self, r: &Row<'static>) -> bool {
9894        let h = norm_hash_row(r, &self.bh, self.fold);
9895        let Some(b) = self.buckets.get_mut(&h) else {
9896            return false;
9897        };
9898        let Some(pos) = b
9899            .iter()
9900            .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
9901        else {
9902            return false;
9903        };
9904        b.swap_remove(pos);
9905        true
9906    }
9907}
9908
9909pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
9910    dedup_by_row(rows, |r| r, fold)
9911}
9912
9913/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
9914/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
9915/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
9916/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
9917/// order is preserved, and correctness needs only the one-way guarantee
9918/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
9919/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
9920fn dedup_by_row<T>(
9921    items: Vec<T>,
9922    row_of: impl Fn(&T) -> &Row<'static>,
9923    fold: FoldSpec<'_>,
9924) -> Vec<T> {
9925    if items.len() <= 32 {
9926        let mut out: Vec<T> = Vec::with_capacity(items.len());
9927        for it in items {
9928            if !out
9929                .iter()
9930                .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
9931            {
9932                out.push(it);
9933            }
9934        }
9935        return out;
9936    }
9937    // ONE BuildHasher instance for the whole pass — the default builder
9938    // is randomly seeded PER INSTANCE, so a fresh one per row would give
9939    // equal rows different hashes and never dedup.
9940    let bh = hashbrown::DefaultHashBuilder::default();
9941    let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
9942    let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9943        hashbrown::HashMap::with_capacity(items.len());
9944    for it in items {
9945        let h = norm_hash_row(row_of(&it), &bh, fold);
9946        let bucket = buckets.entry(h).or_default();
9947        if !bucket
9948            .iter()
9949            .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
9950        {
9951            bucket.push(out.len());
9952            out.push(it);
9953        }
9954    }
9955    out
9956}
9957
9958/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
9959/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
9960/// rows may collide (buckets are re-checked with the exact comparator).
9961///
9962/// Domain design mirrors `value_cmp`'s equivalence classes:
9963/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
9964///   shares one domain: a value that is an integer fitting i64 hashes the
9965///   i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
9966///   anything else hashes the f64 approximation computed by THE SAME
9967///   formula the value_cmp float arms use (`numeric_to_f64`), so
9968///   `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
9969///   hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
9970///   Known un-closable corner: an integer in [2^53, 2^63) can compare
9971///   Equal to a float via value_cmp's lossy f64 arm while hashing in the
9972///   exact-i64 domain — mixed int/float rows at that magnitude may miss a
9973///   dedup (PG itself compares int8↔float8 in the lossy float8 domain).
9974/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
9975///   compares them blank-insensitively; plain Text pairs that differ only
9976///   in trailing blanks merely collide and are separated exactly).
9977/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
9978///   hash their fields under a distinct tag.
9979/// - Everything value_cmp falls back to debug-format ordering for
9980///   (Json, arrays, vectors, geometry, ranges, …) shares one constant
9981///   bucket — degrades to the exact linear scan, never wrong.
9982fn norm_hash_row(
9983    row: &Row<'static>,
9984    bh: &hashbrown::DefaultHashBuilder,
9985    fold: FoldSpec<'_>,
9986) -> u64 {
9987    norm_hash_values(&row.values, bh, fold)
9988}
9989
9990/// v7.39 (round 485) — the same hash over a bare value slice, so the
9991/// DISTINCT probe can run against a reused buffer instead of demanding a
9992/// `Row` that has to be allocated first (see `values_eq_norm`).
9993fn norm_hash_values(
9994    values: &[Value<'static>],
9995    bh: &hashbrown::DefaultHashBuilder,
9996    fold: FoldSpec<'_>,
9997) -> u64 {
9998    use core::hash::{BuildHasher, Hash, Hasher};
9999    let mut h = bh.build_hasher();
10000    for (i, v) in values.iter().enumerate() {
10001        // v7.39 (round 410) — hash the folded key when the MySQL collation
10002        // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
10003        // `'A'` vs `'a '`) share a hash bucket.
10004        //
10005        // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
10006        // byte-wise column that folded here while the comparator did not
10007        // would scatter equal rows across buckets and stop de-duplicating
10008        // at all; the hash and the comparator have to read the same mask.
10009        if fold.folds(i)
10010            && let Some(folded) = mysql_dedup_fold(v)
10011        {
10012            folded.hash(&mut h);
10013            continue;
10014        }
10015        norm_hash_value(v, &mut h);
10016    }
10017    h.finish()
10018}
10019
10020/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
10021///
10022/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
10023const fn pow10_i128(p: u16) -> Option<i128> {
10024    const P: [i128; 39] = {
10025        let mut t = [1i128; 39];
10026        let mut i = 1;
10027        while i < 39 {
10028            t[i] = t[i - 1] * 10;
10029            i += 1;
10030        }
10031        t
10032    };
10033    if (p as usize) < P.len() {
10034        Some(P[p as usize])
10035    } else {
10036        None
10037    }
10038}
10039
10040fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
10041    const TAG_NULL: u8 = 0;
10042    const TAG_BOOL: u8 = 1;
10043    const TAG_NUM_I64: u8 = 2;
10044    const TAG_NUM_F64: u8 = 3;
10045    const TAG_TEXT: u8 = 4;
10046    const TAG_DATE: u8 = 6;
10047    const TAG_TIME: u8 = 7;
10048    const TAG_TIMESTAMP: u8 = 8;
10049    const TAG_TIMETZ: u8 = 10;
10050    const TAG_UUID: u8 = 11;
10051    const TAG_MONEY: u8 = 12;
10052    const TAG_BYTES: u8 = 13;
10053    const TAG_INTERVAL: u8 = 14;
10054    const TAG_CHAR1: u8 = 15;
10055    const TAG_OPAQUE: u8 = 255;
10056    // One shared writer for the numeric family: an integer value
10057    // representable as i64 goes exact (round-trip probe — no_std, so no
10058    // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
10059    // through 0i64, folding it into 0.0 as value_cmp requires.
10060    let num_f64 = |h: &mut H, x: f64| {
10061        if x.is_nan() {
10062            h.write_u8(TAG_NUM_F64);
10063            h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
10064            return;
10065        }
10066        const TWO63: f64 = 9_223_372_036_854_775_808.0;
10067        if (-TWO63..TWO63).contains(&x) {
10068            #[allow(clippy::cast_possible_truncation)]
10069            let n = x as i64;
10070            #[allow(clippy::cast_precision_loss)]
10071            if (n as f64) == x {
10072                h.write_u8(TAG_NUM_I64);
10073                h.write_i64(n);
10074                return;
10075            }
10076        }
10077        h.write_u8(TAG_NUM_F64);
10078        h.write_u64(x.to_bits());
10079    };
10080    match v {
10081        Value::Null => h.write_u8(TAG_NULL),
10082        Value::Bool(b) => {
10083            h.write_u8(TAG_BOOL);
10084            h.write_u8(u8::from(*b));
10085        }
10086        Value::SmallInt(n) => {
10087            h.write_u8(TAG_NUM_I64);
10088            h.write_i64(i64::from(*n));
10089        }
10090        Value::Int(n) => {
10091            h.write_u8(TAG_NUM_I64);
10092            h.write_i64(i64::from(*n));
10093        }
10094        Value::BigInt(n) => {
10095            h.write_u8(TAG_NUM_I64);
10096            h.write_i64(*n);
10097        }
10098        Value::Float(x) => num_f64(h, *x),
10099        Value::Numeric {
10100            scaled,
10101            scale,
10102            kind,
10103        } => match kind {
10104            spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
10105            spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
10106            spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
10107            spg_storage::NumericKind::Finite => {
10108                // Reduce trailing fractional zeros so 1.50 and 1.5 share a
10109                // representation, then: exact integers fitting i64 go to the
10110                // i64 domain; everything else uses numeric_to_f64 — the SAME
10111                // formula value_cmp's Numeric↔Float arm compares with.
10112                // r1044 — the reduction is required (`1.5` and `1.50` are
10113                // one value and must land in one bucket) and it used to
10114                // walk one digit at a time. That is O(scale), and scale
10115                // is not small in practice: `n / 100` on a NUMERIC
10116                // column stores `9.1900000000000000`, scale 16, so the
10117                // loop ran fourteen times PER ROW.
10118                //
10119                // Priced by ablation rather than guessed at — removing
10120                // the loop entirely took `SELECT DISTINCT n FROM t ORDER
10121                // BY n` over 400,000 rows from 52 ms to 14.8, against
10122                // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
10123                // tried first moved it not at all, which is why this one
10124                // was measured before it was written.
10125                //
10126                // Binary search over the same powers finds the whole
10127                // run of trailing zeros in at most six tests and one
10128                // division, instead of one test and one division per
10129                // digit.
10130                let (mut s, mut sc) = (*scaled, *scale);
10131                if sc > 0 && s != 0 {
10132                    let mut lo: u16 = 0;
10133                    let mut hi: u16 = sc;
10134                    while lo < hi {
10135                        let mid = (lo + hi).div_ceil(2);
10136                        match pow10_i128(mid) {
10137                            Some(p) if s % p == 0 => lo = mid,
10138                            _ => hi = mid - 1,
10139                        }
10140                    }
10141                    if lo > 0 {
10142                        if let Some(p) = pow10_i128(lo) {
10143                            s /= p;
10144                            sc -= lo;
10145                        }
10146                    }
10147                }
10148                if sc == 0 {
10149                    if let Ok(n) = i64::try_from(s) {
10150                        h.write_u8(TAG_NUM_I64);
10151                        h.write_i64(n);
10152                    } else {
10153                        num_f64(h, crate::orderby::numeric_to_f64(s, 0));
10154                    }
10155                } else {
10156                    num_f64(h, crate::orderby::numeric_to_f64(s, sc));
10157                }
10158            }
10159        },
10160        // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
10161        // value that also fits i128 reuses the Numeric path above so
10162        // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
10163        // any i128-representable value — constant bucket is safe.
10164        Value::NumericBig(b) => match b.to_i128() {
10165            Some(s) => norm_hash_value(
10166                &Value::Numeric {
10167                    scaled: s,
10168                    scale: b.scale(),
10169                    kind: spg_storage::NumericKind::Finite,
10170                },
10171                h,
10172            ),
10173            None => h.write_u8(TAG_OPAQUE),
10174        },
10175        // value_cmp compares Text↔BpChar blank-insensitively (both sides
10176        // trimmed), so both hash the trimmed bytes. Text pairs differing
10177        // only in trailing blanks collide and are split exactly in-bucket.
10178        Value::Text(s) | Value::BpChar(s) => {
10179            h.write_u8(TAG_TEXT);
10180            h.write(s.trim_end_matches(' ').as_bytes());
10181        }
10182        Value::Char1(c) => {
10183            h.write_u8(TAG_CHAR1);
10184            h.write_u8(*c);
10185        }
10186        Value::Date(d) => {
10187            h.write_u8(TAG_DATE);
10188            h.write_i32(*d);
10189        }
10190        Value::Time(t) => {
10191            h.write_u8(TAG_TIME);
10192            h.write_i64(*t);
10193        }
10194        Value::Timestamp(t) => {
10195            h.write_u8(TAG_TIMESTAMP);
10196            h.write_i64(*t);
10197        }
10198        Value::TimeTz { us, offset_secs } => {
10199            h.write_u8(TAG_TIMETZ);
10200            h.write_i64(*us);
10201            h.write_i32(*offset_secs);
10202        }
10203        Value::Uuid(u) => {
10204            h.write_u8(TAG_UUID);
10205            h.write(u);
10206        }
10207        Value::Money(c) => {
10208            h.write_u8(TAG_MONEY);
10209            h.write_i64(*c);
10210        }
10211        Value::Bytes(b) => {
10212            h.write_u8(TAG_BYTES);
10213            h.write(b.as_ref());
10214        }
10215        Value::Interval {
10216            months,
10217            days,
10218            micros,
10219        } => {
10220            h.write_u8(TAG_INTERVAL);
10221            h.write_i32(*months);
10222            h.write_i32(*days);
10223            h.write_i64(*micros);
10224        }
10225        // v7.37.16 — REAL joined the numeric value_cmp family (widened
10226        // to f64, same formulas as the arms), so it hashes in the shared
10227        // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
10228        // f32→f64 is exact, so equal-under-cmp implies equal bits here.
10229        Value::Real(x) => num_f64(h, f64::from(*x)),
10230        // Json (structural equality), vector families (float rendering),
10231        // arrays / geometry / net / ranges / composites (debug-format
10232        // fallback): one constant bucket — exact linear within.
10233        _ => h.write_u8(TAG_OPAQUE),
10234    }
10235}
10236
10237/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
10238/// treats numerically-equal exact values as one regardless of type or scale
10239/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
10240/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
10241/// `Row` `==` would keep them distinct.
10242/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
10243/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
10244/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
10245/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
10246/// the folded comparison key for a text value, None for anything else (which
10247/// keeps the byte-exact `value_cmp` path).
10248fn mysql_dedup_fold(v: &Value) -> Option<String> {
10249    match v {
10250        Value::Text(s) | Value::BpChar(s) => {
10251            Some(spg_storage::mysql_ci_fold(s.trim_end_matches(' ')))
10252        }
10253        _ => None,
10254    }
10255}
10256
10257/// v7.39 (round 485) — how many projected rows the single-table scan
10258/// builds, and how many of those the DISTINCT probe throws away again.
10259///
10260/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
10261/// 21 % of all samples in malloc/free called straight from the scan
10262/// closure. The closure's one per-row allocation is the projected
10263/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
10264/// instructions later — but "most" is a guess until it is a number, so
10265/// these count it. (Round 480 was spent acting on an inference about a
10266/// branch that turned out never to run.)
10267/// v7.39 (round 488) — reachability counters for round 487's projection
10268/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
10269/// and a never-called-function probe rules out code layout — so the
10270/// question is whether that shape reaches this code at all, which is a
10271/// number, not an inference.
10272pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10273pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10274
10275pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10276pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
10277    core::sync::atomic::AtomicU64::new(0);
10278
10279/// v7.38.13 — how DISTINCT must compare one row of output.
10280///
10281/// The MySQL default collation folds case and trailing spaces when it
10282/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
10283/// must not fold — `e2e_mysql_collate_binary_round370` calls the
10284/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
10285/// one when the schema asked to keep them apart", and names DISTINCT as
10286/// one of the sites that has to honour it.
10287///
10288/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
10289/// value in a MySQL session, because a bool cannot see a column. The
10290/// GROUP BY path consults the schema and was right all along; the test
10291/// only ever exercised that spelling, so the DISTINCT hole was never
10292/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
10293///
10294/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
10295/// which is what a caller with no schema to offer gets.
10296#[derive(Clone, Copy)]
10297pub(crate) struct FoldSpec<'c> {
10298    mysql: bool,
10299    binary: &'c [bool],
10300}
10301
10302impl<'c> FoldSpec<'c> {
10303    /// No column information — every Text position folds under MySQL.
10304    pub(crate) const fn dialect(mysql: bool) -> Self {
10305        Self { mysql, binary: &[] }
10306    }
10307
10308    /// The mask read off the output columns.
10309    pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
10310        Self { mysql, binary }
10311    }
10312
10313    /// Does position `i` fold?
10314    #[inline]
10315    fn folds(&self, i: usize) -> bool {
10316        self.mysql && !self.binary.get(i).copied().unwrap_or(false)
10317    }
10318}
10319
10320/// The fold-exempt mask for a projection.
10321///
10322/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
10323/// projection rebuilds that schema through `ColumnSchema::new`, whose
10324/// collation default is `Binary` — a mask built from it would mark
10325/// EVERY column byte-wise and stop DISTINCT folding at all.
10326pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
10327    projection.iter().map(|p| p.fold_exempt).collect()
10328}
10329
10330pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
10331    values_eq_norm(&a.values, &b.values, fold)
10332}
10333
10334/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
10335/// DISTINCT probe can compare a reused projection buffer against a kept
10336/// row without building a `Row` for it.
10337pub(crate) fn values_eq_norm(
10338    a: &[Value<'static>],
10339    b: &[Value<'static>],
10340    fold: FoldSpec<'_>,
10341) -> bool {
10342    a.len() == b.len()
10343        && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
10344            if fold.folds(i)
10345                && let (Some(fx), Some(fy)) = (mysql_dedup_fold(x), mysql_dedup_fold(y))
10346            {
10347                return fx == fy;
10348            }
10349            crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
10350        })
10351}
10352
10353/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
10354/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
10355/// order via the byte values; vectors are not sortable.
10356pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
10357    // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
10358    // so values sharing a ≥6-byte common prefix (`product_001` vs
10359    // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
10360    // order by their exact bytes instead of the old lossy f64 coarse key.
10361    // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
10362    // matches PG's default C / binary text collation. Every other type
10363    // keeps the lossless-enough `f64` fast path below.
10364    if let Value::Text(s) = v {
10365        return Ok(OrderKey::Text(s.as_ref().into()));
10366    }
10367    // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
10368    // byte order (PG bpcharcmp under C collation), so mixed-pad values of
10369    // the same logical string order equal.
10370    if let Value::BpChar(s) = v {
10371        return Ok(OrderKey::Text(s.trim_end_matches(' ').into()));
10372    }
10373    // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
10374    // carry the parsed value and compare it structurally (see
10375    // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
10376    if let Value::Json(s) = v {
10377        return Ok(match crate::json::parse(s) {
10378            Ok(jv) => OrderKey::Json(jv),
10379            Err(_) => OrderKey::Text(s.as_ref().into()),
10380        });
10381    }
10382    // v7.37 — byte-orderable types PG sorts byte-wise but that have no
10383    // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
10384    // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
10385    // matching PG's network ordering.
10386    match v {
10387        Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
10388        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
10389        Value::NumericBig(b) => {
10390            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
10391                spg_storage::NumericKey::from_big(b),
10392            )));
10393        }
10394        Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
10395        Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
10396        Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
10397        Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
10398        Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
10399            let mut key = alloc::vec::Vec::with_capacity(18);
10400            key.push(*family);
10401            key.extend_from_slice(addr);
10402            key.push(*bits);
10403            return Ok(OrderKey::Bytes(key));
10404        }
10405        _ => {}
10406    }
10407    // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
10408    // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
10409    // own OrderKey so integer arrays sort numerically; a NULL element rides to
10410    // the end via the +INF sentinel.
10411    let inf = || OrderKey::NullBig;
10412    let arr = match v {
10413        Value::IntArray(a) => Some(
10414            a.iter()
10415                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10416                .collect(),
10417        ),
10418        Value::SmallIntArray(a) => Some(
10419            a.iter()
10420                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10421                .collect(),
10422        ),
10423        Value::BigIntArray(a) => Some(
10424            a.iter()
10425                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10426                .collect(),
10427        ),
10428        Value::BoolArray(a) => Some(
10429            a.iter()
10430                .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
10431                .collect(),
10432        ),
10433        Value::TextArray(a) => Some(
10434            a.iter()
10435                .map(|o| o.as_ref().map_or_else(inf, |s| OrderKey::Text(s.clone())))
10436                .collect(),
10437        ),
10438        #[allow(clippy::cast_precision_loss)]
10439        Value::FloatArray(a) => Some(
10440            a.iter()
10441                .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
10442                .collect(),
10443        ),
10444        // r1040 — array elements take the same exact key their scalar
10445        // form does; an f64 projection here would order `{0.1}` against
10446        // `{0.1000000000000000001}` by luck.
10447        Value::NumericArray(a) => Some(
10448            a.iter()
10449                .map(|o| {
10450                    o.map_or_else(inf, |(m, s)| {
10451                        OrderKey::Numeric(alloc::boxed::Box::new(
10452                            spg_storage::NumericKey::from_numeric(
10453                                m,
10454                                s,
10455                                spg_storage::NumericKind::Finite,
10456                            ),
10457                        ))
10458                    })
10459                })
10460                .collect(),
10461        ),
10462        Value::DateArray(a) => Some(
10463            a.iter()
10464                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10465                .collect(),
10466        ),
10467        _ => None,
10468    };
10469    if let Some(elements) = arr {
10470        return Ok(OrderKey::Array(elements));
10471    }
10472    // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
10473    // right, which is exactly the lexicographic element order an Array key
10474    // already gives: `(2,'b') < (9,'a')` because the leading field decides.
10475    if let Value::Composite(fields) = v {
10476        let elements = fields
10477            .iter()
10478            .map(|(_, fv)| value_to_order_key(fv))
10479            .collect::<Result<alloc::vec::Vec<_>, _>>()?;
10480        return Ok(OrderKey::Array(elements));
10481    }
10482    // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
10483    // Projecting these to f64 (the historic path) silently collapses BigInt /
10484    // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
10485    // the wrong order for large ids and microsecond timestamps.
10486    match v {
10487        Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
10488        Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
10489        Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
10490        // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
10491        // integer (days / micros / cents / calendar year); TIMETZ by the
10492        // UTC-equivalent micros (local wall - offset) so the same physical
10493        // instant in different zones sorts equal.
10494        Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
10495        Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
10496        Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
10497        Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
10498        Value::TimeTz { us, offset_secs } => {
10499            return Ok(OrderKey::Int(
10500                i128::from(*us) - i128::from(*offset_secs) * 1_000_000,
10501            ));
10502        }
10503        Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
10504        _ => {}
10505    }
10506    let num = match v {
10507        // Callers without NULLS FIRST/LAST context (array elements,
10508        // histogram sampling) put NULL last, as before.
10509        Value::Null => return Ok(OrderKey::NullBig),
10510        // v7.17.0 Phase 3.P0-38 — range ordering is not supported
10511        // in v7.17.0 (needs lex-then-inclusivity tiebreak).
10512        Value::Range { .. } => {
10513            return Err(EngineError::Unsupported(
10514                "ORDER BY of a range value is not supported in v7.17.0".into(),
10515            ));
10516        }
10517        // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
10518        Value::Hstore(_) => {
10519            return Err(EngineError::Unsupported(
10520                "ORDER BY of a hstore value is not supported".into(),
10521            ));
10522        }
10523        // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
10524        Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
10525            return Err(EngineError::Unsupported(
10526                "ORDER BY of a 2D array is not supported in v7.17.0".into(),
10527            ));
10528        }
10529        // r1039/r1040 — the exact canonical key, not an f64 projection.
10530        //
10531        // r1039 fixed the three specials, which carry a canonical zero in
10532        // `scaled` and so all sorted as the number 0. The projection
10533        // itself was the rest of the defect: "precision losses here only
10534        // matter for tie-breaks well past 15 significant digits" was the
10535        // comment, and the measurement disagreed — f64 called
10536        // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
10537        // returned them in insertion order. Three of ten values came back
10538        // in the wrong place against PG18.4.
10539        Value::Numeric {
10540            scaled,
10541            scale,
10542            kind,
10543        } => {
10544            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
10545                spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
10546            )));
10547        }
10548        Value::Float(x) => *x,
10549        // v7.37.16 — REAL sorts by its exact f64 widening (it had no
10550        // arm and fell through to the unsupported error).
10551        Value::Real(x) => f64::from(*x),
10552        Value::Bool(b) => {
10553            if *b {
10554                1.0
10555            } else {
10556                0.0
10557            }
10558        }
10559        Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
10560            return Err(EngineError::Unsupported(
10561                "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
10562            ));
10563        }
10564        // v7.37 — PG orders INTERVAL by its total time, treating a month as
10565        // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
10566        // f64 is exact for any interval under ~285 years, and only ORDER BY
10567        // tie-breaks past that magnitude lose precision. Matches the
10568        // min/max(interval) comparator in aggregate.rs.
10569        #[allow(clippy::cast_precision_loss)]
10570        Value::Interval {
10571            months,
10572            days,
10573            micros,
10574        } => {
10575            let total = i128::from(*months) * 30 * 86_400_000_000
10576                + i128::from(*days) * 86_400_000_000
10577                + i128::from(*micros);
10578            total as f64
10579        }
10580        Value::Json(_) => {
10581            return Err(EngineError::Unsupported(
10582                "ORDER BY of a JSON value is not supported — cast the document to text first"
10583                    .into(),
10584            ));
10585        }
10586        // v7.5.0 — Value is #[non_exhaustive]; future variants need
10587        // an explicit ORDER BY mapping. Surface as Unsupported until
10588        // engine support is added.
10589        _ => {
10590            return Err(EngineError::Unsupported(
10591                "ORDER BY of this value type is not supported".into(),
10592            ));
10593        }
10594    };
10595    Ok(OrderKey::Num(num))
10596}
10597
10598/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
10599/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
10600/// `EngineError` so the projection-build path keeps `UnknownQualifier`
10601/// vs `ColumnNotFound` distinct.
10602/// PG's name for the physical row identity. It is reserved there — no table
10603/// can have a column called this — which is what lets `*` skip it by name.
10604pub(crate) const CTID_COLUMN: &str = "ctid";
10605
10606/// v7.39 (round 512) — PG's system columns, in the order they are appended.
10607/// All six are reserved names there, which is what lets `*` skip them and
10608/// lets a scan tell them from a user column without a flag.
10609pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
10610
10611/// Is this name one of them?
10612pub(crate) fn is_system_column(name: &str) -> bool {
10613    SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
10614}
10615
10616/// Where the scan's appended system columns begin, if this schema carries
10617/// them: the trailing six, named in order. A catalog view with a column of
10618/// its own called `xmin` does not match, which is the point.
10619fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
10620    let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
10621    cols[start..]
10622        .iter()
10623        .zip(SYSTEM_COLUMNS)
10624        .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
10625        .then_some(start)
10626}
10627
10628/// v7.39 (round 540) — which positions `*` must skip.
10629///
10630/// The rule stays round 512's — the synthetic columns are the trailing
10631/// six of a relation's block, matched by POSITION so a genuine `xmin`
10632/// column is not lost — but a JOINED schema names its columns
10633/// `alias.column` and lays the peers out end to end, so a peer's six sit
10634/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
10635/// "trailing six" test back on the block it was written for.
10636fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
10637    let mut skip = alloc::vec![false; cols.len()];
10638    fn qualifier(n: &str) -> Option<&str> {
10639        n.rsplit_once('.').map(|(q, _)| q)
10640    }
10641    fn bare(n: &str) -> &str {
10642        n.rsplit('.').next().unwrap_or(n)
10643    }
10644    let mut i = 0;
10645    while i < cols.len() {
10646        let q = qualifier(&cols[i].name);
10647        let mut end = i;
10648        while end < cols.len() && qualifier(&cols[end].name) == q {
10649            end += 1;
10650        }
10651        if let Some(start) = (end - i)
10652            .checked_sub(SYSTEM_COLUMNS.len())
10653            .map(|off| i + off)
10654            && cols[start..end]
10655                .iter()
10656                .zip(SYSTEM_COLUMNS)
10657                .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
10658        {
10659            for s in skip.iter_mut().take(end).skip(start) {
10660                *s = true;
10661            }
10662        }
10663        i = end;
10664    }
10665    skip
10666}
10667
10668/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
10669/// read? Only then is the column materialised.
10670pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
10671    let mut found = false;
10672    crate::expr_analysis::visit_expr_columns_and_subqueries(
10673        e,
10674        &mut |c| {
10675            if is_system_column(&c.name) {
10676                found = true;
10677            }
10678        },
10679        &mut |_| {},
10680    );
10681    found
10682}
10683
10684fn references_ctid(stmt: &SelectStatement) -> bool {
10685    let in_expr = expr_references_ctid;
10686    stmt.items.iter().any(|i| match i {
10687        SelectItem::Expr { expr, .. } => in_expr(expr),
10688        _ => false,
10689    }) || stmt.where_.as_ref().is_some_and(in_expr)
10690        || stmt.order_by.iter().any(|o| in_expr(&o.expr))
10691        || stmt
10692            .group_by
10693            .as_ref()
10694            .is_some_and(|g| g.iter().any(in_expr))
10695        || stmt.having.as_ref().is_some_and(in_expr)
10696}
10697
10698/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
10699/// is a name the projection has to TYPE before any row exists.
10700///
10701/// Evaluation has answered this since round T9 (`resolve_column` builds a
10702/// `Value::Composite` of every column), but the typing side below had no
10703/// such branch and raised `column "t" does not exist` first — so the
10704/// feature was unreachable through a projection. Measured against PG18.4:
10705/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
10706///
10707/// The type is `Jsonb` + a composite marker, which is exactly how a
10708/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
10709/// the value travels as a `Value::Composite` and renders in the canonical
10710/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
10711/// so the marker names the alias and no rehydration keys off it — the
10712/// value arrives already built.
10713fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
10714    let mut s = ColumnSchema::new(
10715        alloc::string::String::from(alias),
10716        spg_storage::DataType::Jsonb,
10717        true,
10718    );
10719    s.user_composite_type = Some(alloc::string::String::from(alias));
10720    s
10721}
10722
10723pub(crate) fn resolve_projection_column<'a>(
10724    c: &ColumnName,
10725    schema_cols: &'a [ColumnSchema],
10726    table_alias: &str,
10727) -> Result<Cow<'a, ColumnSchema>, EngineError> {
10728    if let Some(q) = &c.qualifier {
10729        let composite = alloc::format!("{q}.{name}", name = c.name);
10730        if let Some(s) = schema_cols.iter().find(|s| s.name == composite) {
10731            return Ok(Cow::Borrowed(s));
10732        }
10733        // Single-table case: the qualifier may equal the active alias —
10734        // then look for the bare column name.
10735        if q == table_alias
10736            && let Some(s) = schema_cols.iter().find(|s| s.name == c.name)
10737        {
10738            return Ok(Cow::Borrowed(s));
10739        }
10740        // For multi-table schemas the qualifier is unknown only if no
10741        // column bears the "<q>." prefix. For single-table, the alias
10742        // mismatch alone is enough.
10743        let prefix = alloc::format!("{q}.");
10744        let qualifier_known =
10745            q == table_alias || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
10746        if !qualifier_known {
10747            return Err(EngineError::Eval(EvalError::UnknownQualifier {
10748                qualifier: q.clone(),
10749            }));
10750        }
10751        return Err(EngineError::Eval(EvalError::ColumnNotFound {
10752            name: c.name.clone(),
10753        }));
10754    }
10755    if let Some(s) = schema_cols.iter().find(|s| s.name == c.name) {
10756        return Ok(Cow::Borrowed(s));
10757    }
10758    let suffix = alloc::format!(".{name}", name = c.name);
10759    let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
10760    let first = matches.next();
10761    let extra = matches.next();
10762    match (first, extra) {
10763        (Some(s), None) => Ok(Cow::Borrowed(s)),
10764        (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
10765            detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
10766        })),
10767        // The whole-row reference, checked LAST so a real column carrying
10768        // the alias's name still wins — the same precedence
10769        // `resolve_column` applies on the evaluation side.
10770        //
10771        // Two schema shapes reach here. A single-table (or subquery, or
10772        // CTE) scan carries its alias and bare column names, so the name
10773        // has to equal the alias. A JOIN's combined schema carries no
10774        // alias at all and qualifies every column `alias.col`, so the
10775        // alias is identified by the prefix instead — which is exactly
10776        // how `whole_row_composite` picks the fields out on the
10777        // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
10778        // answers `(7,z)` on PG18.4 and errored here until this arm
10779        // covered the joined shape too.
10780        _ if !table_alias.is_empty() && c.name == table_alias => {
10781            Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
10782        }
10783        _ if table_alias.is_empty() && {
10784            let prefix = alloc::format!("{name}.", name = c.name);
10785            schema_cols.iter().any(|s| s.name.starts_with(&prefix))
10786        } =>
10787        {
10788            Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
10789        }
10790        _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
10791            name: c.name.clone(),
10792        })),
10793    }
10794}
10795
10796/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
10797/// parser to carry per-branch GROUPING() masks into a grouping-set query's
10798/// ORDER BY. They must never reach the output. No-op unless such a column is
10799/// present, so the common path is untouched.
10800/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
10801///
10802/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
10803/// a `LIMIT 2` that should have answered two groups answered one.
10804fn apply_deferred_limit(
10805    rows: alloc::vec::Vec<Row<'static>>,
10806    deferred: &(
10807        Option<spg_sql::ast::LimitExpr>,
10808        Option<spg_sql::ast::LimitExpr>,
10809    ),
10810) -> alloc::vec::Vec<Row<'static>> {
10811    let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
10812        Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
10813        _ => None,
10814    };
10815    let mut rows = rows;
10816    if let Some(off) = count(&deferred.1) {
10817        rows = rows.split_off(off.min(rows.len()));
10818    }
10819    if let Some(lim) = count(&deferred.0) {
10820        rows.truncate(lim);
10821    }
10822    rows
10823}
10824
10825fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
10826    let QueryResult::Rows { columns, rows } = result else {
10827        return result;
10828    };
10829    if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
10830        return QueryResult::Rows { columns, rows };
10831    }
10832    let keep: Vec<usize> = columns
10833        .iter()
10834        .enumerate()
10835        .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
10836        .map(|(i, _)| i)
10837        .collect();
10838    let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
10839    let new_rows: Vec<Row<'static>> = rows
10840        .into_iter()
10841        .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
10842        .collect();
10843    QueryResult::Rows {
10844        columns: new_cols,
10845        rows: new_rows,
10846    }
10847}
10848
10849/// v7.39 (round 487) — bind every projection item that is a bare column
10850/// reference to its position, once per query.
10851///
10852/// `#[inline(never)]` and out of line on purpose. Round 486 established
10853/// that adding code inside these scan bodies moves neighbouring hot
10854/// functions around under fat LTO: the first version of this had the loop
10855/// inline in `run_single_table_scan` and four aggregate shapes that never
10856/// touch that function — `full_agg`, `join_agg`, `group_500k`,
10857/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
10858/// the same machine. Keeping it out of line kept them still.
10859#[inline(never)]
10860fn bind_direct_columns(
10861    projection: &[ProjectedItem],
10862    ctx: &eval::EvalContext<'_>,
10863) -> Vec<Option<usize>> {
10864    projection
10865        .iter()
10866        .map(|p| match &p.expr {
10867            Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
10868                // Same exclusion `compile_into` makes: a composite column
10869                // has to be rehydrated from stored JSON, which is not a
10870                // cell read.
10871                ctx.columns
10872                    .get(*pos)
10873                    .is_none_or(|sc| sc.user_composite_type.is_none())
10874            }),
10875            _ => None,
10876        })
10877        .collect()
10878}
10879
10880/// v7.39 (round 505) — the name an un-aliased projected expression reports.
10881///
10882/// PG18 names a call for its function and everything else `?column?`;
10883/// measured with `\gdesc`. SPG used to print the parsed expression back
10884/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
10885/// name-keyed row access found nothing under `upper`.
10886///
10887/// The MySQL half is NOT this rule and is deliberately left alone here:
10888/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
10889/// which needs the parser to hand over spans the AST does not carry yet.
10890/// Until it does, a MySQL session keeps the printed form — closer to what
10891/// MariaDB answers than `?column?` would be.
10892pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
10893    if mysql {
10894        return expr.to_string();
10895    }
10896    spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
10897}
10898
10899pub(crate) fn build_projection(
10900    items: &[SelectItem],
10901    schema_cols: &[ColumnSchema],
10902    table_alias: &str,
10903    mysql: bool,
10904) -> Result<Vec<ProjectedItem>, EngineError> {
10905    build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0)
10906}
10907
10908/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
10909/// invisible to `*`.
10910///
10911/// The windowed-SELECT path appends a synthetic `__win_N` column per window
10912/// function so the rewritten projection can reference the computed values as
10913/// ordinary columns. `*` then expanded them too, and
10914/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
10915/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
10916/// silent one: the row simply had one more field than the client asked for.
10917///
10918/// Hidden by POSITION rather than by name, for the reason round 512 recorded
10919/// about the system columns: a name test looks safe until a real column
10920/// happens to carry the name. These are appended last, so the count is what
10921/// identifies them.
10922pub(crate) fn build_projection_hiding_tail(
10923    items: &[SelectItem],
10924    schema_cols: &[ColumnSchema],
10925    table_alias: &str,
10926    mysql: bool,
10927    hidden_tail: usize,
10928) -> Result<Vec<ProjectedItem>, EngineError> {
10929    let visible = schema_cols.len().saturating_sub(hidden_tail);
10930    // v7.39 (round 462) — a join's combined schema qualifies every column
10931    // `alias.col` so the deferred-join cell lookups resolve by composite
10932    // name. That is an internal convention, and `*` was handing it to the
10933    // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
10934    // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
10935    // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
10936    // already learned this for `q.*`; plain `*` never got the same rule.
10937    //
10938    // The signal is the schema itself, not the call site: only a combined
10939    // join schema arrives with no table alias AND every column qualified.
10940    // A single-table schema carries its alias, an empty schema has nothing
10941    // to strip, and a synthetic schema's names carry no dot.
10942    let joined_schema = table_alias.is_empty()
10943        && !schema_cols.is_empty()
10944        && schema_cols.iter().all(|c| c.name.contains('.'));
10945    let bare_name = |name: &str| -> String {
10946        if !joined_schema {
10947            return name.to_string();
10948        }
10949        match name.split_once('.') {
10950            Some((_, rest)) if !rest.is_empty() => rest.to_string(),
10951            _ => name.to_string(),
10952        }
10953    };
10954    let mut out = Vec::new();
10955    for item in items {
10956        match item {
10957            SelectItem::Wildcard => {
10958                // v7.39 (round 511) — `*` never expands a system column, as
10959                // PG's does not. They join the schema only when the statement
10960                // asked for them, so this matters for the mixed shape
10961                // `SELECT *, ctid FROM t`.
10962                //
10963                // v7.39 (round 512) — by POSITION, not by name. Matching on
10964                // the name alone looked safe because PG reserves them, and it
10965                // is not: `pg_replication_slots` genuinely has a column called
10966                // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
10967                // Only the trailing six, in the order the scan appends them,
10968                // are the synthetic ones.
10969                let sys_skip = synthetic_system_positions(schema_cols);
10970                for (idx, col) in schema_cols.iter().enumerate() {
10971                    if sys_skip[idx] || idx >= visible {
10972                        continue;
10973                    }
10974                    out.push(ProjectedItem {
10975                        expr: Expr::Column(ColumnName {
10976                            qualifier: None,
10977                            name: col.name.clone(),
10978                        }),
10979                        output_name: bare_name(&col.name),
10980                        ty: col.ty,
10981                        nullable: col.nullable,
10982                        user_enum_type: col.user_enum_type.clone(),
10983                        mysql_fsp: col.mysql_fsp,
10984                        collation_name: col.collation_name.clone(),
10985                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
10986                    });
10987                }
10988            }
10989            // v7.39 (round 128) — `q.*` expands to every column belonging to
10990            // the qualifier `q`. Single-table schemas carry bare column names
10991            // reachable via `table_alias`; a join's combined schema carries
10992            // `alias.col` names, so a column belongs to `q` when its name has
10993            // the `q.` prefix. PG labels the expanded columns by their bare
10994            // name, so the `alias.` prefix is stripped from the output name.
10995            SelectItem::QualifiedWildcard(q) => {
10996                let prefix = alloc::format!("{q}.");
10997                let single_table = !table_alias.is_empty() && q == table_alias;
10998                let mut matched = 0usize;
10999                for col in &schema_cols[..visible] {
11000                    let belongs =
11001                        col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
11002                    if !belongs {
11003                        continue;
11004                    }
11005                    matched += 1;
11006                    let output_name = col
11007                        .name
11008                        .strip_prefix(&prefix)
11009                        .unwrap_or(&col.name)
11010                        .to_string();
11011                    out.push(ProjectedItem {
11012                        expr: Expr::Column(ColumnName {
11013                            qualifier: None,
11014                            name: col.name.clone(),
11015                        }),
11016                        output_name,
11017                        ty: col.ty,
11018                        nullable: col.nullable,
11019                        user_enum_type: col.user_enum_type.clone(),
11020                        mysql_fsp: col.mysql_fsp,
11021                        collation_name: col.collation_name.clone(),
11022                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
11023                    });
11024                }
11025                if matched == 0 {
11026                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
11027                        qualifier: q.clone(),
11028                    }));
11029                }
11030            }
11031            SelectItem::Expr { expr, alias } => {
11032                // Plain column ref keeps full schema info (real type +
11033                // nullability). For compound expressions try the
11034                // describe-side function-return-type table first
11035                // (e.g. `SELECT now()` → Timestamptz, `SELECT
11036                // concat(…)` → Text). Falls back to nullable Text
11037                // for shapes the describe path can't resolve.
11038                if let Expr::Column(c) = expr {
11039                    let sch = resolve_projection_column(c, schema_cols, table_alias)?;
11040                    let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
11041                    out.push(ProjectedItem {
11042                        expr: expr.clone(),
11043                        output_name,
11044                        ty: sch.ty,
11045                        nullable: sch.nullable,
11046                        // v7.39 (read01 round 54) — a bare enum column keeps
11047                        // its enum identity through the projection.
11048                        user_enum_type: sch.user_enum_type.clone(),
11049                        mysql_fsp: sch.mysql_fsp,
11050                        collation_name: sch.collation_name.clone(),
11051                        // v7.38.13 — and its byte-wise-ness. This is the
11052                        // site `SELECT DISTINCT t FROM t` arrives at.
11053                        fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
11054                    });
11055                } else if let Some(shape) = describe::describe_expr(expr, schema_cols) {
11056                    let output_name = alias
11057                        .clone()
11058                        .unwrap_or_else(|| default_output_name(expr, mysql));
11059                    out.push(ProjectedItem {
11060                        expr: expr.clone(),
11061                        output_name,
11062                        ty: shape.ty,
11063                        // v7.39 (round 258) — a projected EXPRESSION keeps its
11064                        // enum identity too, not just a bare column. `FROM
11065                        // (VALUES ('happy'::mood), …) t(m)` lowers to constant
11066                        // SELECTs, so the derived column arrived here as a cast
11067                        // and lost the enum — making the outer ORDER BY / min /
11068                        // max / array_agg sort by the label's TEXT.
11069                        nullable: shape.nullable,
11070                        user_enum_type: None,
11071                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11072                        // A bare column reference keeps its collation; any
11073                        // other expression produces a new value and has none.
11074                        collation_name: match expr {
11075                            Expr::Column(c) => schema_cols
11076                                .iter()
11077                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11078                                .and_then(|sc| sc.collation_name.clone()),
11079                            _ => None,
11080                        },
11081                        fold_exempt: match expr {
11082                            Expr::Column(c) => schema_cols
11083                                .iter()
11084                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11085                                .is_some_and(|sc| {
11086                                    matches!(sc.collation, spg_storage::Collation::Binary)
11087                                }),
11088                            // Not a column: no declared collation to honour,
11089                            // so the session default applies and it folds.
11090                            _ => false,
11091                        },
11092                    });
11093                } else {
11094                    let output_name = alias
11095                        .clone()
11096                        .unwrap_or_else(|| default_output_name(expr, mysql));
11097                    out.push(ProjectedItem {
11098                        expr: expr.clone(),
11099                        output_name,
11100                        // A user ENUM has no DataType of its own, so
11101                        // `describe_expr` cannot type `'ok'::mood` and the
11102                        // item lands HERE, defaulting to text — which is why
11103                        // pg_typeof answered `text` and a derived table sorted
11104                        // enum values by their label.
11105                        ty: DataType::Text,
11106                        nullable: true,
11107                        user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
11108                            .map(alloc::string::String::from),
11109                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11110                        collation_name: match expr {
11111                            Expr::Column(c) => schema_cols
11112                                .iter()
11113                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11114                                .and_then(|sc| sc.collation_name.clone()),
11115                            _ => None,
11116                        },
11117                        fold_exempt: match expr {
11118                            Expr::Column(c) => schema_cols
11119                                .iter()
11120                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11121                                .is_some_and(|sc| {
11122                                    matches!(sc.collation, spg_storage::Collation::Binary)
11123                                }),
11124                            // Not a column: no declared collation to honour,
11125                            // so the session default applies and it folds.
11126                            _ => false,
11127                        },
11128                    });
11129                }
11130            }
11131        }
11132    }
11133    Ok(out)
11134}
11135
11136// ---- v4.12 window-function helpers ----
11137// The (partition-key, order-key, original-index) tuple shape used
11138// across these helpers is intrinsic to the planner. Factoring it
11139// into a typedef adds indirection without making the code clearer,
11140// so several lints are allowed inline on the affected functions
11141// rather than module-wide.
11142
11143/// v4.22: pick more specific column types from observed rows when
11144/// the projection builder defaulted to Text (the v1.x behavior for
11145/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
11146/// land an Int column in the CTE storage table rather than failing
11147/// the insert with "expected TEXT, got INT".
11148pub(crate) fn infer_column_types(
11149    columns: &[ColumnSchema],
11150    rows: &[Row<'static>],
11151) -> Vec<ColumnSchema> {
11152    let mut out = columns.to_vec();
11153    for (col_idx, col) in out.iter_mut().enumerate() {
11154        if col.ty != DataType::Text {
11155            continue;
11156        }
11157        let mut inferred: Option<DataType> = None;
11158        let mut all_null = true;
11159        for row in rows {
11160            let Some(v) = row.values.get(col_idx) else {
11161                continue;
11162            };
11163            let ty = match v {
11164                Value::Null => continue,
11165                Value::SmallInt(_) => DataType::SmallInt,
11166                Value::Int(_) => DataType::Int,
11167                Value::BigInt(_) => DataType::BigInt,
11168                Value::Float(_) => DataType::Float,
11169                Value::Bool(_) => DataType::Bool,
11170                Value::Vector(_) => DataType::Vector {
11171                    dim: 0,
11172                    encoding: VecEncoding::F32,
11173                },
11174                // v7.38 (read01 U16) — carry array values through with an
11175                // array type so a recursive CTE that projects an array
11176                // (e.g. a SEARCH/CYCLE ord / path column) types the working
11177                // column as an array, not Text.
11178                Value::TextArray(_) => DataType::TextArray,
11179                Value::IntArray(_) => DataType::IntArray,
11180                Value::BigIntArray(_) => DataType::BigIntArray,
11181                Value::SmallIntArray(_) => DataType::SmallIntArray,
11182                Value::FloatArray(_) => DataType::FloatArray,
11183                Value::BoolArray(_) => DataType::BoolArray,
11184                // v7.39 (GUC knife 2) — an interval projection describes
11185                // as INTERVAL (typed drivers read the RowDescription OID).
11186                Value::Interval { .. } => DataType::Interval,
11187                _ => DataType::Text,
11188            };
11189            all_null = false;
11190            inferred = Some(match inferred {
11191                None => ty,
11192                Some(prev) if prev == ty => prev,
11193                Some(_) => DataType::Text,
11194            });
11195        }
11196        if let Some(t) = inferred {
11197            col.ty = t;
11198            col.nullable = true;
11199        } else if all_null {
11200            col.nullable = true;
11201        }
11202    }
11203    out
11204}
11205
11206/// Numeric widening rank for UNION type resolution (higher = wider).
11207fn numeric_rank(t: DataType) -> Option<u8> {
11208    match t {
11209        DataType::SmallInt => Some(1),
11210        DataType::Int => Some(2),
11211        DataType::BigInt => Some(3),
11212        DataType::Numeric { .. } => Some(4),
11213        DataType::Float => Some(5),
11214        _ => None,
11215    }
11216}
11217
11218/// Resolve the common result type for a UNION / VALUES column from the
11219/// set of concrete (non-NULL) branch types, following the safe subset
11220/// of PG's type resolution:
11221///   * all-numeric  → the widest numeric (int ∪ bigint → bigint, … ∪
11222///     numeric → numeric, … ∪ float → float);
11223///   * DATE ∪ TIMESTAMP → TIMESTAMP;
11224///   * exactly one concrete non-TEXT type mixed with TEXT literals →
11225///     that concrete type (the TEXT cells get parsed into it).
11226/// Returns `None` for anything ambiguous, so the caller leaves the
11227/// column untouched rather than risk a wrong or failing coercion.
11228fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
11229    // NB: types are collected from RUNTIME values, which are coarser
11230    // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
11231    // a single-concrete-type fast path must NOT overwrite the column
11232    // type — it would downgrade tstz to ts. NULL-only unification (PG:
11233    // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
11234    // row's pg_typeof) needs schema-level resolution — recorded, not
11235    // attempted here.
11236    if types.len() < 2 {
11237        return None;
11238    }
11239    if types.iter().all(|t| numeric_rank(*t).is_some()) {
11240        return types
11241            .iter()
11242            .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
11243            .copied();
11244    }
11245    let non_text: Vec<&DataType> = types
11246        .iter()
11247        .filter(|t| !matches!(t, DataType::Text))
11248        .collect();
11249    // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
11250    // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
11251    // if any is timestamp the result is timestamp (ts ∪ date). All values are
11252    // the same UTC-micros instant, so widening date/ts to tstz is lossless.
11253    if non_text.iter().all(|t| {
11254        matches!(
11255            t,
11256            DataType::Date | DataType::Timestamp | DataType::Timestamptz
11257        )
11258    }) && non_text
11259        .iter()
11260        .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
11261    {
11262        if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
11263            return Some(DataType::Timestamptz);
11264        }
11265        return Some(DataType::Timestamp);
11266    }
11267    // A single concrete non-TEXT type mixed with TEXT literals.
11268    if non_text.len() == 1 {
11269        return Some(*non_text[0]);
11270    }
11271    // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
11272    // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
11273    // text): resolve the concrete set first (PG treats the unknown-
11274    // typed string literals as castable to whatever the knowns
11275    // resolve to), then the TEXT cells parse into that target — the
11276    // caller's coercion dry-run still abandons the column if any
11277    // literal doesn't parse.
11278    if !non_text.is_empty() && non_text.len() < types.len() {
11279        let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
11280        return resolve_union_common_type(&concrete);
11281    }
11282    None
11283}
11284
11285/// Coerce every cell of a UNION / VALUES result column to one common
11286/// type (see [`resolve_union_common_type`]). Conservative: a column
11287/// whose branches already agree, or whose types don't resolve, or where
11288/// any cell fails to coerce, is left exactly as it was — this never
11289/// turns a previously-working query into an error.
11290fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
11291    for col_idx in 0..columns.len() {
11292        let mut seen: Vec<DataType> = Vec::new();
11293        for row in rows.iter() {
11294            if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
11295                if !seen.contains(&dt) {
11296                    seen.push(dt);
11297                }
11298            }
11299        }
11300        // v7.37.16 — a single concrete runtime type under a TEXT-typed
11301        // column means the column type came off a NULL (or unknown-text)
11302        // branch: NULL literals describe as TEXT (`L::Null → Text`), so
11303        // `VALUES (NULL),(1.5)` left the column "text" while every
11304        // non-NULL cell is numeric. Adopt the concrete type — schema
11305        // only, no cell changes. tstz-safe by construction: a real
11306        // timestamptz column's schema type is Timestamptz, not Text, so
11307        // the coarser runtime type (Value::Timestamp) can't downgrade it
11308        // through this arm; and a real text column's non-NULL cells are
11309        // Text, which keeps seen == [Text] and skips it.
11310        if seen.len() == 1
11311            && matches!(columns[col_idx].ty, DataType::Text)
11312            && !matches!(seen[0], DataType::Text)
11313        {
11314            columns[col_idx].ty = seen[0];
11315            continue;
11316        }
11317        let Some(target) = resolve_union_common_type(&seen) else {
11318            continue;
11319        };
11320        // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
11321        // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
11322        // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
11323        // existing numeric cell untouched and only promote integers (to scale 0)
11324        // rather than rescaling everything to the widest scale.
11325        let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
11326        // Dry-run the coercion; abandon the whole column if any fails.
11327        let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
11328        let mut ok = true;
11329        for row in rows.iter() {
11330            match row.values.get(col_idx) {
11331                Some(Value::Numeric { .. }) if scale_preserving_numeric => {
11332                    coerced.push(Some(row.values[col_idx].clone()));
11333                }
11334                Some(v) => {
11335                    let cell_target = if scale_preserving_numeric {
11336                        DataType::Numeric {
11337                            precision: 0,
11338                            scale: 0,
11339                        }
11340                    } else {
11341                        target
11342                    };
11343                    match crate::conversions::coerce_value(
11344                        v.clone(),
11345                        cell_target,
11346                        &columns[col_idx].name,
11347                        col_idx,
11348                    ) {
11349                        Ok(cv) => coerced.push(Some(cv)),
11350                        Err(_) => {
11351                            ok = false;
11352                            break;
11353                        }
11354                    }
11355                }
11356                None => coerced.push(None),
11357            }
11358        }
11359        if !ok {
11360            continue;
11361        }
11362        for (row, cv) in rows.iter_mut().zip(coerced) {
11363            if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
11364                *slot = nv;
11365            }
11366        }
11367        columns[col_idx].ty = target;
11368    }
11369}
11370
11371/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
11372/// dedup inside the recursive iteration. Crude but deterministic
11373/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
11374fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
11375    let mut out = Vec::new();
11376    for v in &row.values {
11377        // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
11378        // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
11379        // like PG (and like GROUP BY, which already normalizes). The old
11380        // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
11381        // the exact-decimal family through one scale-stripped canonical form.
11382        match v {
11383            Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11384            Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11385            Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11386            Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
11387            other => {
11388                let s = alloc::format!("{other:?}|");
11389                out.extend_from_slice(s.as_bytes());
11390            }
11391        }
11392    }
11393    out
11394}
11395
11396/// Append a scale-independent canonical key for an exact-decimal value: strip
11397/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
11398/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
11399fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
11400    while scale > 0 && scaled % 10 == 0 {
11401        scaled /= 10;
11402        scale -= 1;
11403    }
11404    let s = alloc::format!("\u{1}{scaled}e-{scale}|");
11405    out.extend_from_slice(s.as_bytes());
11406}
11407
11408/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
11409/// (uncorrelated; outer refs were substituted upstream), then zip
11410/// them in parallel, NULL-padding shorter arrays to the longest
11411/// (PG's ROWS FROM shorthand). Shared by the primary-position
11412/// executor and the join-position materialiser, which both detect
11413/// the parser's `__unnest_zip` marker call.
11414pub(crate) fn unnest_zip_rows(
11415    args: &[Expr],
11416) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
11417    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11418    let ctx = EvalContext::new(&empty_schema, None);
11419    let dummy_row = Row::new(alloc::vec::Vec::new());
11420    let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
11421    let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
11422        alloc::vec::Vec::with_capacity(args.len());
11423    for a in args {
11424        let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
11425        let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = match v {
11426            Value::Null => (DataType::Text, alloc::vec::Vec::new()),
11427            Value::TextArray(xs) => (
11428                DataType::Text,
11429                xs.into_iter()
11430                    .map(|x| x.map(Value::text).unwrap_or(Value::Null))
11431                    .collect(),
11432            ),
11433            Value::IntArray(xs) => (
11434                DataType::Int,
11435                xs.into_iter()
11436                    .map(|x| x.map(Value::Int).unwrap_or(Value::Null))
11437                    .collect(),
11438            ),
11439            Value::BigIntArray(xs) => (
11440                DataType::BigInt,
11441                xs.into_iter()
11442                    .map(|x| x.map(Value::BigInt).unwrap_or(Value::Null))
11443                    .collect(),
11444            ),
11445            other => {
11446                return Err(EngineError::Unsupported(alloc::format!(
11447                    "unnest() expects array arguments, got {}",
11448                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
11449                )));
11450            }
11451        };
11452        dtypes.push(dt);
11453        columns.push(items);
11454    }
11455    let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
11456    let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
11457    for i in 0..max_len {
11458        let vals: alloc::vec::Vec<Value<'static>> = columns
11459            .iter()
11460            .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
11461            .collect();
11462        rows.push(Row::new(vals));
11463    }
11464    Ok((dtypes, rows))
11465}
11466
11467/// Detect the parser's multi-arg unnest marker on an unnest_expr.
11468pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
11469    match expr {
11470        Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
11471        _ => None,
11472    }
11473}
11474
11475/// Evaluate generate_series arguments (uncorrelated — outer refs
11476/// were substituted upstream where applicable) and build the row
11477/// stream. Dispatches on the start value's shape and rejects
11478/// mixed-shape calls early (e.g. start = timestamp, stop =
11479/// integer) so the caller gets a clean error rather than a panic.
11480/// Shared by the primary-position executor and the join-position
11481/// materialiser.
11482pub(crate) fn generate_series_rows(
11483    args: &[Expr],
11484    cancel: &CancelToken<'_>,
11485) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
11486    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11487    let ctx = EvalContext::new(&empty_schema, None);
11488    let dummy_row = Row::new(alloc::vec::Vec::new());
11489    let mut arg_values: alloc::vec::Vec<Value<'static>> =
11490        alloc::vec::Vec::with_capacity(args.len());
11491    for a in args {
11492        arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
11493    }
11494    generate_series_from_values(arg_values, args, cancel)
11495}
11496
11497/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
11498/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
11499/// full integer / numeric / timestamp overload set with the FROM-clause path.
11500/// Before this split the target-list arm reimplemented only the integer case,
11501/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
11502/// NULL for the timestamp column instead of the series. `arg_values` are the
11503/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
11504/// timestamp type resolution (it inspects the argument expressions' types).
11505pub(crate) fn generate_series_from_values(
11506    mut arg_values: alloc::vec::Vec<Value<'static>>,
11507    args: &[Expr],
11508    cancel: &CancelToken<'_>,
11509) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
11510    // PG: a NULL bound or step yields zero rows (also keeps the
11511    // NULL-padded lateral probe alive — schema without data).
11512    if arg_values.iter().any(|v| matches!(v, Value::Null)) {
11513        return Ok((DataType::BigInt, alloc::vec::Vec::new()));
11514    }
11515    // PG resolves `generate_series(date, date, interval)` to the
11516    // timestamp/timestamptz overload by implicitly casting each date
11517    // bound up to a timestamp at midnight (verified vs live PG18.4:
11518    // date args yield rows anchored at 00:00:00). SPG's TZ-naive
11519    // timestamp model renders the same instants, so fold any Date
11520    // bound to its midnight Timestamp (canonical `days *
11521    // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
11522    // the shape match so the existing timestamp arm drives the walk.
11523    // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
11524    // `generate_series(date, date, interval)` has no date overload, and among
11525    // the two candidates PG prefers the timestamptz one (timestamptz is the
11526    // preferred type of the datetime category), so the column comes back
11527    // `timestamp with time zone` — the rows render with a `+00` offset. A
11528    // timestamptz bound obviously lands there too. Only genuinely
11529    // timestamp-typed bounds keep the TZ-naive result type.
11530    let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11531    let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
11532        || args.iter().any(|a| {
11533            crate::describe::describe_expr(a, &empty_cols)
11534                .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
11535        });
11536    for v in &mut arg_values {
11537        if let Value::Date(d) = *v {
11538            *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
11539        }
11540    }
11541    match arg_values.as_slice() {
11542        [Value::Timestamp(start), Value::Timestamp(stop), step] => {
11543            let interval_step = match step {
11544                Value::Interval { .. } => step.clone(),
11545                // v7.38 (read01) — PG resolves an unknown-type string step
11546                // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
11547                // a bare text step by parsing it the same way `::interval` does.
11548                Value::Text(s) => crate::conversions::coerce_value(
11549                    Value::text(s.as_ref()),
11550                    DataType::Interval,
11551                    "",
11552                    0,
11553                )
11554                .map_err(|_| {
11555                    EngineError::Unsupported(alloc::format!(
11556                        "generate_series(timestamp, timestamp, …): \
11557                         could not parse step {s:?} as INTERVAL"
11558                    ))
11559                })?,
11560                other => {
11561                    return Err(EngineError::Unsupported(alloc::format!(
11562                        "generate_series(timestamp, timestamp, …): \
11563                         step must be INTERVAL, got {}",
11564                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
11565                    )));
11566                }
11567            };
11568            let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
11569            Ok((
11570                if tz {
11571                    DataType::Timestamptz
11572                } else {
11573                    DataType::Timestamp
11574                },
11575                rows,
11576            ))
11577        }
11578        [start, stop, step]
11579            if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
11580        {
11581            let s = value_to_i64(start);
11582            let e = value_to_i64(stop);
11583            let st = value_to_i64(step);
11584            // PG types the series by the argument type: int4 args → int4
11585            // elements, int8 (bigint) args → int8. Any BigInt operand widens.
11586            let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
11587            let rows = generate_series_integers(s, e, st, wide, cancel)?;
11588            Ok((
11589                if wide {
11590                    DataType::BigInt
11591                } else {
11592                    DataType::Int
11593                },
11594                rows,
11595            ))
11596        }
11597        [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
11598            let s = value_to_i64(start);
11599            let e = value_to_i64(stop);
11600            let wide = value_is_bigint(start) || value_is_bigint(stop);
11601            let rows = generate_series_integers(s, e, 1, wide, cancel)?;
11602            Ok((
11603                if wide {
11604                    DataType::BigInt
11605                } else {
11606                    DataType::Int
11607                },
11608                rows,
11609            ))
11610        }
11611        // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
11612        // series in exact numeric arithmetic; NaN / infinity bounds and a
11613        // zero step get dedicated wordings, and a mixed int/numeric call
11614        // resolves here via the implicit int→numeric cast.
11615        [_, _] | [_, _, _]
11616            if arg_values
11617                .iter()
11618                .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
11619                && arg_values.iter().all(|v| {
11620                    matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
11621                }) =>
11622        {
11623            use spg_storage::NumericKind as K;
11624            let words: [(&str, &str); 3] = [
11625                (
11626                    "start value cannot be NaN",
11627                    "start value cannot be infinity",
11628                ),
11629                ("stop value cannot be NaN", "stop value cannot be infinity"),
11630                ("step size cannot be NaN", "step size cannot be infinity"),
11631            ];
11632            for (i, v) in arg_values.iter().enumerate() {
11633                if let Value::Numeric { kind, .. } = v {
11634                    if *kind != K::Finite {
11635                        let (nan_w, inf_w) = words[i];
11636                        return Err(EngineError::Unsupported(
11637                            if *kind == K::NaN { nan_w } else { inf_w }.into(),
11638                        ));
11639                    }
11640                }
11641            }
11642            let big =
11643                |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
11644            let start = big(&arg_values[0]);
11645            let stop = big(&arg_values[1]);
11646            let step = if arg_values.len() == 3 {
11647                big(&arg_values[2])
11648            } else {
11649                spg_storage::bignum::BigNumeric::from_i128(1, 0)
11650            };
11651            if step.is_zero() {
11652                return Err(EngineError::Unsupported(
11653                    "step size cannot equal zero".into(),
11654                ));
11655            }
11656            let descending = step.parts().0;
11657            let mut rows = alloc::vec::Vec::new();
11658            let mut cur = start;
11659            const MAX_ROWS: usize = 10_000_000;
11660            loop {
11661                cancel.check()?;
11662                let c = cur.cmp(&stop);
11663                if descending {
11664                    if c == core::cmp::Ordering::Less {
11665                        break;
11666                    }
11667                } else if c == core::cmp::Ordering::Greater {
11668                    break;
11669                }
11670                if rows.len() >= MAX_ROWS {
11671                    return Err(EngineError::Unsupported(alloc::format!(
11672                        "generate_series() result exceeds {MAX_ROWS} rows"
11673                    )));
11674                }
11675                rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
11676                    cur.clone()
11677                )]));
11678                cur = cur.add(&step);
11679            }
11680            Ok((
11681                DataType::Numeric {
11682                    precision: 0,
11683                    scale: 0,
11684                },
11685                rows,
11686            ))
11687        }
11688        _ => Err(EngineError::Unsupported(alloc::format!(
11689            "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
11690             argument shapes; got {}",
11691            arg_values
11692                .iter()
11693                .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
11694                .collect::<alloc::vec::Vec<_>>()
11695                .join(", ")
11696        ))),
11697    }
11698}
11699
11700/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
11701/// Step direction follows the sign: positive step iterates upward
11702/// (stops when current > stop); negative iterates downward; zero
11703/// errors. Caller-facing row stream is `BigInt`-typed so a single
11704/// projection schema covers SmallInt / Int / BigInt callers.
11705fn generate_series_integers(
11706    start: i64,
11707    stop: i64,
11708    step: i64,
11709    wide: bool,
11710    cancel: &CancelToken<'_>,
11711) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11712    if step == 0 {
11713        return Err(EngineError::Unsupported(
11714            "step size cannot equal zero".into(),
11715        ));
11716    }
11717    let mut out = alloc::vec::Vec::new();
11718    let mut cur = start;
11719    // Hard cap to keep a runaway call from eating all memory. PG
11720    // has no such cap but does honour query timeout; SPG's cancel
11721    // token will fire too — this is a defense-in-depth backstop.
11722    const MAX_ROWS: usize = 10_000_000;
11723    loop {
11724        cancel.check()?;
11725        if step > 0 && cur > stop {
11726            break;
11727        }
11728        if step < 0 && cur < stop {
11729            break;
11730        }
11731        out.push(Row::new(alloc::vec![if wide {
11732            Value::BigInt(cur)
11733        } else {
11734            Value::Int(cur as i32)
11735        }]));
11736        if out.len() > MAX_ROWS {
11737            return Err(EngineError::Unsupported(alloc::format!(
11738                "generate_series(): exceeded {MAX_ROWS} rows; \
11739                 narrow start/stop or use a larger step"
11740            )));
11741        }
11742        cur = match cur.checked_add(step) {
11743            Some(n) => n,
11744            None => break,
11745        };
11746    }
11747    Ok(out)
11748}
11749
11750/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
11751/// `Value::Interval { months, micros }` per the caller's guard;
11752/// each iteration adds the interval via `apply_binary_interval`
11753/// so month-shifting handles short-month rollover (PG semantics).
11754fn generate_series_timestamps(
11755    start: i64,
11756    stop: i64,
11757    step: Value,
11758    cancel: &CancelToken<'_>,
11759) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11760    let (months, days, micros) = match &step {
11761        Value::Interval {
11762            months,
11763            days,
11764            micros,
11765        } => (*months, *days, *micros),
11766        _ => unreachable!("caller guards step.is_interval"),
11767    };
11768    if months == 0 && days == 0 && micros == 0 {
11769        return Err(EngineError::Unsupported(
11770            "generate_series(): INTERVAL step cannot be zero".into(),
11771        ));
11772    }
11773    let ascending = months > 0 || days > 0 || micros > 0;
11774    let mut out = alloc::vec::Vec::new();
11775    let mut cur = Value::Timestamp(start);
11776    const MAX_ROWS: usize = 10_000_000;
11777    loop {
11778        cancel.check()?;
11779        let cur_t = match cur {
11780            Value::Timestamp(t) => t,
11781            _ => unreachable!("loop invariant: cur is Timestamp"),
11782        };
11783        if ascending && cur_t > stop {
11784            break;
11785        }
11786        if !ascending && cur_t < stop {
11787            break;
11788        }
11789        out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
11790        if out.len() > MAX_ROWS {
11791            return Err(EngineError::Unsupported(alloc::format!(
11792                "generate_series(): exceeded {MAX_ROWS} rows; \
11793                 narrow start/stop or use a larger step"
11794            )));
11795        }
11796        let next = eval::apply_binary_interval(
11797            spg_sql::ast::BinOp::Add,
11798            &cur,
11799            &Value::Interval {
11800                months,
11801                days,
11802                micros,
11803            },
11804        )
11805        .map_err(EngineError::Eval)?;
11806        cur = match next {
11807            Some(v) => v,
11808            None => break,
11809        };
11810    }
11811    Ok(out)
11812}
11813
11814/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
11815/// WITH TIES` requires an `ORDER BY`. Without one, there's no
11816/// way to identify "ties" deterministically, so PG errors at
11817/// plan time. SPG mirrors that surface so the same DDL / app
11818/// behaviour holds on cutover.
11819fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
11820    if stmt.limit_with_ties && stmt.order_by.is_empty() {
11821        return Err(EngineError::Unsupported(alloc::string::String::from(
11822            "WITH TIES cannot be specified without ORDER BY clause",
11823        )));
11824    }
11825    Ok(())
11826}
11827
11828/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
11829/// (case-insensitive). Used by `exec_select_cancel`'s
11830/// projection loop to detect Set-Returning-Function rows that
11831/// need per-row expansion. Only the top-level call counts —
11832/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
11833/// projection's perspective; it would surface as an "unknown
11834/// function" mismatch downstream, which is what we want
11835/// (multi-SRF / nested SRF is documented carve-out for v7.19).
11836fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
11837    top_level_srf_kind(expr).is_some()
11838}
11839
11840/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
11841/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
11842/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
11843/// source row.
11844#[derive(Clone, Copy, PartialEq, Eq)]
11845pub(crate) enum SrfKind {
11846    Unnest,
11847    /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
11848    /// list. It used to be handled ONLY by the parser's lift into FROM, so a
11849    /// second one in the same list came back as "unknown function".
11850    GenerateSeries,
11851    GenerateSubscripts,
11852    /// `_text` variants unwrap scalars to their lexeme; the plain forms render
11853    /// every value as compact JSON text.
11854    ArrayElements {
11855        as_text: bool,
11856    },
11857    PathQuery,
11858    RegexpMatches,
11859    Each {
11860        as_text: bool,
11861    },
11862    ObjectKeys,
11863}
11864
11865/// Case-insensitive match against any of `names`.
11866fn name_is(name: &str, names: &[&str]) -> bool {
11867    names.iter().any(|n| name.eq_ignore_ascii_case(n))
11868}
11869
11870pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
11871    let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
11872        return None;
11873    };
11874    let n = args.len();
11875    // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
11876    // SELECT list (it returned an array there before) and shares the unnest
11877    // expansion machinery.
11878    if n == 1 && name.eq_ignore_ascii_case("unnest") {
11879        return Some(SrfKind::Unnest);
11880    }
11881    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
11882        return Some(SrfKind::GenerateSeries);
11883    }
11884    if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
11885        return Some(SrfKind::GenerateSubscripts);
11886    }
11887    // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
11888    // per element / match in the SELECT list; they collapsed to a single row
11889    // (a TextArray, or an "unknown function" error for `each`) before.
11890    if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
11891        return Some(SrfKind::ArrayElements { as_text: false });
11892    }
11893    if n == 1
11894        && name_is(
11895            name,
11896            &["jsonb_array_elements_text", "json_array_elements_text"],
11897        )
11898    {
11899        return Some(SrfKind::ArrayElements { as_text: true });
11900    }
11901    // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
11902    if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
11903        return Some(SrfKind::PathQuery);
11904    }
11905    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
11906        return Some(SrfKind::RegexpMatches);
11907    }
11908    if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
11909        return Some(SrfKind::Each { as_text: false });
11910    }
11911    if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
11912        return Some(SrfKind::Each { as_text: true });
11913    }
11914    if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
11915        return Some(SrfKind::ObjectKeys);
11916    }
11917    None
11918}
11919
11920/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
11921/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
11922/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
11923/// rows, as in PG).
11924pub(crate) fn top_level_srf_output(
11925    expr: &spg_sql::ast::Expr,
11926    row: &Row<'static>,
11927    ctx: &EvalContext<'_>,
11928) -> Result<Vec<Value<'static>>, EngineError> {
11929    let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
11930        (top_level_srf_kind(expr), expr)
11931    else {
11932        return Err(EngineError::Unsupported(
11933            "expected a SELECT-list SRF call".into(),
11934        ));
11935    };
11936    match kind {
11937        SrfKind::Unnest => {
11938            // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
11939            // the elements DIRECTLY: the old path built the whole
11940            // Value::Array (one eval + a clone per element) only for
11941            // array_value_to_elements to clone every element back out.
11942            // Any other argument shape (a column, a function result)
11943            // keeps the build-then-split path.
11944            if let spg_sql::ast::Expr::Array(items) = &args[0] {
11945                return items
11946                    .iter()
11947                    .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
11948                    .collect();
11949            }
11950            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11951            array_value_to_elements(&arr)
11952        }
11953        SrfKind::GenerateSeries => {
11954            // v7.39 (read01 round 96) — evaluate the args against the actual
11955            // row, then hand off to the shared core so the numeric and
11956            // timestamp/timestamptz overloads work here too (this arm used to
11957            // handle only integers, silently NULLing a temporal/numeric series
11958            // when it shared a target list with another SRF).
11959            let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
11960            for a in args {
11961                arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
11962            }
11963            let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
11964            Ok(rows
11965                .into_iter()
11966                .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
11967                .collect())
11968        }
11969        SrfKind::GenerateSubscripts => {
11970            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11971            let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
11972            if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
11973                return Ok(Vec::new());
11974            }
11975            let len = array_value_to_elements(&arr)?.len();
11976            Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
11977        }
11978        // One Value per array element (`_text` → text / SQL NULL, plain → the
11979        // element's compact JSON text) — the element list the FROM-clause form
11980        // materialises.
11981        SrfKind::ArrayElements { as_text } => {
11982            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11983            if matches!(arg, Value::Null) {
11984                return Ok(Vec::new());
11985            }
11986            let items =
11987                crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
11988            Ok(items
11989                .into_iter()
11990                .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
11991                .collect())
11992        }
11993        // The scalar form already yields a TextArray of the keys (or errors on
11994        // a non-object, like PG); expand it into rows.
11995        SrfKind::ObjectKeys => {
11996            let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
11997            array_value_to_elements(&v)
11998        }
11999        // One row per match, each a text[] of the pattern's capture groups.
12000        SrfKind::RegexpMatches => {
12001            let vals: Vec<Value<'static>> = args
12002                .iter()
12003                .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
12004                .collect::<Result<_, _>>()?;
12005            crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
12006        }
12007        // One composite `(key, value)` row per object member (plain → jsonb
12008        // value, `_text` → text / SQL NULL).
12009        SrfKind::Each { as_text } => {
12010            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12011            if matches!(arg, Value::Null) {
12012                return Ok(Vec::new());
12013            }
12014            let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
12015            Ok(pairs
12016                .into_iter()
12017                .map(|(k, v)| {
12018                    let val = if as_text {
12019                        v.map(Value::text).unwrap_or(Value::Null)
12020                    } else {
12021                        v.map(Value::json).unwrap_or(Value::Null)
12022                    };
12023                    Value::Composite(alloc::vec![
12024                        ("key".to_string(), Value::text(k)),
12025                        ("value".to_string(), val),
12026                    ])
12027                })
12028                .collect())
12029        }
12030        // One Value per matched JSON value.
12031        SrfKind::PathQuery => {
12032            let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12033            let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
12034            // v7.39 — optional vars document (3rd arg).
12035            let vars = match args.get(2) {
12036                Some(a) => {
12037                    let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
12038                    crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
12039                }
12040                None => None,
12041            };
12042            match crate::json::path_query_vars(&doc, &path, vars.as_ref())
12043                .map_err(EngineError::Eval)?
12044            {
12045                Value::Null => Ok(Vec::new()),
12046                Value::TextArray(items) => Ok(items
12047                    .into_iter()
12048                    .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
12049                    .collect()),
12050                other => Ok(alloc::vec![other]),
12051            }
12052        }
12053    }
12054}
12055
12056/// v7.19 P5 — turn an array-typed `Value` into the element list
12057/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
12058/// = (no rows)`). Non-array values fall through to a type-mismatch
12059/// error.
12060pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
12061    // v7.39 (round 236) — PG unnests a multidimensional array into its
12062    // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
12063    // rows). SPG stores 2-D arrays as their own variants, which fell
12064    // through to the type-mismatch arm below.
12065    if let Some(flat) = crate::eval::values::flatten_2d(v) {
12066        return array_value_to_elements(&flat);
12067    }
12068    match v {
12069        Value::Null => Ok(Vec::new()),
12070        Value::TextArray(items) => Ok(items
12071            .iter()
12072            .map(|opt| {
12073                opt.as_ref()
12074                    .map(|s| Value::text(s.clone()))
12075                    .unwrap_or(Value::Null)
12076            })
12077            .collect()),
12078        Value::IntArray(items) => Ok(items
12079            .iter()
12080            .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
12081            .collect()),
12082        Value::BigIntArray(items) => Ok(items
12083            .iter()
12084            .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
12085            .collect()),
12086        // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
12087        // range per canonical span.
12088        Value::Multirange { kind, ranges } => Ok(ranges
12089            .iter()
12090            .map(|s| Value::Range {
12091                kind: *kind,
12092                lower: s.lower.clone(),
12093                upper: s.upper.clone(),
12094                lower_inc: s.lower_inc,
12095                upper_inc: s.upper_inc,
12096                empty: false,
12097            })
12098            .collect()),
12099        other => Err(EngineError::Eval(EvalError::TypeMismatch {
12100            detail: alloc::format!(
12101                "unnest() expects an array argument, got {}",
12102                crate::conversions::pg_type_name_for_error_opt(other.data_type())
12103            ),
12104        })),
12105    }
12106}
12107
12108impl Engine {
12109    /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
12110    /// the SELECT's FROM / JOIN graph, re-parse each view's body
12111    /// source, and prepend it as a synthetic CTE on the
12112    /// returned SelectStatement. Returns `None` when no view
12113    /// references are found (caller proceeds with the original
12114    /// statement); returns `Some(rewritten)` otherwise (caller
12115    /// re-runs exec_select_cancel on the rewritten form so the
12116    /// regular CTE materialiser handles it).
12117    fn expand_views_in_select(
12118        &self,
12119        stmt: &SelectStatement,
12120    ) -> Result<Option<SelectStatement>, EngineError> {
12121        let cat = self.active_catalog();
12122        let mut referenced: Vec<String> = Vec::new();
12123        if let Some(from) = &stmt.from {
12124            collect_view_refs(&from.primary, cat, &mut referenced);
12125            for j in &from.joins {
12126                collect_view_refs(&j.table, cat, &mut referenced);
12127            }
12128        }
12129        // Don't expand a view name that's already shadowed by a
12130        // CTE on the same SELECT — the CTE wins per PG.
12131        referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
12132        if referenced.is_empty() {
12133            return Ok(None);
12134        }
12135        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
12136        for name in &referenced {
12137            let view = cat.view(name).ok_or_else(|| {
12138                EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
12139                    "view {name:?} disappeared mid-expansion"
12140                )))
12141            })?;
12142            let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
12143                EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
12144            })?;
12145            let Statement::Select(body) = parsed else {
12146                return Err(EngineError::Unsupported(alloc::format!(
12147                    "view {name:?} body is not a SELECT (catalog corruption)"
12148                )));
12149            };
12150            new_ctes.push(spg_sql::ast::Cte {
12151                name: name.clone(),
12152                body: spg_sql::ast::CteBody::Select(body),
12153                recursive: false,
12154                column_overrides: view.columns.clone(),
12155                search: None,
12156                cycle: None,
12157            });
12158        }
12159        let mut out = stmt.clone();
12160        // Prepend so view CTEs are visible to caller-supplied CTEs.
12161        new_ctes.extend(out.ctes);
12162        out.ctes = new_ctes;
12163        Ok(Some(out))
12164    }
12165
12166    /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
12167    /// any partition-parent table, rewrite the SELECT so each parent
12168    /// reference resolves to a CTE whose body is a `UNION ALL` over the
12169    /// children that pass the WHERE-derived partition-key range. Returns
12170    /// `None`(no rewrite needed)when no parent is referenced or all
12171    /// references are shadowed by a same-name CTE.
12172    ///
12173    /// Pruning vocabulary at v7.37.6-B:
12174    ///   * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
12175    ///     and `<key> BETWEEN literal AND literal`.
12176    ///   * Anything outside that(OR / nested IN / function call on the
12177    ///     key)defaults to "no pruning" — every child + DEFAULT lands
12178    ///     in the UNION. Correctness is preserved; only the plan size
12179    ///     widens.
12180    fn expand_partition_parents_in_select(
12181        &self,
12182        stmt: &SelectStatement,
12183    ) -> Result<Option<SelectStatement>, EngineError> {
12184        let cat = self.active_catalog();
12185        let Some(from) = &stmt.from else {
12186            return Ok(None);
12187        };
12188        let mut parent_refs: Vec<String> = Vec::new();
12189        collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
12190        for j in &from.joins {
12191            collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
12192        }
12193        // Drop names shadowed by a CTE on the same SELECT(PG semantics
12194        // — same as view expansion above).
12195        parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
12196        if parent_refs.is_empty() {
12197            return Ok(None);
12198        }
12199        // Synthesise a CTE name per parent so the existing
12200        // "CTE shadows a real table" guard doesn't fire (the parent
12201        // IS a real table in the catalog, unlike VIEW expansion's
12202        // case). The FROM-clause TableRef walker below rewrites
12203        // every parent reference to point at the synthetic CTE.
12204        let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
12205        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
12206        let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
12207        for parent_name in &parent_refs {
12208            // No children = no rewrite. The parent itself is a real
12209            // (empty-rows) table — the regular FROM-resolution path
12210            // will scan it and return 0 rows, matching the
12211            // "partition parent with no children" plan. Skipping the
12212            // CTE here also avoids `SELECT * FROM parent` re-entering
12213            // this rewrite on the synthetic body (infinite recursion).
12214            let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
12215                continue;
12216            };
12217            new_ctes.push(spg_sql::ast::Cte {
12218                name: synth_name(parent_name),
12219                body: spg_sql::ast::CteBody::Select(body),
12220                recursive: false,
12221                column_overrides: Vec::new(),
12222                search: None,
12223                cycle: None,
12224            });
12225            expanded_parents.push(parent_name.clone());
12226        }
12227        if expanded_parents.is_empty() {
12228            return Ok(None);
12229        }
12230        let mut out = stmt.clone();
12231        if let Some(from) = out.from.as_mut() {
12232            rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
12233            for j in &mut from.joins {
12234                rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
12235            }
12236        }
12237        new_ctes.extend(out.ctes);
12238        out.ctes = new_ctes;
12239        Ok(Some(out))
12240    }
12241
12242    /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
12243    /// Children include every overlap-hit `Range` plus(always)the
12244    /// `Default` child(if any). Returns `Ok(None)` when no children
12245    /// would survive — caller skips the CTE injection and lets the
12246    /// parent fall through to the regular(empty-rows)scan path,
12247    /// avoiding the infinite recursion that an empty-body CTE
12248    /// referencing the parent name would trigger.
12249    /// v7.37.16 (16.10) — public helper invoked from explain.rs to
12250    /// surface "which children survive the WHERE-clause prune" in
12251    /// EXPLAIN output. Returns `None` when `parent_name` isn't
12252    /// actually a partition parent; otherwise returns the list of
12253    /// children the planner would scan (same algorithm as
12254    /// [`Self::build_partition_parent_union_body`] but without the
12255    /// SQL re-parse).
12256    /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
12257    /// expression (the PG-shaped EXPLAIN's scan builder has no full
12258    /// SelectStatement in hand). Wraps the original by synthesising a
12259    /// minimal statement carrying just the predicate.
12260    pub(crate) fn explain_partition_kept_children_by_where(
12261        &self,
12262        parent_name: &str,
12263        where_: Option<&spg_sql::ast::Expr>,
12264    ) -> Option<Vec<alloc::string::String>> {
12265        let mut synth = SelectStatement::default();
12266        synth.where_ = where_.cloned();
12267        self.explain_partition_kept_children(parent_name, &synth)
12268    }
12269
12270    pub(crate) fn explain_partition_kept_children(
12271        &self,
12272        parent_name: &str,
12273        outer: &SelectStatement,
12274    ) -> Option<Vec<alloc::string::String>> {
12275        use spg_storage::PartitionRole;
12276        let cat = self.active_catalog();
12277        let parent = cat.get(parent_name)?;
12278        let (key_position, parent_kind) = match &parent.schema().partition_role {
12279            Some(PartitionRole::Parent {
12280                key_column_positions,
12281                kind,
12282                ..
12283            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
12284            _ => return None,
12285        };
12286        let key_col_name = parent.schema().columns[key_position].name.clone();
12287        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
12288            Some(expr) => extract_key_range(expr, &key_col_name),
12289            None => (None, None),
12290        };
12291        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
12292            Some(expr) => extract_key_eq_value(expr, &key_col_name),
12293            None => None,
12294        };
12295        let children = crate::partition::children_of_parent(cat, parent_name);
12296        let mut kept: Vec<alloc::string::String> = Vec::new();
12297        let mut default_child: Option<alloc::string::String> = None;
12298        for child_name in &children {
12299            let Some(child) = cat.get(child_name) else {
12300                continue;
12301            };
12302            match &child.schema().partition_role {
12303                Some(PartitionRole::Range { lower, upper, .. }) => {
12304                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
12305                        kept.push(child_name.clone());
12306                    }
12307                }
12308                Some(PartitionRole::List { values, .. }) => match &eq_value {
12309                    Some(v) => {
12310                        if values.iter().any(|b| b.equals_value(v)) {
12311                            kept.push(child_name.clone());
12312                        }
12313                    }
12314                    None => kept.push(child_name.clone()),
12315                },
12316                Some(PartitionRole::Hash {
12317                    modulus, remainder, ..
12318                }) => match &eq_value {
12319                    Some(v) => {
12320                        let h = crate::partition::pg_compatible_hash(v);
12321                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
12322                            kept.push(child_name.clone());
12323                        }
12324                    }
12325                    None => kept.push(child_name.clone()),
12326                },
12327                Some(PartitionRole::Default { .. }) => {
12328                    default_child = Some(child_name.clone());
12329                }
12330                _ => {}
12331            }
12332        }
12333        let _ = parent_kind;
12334        if let Some(d) = default_child {
12335            if kept.is_empty() || eq_value.is_none() {
12336                kept.push(d);
12337            }
12338        }
12339        Some(kept)
12340    }
12341
12342    fn build_partition_parent_union_body(
12343        &self,
12344        parent_name: &str,
12345        outer: &SelectStatement,
12346    ) -> Result<Option<SelectStatement>, EngineError> {
12347        use spg_storage::PartitionRole;
12348        let cat = self.active_catalog();
12349        let parent = cat.get(parent_name).ok_or_else(|| {
12350            EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
12351                "partition parent {parent_name:?} disappeared mid-expansion"
12352            )))
12353        })?;
12354        let (key_position, parent_kind) = match &parent.schema().partition_role {
12355            Some(PartitionRole::Parent {
12356                key_column_positions,
12357                kind,
12358                ..
12359            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
12360            // v7.39 (round 645) — an INHERITANCE parent, which has no
12361            // role of its own: the relationship is recorded only in the
12362            // children. Three things differ from a partition parent and
12363            // all three are in this body.
12364            //
12365            //   * The parent HOLDS ROWS, so it is a term of the union —
12366            //     `FROM ONLY`, or expanding it would recurse.
12367            //   * There is no partition key, so there is nothing to
12368            //     prune: every child is a term.
12369            //   * A child may declare columns of its own, so the terms
12370            //     name the PARENT's columns rather than `*`. PG's
12371            //     `SELECT * FROM parent` returns the parent's shape.
12372            //
12373            // Answered from this match rather than a branch before it —
12374            // round 644 measured what an extra early return beside an
12375            // existing test costs in this file.
12376            _ if crate::partition::has_inheritance_children(cat, parent_name) => {
12377                let cols = parent
12378                    .schema()
12379                    .columns
12380                    .iter()
12381                    .map(|c| quote_ident_for_sql(&c.name))
12382                    .collect::<Vec<_>>()
12383                    .join(", ");
12384                let carry_sys = references_ctid(outer);
12385                let sys = if carry_sys {
12386                    let mut t = alloc::string::String::new();
12387                    for s in SYSTEM_COLUMNS {
12388                        t.push_str(", ");
12389                        t.push_str(s);
12390                    }
12391                    t
12392                } else {
12393                    alloc::string::String::new()
12394                };
12395                let mut body = alloc::format!(
12396                    "SELECT {cols}{sys} FROM ONLY {}",
12397                    quote_ident_for_sql(parent_name)
12398                );
12399                for child in crate::partition::children_of_parent(cat, parent_name) {
12400                    body.push_str(&alloc::format!(
12401                        " UNION ALL SELECT {cols}{sys} FROM {}",
12402                        quote_ident_for_sql(&child)
12403                    ));
12404                }
12405                return parse_select_or_corrupt(&body).map(Some);
12406            }
12407            _ => {
12408                return Err(EngineError::Unsupported(alloc::format!(
12409                    "partition expansion: {parent_name:?} is not a parent"
12410                )));
12411            }
12412        };
12413        let key_col_name = parent.schema().columns[key_position].name.clone();
12414        // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
12415        // off the WHERE; for LIST / HASH we extract a single `=`
12416        // literal (and the rest of the planner falls back to "keep
12417        // every child" — same conservative path as 16.1/16.2).
12418        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
12419            Some(expr) => extract_key_range(expr, &key_col_name),
12420            None => (None, None),
12421        };
12422        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
12423            Some(expr) => extract_key_eq_value(expr, &key_col_name),
12424            None => None,
12425        };
12426        let children = crate::partition::children_of_parent(cat, parent_name);
12427        let mut kept: Vec<String> = Vec::new();
12428        let mut default_child: Option<String> = None;
12429        // First pass — apply per-strategy gates, defer DEFAULT until
12430        // we know whether some non-DEFAULT child matched.
12431        for child_name in &children {
12432            let Some(child) = cat.get(child_name) else {
12433                continue;
12434            };
12435            match &child.schema().partition_role {
12436                Some(PartitionRole::Range { lower, upper, .. }) => {
12437                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
12438                        kept.push(child_name.clone());
12439                    }
12440                }
12441                // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
12442                // = <lit>`, only the child whose values contain that
12443                // literal survives. Otherwise (no equality predicate
12444                // or planner couldn't extract one) keep the child
12445                // conservatively.
12446                Some(PartitionRole::List { values, .. }) => match &eq_value {
12447                    Some(v) => {
12448                        if values.iter().any(|b| b.equals_value(v)) {
12449                            kept.push(child_name.clone());
12450                        }
12451                    }
12452                    None => kept.push(child_name.clone()),
12453                },
12454                // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
12455                // we know the residue class deterministically, so
12456                // only the matching REMAINDER child survives.
12457                Some(PartitionRole::Hash {
12458                    modulus, remainder, ..
12459                }) => match &eq_value {
12460                    Some(v) => {
12461                        let h = crate::partition::pg_compatible_hash(v);
12462                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
12463                            kept.push(child_name.clone());
12464                        }
12465                    }
12466                    None => kept.push(child_name.clone()),
12467                },
12468                Some(PartitionRole::Default { .. }) => {
12469                    default_child = Some(child_name.clone());
12470                }
12471                _ => {}
12472            }
12473        }
12474        // PG-style DEFAULT semantics: the DEFAULT child must be
12475        // scanned iff some row could fall outside every concrete
12476        // child's bound predicate. We approximate that as "no
12477        // concrete child matched" (== full prune) — strictly
12478        // conservative for LIST / HASH (DEFAULT also catches rows
12479        // outside the union of value-sets / residues), and matches
12480        // PG for the equality case where we *do* know the routing
12481        // outcome.
12482        let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
12483        if let Some(d) = default_child {
12484            if kept.is_empty() {
12485                kept.push(d);
12486            } else if eq_value.is_none() {
12487                // Without an equality literal, the DEFAULT child may
12488                // still hold matching rows (e.g. LIKE on TEXT keys
12489                // for which a LIST partition exists). Keep it.
12490                kept.push(d);
12491            }
12492        }
12493        // Build the UNION ALL body text and re-parse — keeps the
12494        // rewrite expressible in surface SQL so the engine's existing
12495        // parser path handles the AST shape uniformly.
12496        if kept.is_empty() {
12497            // No children survive — caller falls back to scanning the
12498            // (empty) parent table. Returning None here is what
12499            // prevents the synthetic CTE from referring back to the
12500            // parent name and re-entering this rewrite pass.
12501            let _ = parent_name;
12502            return Ok(None);
12503        }
12504        // v7.39 (round 622, S05a) — the system columns of the CHILD the row
12505        // actually lives in.
12506        //
12507        // The parent is read through a synthetic CTE, so a `tableoid` on it
12508        // resolved against that CTE: every row of every child reported
12509        // `__spg_partition_pm`, an internal name no user ever typed, where
12510        // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
12511        // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
12512        // one asks "which partition is this row in", answering 0 rows where
12513        // PG answers 1. `ctid` had the same shape: it numbered the CTE's
12514        // output, so rows in different children got distinct ctids instead
12515        // of each child's own physical position.
12516        //
12517        // Naming them in the term is what carries them: the child scan
12518        // materialises its own six because the statement now references
12519        // them, and they land in SYSTEM_COLUMNS order right after the user
12520        // columns — the exact layout the positional `*` skip already
12521        // expects. Only done when the outer statement asks for one, so a
12522        // plain `SELECT * FROM parent` scans exactly what it scanned.
12523        let carry_sys = references_ctid(outer);
12524        let mut body = alloc::string::String::new();
12525        for (i, child_name) in kept.iter().enumerate() {
12526            if i > 0 {
12527                body.push_str(" UNION ALL ");
12528            }
12529            body.push_str("SELECT *");
12530            if carry_sys {
12531                for sys in SYSTEM_COLUMNS {
12532                    body.push_str(", ");
12533                    body.push_str(sys);
12534                }
12535            }
12536            body.push_str(" FROM ");
12537            body.push_str(&quote_ident_for_sql(child_name));
12538        }
12539        parse_select_or_corrupt(&body).map(Some)
12540    }
12541}
12542
12543/// Rewrite a `TableRef` pointing at a partition parent so it
12544/// references the synthetic CTE created by the expansion. If the
12545/// original ref had no alias, preserve the parent name as an alias
12546/// so column references like `events_partitioned.received_at`
12547/// keep resolving.
12548fn rewrite_partition_parent_table_ref(
12549    t: &mut spg_sql::ast::TableRef,
12550    parents: &[alloc::string::String],
12551    synth_name: &impl Fn(&str) -> alloc::string::String,
12552) {
12553    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
12554        return;
12555    }
12556    // v7.39 (round 644) — an ONLY reference stays pointed at the parent
12557    // itself. The rewrite is keyed on the NAME, so in
12558    // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
12559    // parent list and this then rewrote BOTH — including the one that
12560    // asked not to descend. PG answers 0 for that join; SPG answered 2.
12561    // Folded into the existing test — see the note in
12562    // `collect_partition_parent_refs` for what a separate one cost.
12563    if t.only || !parents.iter().any(|p| p == &t.name) {
12564        return;
12565    }
12566    if t.alias.is_none() {
12567        t.alias = Some(t.name.clone());
12568    }
12569    t.name = synth_name(&t.name);
12570}
12571
12572/// Walk a `TableRef` and push its `name` if it resolves to a partition
12573/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
12574/// `generate_series_args` references — those aren't catalog tables.
12575fn collect_partition_parent_refs(
12576    t: &spg_sql::ast::TableRef,
12577    cat: &spg_storage::Catalog,
12578    out: &mut Vec<alloc::string::String>,
12579) {
12580    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
12581        return;
12582    }
12583    // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
12584    // The keyword used to be absorbed at parse time, so this fanned out
12585    // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
12586    // answered 2 where PG answers 0.
12587    //
12588    // Folded into the existing test rather than given an early return of
12589    // its own: as two extra lines in this function's body it cost
12590    // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
12591    // outside the panel. Rounds 641 and 643 met the same wall from the
12592    // other two directions — adding to a hot function and taking away
12593    // from a cold one. What goes in a body near the row loop is a
12594    // codegen decision whatever its shape.
12595    if !t.only && crate::partition::has_children(cat, &t.name) {
12596        out.push(t.name.clone());
12597    }
12598}
12599
12600/// v7.37.6-B partition-key range derived from a WHERE expression.
12601/// `i64` microseconds since epoch with the same sign convention as
12602/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
12603/// / `=`),`false` ⇒ exclusive(`>` / `<`).
12604#[derive(Debug, Clone, Copy)]
12605pub(crate) struct PartitionFilterBound {
12606    pub micros: i64,
12607    pub inclusive: bool,
12608}
12609
12610/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
12611/// shapes; tighten the running lo / hi as we go. Anything outside that
12612/// (OR / nested calls / non-key columns)is ignored — caller treats
12613/// `None` as "no constraint on that side."
12614fn extract_key_range(
12615    expr: &spg_sql::ast::Expr,
12616    key_col: &str,
12617) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
12618    let mut lo: Option<PartitionFilterBound> = None;
12619    let mut hi: Option<PartitionFilterBound> = None;
12620    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12621    while let Some(e) = stack.pop() {
12622        match e {
12623            spg_sql::ast::Expr::Binary {
12624                lhs,
12625                op: spg_sql::ast::BinOp::And,
12626                rhs,
12627            } => {
12628                stack.push(lhs);
12629                stack.push(rhs);
12630            }
12631            // BETWEEN is desugared at parse time into `lhs >= low AND
12632            // lhs <= high`, so it lands here as two regular Binary
12633            // arms via the AND walker above.
12634            spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
12635                let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
12636                    (Some(lhs.as_ref()), rhs.as_ref(), false)
12637                } else if is_column_ref(rhs, key_col) {
12638                    (Some(rhs.as_ref()), lhs.as_ref(), true)
12639                } else {
12640                    (None, lhs.as_ref(), false)
12641                };
12642                if col_ref.is_none() {
12643                    continue;
12644                }
12645                let Some(lit) = literal_to_micros(lit_side) else {
12646                    continue;
12647                };
12648                use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
12649                let effective_op = if swapped {
12650                    match op {
12651                        Lt => Gt,
12652                        LtEq => GtEq,
12653                        Gt => Lt,
12654                        GtEq => LtEq,
12655                        other => *other,
12656                    }
12657                } else {
12658                    *op
12659                };
12660                match effective_op {
12661                    Eq => {
12662                        tighten_lo(
12663                            &mut lo,
12664                            PartitionFilterBound {
12665                                micros: lit,
12666                                inclusive: true,
12667                            },
12668                        );
12669                        tighten_hi(
12670                            &mut hi,
12671                            PartitionFilterBound {
12672                                micros: lit,
12673                                inclusive: true,
12674                            },
12675                        );
12676                    }
12677                    GtEq => {
12678                        tighten_lo(
12679                            &mut lo,
12680                            PartitionFilterBound {
12681                                micros: lit,
12682                                inclusive: true,
12683                            },
12684                        );
12685                    }
12686                    Gt => {
12687                        tighten_lo(
12688                            &mut lo,
12689                            PartitionFilterBound {
12690                                micros: lit,
12691                                inclusive: false,
12692                            },
12693                        );
12694                    }
12695                    LtEq => {
12696                        tighten_hi(
12697                            &mut hi,
12698                            PartitionFilterBound {
12699                                micros: lit,
12700                                inclusive: true,
12701                            },
12702                        );
12703                    }
12704                    Lt => {
12705                        tighten_hi(
12706                            &mut hi,
12707                            PartitionFilterBound {
12708                                micros: lit,
12709                                inclusive: false,
12710                            },
12711                        );
12712                    }
12713                    _ => {}
12714                }
12715            }
12716            _ => {}
12717        }
12718    }
12719    (lo, hi)
12720}
12721
12722fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12723    match slot {
12724        None => *slot = Some(new),
12725        Some(cur) => {
12726            if new.micros > cur.micros
12727                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12728            {
12729                *slot = Some(new);
12730            }
12731        }
12732    }
12733}
12734
12735fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12736    match slot {
12737        None => *slot = Some(new),
12738        Some(cur) => {
12739            if new.micros < cur.micros
12740                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12741            {
12742                *slot = Some(new);
12743            }
12744        }
12745    }
12746}
12747
12748fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
12749    if let spg_sql::ast::Expr::Column(c) = e {
12750        c.name.eq_ignore_ascii_case(key_col)
12751    } else {
12752        false
12753    }
12754}
12755
12756/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
12757/// `key_col = <literal>` predicate out for LIST/HASH partition
12758/// pruning. Returns `None` when no equality literal can be lifted
12759/// (planner then keeps every child — correctness preserved). The
12760/// returned `Value<'static>` is an owned coercion so the caller can
12761/// outlive any AST node it was extracted from.
12762pub(crate) fn extract_key_eq_value(
12763    expr: &spg_sql::ast::Expr,
12764    key_col: &str,
12765) -> Option<spg_storage::Value<'static>> {
12766    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12767    while let Some(e) = stack.pop() {
12768        match e {
12769            spg_sql::ast::Expr::Binary {
12770                lhs,
12771                op: spg_sql::ast::BinOp::And,
12772                rhs,
12773            } => {
12774                stack.push(lhs);
12775                stack.push(rhs);
12776            }
12777            spg_sql::ast::Expr::Binary {
12778                lhs,
12779                op: spg_sql::ast::BinOp::Eq,
12780                rhs,
12781            } => {
12782                let lit_side = if is_column_ref(lhs, key_col) {
12783                    rhs.as_ref()
12784                } else if is_column_ref(rhs, key_col) {
12785                    lhs.as_ref()
12786                } else {
12787                    continue;
12788                };
12789                let cloned = lit_side.clone();
12790                let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
12791                    continue;
12792                };
12793                // Coerce to an owned Value<'static> so the caller
12794                // can hold it past the WHERE expression's lifetime.
12795                let owned: spg_storage::Value<'static> = match v {
12796                    spg_storage::Value::Text(s) => {
12797                        spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
12798                    }
12799                    spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
12800                    spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
12801                    spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
12802                    spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
12803                    spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
12804                    spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
12805                    spg_storage::Value::Null => spg_storage::Value::Null,
12806                    // Anything else (Vector / Json / Bytes / Numeric /
12807                    // arrays / interval / …) isn't a current partition
12808                    // key type; skip without pruning.
12809                    _ => continue,
12810                };
12811                return Some(owned);
12812            }
12813            _ => {}
12814        }
12815    }
12816    None
12817}
12818
12819/// Coerce a literal Expr(after the parser folded sequence calls etc.)
12820/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
12821/// pruning and routing agree on the literal vocabulary. Returns
12822/// `None` when the literal isn't recognised(planner then skips
12823/// pruning on that branch — correctness preserved).
12824fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
12825    let cloned = e.clone();
12826    let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
12827    match value {
12828        spg_storage::Value::Timestamp(m) => Some(m),
12829        spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
12830        spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
12831        _ => None,
12832    }
12833}
12834
12835/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
12836/// satisfying the WHERE-derived filter range. PG-style half-open:
12837/// child upper exclusive. Filter inclusivity is honoured per-bound.
12838fn range_satisfies_filter(
12839    range_lo: &spg_storage::PartitionBound,
12840    range_hi: &spg_storage::PartitionBound,
12841    filter_lo: Option<&PartitionFilterBound>,
12842    filter_hi: Option<&PartitionFilterBound>,
12843) -> bool {
12844    use spg_storage::PartitionBound;
12845    // For each filter side, reject children that can't host any row
12846    // matching the predicate.
12847    if let Some(lo) = filter_lo {
12848        // child upper bound vs filter lower:
12849        //   if filter is x >= L, child rejects iff child.hi <= L
12850        //   if filter is x  > L, child rejects iff child.hi <= L
12851        //   (child.hi exclusive, so equality with L still rejects)
12852        match range_hi {
12853            PartitionBound::MinValue => return false,
12854            PartitionBound::MaxValue => {}
12855            PartitionBound::TimestampTz(hi) => {
12856                if *hi <= lo.micros {
12857                    return false;
12858                }
12859            }
12860            // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
12861            // matched against TIMESTAMPTZ filters here; keep child
12862            // (conservative: don't prune).
12863            PartitionBound::BigInt(_)
12864            | PartitionBound::Int(_)
12865            | PartitionBound::SmallInt(_)
12866            | PartitionBound::Date(_)
12867            | PartitionBound::Text(_) => {}
12868        }
12869    }
12870    if let Some(hi) = filter_hi {
12871        // child lower bound vs filter upper:
12872        //   if filter is x <= U, child rejects iff child.lo > U
12873        //   if filter is x  < U, child rejects iff child.lo >= U
12874        match range_lo {
12875            PartitionBound::MaxValue => return false,
12876            PartitionBound::MinValue => {}
12877            PartitionBound::TimestampTz(lo) => {
12878                let rejects = if hi.inclusive {
12879                    *lo > hi.micros
12880                } else {
12881                    *lo >= hi.micros
12882                };
12883                if rejects {
12884                    return false;
12885                }
12886            }
12887            PartitionBound::BigInt(_)
12888            | PartitionBound::Int(_)
12889            | PartitionBound::SmallInt(_)
12890            | PartitionBound::Date(_)
12891            | PartitionBound::Text(_) => {}
12892        }
12893    }
12894    true
12895}
12896
12897fn quote_ident_for_sql(name: &str) -> alloc::string::String {
12898    // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
12899    // identifier, otherwise quoted). Conservative: always quote so
12900    // children with reserved names round-trip safely through the
12901    // CTE-body parse.
12902    let mut out = alloc::string::String::with_capacity(name.len() + 2);
12903    out.push('"');
12904    for c in name.chars() {
12905        if c == '"' {
12906            out.push('"');
12907        }
12908        out.push(c);
12909    }
12910    out.push('"');
12911    out
12912}
12913
12914fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
12915    let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
12916        EngineError::Unsupported(alloc::format!(
12917            "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
12918        ))
12919    })?;
12920    let Statement::Select(body) = parsed else {
12921        return Err(EngineError::Unsupported(alloc::format!(
12922            "partition expansion: generated SQL {sql:?} is not a SELECT"
12923        )));
12924    };
12925    Ok(body)
12926}
12927
12928/// v7.39 (read01 round 65/66) — the column shape a set-returning function
12929/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
12930/// yields ONE column named after the call's alias when there is one (`FROM
12931/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
12932/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
12933fn setof_column_shape_from(
12934    declared: &str,
12935    name: &str,
12936    alias: Option<&str>,
12937    got: &[ColumnSchema],
12938) -> alloc::vec::Vec<ColumnSchema> {
12939    let upper = declared.to_ascii_uppercase();
12940    if upper.starts_with("TABLE(") {
12941        let raw = &declared["TABLE(".len()..declared.len() - 1];
12942        return raw
12943            .split(',')
12944            .zip(got.iter())
12945            .map(|(decl, g)| {
12946                let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
12947                ColumnSchema::new(cname.to_string(), g.ty, true)
12948            })
12949            .collect();
12950    }
12951    let cname = alias.unwrap_or(name);
12952    got.first()
12953        .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
12954        .unwrap_or_default()
12955}
12956
12957/// The plpgsql twin: the interpreter hands back raw value rows, so the types
12958/// come off the first row.
12959fn setof_column_shape(
12960    declared: &str,
12961    name: &str,
12962    alias: Option<&str>,
12963    first_row: Option<&alloc::vec::Vec<Value<'static>>>,
12964) -> alloc::vec::Vec<ColumnSchema> {
12965    let got: alloc::vec::Vec<ColumnSchema> = first_row
12966        .map(|r| {
12967            r.iter()
12968                .enumerate()
12969                .map(|(i, v)| {
12970                    ColumnSchema::new(
12971                        alloc::format!("col{i}"),
12972                        v.data_type().unwrap_or(DataType::Text),
12973                        true,
12974                    )
12975                })
12976                .collect()
12977        })
12978        .unwrap_or_default();
12979    setof_column_shape_from(declared, name, alias, &got)
12980}
12981
12982/// v7.39 (read01 round 67) — expand every set-returning call in a target list
12983/// for ONE input row, PG's ProjectSet semantics.
12984///
12985/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
12986/// output has as many rows as the LONGEST of them, and a shorter one is padded
12987/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
12988/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
12989/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
12990/// is zero rows, not one NULL row.
12991///
12992/// Non-SRF items repeat, evaluated once per output row from the same input row.
12993/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
12994/// used to reach the scalar function dispatcher, which reported the aggregate as
12995/// an *unknown function* — the same "symptom two layers above the cause" shape
12996/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
12997/// sees a call, not the clause it came from. The statement knows.
12998/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
12999/// clause may appear.
13000///
13001/// PG rejects `FOR UPDATE` on exactly the shapes that have no
13002/// identifiable base row to lock, each with its own wording. SPG
13003/// accepted all of them and locked nothing, so a query that PG refuses
13004/// outright came back looking like it had taken locks.
13005///
13006/// Every wording read off live PG 18.4.
13007fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
13008    let Some(lock) = &stmt.locking else {
13009        return Ok(());
13010    };
13011    let verb = lock_clause_verb(lock.strength);
13012    let refuse = |what: &str| {
13013        Err(EngineError::Unsupported(alloc::format!(
13014            "{verb} is not allowed with {what}"
13015        )))
13016    };
13017    if !stmt.unions.is_empty() {
13018        return refuse("UNION/INTERSECT/EXCEPT");
13019    }
13020    if stmt.distinct || !stmt.distinct_on.is_empty() {
13021        return refuse("DISTINCT clause");
13022    }
13023    if stmt.group_by.is_some() || stmt.group_by_all {
13024        return refuse("GROUP BY clause");
13025    }
13026    let has_agg = stmt.items.iter().any(|it| match it {
13027        spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
13028        _ => false,
13029    });
13030    if has_agg {
13031        return refuse("aggregate functions");
13032    }
13033    // `FOR UPDATE OF t` must name a relation that is actually in FROM.
13034    for want in &lock.of_tables {
13035        if !locking_from_names(stmt)
13036            .iter()
13037            .any(|n| n.eq_ignore_ascii_case(want))
13038        {
13039            return Err(EngineError::Unsupported(alloc::format!(
13040                "relation \"{want}\" in {verb} clause not found in FROM clause"
13041            )));
13042        }
13043    }
13044    Ok(())
13045}
13046
13047/// How PG names the clause in its diagnostics.
13048const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
13049    use spg_sql::ast::LockStrength as LS;
13050    match s {
13051        LS::Update => "FOR UPDATE",
13052        LS::NoKeyUpdate => "FOR NO KEY UPDATE",
13053        LS::Share => "FOR SHARE",
13054        LS::KeyShare => "FOR KEY SHARE",
13055    }
13056}
13057
13058/// Every relation name (or alias) the FROM clause exposes.
13059fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
13060    let mut out = alloc::vec::Vec::new();
13061    if let Some(f) = &stmt.from {
13062        let mut push = |t: &spg_sql::ast::TableRef| {
13063            if let Some(a) = &t.alias {
13064                out.push(a.clone());
13065            }
13066            out.push(t.name.clone());
13067        };
13068        push(&f.primary);
13069        for j in &f.joins {
13070            push(&j.table);
13071        }
13072    }
13073    out
13074}
13075
13076fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
13077    use spg_sql::ast::Expr;
13078    if let Some(w) = &stmt.where_
13079        && aggregate::contains_aggregate(w)
13080    {
13081        return Err(EngineError::Unsupported(
13082            "aggregate functions are not allowed in WHERE".into(),
13083        ));
13084    }
13085    let mut nested = false;
13086    let mut check = |e: &Expr| {
13087        let mut probe = e.clone();
13088        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13089            let args = match n {
13090                Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
13091                _ => return false,
13092            };
13093            if args.iter().any(aggregate::contains_aggregate) {
13094                nested = true;
13095            }
13096            false
13097        });
13098    };
13099    for it in &stmt.items {
13100        if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
13101            check(expr);
13102        }
13103    }
13104    if let Some(h) = &stmt.having {
13105        check(h);
13106    }
13107    for o in &stmt.order_by {
13108        check(&o.expr);
13109    }
13110    if nested {
13111        return Err(EngineError::Unsupported(
13112            "aggregate function calls cannot be nested".into(),
13113        ));
13114    }
13115    Ok(())
13116}
13117
13118/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
13119/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
13120/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
13121/// to a set and then applies the enclosing expression once per element. SPG only
13122/// ever recognised an SRF that WAS the item, so everything above died on
13123/// "unknown function unnest" — the set-returning call, wrapped in anything at
13124/// all, fell through to the scalar function dispatcher which has no such name.
13125///
13126/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
13127/// rewritten to read that column, and the rewritten expression is evaluated once
13128/// per output row against the input row extended with the lifted values. The
13129/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
13130/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
13131/// executors (the single-table scan, the synthetic-table pipeline, and the
13132/// unnest FROM path) each evaluated the key as an ordinary expression, where the
13133/// literal `n` is just the constant n — the same sort key for every row. The
13134/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
13135/// back in input order, not in a wrong order. Statement prep resolves the common
13136/// case, but only when the SELECT item is an expression — a `*` is not one, and
13137/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
13138/// spelling landed on exactly the shape prep could not resolve.
13139///
13140/// A set-returning item is left alone: copying it into ORDER BY would make the
13141/// key "the whole set", evaluated once per INPUT row.
13142fn resolve_positional_order_by(
13143    order_by: &[spg_sql::ast::OrderBy],
13144    projection: &[ProjectedItem],
13145) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
13146    order_by
13147        .iter()
13148        .filter_map(|o| {
13149            let mut o = o.clone();
13150            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
13151                && *n >= 1
13152                && let Ok(idx) = usize::try_from(*n - 1)
13153                && let Some(item) = projection.get(idx)
13154                && !expr_contains_builtin_srf(&item.expr)
13155            {
13156                // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
13157                // item is itself an integer LITERAL must not be
13158                // substituted textually: the literal would read as an
13159                // ordinal again downstream, and `SELECT 10 … ORDER BY
13160                // 1` died with "position 10 is not in select list"
13161                // where PG happily returns the rows. Ordering by a
13162                // constant orders nothing, so the key drops.
13163                if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
13164                    return None;
13165                }
13166                o.expr = item.expr.clone();
13167            }
13168            Some(o)
13169        })
13170        .collect()
13171}
13172
13173/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
13174/// this expression? Statement preparation (`resolve_order_by_position`) runs
13175/// before any catalog is in hand, and it only needs to know "is this item's value
13176/// a set", which the builtin SRFs answer syntactically.
13177pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
13178    let mut found = false;
13179    let mut probe = e.clone();
13180    crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13181        if is_top_level_unnest(n) {
13182            found = true;
13183            return true;
13184        }
13185        false
13186    });
13187    found
13188}
13189
13190/// v7.39 (round 599) — everything about a target-list SRF that does not
13191/// depend on the row.
13192///
13193/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
13194/// each SRF-bearing projection expression, walked and rewrote the tree,
13195/// formatted a `__srf_N` name per node, and copied the whole column schema.
13196/// A counting allocator put the path at 24 allocations per input row for a
13197/// single-element `unnest`, against 0 for the same scan without one — 211 MB
13198/// where the plain scan took 4.3 — and the shape held whatever the array
13199/// contained, which is what invariant work looks like.
13200struct SrfPlan {
13201    /// The lifted SRF calls, in slot order.
13202    nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
13203    /// Per projection position, the expression with its SRF calls replaced
13204    /// by `__srf_N` column references. `None` means the item has none.
13205    rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
13206    /// The input schema followed by one column per slot. Only the slots'
13207    /// TYPES vary per row, and they are patched in place.
13208    ext_cols: alloc::vec::Vec<ColumnSchema>,
13209    /// v7.39 (round 743) — the rewritten projection COMPILED against the
13210    /// extended schema, once per plan. The per-output-row evaluation ran
13211    /// the interpreter (~560 ns/row on the unnest panel cell); the Step
13212    /// VM reads the `__srf_N` slots as plain columns. `None` = that item
13213    /// is not fully compilable and keeps the interpreter.
13214    compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
13215    base_cols: usize,
13216}
13217
13218fn build_srf_plan(
13219    engine: &Engine,
13220    projection: &[ProjectedItem],
13221    srf_idxs: &[usize],
13222    ctx: &EvalContext<'_>,
13223) -> Result<SrfPlan, EngineError> {
13224    // Lift every SRF node out of every item that contains one.
13225    let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
13226    let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
13227    let mut reject: Option<EngineError> = None;
13228    for &i in srf_idxs {
13229        let mut e = projection[i].expr.clone();
13230        crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
13231            if reject.is_some() {
13232                return true;
13233            }
13234            // PG refuses a set-returning function inside a conditional: the set
13235            // would have to be produced before anyone knows whether the branch
13236            // is even taken.
13237            let conditional = match n {
13238                spg_sql::ast::Expr::Case { .. } => Some("CASE"),
13239                spg_sql::ast::Expr::FunctionCall { name, .. }
13240                    if name.eq_ignore_ascii_case("coalesce") =>
13241                {
13242                    Some("COALESCE")
13243                }
13244                _ => None,
13245            };
13246            if let Some(kind) = conditional
13247                && engine.expr_contains_srf(n)
13248            {
13249                reject = Some(EngineError::Unsupported(alloc::format!(
13250                    "set-returning functions are not allowed in {kind}"
13251                )));
13252                return true;
13253            }
13254            if !engine.is_srf_node(n) {
13255                return false;
13256            }
13257            let slot = nodes.len();
13258            nodes.push(n.clone());
13259            *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
13260                qualifier: None,
13261                name: alloc::format!("__srf_{slot}"),
13262            });
13263            true
13264        });
13265        rewritten[i] = Some(e);
13266    }
13267    if let Some(err) = reject {
13268        return Err(err);
13269    }
13270    let base_cols = ctx.columns.len();
13271    let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
13272    for slot in 0..nodes.len() {
13273        ext_cols.push(ColumnSchema::new(
13274            alloc::format!("__srf_{slot}"),
13275            DataType::Text,
13276            true,
13277        ));
13278    }
13279    // v7.39 (round 743) — compile the rewritten items against the
13280    // EXTENDED schema. The slot columns' declared type is a per-row
13281    // patched detail the compiled column read does not consult.
13282    let compiled: Vec<Option<eval::CompiledExpr>> = {
13283        let mut ext_ctx = ctx.clone();
13284        ext_ctx.columns = &ext_cols;
13285        projection
13286            .iter()
13287            .enumerate()
13288            .map(|(i, p)| {
13289                let e = rewritten[i].as_ref().unwrap_or(&p.expr);
13290                if eval::fully_compilable(e) {
13291                    Some(eval::compile_expr(e, &ext_ctx))
13292                } else {
13293                    None
13294                }
13295            })
13296            .collect()
13297    };
13298    Ok(SrfPlan {
13299        nodes,
13300        rewritten,
13301        ext_cols,
13302        compiled,
13303        base_cols,
13304    })
13305}
13306
13307/// One input row expanded through a plan built once for the whole scan.
13308/// v7.39 (round 621) — expand a projection whose target list contains
13309/// set-returning items, remembering which INPUT row each output row came from.
13310///
13311/// The three materialised-source tails — `FROM unnest(…)`, `FROM
13312/// generate_series(…)`, and the one that serves VALUES / a derived table /
13313/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
13314/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
13315/// v(x)` answered `function unnest(integer[]) does not exist` on all the
13316/// others, for a query PG answers. Sharing the expansion is the point: a
13317/// fourth copy would have been the fourth place to forget.
13318fn expand_projection_srfs(
13319    engine: &Engine,
13320    projection: &[ProjectedItem],
13321    srf_idxs: &[usize],
13322    filtered: &[Row<'static>],
13323    ctx: &EvalContext<'_>,
13324) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
13325    let mut out = alloc::vec::Vec::with_capacity(filtered.len());
13326    let mut src = alloc::vec::Vec::with_capacity(filtered.len());
13327    // v7.39 (round 726) — ONE plan for the whole scan. The per-row
13328    // spelling rebuilt it for every input row: a full clone of the
13329    // rewritten projection trees and the extended schema, 50k times on
13330    // the panel's unnest cell.
13331    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
13332    // v7.39 (round 733) — shard the expansion. Each shard clones the
13333    // plan (its ext_cols slot types are per-row mutable) and builds a
13334    // MINIMAL context — EvalContext is not Sync — which is sound only
13335    // when every expression involved is pure: the whole projection and
13336    // every SRF argument must be fully_compilable, or the row loop
13337    // stays serial with the full session context.
13338    // The projection is judged in its REWRITTEN form — the SRF call
13339    // itself is never compilable, but after the lift it is a plain
13340    // `__srf_N` column reference.
13341    let all_pure = projection
13342        .iter()
13343        .enumerate()
13344        .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
13345        && plan.nodes.iter().all(|n| match n {
13346            Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
13347            other => eval::fully_compilable(other),
13348        });
13349    if all_pure
13350        && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
13351        && let Some(r) = engine.parallel_runner.0.as_deref()
13352    {
13353        let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
13354        let chunk = filtered.len().div_ceil(n_shards);
13355        type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
13356        let schema_cols = ctx.columns;
13357        let alias = ctx.table_alias;
13358        let mysql = ctx.mysql_dialect;
13359        let style = ctx.render_style;
13360        let plan_ref = &plan;
13361        let results = r.run_shards(n_shards, &|si| {
13362            let lo = si * chunk;
13363            let hi = ((si + 1) * chunk).min(filtered.len());
13364            let mut sctx = eval::EvalContext::new(schema_cols, alias);
13365            sctx.mysql_dialect = mysql;
13366            sctx.render_style = style;
13367            // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
13368            // compiled programs); each shard rebuilds it, which also
13369            // recompiles against the shard's own context. Build errors
13370            // were already surfaced by the outer build above.
13371            let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
13372                Ok(p) => p,
13373                Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
13374            };
13375            let mut run = || -> ShardOut {
13376                let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
13377                let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
13378                for (i, row) in filtered[lo..hi].iter().enumerate() {
13379                    let expanded =
13380                        expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
13381                    sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
13382                    o.extend(expanded);
13383                }
13384                Ok((o, sidx))
13385            };
13386            alloc::boxed::Box::new(run())
13387        });
13388        for boxed in results {
13389            let shard = boxed
13390                .downcast::<ShardOut>()
13391                .expect("runner echoes the closure's box");
13392            let (o, sidx) = (*shard)?;
13393            out.extend(o);
13394            src.extend(sidx);
13395        }
13396        return Ok((out, src));
13397    }
13398    for (i, row) in filtered.iter().enumerate() {
13399        let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
13400        src.extend(core::iter::repeat_n(i, expanded.len()));
13401        out.extend(expanded);
13402    }
13403    Ok((out, src))
13404}
13405
13406/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
13407///
13408/// A key that names a select-list item reads it out of the EXPANDED row,
13409/// because PG sorts after the expansion. A key that names a source column the
13410/// query does not project is evaluated against the input row that output row
13411/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
13412fn srf_order_key(
13413    ob: &spg_sql::ast::OrderBy,
13414    out_col: Option<usize>,
13415    out: &Row<'static>,
13416    src: &Row<'static>,
13417    ctx: &EvalContext<'_>,
13418) -> Result<Value<'static>, EngineError> {
13419    match out_col {
13420        Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
13421        None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
13422    }
13423}
13424
13425fn expand_srf_row_with(
13426    engine: &Engine,
13427    plan: &mut SrfPlan,
13428    projection: &[ProjectedItem],
13429    row: &Row<'static>,
13430    ctx: &EvalContext<'_>,
13431) -> Result<Vec<Row<'static>>, EngineError> {
13432    let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
13433    for n in &plan.nodes {
13434        lists.push(engine.srf_values(n, row, ctx)?);
13435    }
13436    let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
13437    // Only the slots' element types depend on the row; the names and the
13438    // input schema around them do not.
13439    for (slot, list) in lists.iter().enumerate() {
13440        plan.ext_cols[plan.base_cols + slot].ty = list
13441            .iter()
13442            .find_map(|v| v.data_type())
13443            .unwrap_or(DataType::Text);
13444    }
13445    let mut ext_ctx = ctx.clone();
13446    ext_ctx.columns = &plan.ext_cols;
13447    let mut out = Vec::with_capacity(n_rows);
13448    // v7.39 (round 726) — the base columns are the SAME for every
13449    // expanded row; clone them once and rewrite only the SRF slots per
13450    // k. The old form cloned the whole input row per OUTPUT row — for
13451    // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
13452    // TEXT column the projection never reads.
13453    let base_len = row.values.len();
13454    let mut ext_vals = row.values.clone();
13455    ext_vals.resize(base_len + lists.len(), Value::Null);
13456    let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
13457    for k in 0..n_rows {
13458        for (slot, list) in lists.iter().enumerate() {
13459            // Past the end of THIS srf's rows → NULL (PG pads).
13460            ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
13461        }
13462        let ext_row = Row::new(core::mem::take(&mut ext_vals));
13463        let mut vals = Vec::with_capacity(projection.len());
13464        for (i, p) in projection.iter().enumerate() {
13465            // v7.39 (round 743) — compiled when possible; the
13466            // interpreter for the rest, with its exact wording.
13467            vals.push(match &plan.compiled[i] {
13468                Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
13469                    .map_err(EngineError::Eval)?,
13470                None => {
13471                    let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
13472                    eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
13473                }
13474            });
13475        }
13476        ext_vals = ext_row.values;
13477        out.push(Row::new(vals));
13478    }
13479    Ok(out)
13480}
13481
13482/// The one-shot spelling, for the callers that expand a single row.
13483/// v7.39 (round 600) — which output column each ORDER BY key names, for a
13484/// query whose target list contains a set-returning function.
13485///
13486/// The keys used to be built from the INPUT row, before the SRF expanded, so
13487/// anything that named the SRF's own output was evaluated as a scalar call:
13488/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
13489/// "function unnest(integer[]) does not exist", and so did the spellings that
13490/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
13491/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
13492/// back in input order. PG sorts AFTER the expansion, so a key that names a
13493/// select-list item reads that item's value out of the expanded row.
13494///
13495/// `None` keeps the key on the input row, which is where an ORDER BY naming
13496/// a column the query does not project has to be evaluated.
13497fn srf_order_output_cols(
13498    order_by: &[spg_sql::ast::OrderBy],
13499    projection: &[ProjectedItem],
13500) -> Vec<Option<usize>> {
13501    order_by
13502        .iter()
13503        .map(|ob| {
13504            // A positive ordinal is the Nth output column, directly.
13505            // `resolve_positional_order_by` deliberately leaves an ordinal
13506            // pointing at a set-returning item alone — copying the call into
13507            // ORDER BY would have made the key "the whole set" back when keys
13508            // came from the input row. Reading the expanded row's column is
13509            // what it should have meant, and is what this does.
13510            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
13511                && *n >= 1
13512                && let Ok(idx) = usize::try_from(*n - 1)
13513                && idx < projection.len()
13514            {
13515                return Some(idx);
13516            }
13517            // An unqualified name matching exactly one output name. SQL
13518            // resolves ORDER BY against the select list first, so this wins
13519            // over an input column of the same name — which is the whole
13520            // point of `SELECT g AS id … ORDER BY id`.
13521            if let Expr::Column(c) = &ob.expr
13522                && c.qualifier.is_none()
13523            {
13524                let mut hit = None;
13525                for (i, p) in projection.iter().enumerate() {
13526                    if p.output_name.eq_ignore_ascii_case(&c.name) {
13527                        if hit.is_some() {
13528                            hit = None;
13529                            break;
13530                        }
13531                        hit = Some(i);
13532                    }
13533                }
13534                if hit.is_some() {
13535                    return hit;
13536                }
13537            }
13538            // Or the same expression as a select-list item — which is what
13539            // `ORDER BY 1` becomes once `resolve_positional_order_by` has
13540            // run, and what a repeated `ORDER BY unnest(…)` is.
13541            projection.iter().position(|p| p.expr == ob.expr)
13542        })
13543        .collect()
13544}
13545
13546fn expand_srf_row(
13547    engine: &Engine,
13548    projection: &[ProjectedItem],
13549    srf_idxs: &[usize],
13550    row: &Row<'static>,
13551    ctx: &EvalContext<'_>,
13552) -> Result<Vec<Row<'static>>, EngineError> {
13553    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
13554    expand_srf_row_with(engine, &mut plan, projection, row, ctx)
13555}
13556
13557impl Engine {
13558    /// The rows one target-list SRF yields for an input row. `None` from
13559    /// `srf_target_idxs` means the expression is not set-returning at all.
13560    fn srf_values(
13561        &self,
13562        expr: &spg_sql::ast::Expr,
13563        row: &Row<'static>,
13564        ctx: &EvalContext<'_>,
13565    ) -> Result<Vec<Value<'static>>, EngineError> {
13566        if top_level_srf_kind(expr).is_some() {
13567            return top_level_srf_output(expr, row, ctx);
13568        }
13569        // A user set-returning function. Its body runs through the real
13570        // executor, like every function body since round 63.
13571        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13572            return Err(EngineError::Unsupported(
13573                "expected a SELECT-list SRF call".into(),
13574            ));
13575        };
13576        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
13577        for a in args {
13578            vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13579        }
13580        let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
13581        // v7.39 (read01 round 68) — in a target list a multi-column function is
13582        // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
13583        // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
13584        // what it is for. A single-column function contributes its bare value.
13585        Ok(rows
13586            .into_iter()
13587            .map(|r| {
13588                if r.values.len() == 1 {
13589                    r.values.into_iter().next().unwrap_or(Value::Null)
13590                } else {
13591                    Value::Composite(
13592                        cols.iter()
13593                            .map(|c| c.name.clone())
13594                            .zip(r.values)
13595                            .collect::<alloc::vec::Vec<_>>(),
13596                    )
13597                }
13598            })
13599            .collect())
13600    }
13601
13602    /// Is THIS node a set-returning call: one of the builtin kinds, or a user
13603    /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
13604    fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
13605        if is_top_level_unnest(e) {
13606            return true;
13607        }
13608        let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
13609            return false;
13610        };
13611        self.active_catalog().functions_named(name).iter().any(|f| {
13612            let r = f.returns.trim().to_ascii_uppercase();
13613            r.starts_with("SETOF") || r.starts_with("TABLE(")
13614        })
13615    }
13616
13617    /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
13618    fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
13619        let mut found = false;
13620        let mut probe = e.clone();
13621        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13622            if self.is_srf_node(n) {
13623                found = true;
13624                return true;
13625            }
13626            false
13627        });
13628        found
13629    }
13630
13631    /// Which projection items CONTAIN a set-returning call. Before round 78 this
13632    /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
13633    /// ordinary scalar call all the way down to the function dispatcher, which
13634    /// then reported `unnest` as an unknown function.
13635    fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
13636        projection
13637            .iter()
13638            .enumerate()
13639            .filter(|(_, p)| self.expr_contains_srf(&p.expr))
13640            .map(|(i, _)| i)
13641            .collect()
13642    }
13643}
13644
13645impl Engine {
13646    /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
13647    /// no `(f(args)).*` item.
13648    fn lower_record_expansion(
13649        &self,
13650        stmt: &SelectStatement,
13651    ) -> Result<Option<SelectStatement>, EngineError> {
13652        use spg_sql::ast::{Expr, SelectItem};
13653        let is_marker = |it: &SelectItem| {
13654            matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
13655                if name == "__record_expand")
13656        };
13657        if !stmt.items.iter().any(is_marker) {
13658            return Ok(None);
13659        }
13660        let mut out = stmt.clone();
13661        let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
13662        let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
13663        for (n, item) in stmt.items.iter().enumerate() {
13664            if !is_marker(item) {
13665                items.push(item.clone());
13666                continue;
13667            }
13668            let SelectItem::Expr {
13669                expr: Expr::FunctionCall { args, .. },
13670                ..
13671            } = item
13672            else {
13673                unreachable!("checked by is_marker");
13674            };
13675            let Some(Expr::FunctionCall {
13676                name: fname,
13677                args: fargs,
13678            }) = args.first()
13679            else {
13680                return Err(EngineError::Unsupported(
13681                    "(<expr>).* expands a function's record — it needs a function call".into(),
13682                ));
13683            };
13684            let cols = self.setof_declared_columns(fname)?;
13685            let alias = alloc::format!("__rec{n}");
13686            let mut tref = bare_table_ref_named(&alias);
13687            tref.table_fn_call = Some(alloc::boxed::Box::new((
13688                fname.to_ascii_lowercase(),
13689                fargs.clone(),
13690            )));
13691            tref.alias = Some(alias.clone());
13692            lateral_refs.push(tref);
13693            for c in cols {
13694                items.push(SelectItem::Expr {
13695                    expr: Expr::Column(spg_sql::ast::ColumnName {
13696                        qualifier: Some(alias.clone()),
13697                        name: c,
13698                    }),
13699                    alias: None,
13700                });
13701            }
13702        }
13703        out.items = items;
13704        // The function joins the FROM. With no FROM it BECOMES the FROM; with one
13705        // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
13706        // (the arguments may reference the outer row — the round-69 correlation).
13707        for tref in lateral_refs {
13708            match &mut out.from {
13709                None => {
13710                    out.from = Some(spg_sql::ast::FromClause {
13711                        primary: tref,
13712                        joins: alloc::vec::Vec::new(),
13713                    });
13714                }
13715                Some(from) => from.joins.push(spg_sql::ast::FromJoin {
13716                    kind: spg_sql::ast::JoinKind::Cross,
13717                    table: tref,
13718                    on: None,
13719                    using_cols: None,
13720                    natural: false,
13721                }),
13722            }
13723        }
13724        Ok(Some(out))
13725    }
13726
13727    /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
13728    /// v text)` names them; a `SETOF <scalar>` is one column named after the
13729    /// function.
13730    fn setof_declared_columns(
13731        &self,
13732        name: &str,
13733    ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
13734        let cat = self.active_catalog();
13735        let overloads = cat.functions_named(name);
13736        let def = overloads.first().ok_or_else(|| {
13737            EngineError::Unsupported(alloc::format!("function {name} does not exist"))
13738        })?;
13739        let declared = def.returns.trim();
13740        let upper = declared.to_ascii_uppercase();
13741        if upper.starts_with("TABLE(") {
13742            let raw = &declared["TABLE(".len()..declared.len() - 1];
13743            return Ok(raw
13744                .split(',')
13745                .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
13746                .collect());
13747        }
13748        Ok(alloc::vec![name.to_string()])
13749    }
13750}
13751
13752/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
13753/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
13754/// COLUMNS list (data-independent), NESTED children inlined in
13755/// declaration order (PG's flattened output shape).
13756/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
13757/// correlated JSON_TABLE's static schema without evaluating its doc.
13758pub(crate) fn json_table_schema_pub(
13759    cols: &[spg_sql::ast::JsonTableColumn],
13760) -> alloc::vec::Vec<ColumnSchema> {
13761    json_table_schema(cols)
13762}
13763
13764fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
13765    use spg_sql::ast::JsonTableColumn as C;
13766    let mut out = alloc::vec::Vec::new();
13767    for c in cols {
13768        match c {
13769            C::Ordinality { name } => {
13770                out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
13771            }
13772            C::Regular {
13773                name, ty, exists, ..
13774            } => {
13775                let dt = if *exists {
13776                    DataType::Bool
13777                } else {
13778                    crate::conversions::column_type_to_data_type(*ty)
13779                };
13780                out.push(ColumnSchema::new(name.clone(), dt, true));
13781            }
13782            C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
13783        }
13784    }
13785    out
13786}
13787
13788/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
13789/// JSON_TABLE column's declared type (the DEFAULT expr may be a
13790/// string literal like `'none'` that must land as the column type).
13791fn coerce_json_table_default(
13792    v: Value<'static>,
13793    ty: spg_sql::ast::ColumnTypeName,
13794    name: &str,
13795) -> Result<Value<'static>, EngineError> {
13796    if v.is_null() {
13797        return Ok(Value::Null);
13798    }
13799    let dt = crate::conversions::column_type_to_data_type(ty);
13800    crate::conversions::coerce_value(v, dt, name, 0)
13801}
13802
13803/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
13804fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
13805    use crate::json::JsonValue as J;
13806    match v {
13807        Value::Null => J::Null,
13808        Value::Bool(b) => J::Bool(*b),
13809        Value::SmallInt(n) => J::Number(f64::from(*n)),
13810        Value::Int(n) => J::Number(f64::from(*n)),
13811        Value::BigInt(n) => J::Number(*n as f64),
13812        Value::Float(x) => J::Number(*x),
13813        Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
13814        other => J::String(crate::eval::value_to_text(other)),
13815    }
13816}
13817
13818fn bare_table_ref_named(name: &str) -> TableRef {
13819    TableRef {
13820        name: name.to_string(),
13821        alias: None,
13822        only: false,
13823        as_of_segment: None,
13824        unnest_expr: None,
13825        unnest_column_aliases: alloc::vec::Vec::new(),
13826        with_ordinality: false,
13827        generate_series_args: None,
13828        lateral_subquery: None,
13829        jsonb_each_text_arg: None,
13830        table_fn_call: None,
13831        rows_from: None,
13832        json_table: None,
13833        scalar_fn_item: false,
13834    }
13835}
13836
13837impl Engine {
13838    /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
13839    /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
13840    /// entries are the array-able SRFs, already lowered by the parser into their
13841    /// scalar array form.
13842    fn rows_from_rows(
13843        &self,
13844        primary: &TableRef,
13845    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
13846        let entries = primary
13847            .rows_from
13848            .as_ref()
13849            .expect("caller guards rows_from.is_some()");
13850        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13851        let ctx = self.ev_ctx(&empty, None);
13852        let dummy = Row::new(alloc::vec::Vec::new());
13853        let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
13854        let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13855        for (name, args) in entries {
13856            let (vals, colname) = if name == "__array" {
13857                // The parser lowered this one to `<array expr>`; its rows are the
13858                // array's elements.
13859                let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
13860                (
13861                    array_value_to_elements(&arr)?,
13862                    alloc::string::String::from("unnest"),
13863                )
13864            } else {
13865                let call = spg_sql::ast::Expr::FunctionCall {
13866                    name: name.clone(),
13867                    args: args.clone(),
13868                };
13869                (self.srf_values(&call, &dummy, &ctx)?, name.clone())
13870            };
13871            let ty = vals
13872                .first()
13873                .and_then(spg_storage::Value::data_type)
13874                .unwrap_or(DataType::Text);
13875            cols.push(ColumnSchema::new(colname, ty, true));
13876            lists.push(vals);
13877        }
13878        let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
13879        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
13880        for k in 0..n {
13881            let mut vals: alloc::vec::Vec<Value<'static>> =
13882                alloc::vec::Vec::with_capacity(lists.len() + 1);
13883            for l in &lists {
13884                vals.push(l.get(k).cloned().unwrap_or(Value::Null));
13885            }
13886            rows.push(Row::new(vals));
13887        }
13888        if primary.with_ordinality {
13889            cols.push(ColumnSchema::new(
13890                "ordinality".to_string(),
13891                DataType::BigInt,
13892                false,
13893            ));
13894            rows = rows
13895                .into_iter()
13896                .enumerate()
13897                .map(|(i, r)| {
13898                    let mut v = r.values;
13899                    v.push(Value::BigInt(i as i64 + 1));
13900                    Row::new(v)
13901                })
13902                .collect();
13903        }
13904        Ok((rows, cols))
13905    }
13906}
13907
13908/// v7.39 (round 232) — PG names the offending set operation in its
13909/// arity / type-mismatch messages ("each UNION query must have the same
13910/// number of columns"). `UNION ALL` is still spelled UNION there.
13911fn set_op_name(kind: UnionKind) -> &'static str {
13912    match kind {
13913        UnionKind::All | UnionKind::Distinct => "UNION",
13914        UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
13915        UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
13916    }
13917}
13918
13919/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
13920/// type: a bare string or NULL literal that no context has typed yet. SPG
13921/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
13922/// be the syntax. A wildcard or a non-literal expression is never unknown.
13923/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
13924/// LABELS as text (the wire render) but the value is an oid-carrying
13925/// dual, so a UNION with a numeric column must not be refused on the
13926/// label (pg_dump: `SELECT classid … UNION ALL SELECT
13927/// 'pg_opfamily'::regclass …`).
13928fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
13929    fn is_regcast(e: &Expr) -> bool {
13930        matches!(
13931            e,
13932            Expr::Cast {
13933                target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
13934                ..
13935            }
13936        )
13937    }
13938    stmt.items
13939        .iter()
13940        .map(|item| match item {
13941            SelectItem::Expr { expr, .. } => is_regcast(expr),
13942            _ => false,
13943        })
13944        .collect()
13945}
13946
13947fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
13948    stmt.items
13949        .iter()
13950        .map(|item| match item {
13951            SelectItem::Expr { expr, .. } => matches!(
13952                expr,
13953                Expr::Literal(spg_sql::ast::Literal::String(_))
13954                    | Expr::Literal(spg_sql::ast::Literal::Null)
13955            ),
13956            _ => false,
13957        })
13958        .collect()
13959}
13960
13961/// v7.39 (round 233) — retype one branch column's cells, reporting the
13962/// conversion failure the way PG does rather than leaving the column
13963/// half-converted. Used when the other branch typed an untyped literal.
13964fn coerce_branch_column(
13965    rows: &mut [Row<'static>],
13966    col_idx: usize,
13967    target: DataType,
13968    col_name: &str,
13969) -> Result<(), EngineError> {
13970    for row in rows.iter_mut() {
13971        let Some(slot) = row.values.get_mut(col_idx) else {
13972            continue;
13973        };
13974        if matches!(slot, Value::Null) {
13975            continue;
13976        }
13977        *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
13978    }
13979    Ok(())
13980}
13981
13982/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
13983/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
13984/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
13985/// reference to q's output columns substituted by the underlying column.
13986///
13987/// Admission is deliberately narrow — anything that changes cardinality,
13988/// order, or scope stays on the materialising path:
13989/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
13990///   FROM with no ordinality or positional column aliases, and no
13991///   subquery anywhere its expressions (an inner scope could reference
13992///   q too — descending is a later knife);
13993/// * inner: one stored table, bare-column projection only, no
13994///   CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
13995/// * every outer column reference must resolve inside q's output list —
13996///   a name that does not is an ERROR today, and flattening would
13997///   silently legalise it against the base table.
13998fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
13999    use spg_sql::ast::SelectItem;
14000    let inner = primary.lateral_subquery.as_deref()?;
14001    // Outer shape.
14002    if !stmt.ctes.is_empty()
14003        || !stmt.unions.is_empty()
14004        || stmt.distinct
14005        || !stmt.distinct_on.is_empty()
14006        || !stmt.window_check_exprs.is_empty()
14007        || stmt.locking.is_some()
14008        || primary.with_ordinality
14009        || !primary.unnest_column_aliases.is_empty()
14010    {
14011        return None;
14012    }
14013    // Inner shape.
14014    if !inner.ctes.is_empty()
14015        || !inner.unions.is_empty()
14016        || inner.distinct
14017        || !inner.distinct_on.is_empty()
14018        || inner.group_by.is_some()
14019        || inner.group_by_all
14020        || inner.having.is_some()
14021        || !inner.order_by.is_empty()
14022        || inner.limit.is_some()
14023        || inner.offset.is_some()
14024        || !inner.window_check_exprs.is_empty()
14025        || inner.locking.is_some()
14026    {
14027        return None;
14028    }
14029    let ifrom = inner.from.as_ref()?;
14030    let it = &ifrom.primary;
14031    if !ifrom.joins.is_empty()
14032        || it.name.is_empty()
14033        || it.lateral_subquery.is_some()
14034        || it.unnest_expr.is_some()
14035        || it.generate_series_args.is_some()
14036        || it.as_of_segment.is_some()
14037        || it.jsonb_each_text_arg.is_some()
14038        || it.table_fn_call.is_some()
14039        || it.rows_from.is_some()
14040        || it.json_table.is_some()
14041        || it.with_ordinality
14042        || !it.unnest_column_aliases.is_empty()
14043    {
14044        return None;
14045    }
14046    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
14047        return None;
14048    }
14049    // The output map: q's visible name -> the underlying column.
14050    let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
14051    let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
14052        alloc::collections::BTreeMap::new();
14053    for item in &inner.items {
14054        let SelectItem::Expr { expr, alias } = item else {
14055            return None;
14056        };
14057        let Expr::Column(c) = expr else {
14058            return None;
14059        };
14060        if let Some(q) = c.qualifier.as_deref()
14061            && !q.eq_ignore_ascii_case(&inner_alias)
14062        {
14063            return None;
14064        }
14065        let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
14066        // A duplicated output name would make substitution ambiguous.
14067        if map
14068            .insert(out_name.to_ascii_lowercase(), c.clone())
14069            .is_some()
14070        {
14071            return None;
14072        }
14073    }
14074    if map.is_empty() {
14075        return None;
14076    }
14077    let derived_alias = primary
14078        .alias
14079        .clone()
14080        .unwrap_or_else(|| primary.name.clone())
14081        .to_ascii_lowercase();
14082    // Substitute in a clone; bail (None) on the first reference the map
14083    // cannot answer.
14084    let mut out = stmt.clone();
14085    let ok = core::cell::Cell::new(true);
14086    let mut subst = |e: &mut Expr| -> bool {
14087        match e {
14088            Expr::Column(c) => {
14089                match c.qualifier.as_deref() {
14090                    Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
14091                    None => {}
14092                    Some(_) => {
14093                        ok.set(false);
14094                        return true;
14095                    }
14096                }
14097                match map.get(&c.name.to_ascii_lowercase()) {
14098                    Some(target) => *c = target.clone(),
14099                    None => ok.set(false),
14100                }
14101                true
14102            }
14103            // Any subquery could reference q from its own scope;
14104            // descending is a later knife — bail for now.
14105            Expr::ScalarSubquery(_)
14106            | Expr::Exists { .. }
14107            | Expr::InSubquery { .. }
14108            | Expr::RowInSubquery { .. }
14109            | Expr::RowCmpSubquery { .. } => {
14110                ok.set(false);
14111                true
14112            }
14113            _ => false,
14114        }
14115    };
14116    for item in &mut out.items {
14117        match item {
14118            SelectItem::Expr { expr, .. } => {
14119                crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
14120            }
14121            // `SELECT * FROM (…) q` means q's columns, in q's order.
14122            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
14123        }
14124    }
14125    if let Some(w) = &mut out.where_ {
14126        crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
14127    }
14128    if let Some(gs) = &mut out.group_by {
14129        for g in gs {
14130            crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
14131        }
14132    }
14133    if let Some(h) = &mut out.having {
14134        crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
14135    }
14136    for o in &mut out.order_by {
14137        crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
14138    }
14139    for d in &mut out.distinct_on {
14140        crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
14141    }
14142    if !ok.get() {
14143        return None;
14144    }
14145    // FROM becomes the stored table; the filters conjoin.
14146    out.from = Some(spg_sql::ast::FromClause {
14147        primary: it.clone(),
14148        joins: Vec::new(),
14149    });
14150    out.where_ = match (inner.where_.clone(), out.where_.take()) {
14151        (Some(a), Some(b)) => Some(Expr::Binary {
14152            lhs: alloc::boxed::Box::new(a),
14153            op: spg_sql::ast::BinOp::And,
14154            rhs: alloc::boxed::Box::new(b),
14155        }),
14156        (Some(a), None) => Some(a),
14157        (None, b) => b,
14158    };
14159    Some(out)
14160}
14161
14162/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
14163/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
14164/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
14165/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
14166/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
14167/// DISTINCT, an SRF, or an unprovable inner shape stays put.
14168fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
14169    use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
14170    let inner = primary.lateral_subquery.as_deref()?;
14171    // Outer: exactly `SELECT count(*)`, nothing else.
14172    if !stmt.ctes.is_empty()
14173        || !stmt.unions.is_empty()
14174        || stmt.distinct
14175        || !stmt.distinct_on.is_empty()
14176        || stmt.where_.is_some()
14177        || stmt.group_by.is_some()
14178        || stmt.having.is_some()
14179        || !stmt.order_by.is_empty()
14180        || stmt.limit.is_some()
14181        || stmt.offset.is_some()
14182        || stmt.items.len() != 1
14183    {
14184        return None;
14185    }
14186    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
14187        return None;
14188    };
14189    let E::FunctionCall { name, args } = expr else {
14190        return None;
14191    };
14192    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
14193        return None;
14194    }
14195    // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
14196    let Some(LimitExpr::Literal(k)) = &inner.offset else {
14197        return None;
14198    };
14199    let k = i64::from(*k);
14200    if inner.limit.is_some() || inner.order_by.is_empty() {
14201        return None;
14202    }
14203    let mut counted = inner.clone();
14204    counted.order_by = Vec::new();
14205    counted.offset = None;
14206    // The stripped inner must now be a provable simple shape (its
14207    // items become irrelevant — count(*) reads none of them — but an
14208    // SRF item would change the row count, so the flatten predicate's
14209    // scrutiny still applies).
14210    let base = matview_flatten_probe(&counted)?;
14211    let mut out = stmt.clone();
14212    out.items = alloc::vec![SelectItem::Expr {
14213        expr: E::FunctionCall {
14214            name: String::from("greatest"),
14215            args: alloc::vec![
14216                E::Binary {
14217                    lhs: alloc::boxed::Box::new(E::FunctionCall {
14218                        name: String::from("count_star"),
14219                        args: alloc::vec![],
14220                    }),
14221                    op: spg_sql::ast::BinOp::Sub,
14222                    rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
14223                },
14224                E::Literal(spg_sql::ast::Literal::Integer(0)),
14225            ],
14226        },
14227        alias: Some(String::from("count")),
14228    }];
14229    out.from = Some(spg_sql::ast::FromClause {
14230        primary: base,
14231        joins: Vec::new(),
14232    });
14233    out.where_ = counted.where_.clone();
14234    Some(out)
14235}
14236
14237/// The inner-shape probe `try_count_over_offset` shares with the
14238/// flatten: single stored table, no modifiers, no subqueries, no SRF
14239/// items. Returns the base TableRef.
14240fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
14241    use spg_sql::ast::SelectItem;
14242    if !inner.ctes.is_empty()
14243        || !inner.unions.is_empty()
14244        || inner.distinct
14245        || !inner.distinct_on.is_empty()
14246        || inner.group_by.is_some()
14247        || inner.group_by_all
14248        || inner.having.is_some()
14249        || !inner.order_by.is_empty()
14250        || inner.limit.is_some()
14251        || inner.offset.is_some()
14252        || !inner.window_check_exprs.is_empty()
14253        || inner.locking.is_some()
14254    {
14255        return None;
14256    }
14257    let ifrom = inner.from.as_ref()?;
14258    let it = &ifrom.primary;
14259    if !ifrom.joins.is_empty()
14260        || it.name.is_empty()
14261        || it.lateral_subquery.is_some()
14262        || it.unnest_expr.is_some()
14263        || it.generate_series_args.is_some()
14264        || it.as_of_segment.is_some()
14265        || it.jsonb_each_text_arg.is_some()
14266        || it.table_fn_call.is_some()
14267        || it.rows_from.is_some()
14268        || it.json_table.is_some()
14269        || it.with_ordinality
14270    {
14271        return None;
14272    }
14273    for item in &inner.items {
14274        match item {
14275            SelectItem::Expr { expr, .. } => {
14276                if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
14277                    return None;
14278                }
14279            }
14280            SelectItem::Wildcard => {}
14281            SelectItem::QualifiedWildcard(_) => return None,
14282        }
14283    }
14284    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
14285        return None;
14286    }
14287    Some(it.clone())
14288}
14289
14290/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
14291/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
14292/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
14293/// constant-LENGTH array literal unnests to exactly k rows per input
14294/// row (NULL elements are rows too). One SRF item only, elements
14295/// subquery-free, and the stripped inner must pass the same probe the
14296/// count-over-offset rewrite uses.
14297fn try_count_over_const_unnest(
14298    stmt: &SelectStatement,
14299    primary: &TableRef,
14300) -> Option<SelectStatement> {
14301    use spg_sql::ast::{Expr as E, SelectItem};
14302    let inner = primary.lateral_subquery.as_deref()?;
14303    if !stmt.ctes.is_empty()
14304        || !stmt.unions.is_empty()
14305        || stmt.distinct
14306        || !stmt.distinct_on.is_empty()
14307        || stmt.where_.is_some()
14308        || stmt.group_by.is_some()
14309        || stmt.having.is_some()
14310        || !stmt.order_by.is_empty()
14311        || stmt.limit.is_some()
14312        || stmt.offset.is_some()
14313        || stmt.items.len() != 1
14314    {
14315        return None;
14316    }
14317    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
14318        return None;
14319    };
14320    let E::FunctionCall { name, args } = expr else {
14321        return None;
14322    };
14323    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
14324        return None;
14325    }
14326    // Inner: exactly one item, and it is unnest(ARRAY[...]).
14327    if inner.items.len() != 1
14328        || !inner.order_by.is_empty()
14329        || inner.limit.is_some()
14330        || inner.offset.is_some()
14331    {
14332        return None;
14333    }
14334    let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
14335        return None;
14336    };
14337    let E::FunctionCall {
14338        name: fname,
14339        args: fargs,
14340    } = item
14341    else {
14342        return None;
14343    };
14344    if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
14345        return None;
14346    }
14347    let E::Array(elems) = &fargs[0] else {
14348        return None;
14349    };
14350    if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
14351        return None;
14352    }
14353    let k = elems.len() as i64;
14354    // The stripped inner (the SRF item replaced by a plain constant)
14355    // must be the provable simple shape.
14356    let mut counted = inner.clone();
14357    counted.items = alloc::vec![SelectItem::Expr {
14358        expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
14359        alias: None,
14360    }];
14361    let base = matview_flatten_probe(&counted)?;
14362    let mut out = stmt.clone();
14363    out.items = alloc::vec![SelectItem::Expr {
14364        expr: E::Binary {
14365            lhs: alloc::boxed::Box::new(E::FunctionCall {
14366                name: String::from("count_star"),
14367                args: alloc::vec![],
14368            }),
14369            op: spg_sql::ast::BinOp::Mul,
14370            rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
14371        },
14372        alias: Some(String::from("count")),
14373    }];
14374    out.from = Some(spg_sql::ast::FromClause {
14375        primary: base,
14376        joins: Vec::new(),
14377    });
14378    out.where_ = counted.where_.clone();
14379    Some(out)
14380}